【算法详解】状态压缩DP

关于状态压缩

对于某些题目,一般的DP无法正常解决,例如过程的设置较为复杂,状态的枚举效率不够高或无法利用一般的数组完成,这时候就要用到二进制状态压缩来利用内部二进制的特性来简单的表达状态。

由于和二进制结合起来,状态压缩的DP一般效率较高,因为需要用二进制的每一位来表示每一个状态,所以往往复杂度中含有 2 n 2^n 2n项,因此状态压缩的题目 n n n一般不大于 20 20 20.

如果要熟练的运用二进制的状态压缩,我们要学会使用位运算。

位运算

首先要熟悉 a n d / & , o r / ∣ , n o t / ! and/\&,or/|,not/! and/&,or/,not/!的操作。

下面有一些基本的操作:
1.将x去掉最后一位数: x = x > > 1 x=x>>1 x=x>>1
2.将x的末尾加上0: x = x &lt; &lt; 1 x=x&lt;&lt;1 x=x<<1
3.将x的第k位变成0: x = x &amp; ( n o t ( 1 &lt; &lt; k − 1 ) ) x=x\&amp;(not(1&lt;&lt;k-1)) x=x&(not(1<<k1))
4.将x的第k位变成1: x = x ∣ ( 1 &lt; &lt; k − 1 ) x=x|(1&lt;&lt;k-1) x=x(1<<k1)
5.x的第k位数字为: 1 &amp; ( x &gt; &gt; k − 1 ) 1\&amp;(x&gt;&gt;k-1) 1&(x>>k1)
6.将x的第k位取反: x = x   x o r   ( 1 &lt; &lt; k − 1 ) x=x\ xor\ (1&lt;&lt;k-1) x=x xor (1<<k1)
7.x位的每一位都位1的数: ( 1 &lt; &lt; x ) − 1 (1&lt;&lt;x)-1 (1<<x)1
8.判断第x位是否在状态p中,在表示0,不在表示1: p &amp; ( 1 &lt; &lt; x − 1 ) p\&amp;(1&lt;&lt;x-1) p&(1<<x1)
具体还有这一些:
在这里插入图片描述

最短Hamilton路径

题目描述
给定一张 n(n≤20) 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次。
输入和输出
Input
第一行一个整数n。 接下来n行每行n个整数,其中第i行第j个整数表示点i到j的距离(一个不超过10^7的正整数,记为a[i,j])。 对于任意的x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]>=a[x,z]。
Output
一个整数,表示最短Hamilton路径的长度。
样例
Sample Input

4
0 2 1 3
2 0 2 1
1 2 0 1
3 1 1 0
Sample Output
4
数据规模与约定
时间限制:1s
空间限制:256MB

solution

对于这道题,显然要用状态压缩在设置状态。

可以设 f [ i ] [ j ] f[i][j] f[i][j]表示当前路径中到达 j j j,经过的点数状态为 j j j 1 1 1表示经过, 0 0 0表示没经过)的最小长度。
状态转移方程: f [ k ] [ i ∣ ( 1 &lt; &lt; k − 1 ) ] = m i n ( f [ k ] [ i ∣ ( 1 &lt; &lt; k − 1 ) ] , f [ j ] [ i ] + a [ k ] [ j ] ) f[k][i|(1&lt;&lt;k-1)]=min(f[k][i|(1&lt;&lt;k-1)],f[j][i]+a[k][j]) f[k][i(1<<k1)]=min(f[k][i(1<<k1)],f[j][i]+a[k][j])

其中i表示原来的状态, j j j表示最后经过的店, k k k表示新经过的点:

i ∣ ( 1 &lt; &lt; k − 1 ) i|(1&lt;&lt;k-1) i(1<<k1)表示原来的状态下新经过点 k k k的状态。

显然这道题目只要二进制了解了,DP的状态转移方程还是很简单的。

