[luogu3943] 星空 题面 具体代码 完

​ 这个题目大意上是这样的:给定一个长度为n的01串, 其中只有k个0, 每次操作时, 从给定的m种长度中选择一种, 选择序列上长度为这种的进行反转操作, 求至少需要多少次操作使得整个串全变为1.

​ 考虑状压dp, 枚举每一位出现情况, (f[i])表示状态为(i)时最少需要翻转的方案数. 但是(100pts)的数据(n)(4e4), 所以状压dp会达到一个令人恐怖的复杂度$ O(2 ^ {40000}) (, 因为)k(较小, 考虑将问题的状压转化为与)k(相关. 由于需要更改整个数列, 从这个上面想到了什么优化的方法吗? 差分, 差分对于序列修改是一个比较实用的优秀方法, 我们可以设差分数组)cha[i] = seq[i] - seq[i - 1](, 这样的话对序列)[l, r](进行修改也就是)cha[l] + 1, cha[r + 1] - 1(, 这样优化后我们就将)O(n)(的序列修改变成了)O(1)$的简单操作.

​ 于是, 问题转化为这样: 给定一个长度为(n)的01串, 其中0的数目不超过(2k), 每次操作时, 从给定的(m)种给定的距离中选择一种, 选择序列上距离为这种的两个位同时取反, 求最少需要多少次能使得整个串变为1. 观察到, 如果一个位为0, 我们一定会选择一个方式去消去这个0, 要么是一个1和他进行操作, 此时可视为交换这两个数, 要么是一个0与他进行操作, 此时可视为这两个数同时消去, 这样我们又可以转化问题了

​ 问题再次转化为这样:给定一个有n个点的图, 其中只有不超过(2k)的存在物品, 每次操作时, 从给定的m种距离中选择一种, 移动这个物品或与与他相距为这个距离的点同时消去, 问最少需要多少次操作使得所有的距离都消失.消去一个物品相当于将其中一个物品移动至另外一个物品的位置, 代价即为最少所需的步数, 同时, 每个点只有m条边, 我们发现这种操作可以用时间复杂度只有(O(nmk))(BFS)来解决.

​ 问题转化为:图上有2k个物品, 选择其中两个可以消去, 分别有不同的代价, 求使得所有物品小时的最小代价, 至此, 我们成功将问题转化为与k相关的状压dp, 多好啊.

具体代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
#define N 40005
using namespace std;

int n, k, m, cr[N], lth[100], pos[100], num[N], dis[20][20], f[1 << 20], step[N], cnt; 

inline int read()
{
	int x = 0, w = 1;
	char c = getchar();
	while(c < '0' || c > '9') { if (c == '-') w = -1; c = getchar(); }
	while(c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
	return x * w;
}

inline void SPFA(int x)
{
	memset(step, -1, sizeof(step));
	queue <int> q;
	q.push(pos[x]); step[pos[x]] = 0;
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		for(int i = 1; i <= m; i++)
		{
			for(int j = -1; j <= 1; j += 2)
			{
				int end = u + lth[i] * j; 
				if(end <= 0 || end > n || step[end] != -1) continue;
				step[end] = step[u] + 1; q.push(end); 
			}
		}
	}
	for(int i = 1; i <= n; i++) if(num[i] != 0) dis[x][num[i]] = step[i]; 
}

int main()
{
//	freopen("starlit.in", "r", stdin);
//	freopen("starlit.out", "w", stdout); 
	n = read(); k = read(); m = read();
	for(int i = 1; i <= k; i++) cr[read()] = 1;
	for(int i = 1; i <= m; i++) lth[i] = read();
	n++;
	for(int i = 1; i <= n; i++) if(cr[i] ^ cr[i - 1]) { pos[++cnt] = i; num[i] = cnt; }
    //记录下差分数组中为1(其实就是上面所说的为0)的位置.
	memset(dis, -1, sizeof(dis)); 
	for(int i = 1; i <= cnt; i++) SPFA(i);//BFS其实就是SPFA变一下.
	memset(f, 0x3f, sizeof(f)); 
	f[0] = 0; 
	for(int u = 0; u < (1 << cnt); u++)
		for(int i = 1; i <= cnt; i++)
			if(!(u & (1 << (i - 1))))
				for(int j = i + 1; j <= cnt; j++)
					if(!(u & (1 << (j - 1))))
						if(dis[i][j] != -1)
						{
							int v = u | (1 << (i - 1)) | (1 << (j - 1));
							f[v] = min(f[v], f[u] + dis[i][j]); 
						}//状压dp, 各位看官应该都看得懂吧...
	printf("%d
", f[(1 << cnt) - 1]); 
	return 0;
}

​ 星空这道题与这道题有异曲同工之妙, 可以对比着做一下.