Living-Dream 系列笔记 第54期

Posted on 2024-04-20 19:02  _XOFqwq  阅读(6)  评论(0编辑  收藏  举报

状压 dp:

  • 是对 dp 状态表示的优化。

  • 若有多个维度,每个维度仅有 \(0/1\),则将状态转为一个二进制数,并以十进制数表示。

位操作(全 文 背 诵):

  • 任意二进制数位 \(\operatorname{and} \ 1\) 得本身。

  • 任意二进制数位 \(\operatorname{xor} \ 1\) 得本身。

  • 任意二进制数位 \(\operatorname{and} \ 0\) 赋值 \(0\)

  • 任意二进制数位 \(\operatorname{or} \ 1\) 赋值 \(1\)

  • (n>>k)&1 取二进制下 \(n\) 从右往左的第 \(k\) 位。

  • n&((1<<k)-1) 取二进制下 \(n\) 的右 \(k\) 位。

  • (1<<k)-1 得到 \(k\)\(1\)

  • n^(1<<k) 将二进制下 \(n\)\(k\) 位取反。

  • n|(1<<k) 将二进制下 \(n\)\(k\) 位赋值 \(1\)

  • n&(~(1<<k)) 将二进制下 \(n\)\(k\) 位赋值 \(0\)

优先级:

  • 四则运算 \(>\) 位移运算符 \(>\) 位运算符 \(>\) 逻辑运算符

T1

本题的实质即为求所有哈密尔顿路径中边权和最小的那条路径的边权和。

求哈密尔顿路径是个 NP-Hard 问题,但本题可用状压 dp 求解。

具体地,我们发现在表示状态时,每个点都和其他的所有点有关。

因此我们直接用 \(dp_{0/1,0/1,...,0/1,i}\) 表示当前走到了点 \(i\),且其余点的状态为 \(0/1,0/1,...,0/1\),其中 \(0/1\) 表示 走过/没走过。

这个状态最多可达 16 维,实现起来很不好写。

于是我们考虑将前面的 \(0/1,0/1,...,0/1\) 压缩成一个二进制数来表示,

即令 \(dp_{i,j}\) 表示当前走到了点 \(j\),且其余点的状态为二进制下的 \(j\)

因为初始站在点 \(0\)(状压的题尽量用 \(0\) 下标,省一半空间),

所以有初始状态:\(dp_{1,0}=0\),其余为 \(\infty\)

然后显然答案为 \(dp_{2^n-1,n-1}\),其中 \(2^n-1\) 等价于 (1<<n)-1,表示二进制下 \(n\)\(1\)

接着易得转移

\[dp_{i,j}=\max(dp_{i,j},dp_{i \operatorname{xor} 2^j}) \]

其中 \(i \operatorname{xor} 2^j\) 等价于 n^(1<<j),表示将二进制下 \(n\)\(j\) 位取反,即退回一步到点 \(j\) 的上一个点。

时间复杂度 \(O(n^22^n)\)

code
#include<bits/stdc++.h>
using namespace std;

const int N=20,W=1e5+5;
int n;
int w[N][N];
int dp[W][N];

int main(){
	memset(dp,0x3f,sizeof(dp));
	cin>>n;
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++)
			cin>>w[i][j];
	dp[1][0]=0;
	for(int i=0;i<(1<<n);i++)
		for(int j=0;j<n;j++)
			if((i>>j)&1)
				for(int k=0;k<n;k++)
					if((i>>k)&1)
						dp[i][j]=min(dp[i][j],dp[i^(1<<j)][k]+w[k][j]);
	cout<<dp[(1<<n)-1][n-1];
	return 0;
}

一些常数优化:

  • 循环 \(i\) 时,可以每次 \(i+2 \to i\)

  • 循环 \(j\) 时,可以从 \(1\) 开始。

T2

本题的实质即为求所有哈密尔顿回路中边权和最小的那条回路的边权和。

方法与上题类似,仅需改变初始状态与加上终点到起点的边权即可,然后终点枚举取 \(\max\) 即可。

code
#include<bits/stdc++.h>
using namespace std;

const int N=20,W=1e6+1e5;
int n;
int val[N][N];
int dp[W][N];

int main(){
	memset(dp,0x3f,sizeof(dp));
	cin>>n;
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++)
			cin>>val[i][j];
	dp[2][1]=0;
	for(int i=0;i<(1<<n);i++)
		for(int j=0;j<n;j++)
			if((i>>j)&1)
				for(int k=0;k<n;k++)
					if((i>>k)&1)
						dp[i][j]=min(dp[i][j],dp[i^(1<<j)][k]+val[k][j]);
	int ans=1e9;
	for(int i=0;i<n;i++)
		ans=min(ans,dp[(1<<n)-1][i]+val[i][1]);
	cout<<ans;
	return 0;
}

作业 T1

考虑怎么用状压做。

观察到本题的行、列数都很小,因此怎么状压都行,这里以状压行为例。

\(dp_{i,j}\) 表示第 \(i\) 行种草状态为二进制下的 \(j\) 时的方案数。

显然答案为 \(\sum^{2^n-1}_{i=0} dp_{m,i} \bmod 10^8\)

并有初始状态:\(dp_{0,0}=1\)(所有地都不中也是一种方案)。

转移也十分显然

