首先来说一下状态压缩
状态压缩就是使用某种方法,简明扼要地以最小代价来表示某种状态,通常是用一串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;
}