【动态规划】 状态压缩 dp

当我们有时无法用普通的dp以及一些算法表示一些状态、解决一些问题的时候,我们就可以想到状态压缩动态规划。

状态压缩动态规划一般使用二进制来表示一个问题的子问题是否使用过。
例如:1 0 1(5)

这上面的二进制指的是第一个和第三个问题已经求解过或者已经用过,但是第二个还没用过。
当然存储时我们是不能用二进制存储的,所以我们一般都用二进制所对应的十进制存储

正是这个原因,导致状压dp的效率与空间十分不理想,全部都是指数级别,正是这个缺陷,使状态压缩的数据范围都非常的小,这也是我们辨别此题是否是状压dp的一个标准。

由于状压dp一般要用位运算,所以以下是一些位运算的常规操作:
1、 x > > n x>>n x>>n 将x右移n位,相当于x/2^n
2、 x &lt; &lt; n x&lt;&lt;n x<<n 将x左移n位,相当于x*2^n
3、 x &amp; y x\&amp;y x&y 将x和y进行与运算,都为1则1,否则为0
4、 x ∣ y x|y xy 将x和y进行或运算,有1则1,否则为0
5、 x   ( x o r ) y x^ \ (xor)y x (xor)y 将x和y进行异或运算,相同为0,不同为1

还有一些组合操作:
1、 1 &lt; &lt; i 1&lt;&lt;i 1<<i 相当于2^i
2、 x ∣ ( 1 &lt; &lt; i − 1 ) x|(1&lt;&lt;i-1) x(1<<i1) 将x的第i位变为1
3、 i f ( x &amp; ( 1 &lt; &lt; i − 1 ) ) if (x\&amp;(1&lt;&lt;i-1)) if(x&(1<<i1)) 判断x的第i位是否为1
4、 f o r ( i n t   i = x ; i ; i = ( i − 1 ) &amp; x ) for (int\ i=x;i;i=(i-1)\&amp;x) for(int i=x;i;i=(i1)&x) 枚举x的子集
(PS:如果x|y==x,y为x的子集)
5、-x=(x)+1 x&(-x)是返回最后一个1的位置
6、a ^ =b ^ = a ^ = b 就是a与b交换。具体为何易证,自己去模拟

从网上摘来一个图,有错勿喷:

在这里插入图片描述


那么接下来上几道例题:

一、最短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


分析:状压dp,设dp[i][j]表示当前在第i个点,当前状态为j(其实就是一串二进制,二进制数为1表示当前的星球已经到达过,0表示没到达过)。我们先枚举当前的状态i,找出当前状态中到达过的星球j(二进制所对应的位是1),然后在枚举终点k,如果发现k是当前状态中没到达的点,那么就进行转移。
具体的转移方程如下:
d p [ k ] [ i ∣ ( 1 &lt; &lt; k − 1 ) ] = m i n ( d p [ k ] [ i ∣ 1 &lt; &lt; k − 1 ] , d p [ j ] [ i ] + a [ j ] [ k ] ) ; dp[k][i|(1&lt;&lt;k-1)]=min(dp[k][i|1&lt;&lt;k-1],dp[j][i]+a[j][k]); dp[k][i(1<<k1)]=min(dp[k][i1<<k1],dp[j][i]+a[j][k]);

稍微注意一下空间。

那么具体代码如下:

#include<bits/stdc++.h>
using namespace std;
int n;
int dp[51][1048580];
int a[1001][1001];

int main(){
	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(dp,20,sizeof(dp));
	dp[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)))
		        dp[k][i|(1<<k-1)]=min(dp[k][i|1<<k-1],dp[j][i]+a[j][k]);
		}
	printf("%d",dp[n][(1<<n)-1]);
	fclose(stdin);
	fclose(stdout);
	return 0;	
}

二、玉米田
题目描述:

农场主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,设 d p [ i ] [ j ] dp[i][j] dp[i][j]为第i行状态为j的方案数。开始用一个数组f[i]
f[i]预处理出每一行的二进制所对应得十进制数。转移时,枚举上下两行,不难发现,如果当前种的田是合理的,那么就肯定满足 ( i &amp; ( i &gt; &gt; 1 ) = = 0 ) &amp; &amp; ( i &amp; ( i &lt; &lt; 1 ) = = 0 ) (i\&amp;(i&gt;&gt;1)==0)\&amp;\&amp;(i\&amp;(i&lt;&lt;1)==0) (i&(i>>1)==0)&&(i&(i<<1)==0),以及是放在农田不贫瘠的土上的 ( j ∣ f [ k ] = = j ) (j∣f[k]==j) (jf[k]==j)

(j∣f[k]==j)。转移时就从上层转移累加即可.

不难处理出预处理部分:

    for (int i=1,x;i<=n;i++){
	    int sum=0;
	    for (int j=1;j<=m;j++) scanf("%d",&x),sum=sum*2+x;
	    f[i]=sum;
	}

dp部分如下:

	for (int i=1;i<=n;i++)
	  for (int j=0;j<=sum;j++)
	    if (vis[j]&&((j&f[i])==j))
	      for (int k=0;k<=sum;k++)
	        if (!(k&j))
	          dp[i][j]=(dp[i][j]+dp[i-1][k])%P;

这两部分是核心,需要注意,不过值得一提的是这道题的初值也比较特殊。

那么具体代码如下:

#include<bits/stdc++.h>
using namespace std;
#define P 100000000
int f[100001];
int dp[13][16777217];
bool vis[16777217];
int n,m;