\[dp_{i,j}=(dp_{i,j}+dp_{i-1,k}) \bmod 10^8 \]

关键是如何判定某一种草方案是否合法。

  • 地的限制

    我们用 \(dirt_i\) 记录第 \(i\) 行的土地贫瘠状况,\(0/1\) 表示 肥沃/贫瘠的土地。

    对于某一种草方案 \(i\),容易发现当 \(dirt_i \operatorname{and} i=i\) 时,说明这一种草方案的草都种在了肥沃土地上。

  • 左右限制

    对于某一种草方案 \(i\),容易发现当 \(\frac{i}{2} \operatorname{and} i=0\) 时,说明这一种草方案没有左右相邻的两块草地。其中 \(\frac{i}{2}\) 等价于 i>>1

  • 上下限制

    对于某一种草方案 \(i\) 与其上一排的种草方案 \(j\),容易发现当 \(i \operatorname{and} j=0\) 时,说明这一种草方案没有上下相邻的两块草地。

时间复杂度 \(O(2^{2n}m)\)

code
#include<bits/stdc++.h>
using namespace std;

const int N=15,MOD=1e8;
int m,n;
int dirt[N];
int dp[N][1<<N];

int main(){
	cin>>m>>n;
	int x;
	for(int i=1;i<=m;i++)
		for(int j=1;j<=n;j++)
			cin>>x,dirt[i]=(dirt[i]<<1)+x;
	dp[0][0]=1;
	for(int i=1;i<=m;i++)
		for(int j=0;j<(1<<n);j++)
			if(((dirt[i]&j)==j)&&(((j<<1)&j)==0))
				for(int k=0;k<(1<<n);k++)
					if((j&k)==0)
						dp[i][j]=(dp[i][j]+dp[i-1][k])%MOD;
	int ans=0;
	for(int i=0;i<(1<<n);i++)
		ans=(ans+dp[m][i])%MOD;
	cout<<ans;
	return 0;
}

作业 T2

典中典。似乎是 作业 T1 的加强版。

考虑到 \(N\) 较大而 \(M\) 较小,因此我们仍然状压行。

因为这题关系到上两行,所以状态应多带一个维度,即令 \(dp_{i,j,k}\) 表示第 \(i\) 行的状态为二进制下的 \(j\),且第 \(i-1\) 行的状态为二进制下的 \(k\) 时最多能摆放的炮兵部队的数量。

因此答案即为 \(\max^{2^n-1}_{i=0} \max^{2^n-1}_{j=0} dp_{n-1,i,j}\)(以第 \(0\) 行为第一行,第 \(n-1\) 行为最后一行)。

初始状态:

  • 对于第 \(0\) 行的所有合法方案 \(i\),令 \(dp_{0,i,0}=sum_i\)

  • 对于第 \(0,1\) 行的所有合法方案 \(i,j\),令 \(dp_{1,i,j}=sum_i+sum_j\)

其中 \(sum_i\) 表示某一方案 \(i\) 二进制下 \(1\) 的个数,等价于 __builtin_popcount(i)

转移也就自然可以得出

\[dp_{i,j,k}=\max(dp_{i,j,k},dp_{i-1,k,l}+sum_i) \]

\(i,j,k,l\) 表示 当前行,当前行的状态与上两行的状态)

判定合法的方法同作业 T1,此处不再赘述。

code
#include<bits/stdc++.h>
using namespace std;

const int N=12,M=1e2+5;
int n,m;
int land[M];
int dp[3][1<<N][1<<N];

int val(int x){
	int sum=0;
	for(;x;x>>=1) if(x&1) sum++;
	return sum;
}

int main(){
	cin>>n>>m;
	char x;
	for(int i=0;i<n;i++)
		for(int j=0;j<m;j++)
			cin>>x,land[i]=(land[i]<<1)+(x=='P'?1:0);
	for(int i=0;i<(1<<m);i++)
		if(!(((i&land[0])!=i)||(i&(i<<1))||(i&(i<<2))))
			dp[0][i][0]=val(i);
	for(int i=0;i<(1<<m);i++)
		for(int j=0;j<(1<<m);j++)
			if(!(((i&land[1])!=i)||(i&(i<<1))||(i&(i<<2))||((j&land[0])!=j)||(j&(j<<1))||(j&(j<<2))||(i&j)))
				dp[1][i][j]=val(i)+val(j);
	for(int i=2;i<n;i++){
		for(int j=0;j<(1<<m);j++){
			if((j&(j<<1))||(j&(j<<2))||((land[i]&j)!=j))
				continue;
			for(int k=0;k<(1<<m);k++){
				if((j&k)||(k&(k<<1))||(k&(k<<2))||((land[i-1]&k)!=k))
					continue;
				for(int l=0;l<(1<<m);l++){
					if((j&l)||(k&l)||(l&(l<<1))||(l&(l<<2))||((land[i-2]&l)!=l))
						continue;
	 				dp[i%3][j][k]=max(dp[i%3][j][k],dp[(i-1)%3][k][l]+val(j));
				}
			}
		}
	}
	int ans=-1e9;
	for(int i=0;i<(1<<m);i++)
		for(int j=0;j<(1<<m);j++)
			ans=max(ans,dp[(n-1)%3][i][j]);
	cout<<ans;
	return 0;
}