时间复杂度: O ( 2 n ∗ n ) O(2^n*n) O(2nn)

代码如下:

#include<bits/stdc++.h>
using namespace std;
int n;
int a[21][21];
int f[21][(1<<20)+10];
int main(void)
{
	freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);
	scanf("%d",&n);
	for (int i=1;i<=n;++i)
	    for (int j=1;j<=n;++j)
	        scanf("%d",&a[i][j]);
	memset(f,100,sizeof(f));
	f[1][1]=0;
	for (int i=1;i<(1<<n);++i) 
	    for (int j=1;j<=n;++j)
		    if (i&(1<<j-1))
			    for (int k=1;k<=n;++k)
				    if (!(i&(1<<k-1))) 
				        f[k][i|(1<<k-1)]=min(f[k][i|(1<<k-1)],f[j][i]+a[k][j]);
	cout<<f[n][(1<<n)-1]<<endl; 
	return 0;
}

关灯问题

题目描述
现有n盏灯,以及m个按钮。每个按钮可以同时控制这n盏灯——按下了第i个按钮,对于所有的灯都有一个效果。按下i按钮对于第j盏灯,是下面3中效果之一:如果a[i][j]为1,那么当这盏灯开了的时候,把它关上,否则不管;如果为-1的话,如果这盏灯是关的,那么把它打开,否则也不管;如果是0,无论这灯是否开,都不管。
现在这些灯都是开的,给出所有开关对所有灯的控制效果,求问最少要按几下按钮才能全部关掉。
输入输出格式
输入格式:
前两行两个数,n m
接下来m行,每行n个数,a[i][j]表示第i个开关对第j个灯的效果。
输出格式:
一个整数,表示最少按按钮次数。如果没有任何办法使其全部关闭,输出-1

solution

对于此题,我们同样选择使用状压DP解决。

我们需要预处理每一种状态 i i i,经过操作第 j j j个操作,得到的状态 k k k。即 n e x t [ j ] [ i ] = k . next[j][i]=k. next[j][i]=k.
如何处理?枚举每一盏灯和开关进行匹配即可。

然后我们就可以去跑广搜了,以状态 ( 1 &lt; &lt; n ) − 1 (1&lt;&lt;n)-1 (1<<n)1为起点,表示所有灯都亮着;以状态0位终点,表示所有灯都灭。对于每一个点都根据next数组去不断扩展状态即可。

因为无解输出 − 1 -1 1,所以 d i s dis dis在初始化时就可以赋值为 − 1 -1 1.

#include<bits/stdc++.h>
using namespace std;
int d[1<<11];
int q[1<<11];
int v[1<<11];
int a[200][200];
int next[1<<11][200];
int main(void)
{
	int n,m;
	cin>>n>>m;
	for (int i=1;i<=m;++i) 
	    for (int j=1;j<=n;++j)
		    cin>>a[i][j];
	for (int i=0;i<(1<<n);++i)
		for (int j=1;j<=m;++j)
		{
			next[i][j]=i;
		    for (int k=1;k<=n;++k)
		    {
		        if (a[j][k]==1 && ((i>>k-1)&1)==1)           
		            next[i][j]^=(1<<k-1);
		        如果为1,且灯开着
		        if (a[j][k]==-1 && ((i>>k-1)&1)==0)
		            next[i][j]^=(1<<k-1);
		        如果为0,且灯暗着
		    }
		}
	memset(d,-1,sizeof(d));
	memset(v,0,sizeof(v));
	q[1]=(1<<n)-1;
	d[(1<<n)-1]=0;
	v[(1<<n)-1]=1;
	int h=1,t=1;
	for (;h<=t;++h)
	{
		int now=q[h];
		for (int i=1;i<=m;++i) 
		    if (!v[next[now][i]])
		    {
		    	q[++t]=next[now][i];
		    	v[next[now][i]]=1;
		    	d[next[now][i]]=d[now]+1;
			}
	}
	cout<<d[0]<<endl;
	return 0;
}

