简介:
状压 \(dp\) 很明显是将状态压缩后进行 \(dp\),这种算法多用于状态只有两种的情况,且一般给定范围较小,如 \(n \leq 16\) 等,遇到这种情况就可以考虑去状压 \(dp\)。
前置知识:
我们知道一个数可以表示成二进制,如 \((25)_{10}=(1101)_2\) 那我们就可以将一个区间内所有的状态表示成二进制,然后在转换成十进制,这样就将一个区间内的状态压缩成一个数字。再由这个状态进行转移。
实现
这里给一道例题来看看状压 \(dp\) 的实现过程:
P1896 [SCOI2005] 互不侵犯
阅读完题目可以发现其,棋盘大小较小,那很明显是状压 \(dp\),那我们就将一行的状态给压缩起来,即在二进制中对应位就是在该列对应的位置,若这个位置上有棋那就为 \(1\) 反之为 \(0\) 那这样,就储存下来了。
那我们再来看看这些状态有什么限制:
- 首先根据题意可以知道,同一行的棋子不能相邻,即其左右都不能有棋子,对应到二进制就是一个 \(1\) 左移一位和右移一位都不为 \(1\),那么我们可以用二进制的运算法则来看就相当于
(x<<1)&x==0 && (x>>1)&x==0
,因为与运算要求是都为 \(1\) 才返回 \(1\) 那如果其左右两位返回的不是零,那很明显其左右两位有 \(1\) 就不符合要求。上面的关系还能写成!(((x<<1)|(x>>1))&x)
- 其次还要判断其是否在上面棋子的禁止范围内,那我们令 \(s1,s2\)分别代表当前行的状态和上一行的状态,那同理
(s1<<1)&s2==0 && (s1>>1)&s2==0 && s1&s2==0
。即左移一位,右移一位和不移都与上一行没有重复的 \(1\),同样可以写成!(((s1<<1)|(s1>>1)|s1)&s2)
,这样就可以在枚举时判断是否满足这两条性质,再来进行转移。
状态转移:
转移方程: 我们假定 \(dp[x][y][z]\) 表示第 \(x\) 行,用了 \(y\) 个棋子,当前行的状态为 \(z\) 的方案数。那很显然要先枚举行数 \(i\),在枚举当前行的状态 \(s1\) 与上一行的状态 \(s2\),最后在枚举用的棋子数 \(len\),那如果满足限制的前提下转移方程就为
其中 \(cnt[s1]\)表示当前状态的所用棋子数。
初始化: 而对于第 \(0\) 行,用了 \(0\) 个棋子,状态为 \(0\) 的方案数为 \(1\),即:
统计答案: 只需要统计最后一行,用完棋子的所有状态的方案数之和即可。
优化:
对于性质 \(1\) 和 \(cnt[s1]\) 可以提前预处理出来。
暴力枚举所有的状态,对于每个状态都统计一下 \(1\) 的个数,即 \(cnt[s1]\)。然后看他是否满足性质 \(1\),若满足,就加入 \(ok[++num]\) 中,方便后面调用:
完整代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2010;
int ok[N],cnt[N];
int dp[10][100][N];
signed main(){int n,m;scanf("%lld%lld",&n,&m);int num=0;for(int i=0;i<(1<<n);i++){int tot=0;int s=i;while(s){if(s&1)tot++;s>>=1;}cnt[i]=tot;if(!(((i<<1)|(i>>1))&i)){ok[++num]=i;}}dp[0][0][0]=1;for(int i=1;i<=n;i++){for(int j=1;j<=num;j++){for(int k=1;k<=num;k++){int s1=ok[j],s2=ok[k];if(!((s2|(s2<<1)|(s2>>1))&s1)){for(int len=0;len<=m;len++){if(len-cnt[s1]>=0){dp[i][len][s1]+=dp[i-1][len-cnt[s1]][s2];}}}}}}int ans=0;for(int i=1;i<=num;i++){ans+=dp[n][m][ok[i]];}printf("%lld",ans);
}