0
点赞
收藏
分享

微信扫一扫

状压DP 学习笔记

一葉_code 2022-03-19 阅读 72

首先来说一下状态压缩

状态压缩就是使用某种方法,简明扼要地以最小代价来表示某种状态,通常是用一串01数字(二进制数)来表示各个点的状态。这就要求使用状态压缩的对象的点的状态必须只有两种,0 或 1;当然如果有三种状态用三进制来表示也未尝不可。

使用条件

1. 解法需要保存一定的状态数据(表示一种状态的一个数据值),每个状态数据通常情况下是可以通过2进制来表示的。这就要求状态数据的每个单元只有两种状态,比如说棋盘上的格子,放棋子或者不放,或者是硬币的正反两面。这样用0或者1来表示状态数据的每个单元,而整个状态数据就是一个一串0和1组成的二进制数。

2. 解法需要将状态数据实现为一个基本数据类型,比如int,long等等,即所谓的状态压缩。状态压缩的目的一方面是缩小了数据存储的空间,另一方面是在状态对比和状态整体处理时能够提高效率。这样就要求状态数据中的单元个数不能太大,比如用int来表示一个状态的时候,状态的单元个数不能超过32(32位的机器),所以题目一般都是至少有一维的数据范围很小。

个人理解:把本来应该用十进制存的状态,通过二进制存下来,省掉DP的维,通过对二进制存的状态来转移DP。

状压DP

用压缩状态来进行DP(好像说了跟没说一样)

状压DP最重要的就是位运算

(借用网图)

 关键就是看如何求出这一位是0或者是1,并和其它位比较。

具体就来看题吧。

Example 1

 分析一下题目:1表示能种草,0表示不能种草。如果一块地种草了,那它周围的地就不能接着种草,求将一块给定的草地分成如上所说的方案数。

首先我们可以先用一个数组来把每一行给存下来,就比如第一行是0 1 0,那么就用一个数组里面记录一个十进制的数2,表示0 1 0。

for(register int i(1) ; i<=m ; i=-~i){
	for(register int j(1) ; j<=n ; j=-~j){
		int x;
	    scanf("%d",&x);
		zt[i] = (zt[i]<<1) + x; //左移一正好将最后面这一位空出来
	}
}

然后我们需要记录什么是合法状态,就比如二进制0 1 0就是合法的,0 1 1就是不合法的,因为有两块连在一起的草地都种草了。

首先我们会发现一点,当 i < (1 << n)的时候,i的二进制位数 = 列数,也就是每行有几个数。

比如n=3的时候,正好有 1<<n = 8种状态  0 0 0  ; 0 0 1 ; 0 1 0 ; 0 1 1 ; 1 0 0 ; 1 0 1 ; 1 1 0;1 1 1.

所以说我们可以通过位运算来判断这种状态是否合理。

看下面这段代码,分别将i左移1位和右移一位,如果都是0的话,当前状态就可以。

for(register int i(0) ; i<(1<<n) ; i=-~i) g[i] = (!(i&(i<<1))) && (!(i&(i>>1)));

预处理完,接下来就是状压DP了 。放在代码里讲。

#include<bits/stdc++.h>
using namespace std;
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch == '-') f=-1 ; ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48) ; ch=getchar();}
	return x*f;
}
const int M = 15;
const int mod = 1e8;
bool g[1<<M];
int zt[M];
int f[M][1<<M];
int m,n;
signed main(){
	scanf("%d%d",&m,&n);
	for(register int i(1) ; i<=m ; i=-~i){
		for(register int j(1) ; j<=n ; j=-~j){
			int x;
			scanf("%d",&x);
			zt[i] = (zt[i]<<1) + x;
		}
	}
	for(register int i(0) ; i<(1<<n) ; i=-~i) g[i] = (!(i&(i<<1))) && (!(i&(i>>1)));
	f[0][0] = 1; // 初始化
	for(register int i(1) ; i<=m ; i=-~i){ //一共m行
		for(register int j(0) ; j<(1<<n) ; j=-~j){ //这么多种状态
			if(g[j] && ((zt[i] & j) == j)){ //如果g数组表示可行
                                            //zt记录的数字与j记录的状态是一样的话
                                            //就表示找到了与当前行相同的状态
				for(register int k(0) ; k<(1<<n) ; k=-~k){ //表示上一行有这么多种状态
					if((k & j) == 0) f[i][j] = (f[i][j] + f[i-1][k])%mod;
                    //表示正好可以,因为&的结果是0,说明上下两行正好没有同时是1的
                    //所以满足题目中给的条件互不相邻
				}
			}
		}
	}
	int ans=0;
	for(register int i(0) ; i<(1<<n) ; i=-~i) ans = (ans + f[m][i])%mod;
    //要加上最后一行的所有状态的结果求和
	printf("%d",ans);
	return 0;
}

 

Example 2

 

在此感谢范绪杰大佬对本菜鸡耐心的教导,让我彻底明白了此题  %%%%%orzorz

顺手推一下大佬的博客https://blog.csdn.net/BUG_Creater_jie?type=blog 

好了,开始讲这道题。题目描述:需要满足走过所有节点,并且使经过的长度最小。

这道题与上一道题不同的是,需要记录上一个点是从哪跳过来的,也就是转移的关系。

#include<bits/stdc++.h>
using namespace std;
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch == '-') f=-1 ; ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48) ; ch=getchar();}
	return x*f;
}
const int M = 30;
int a[M][M];
int f[M][1<<20];
int n;
signed main(){
	n=read();
	for(register int i(0) ; i<n ; i=-~i){
		for(register int j(0) ; j<n ; j=-~j){
			a[i][j] = read();
		}
	}

    //设f[i][j]表示处理到第i个结点,所有结点的访问状态为j

	memset(f,0x3f,sizeof(f)); //一定要先赋成最大值!
	f[0][1] = 0; //很显然,处理第0个节点,状态为1的时候为0
	for(register int j(0) ; j<(1<<n) ; j=-~j){ 
        //注意循环的顺序,j枚举的就是状态,跟上一道题一样,有这么多种状态
		for(register int i(0) ; i<n ; i=-~i){
            //枚举这是第几个点
			if(!(j & (1<<i))) continue;
            //关键1 首先1<<i表示第几个点,如果j记录的状态这个点是0的话
            //说明矛盾,这个状态不成立
            //举个例子,比如f[1][101]就不成立,因为前面表示到1了,后面却说没走到1

			for(register int k(0) ; k<n ; k=-~k){
				if(!((j^(1<<i))>>k) & 1) continue;
                //关键2 j^(1<<i)表示可以从哪个状态转移过来
                //举个例子 j为101,i为2,考虑能转移过来的状态
                //肯定是j从001转移过来(右面的第一个为第0结点)
                //此时会发现i应该从0转移过来,因为上面的001,第一位从1变成了0
                //所以j^(1<<i)正好就转化为101^100 答案是001
                //再右移k什么意思? 很显然表示可以转移过来的i是第几个结点
                //具体就是下面的转移式

				f[i][j] = min(f[i][j],f[k][j^(1<<i)] + a[i][k]);
                //f[i][j]这个状态可以从上一个可以转移过来的点k,状态为j^(1<<i)转移
                //上面已经讲了,最后别忘了要加上该路径的长度求最小值

			}
		}
	}
	printf("%d",f[n-1][(1<<n)-1]); //因为要每一个点都走到
                                   //所以n-1个点,后面的全是1,表示每个点都走到了
	return 0;
}

最后放上fxj大佬的总结:状压主要就是把那些位运算都整明白在干什么就好了。

举报

相关推荐

0 条评论