[USACO06NOV]玉米田Corn Fields

题目描述
Farmer John has purchased a lush new rectangular pasture composed of M by N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) square parcels. He wants to grow some yummy corn for the cows on a number of squares. Regrettably, some of the squares are infertile and can’t be planted. Canny FJ knows that the cows dislike eating close to each other, so when choosing which squares to plant, he avoids choosing squares that are adjacent; no two chosen squares share an edge. He has not yet made the final choice as to which squares to plant.
Being a very open-minded man, Farmer John wants to consider all possible options for how to choose the squares for planting. He is so open-minded that he considers choosing no squares as a valid option! Please help Farmer John determine the number of ways he can choose the squares to plant.
农场主John新买了一块长方形的新牧场,这块牧场被划分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一块正方形的土地。John打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。
遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是John不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。
John想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)
输入输出格式
输入格式:

第一行:两个整数M和N,用空格隔开。
第2到第M+1行:每行包含N个用空格隔开的整数,描述了每块土地的状态。第i+1行描述了第i行的土地,所有整数均为0或1,是1的话,表示这块土地足够肥沃,0则表示这块土地不适合种草。
输出格式:
一个整数,即牧场分配总方案数除以100,000,000的余数。

对于这道题,我们照样可以使用状压DP来解决。

因为每一个横行可以形成一个状态,而每一行的状态是否合法仅和上一行和当前行有关。
我们只需要通过第一行不断的通过行与行之间的关系即可。

我们可以将读入的数据转化成二进制存在数组里面。

先确定第一行的合法状态:当每一格子不相邻且种在肥沃的土地上即可。
对于前者,只要左移或右移一位并与原数做 a n d and and运算即可。即 i &amp; ( i &lt; &lt; 1 ) i\&amp;(i&lt;&lt;1) i&(i<<1),若该值为1说明一位以后有相邻的,不合法;为 0 0 0说明合法。

对于后者,这需要满足 ( i ∣ s t [ 1 ] ) = = s t [ 1 ] (i|st[1])==st[1] (ist[1])==st[1]这个条件即可。

我们设 f [ i ] [ j ] f[i][j] f[i][j]表示第 i i i行状态为 j j j的方案数。

然后就可以状态转移:
只要满足上面说的两个条件和 ( j &amp; k ) = = 0 (j\&amp;k)==0 (j&k)==0,即两个每一相邻部分即可。其中j和k是两个不同的状态。

状态转移方程就特别简单: f [ i ] [ k ] = ( f [ i ] [ k ] + f [ i − 1 ] [ j ] ) f[i][k]=(f[i][k]+f[i-1][j])%P f[i][k]=(f[i][k]+f[i1][j])

时间复杂度: O ( n ∗ 4 n ) O(n*4^n) O(n4n)
代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,m;
int st[13];
int f[13][1<<13];
const int P=100000000;
int main(void)
{
	cin>>m>>n;
	for (int i=1;i<=m;++i)
	{
		int sum=0,k;
		for (int i=1;i<=n;++i)
		{
			cin>>k;
			sum=sum*2+k;
		}
		st[i]=sum;
	}
	memset(f,0,sizeof(f));
	for (int i=0;i<(1<<n);++i) 
	    if (not(i&(i<<1)) && (i|st[1])==st[1])
	        f[1][i]=1;
	for (int i=2;i<=m;++i)
	    for (int j=0;j<(1<<n);++j)
	        if (f[i-1][j])
	            for (int k=0;k<(1<<n);++k)
	                if ((k&(k<<1))==0 && (j&k)==0 && (k|st[i])==st[i])
	                    f[i][k]=(f[i][k]+f[i-1][j])%P;
	int ans=0;
	for (int i=0;i<(1<<n);++i) ans=(ans+f[m][i])%P;
	cout<<ans<<endl;
	return 0;
}
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值