感觉好多讲状压DP的博客都有点乱,我就结合各路大佬的博客,加上我自己的理解,总结出一篇博客来,供初学者参考
一、概述
1.状态压缩
状态压缩就是使用某种方法,简明扼要地以最小代价来表示某种状态,通常是用一串01数字(二进制数)来表示各个点的状态。这就要求使用状态压缩的对象的点的状态必须只有两种,0 或 1;当然如果有三种状态用三进制来表示也未尝不可。
2.使用条件
从状态压缩的特点来看,这个算法适用的题目符合以下的条件:
- 解法需要保存一定的状态数据(表示一种状态的一个数据值),每个状态数据通常情况下是可以通过2进制来表示的。这就要求状态数据的每个单元只有两种状态,比如说棋盘上的格子,放棋子或者不放,或者是硬币的正反两面。这样用0或者1来表示状态数据的每个单元,而整个状态数据就是一个一串0和1组成的二进制数。
- 解法需要将状态数据实现为一个基本数据类型,比如int,long等等,即所谓的状态压缩。状态压缩的目的一方面是缩小了数据存储的空间,另一方面是在状态对比和状态整体处理时能够提高效率。这样就要求状态数据中的单元个数不能太大,比如用int来表示一个状态的时候,状态的单元个数不能超过32(32位的机器),所以题目一般都是至少有一维的数据范围很小。
3.状压DP
状压DP,顾名思义,就是使用状态压缩的动态规划。
动态规划问题通常有两种,一种是对递归问题的记忆化求解,另一种是把大问题看作是多阶段的决策求解。这里用的便是后一种,这带来一个需求,即存储之前的状态,再由状态及状态对应的值推演出状态转移方程最终得到最优解。
二、位运算
一般基础的状压就是将一行的状态压成一个数,这个数的二进制形式反映了这一行的情况。由于使用二进制数来保存被压缩的状态,所以要用到神奇的二进制位运算操作,将一个十进制数转成二进制进行位运算操作再转回十进制数。
包括
- 按位与&(有0为0,其实就是且)
- 按位或|(有1为1,其实就是或)
- 按位取反~(注意负数补码的符号,最前面的第一位是1)
- 异或^(相同为0,不同为1)
- 左移<<
- 右移>>
对于位运算还是不太熟悉的同学请点我复习一下
下面是由江苏省淮阴中学薛志坚同(da)学(lao)整理的一些常见操作:
%%%
建议花几分钟把每一条都搞懂,然后跟我一起大(mo)赞(bai)位运算的神奇 %%%
初试化状态的时候要看清条件,什么要,什么不要。
一般情况下要预处理前k行(k由题目定)。
Dp时题目给的条件和fit函数、state数组都要检查。
最最重要的一点:
位反(~ ) > 算术 > 位左移、位右移 > 关系运算
> 位与 > 位或 > 位异或 > 逻辑运算
所以一般位运算最好打括号。
三、例题引入
1、入门例题【例1】填满棋盘
有一个NM(N<=5,M<=1000)的棋盘,现在有12及2*1的小木块无数个,要盖满整个棋盘,有多少种方式?答案只需要mod1,000,000,007即可。
例如:对于一个22的棋盘,有两种方法,一种是使用2个12的,一种是使用2个2*1的。
【算法分析】
在这道题目中,N和M的范围本应该是一样的,但实际上,N和M的范围却差别甚远,对于这种题目,首先应该想到的就是,正确算法与这两个范围有关!N的范围特别小,因此可以考虑使用状态压缩动态规划的思想,请看下面的图:
本思路来自博客动态规划之状态压缩dp入门,不过原博没有图,我帮他补个图,再优化一下内容。
假设第一列已经填满,则第二列的摆设方式,只与第一列对第二列的影响有关。同理,第三列的摆设方式也只与第二列对它的影响有关。那么,使用一个长度为 N的二进制数 state来表示这个影响,例如: 4(00100)就表示了图上第二列的状态。
因此,本题的状态可以这样表示:
dp[i][state]表示该填充第 i 列,第 i−1 列对它的影响是 state 的时候的方法数。 i<=M,0<=state<2N
对于每一列,情况数也有很多,但由于 N 很小,所以可以采取搜索的办法去处理。对于每一列,搜索所有可能的放木块的情况,并记录它对下一列的影响,之后更新状态。状态转移方程如下:
dp[i][state]=∑dp[i−1][pre]每一个 pre可以通过填放成为 state
对于每一列的深度优先搜索,写法如下:
//第i列,枚举到了第j行,当前状态是state,对下一列的影响是nex
void dfs(ll i,ll j,ll state,ll nex)
{
if(j==n)//最后一行
{
dp[i+1][nex]+=dp[i][state];
dp[i+1][nex]%=mod;
return;
}
//如果这个位置(第j行)已经被上一列给站了(state的第j位为1),所以就直接跳过
if(state&(1<<j)>0)
dfs(i,j+1,state,nex);
//如果这个位置是空的,那么就尝试放一个1*2的棋子
if(state&(1<<j)==0)
dfs(i,j+1,state,nex|(1<<j));//(使nex的第j位变成1)
//如果这个位置以及下一行都空的,那么就放一个2*1的棋子并直接跳到下下行
if(j+1<n&&state&(1<<j)==0&&state&(1<<j+1)==0)//注意要特判第j行下面是否还有一行
dfs(i,j+2,state,nex);
return;
}
最终,答案就是 dp[M+1][0]。
完整代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=2000+7;
const ll mod=1000000007;
ll n,m;
ll dp[N][N];//dp[i][state]; 铺满前i-1列的所有方案数
//第i列,枚举到了第j行,当前状态是state,对下一列的影响是nex
void dfs(ll i,ll j,ll state,ll nex)
{
if(j==n)//最后一行
{
dp[i+1][nex]+=dp[i][state];//nex一直接受前面的变化直到最后一行当前的可能性已经遍历完了就+上即可
dp[i+1][nex]%=mod;
return;
}
//如果这个位置(第j行)已经被上一列给占了(state的第j位为1),所以就直接跳过
if(((1<<j)&state)>0)
dfs(i,j+1,state,nex);//不会对下一列有什么影响
//如果这个位置是空的,那么就尝试放一个1*2的棋子
if(((1<<j)&state)==0)
dfs(i,j+1,state,nex|(1<<j));//(使nex的第j位变成1)横着放一个1*2的棋子会对下一列造成影响
//如果这个位置以及下一行都空的,那么就放一个2*1的棋子并直接跳到下下行
if(j+1<n&&((1<<j)&state)==0&&((1<<(j+1))&state)==0)//注意要特判第j行下面是否还有一行,以及要加上足够的括号以免因为位运算的优先级问题而导致出bug
dfs(i,j+2,state,nex);//不会对下一列造成影响
return;
}
int main()
{
while(~scanf("%lld %lld",&n,&m))
{
if(n==0&&m==0)break;
memset(dp,0,sizeof dp);
dp[1][0]=1;//注意初始化
for(int i=1;i<=m;++i)//一共m列//枚举第i列 -> 影响第i+1列
{
for(int j=0;j<(1<<n);++j)//到2^n,遍历一遍,二进制会把所有填充的情况都列举一遍
{
if(dp[i][j])//如果存在方案数 -> 则可以推广到i+1列
dfs(i,0,j,0);
}
}
printf("%lld\n",dp[m+1][0]);
}
return 0;
}
要注意多加几个括号,能加就加,以免因为位运算的优先级问题而导致出 bug。
2、入门例题【例二】玉米地
P1879 [USACO06NOV]Corn Fields G
题目链接
输入
2 3
1 1 1
0 1 0
输出
9
这里是详解链接………………………………………………………………
参考博客
动态规划之状态压缩dp入门
注:如果您通过本文,有(qi)用(guai)的知识增加了,请您点个赞再离开,如果不嫌弃的话,点个关注再走吧 ! 当然,也欢迎在讨论区指出此文的不足处,作者会及时对文章加以修正 !