int main(){
    scanf("%d %d",&n,&m);
    for (int i=1,x;i<=n;i++){
	    int sum=0;
	    for (int j=1;j<=m;j++) scanf("%d",&x),sum=sum*2+x;
	    f[i]=sum;
	}
//	for (int i=1;i<=n;i++) cout<<f[i]<<endl;
	int sum=(1<<m)-1;
	for (int i=0;i<=sum;i++)
	  if (!(i&(i<<1))&&(i|f[1])==f[1]) dp[1][i]=1;
	for (int i=0;i<=sum;i++)
	  vis[i]=(!(i&(i>>1)))&&(!(i&(i<<1)));
	for (int i=1;i<=n;i++)
	  for (int j=0;j<=sum;j++)
	    if (vis[j]&&((j&f[i])==j))
	      for (int k=0;k<=sum;k++)
	        if (!(k&j))
	          dp[i][j]=(dp[i][j]+dp[i-1][k])%P;
	int ans=0;
	for (int i=0;i<=sum;i++) ans=(ans+dp[n][i])%P;
	printf("%d",ans);
}

安利一下题解

三、 关灯问题II
题面描述:现有n盏灯,以及m个按钮。每个按钮可以同时控制这n盏灯——按下了第i个按钮,对于所有的灯都有一个效果。按下i按钮对于第j盏灯,是下面3中效果之一:如果a[i][j]为1,那么当这盏灯开了的时候,把它关上,否则不管;如果为-1的话,如果这盏灯是关的,那么把它打开,否则也不管;如果是0,无论这灯是否开,都不管。

现在这些灯都是开的,给出所有开关对所有灯的控制效果,求问最少要按几下按钮才能全部关掉。


输入输出格式
输入格式:
前两行两个数,n m
接下来m行,每行n个数,a[i][j]表示第i个开关对第j个灯的效果。


输出格式:
一个整数,表示最少按按钮次数。如果没有任何办法使其全部关闭,输出-1


输入输出样例
输入样例#1:
3
2
1 0 1
-1 1 0

输出样例#1:
2


说明

对于20%数据,输出无解可以得分。

对于20%数据,n<=5

对于20%数据,m<=20

上面的数据点可能会重叠。

对于100%数据 n<=10,m<=100


分析:

很多人看到这道题的取值范围后,第一时间的想法就是——搜索??

不过,这并不是搜索,而是一道状压dp,其实是否是状压dp其实很好判断,你只要看一看数据范围就可以了,状压的数据范围一般不会超过30,只要数据范围过了关,再看一眼题目是否像状压dp就可以了。

接下来看怎么做:

我们先预处理出t[i][j]表示第i个开关开j状态后的状态。将每一个状态,每一个开关打开后所对应得状态预处理出来.

预处理部分如下:

    for (int i=0;i<(1<<n);i++) 
      for (int j=1;j<=m;j++){
      	int ans=0;
	    for (int k=1;k<=n;k++){
		    if (a[j][k]==1) if (i&(1<<n-k)) ans=ans;else;//如果当前的操作是1,如果当前的灯是亮着的,把它关上,否则不管 
		    else if (a[j][k]==-1) ans=ans+(1<<n-k);//如果当前的操作是-1,直接加上 
		    else if (a[j][k]==0) if (i&(1<<n-k))ans+=(1<<n-k);else;else;//如果当前的操作是0,如果当前的灯是开着的,累加;否则不管 
		}
		t[j][i]=ans;
	}
预处理完之后,我们很容易知道题目的意思就是让我们求所有的灯都是0的状态,即让我们求0的时候的状态,因为开始时所有的灯都是开的,所以开始时的状态就是(1<<n)-1的状态。我们只要以(1<<n)-1为起点,0为终点做一遍BFS,记录步数即可

那么具体代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[110][110];
queue < int > q;
int t[2001][2001];
int dp[2001];
int main(){
    scanf("%d %d",&n,&m);
    for (int i=1;i<=m;i++)
      for (int j=1;j<=n;j++) scanf("%d",&a[i][j]);
    for (int i=0;i<(1<<n);i++) 
      for (int j=1;j<=m;j++){
      	int ans=0;
        for (int k=1;k<=n;k++){
            if (a[j][k]==1) if (i&(1<<n-k)) ans=ans;else;//如果当前的操作是1,如果当前的灯是亮着的,把它关上,否则不管 
            else if (a[j][k]==-1) ans=ans+(1<<n-k);//如果当前的操作是-1,直接加上 
            else if (a[j][k]==0) if (i&(1<<n-k))ans+=(1<<n-k);else;else;//如果当前的操作是0,如果当前的灯是开着的,累加;否则不管 
        }
        t[j][i]=ans;
    }
//	for (int i=0;i<(1<<n);i++){
//	    for (int j=1;j<=m;j++) cout<<t[j][i]<<' ';
//	    cout<<endl;
//	}
    memset(dp,-1,sizeof(dp));
    dp[(1<<n)-1]=0;
    q.push((1<<n)-1);
    while (!q.empty()){
        int x=q.front();q.pop();
        for (int i=1;i<=m;i++){
            int y=t[i][x];
            if (dp[y]==-1) dp[y]=dp[x]+1,q.push(y);
        }
    }
//	for (int i=(1<<n)-1;i>=0;i--) cout<<dp[i]<<' ';
//	cout<<endl;
    printf("%d",dp[0]);
    return 0;
}

其实跟我的题解也差不多

状态压缩具体介绍到这里,有兴趣的人可以去继续看一看我其它的博客

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值