状态压缩前言
-
对于 n < = 20 n<=20 n<=20 的数据范围,不妨考虑一下记忆化搜索和状态压缩
-
主要思想: 二进制位运算
-
举个例子: 一条街上有n个路灯,路灯只有亮与不亮两种状态。如果去描述这n个路灯的状态呢???用数组描述显然可以,但有没有其他的方法???
-
二进制下的一个整数 k !!!
-
例如:n 等于 8,k = 1011011 1 ( 2 ) 10110111_{(2)} 10110111(2)
-
其他功能怎么办??? 比如判第i个灯是否亮,关闭第i个灯,如何关闭多个灯之类的
-
位运算来了!!!
int main(){
if(k&(1<<i));//判断第i位是否为1
k=k|(1<<i);//将第i位为变成1
k=k&(!(1<<i));//将第i位为变成0
if(i&j);//状态i与状态j有重复
if((i<<1)&j);//状态i与状态j存在右上左下同时1
if(i&(j<<1));//状态i与状态j存在左上右下同时1
if(j&(j<<1));//状态j自身存在连续1
if(j&(~a[i]));//状态j在不该存在1的地方存在了1
}
状态压缩实现记忆化搜索
套路
- 对于选与不选问题,我们有暴力 O ( 2 n ) O(2^n) O(2n) 的算法。
- 对于第 i 次选哪个问题,我们有暴力 O ( n n ) O(n^n) O(nn) 或者 O ( n ! ) O(n!) O(n!) 的算法。
- 记忆化搜索: 要是我们能记录下已经选择了若干个的状态的最优值,在我们遇到重复状态时,我们就可以直接返回记忆化的值了。数组的下标只能是整数,我们可以用二进制去存储选取状态,这样选择若干个的状态就可以用一个整数代替了。
例题1:单词接龙
- 题目描述: 有 n 个字符串,对其做单次接龙(当前单词尾字母与下一个单词的首字母相同),求连接成的长度最长为多少。第一个单词可以任意选,要求每个单词只能用一次。n<=18
- 状态表示: f [ s t a t e ] [ l a s t ] f[state][last] f[state][last]:选取状态为state,且最后一个单词为第 last 个的后续 dfs 可以达到的最长长度(不包括第 last 个及以前的)。 然后记忆化搜索即可。
#include<bits/stdc++.h>
using namespace std;
string s[100];
int n,f[1000010][20];
int dfs(int state,int last){
if(f[state][last])return f[state][last];
int ans=0;
for(int i=0;i<n;i++){
if((1<<i)&state)continue;
if(s[i][0]==s[last][s[last].size()-1])ans=max(ans,dfs((1<<i)|state,i));
}
return f[state][last]=ans+s[last].size();
}
int main() {
cin>>n;
for(int i=0;i<n;i++)cin>>s[i];
int ans=0;
for(int i=0;i<n;i++)ans=max(ans,dfs(1<<i,i));
cout<<ans;
return 0;
}
集合问题
套路
例题1:最小分组
- 题目描述: n个物品,体积分别为 w[i],现把其分成若干组,要求每组总体积<=W,问最小分组。n<=18
- 问题分析: n 这么小,考虑状态压缩。为了使与二进制挂钩,自然将 n 个物品变为选与不选的情况。不妨尝试以下状态表示。
- 状态表示: f [ j ] f[j] f[j]:选取情况为状态 j 时,需要多少个分组。 s i z e [ j ] size[j] size[j]:选取情况为状态 j 时,
- 转移方程: f [ i ] [ j ] = f[i][j]= f[i][j]=
棋盘放置问题(记方案数)
套路
- 状态表示: 通常设 f [ i ] [ j ] [ ] f[i][j][] f[i][j][] 为摆放完了前 i 行,第 i 行状态为 j ,前 i 行一共满足了多少要求下的方案数。
- 预处理: 该类型的 i 通常从 2 开始, f [ 1 ] f[1] f[1] 通常需要通过预处理来得出
- 限制: 棋盘放置问题通常有棋子不可相邻放置等限制,这时候我们需要通过一些位运算,来将不合法的状态转移给去掉。
例题1:棋盘放置国王
- 题目描述: 在 N × N N×N N×N 的棋盘里面放 K K K 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。 n < = 9 n<=9 n<=9
- 状态表示: f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]:从第 1 行到第 i 行,第 i 行放置情况为 j 状态下,一共放了 k 个国王的方案总数。
- 转移方程: f [ i ] [ j ] [ k ] + = f [ i − 1 ] [ p ] [ k − k i n g [ j ] ] f[i][j][k]+=f[i-1][p][k-king[j]] f[i][j][k]+=f[i−1][p][k−king[j]]
#include<bits/stdc++.h>
using namespace std;
long long f[12][1000][100],king[1000],n,k;
int main() {
//预处理下每个状态相当于放置了多少个国王
for(int j=0; j<(1<<n); j++) {
int t=j;
while(t) {
king[j]+=t%2;
t=t/2;
}
}
//预处理初始值
for(int j=0;j<(1<<n);j++){
if(j&(j<<1))continue;
f[1][j][king[j]]=1;
}
//状态转移
for(int i=2; i<=n; i++) {
for(int j=0; j<(1<<n); j++) {
if(j&(j<<1))continue;
for(int p=0; p<(1<<n); p++) {
if(j&p)continue;
if(j&(p<<1))continue;
if(j&(p>>1))continue;
for(int k=K; k>=king[j]; k--) {
f[i][j][k]+=f[i-1][p][k-king[j]];
}
}
}
}
long long ans=0;
for(int j=0;j<(1<<n);j++)ans+=f[n][j][K];
cout<<ans;
}
例题2:牧场种草
- 题目描述: 要求在 n × m n\times m n×m 的牧场种草,草不能相邻放置。同时有些位置不能种草。请问一共有多少种选择草的方式 。n,m<=15
- 状态表示: f [ i ] [ j ] f[i][j] f[i][j]:从第 1 行到第 i 行,第 i 行放置情况为 j 状态下,一共有多少种方案。
- 转移方程: f [ i ] [ j ] + = f [ i − 1 ] [ p ] f[i][j]+=f[i-1][p] f[i][j]+=f[i−1][p]
#include<bits/stdc++.h>
using namespace std;
long long f[15][5000],a[15],mod=100000000,n,m,x;
int main() {
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>x;
a[i]=a[i]*2+x;
}
}
//预处理初始值
for(int j=0;j<(1<<m);j++){
if(j&(j<<1))continue;
if(j&(~a[1]))continue;
f[1][j]=1;
}
//状态转移
for(int i=2;i<=n;i++){
for(int j=0;j<(1<<m);j++){
if(j&(j<<1))continue;
if(j&(~a[i]))continue;
for(int p=0;p<(1<<m);p++){
if(p&(p<<1))continue;
if(j&p)continue;
if(p&(~a[i-1]))continue;
f[i][j]=(f[i][j]+f[i-1][p])%mod;
}
}
}
long long ans=0;
for(int j=0;j<(1<<m);j++)ans=(ans+f[n][j])%mod;
cout<<ans;
}
例题3:地图放炮
- 题目描述: n × m n\times m n×m 的地图上放置炮,炮的攻击范围为上下左右距离 1 和 2 的点,有若干点不能放炮。要求在炮与炮不会互相攻击的情况下,最多可以放置多少个炮。 n < = 100 , m < = 10 n<=100,m<=10 n<=100,m<=10
- 问题分析: 很显然,每层的放置不仅与上层有关还与上上层有关,因此我们还需多维护一维,即 令 j 位第 i 层的放置情况,令 p 位第 i-1 层的放置状态,令 q 位第 i-2 层的放置状态。
- 状态表示: f [ i ] [ j ] [ p ] f[i][j][p] f[i][j][p]:从第 1 行到第 i 行,第 i 行放置情况为状态 j ,第 i-1 行放置情况为状态 p 下,一共有多少种方案。
- 转移方程: f [ i ] [ j ] [ p ] = m a x ( f [ i ] [ j ] [ p ] , f [ i − 1 ] [ p ] [ q ] + k i n g [ j ] ) f[i][j][p]=max(f[i][j][p],f[i-1][p][q]+king[j]) f[i][j][p]=max(f[i][j][p],f[i−1][p][q]+king[j])
- 初始值: 要预处理两层了,即第 1 层状态为 j,第二层状态为 p 下的最多放炮数量
- MLE:超内存了,滚动数组优化一下
#include<bits/stdc++.h>
using namespace std;
long long a[105],f[3][1025][1025],king[1025];
int main() {
int n,m;
char c;
cin>>n>>m;
for(int i=1; i<=n; i++) {
for(int j=1; j<=m; j++) {
cin>>c;
if(c=='P')a[i]=a[i]*2+1;
else a[i]=a[i]*2;
}
}
//预处理每个状态下的放置的炮的数量
for(int j=0; j<(1<<m); j++) {
int t=j;
while(t) {
king[j]+=t%2;
t=t/2;
}
}
//预处理初始值
for(int j=0; j<(1<<m); j++) {
if(j&(j<<1))continue;
if(j&(j<<2))continue;
if(j&(~a[2]))continue;
for(int p=0; p<(1<<m); p++) {
if(p&(p<<1))continue;
if(p&(p<<2))continue;
if(j&p)continue;
if(p&(~a[1]))continue;
f[2][j][p]=king[j]+king[p];
}
}
//状态转移
for(int i=3; i<=n; i++) {
for(int j=0; j<(1<<m); j++) {
if(j&(j<<1))continue;
if(j&(j<<2))continue;
if(j&(~a[i]))continue;
for(int p=0; p<(1<<m); p++) {
if(p&(p<<1))continue;
if(p&(p<<2))continue;
if(j&p)continue;
if(p&(~a[i-1]))continue;
for(int q=0; q<(1<<m); q++) {
if(q&(q<<1))continue;
if(q&(q<<2))continue;
if(j&q)continue;
if(p&q)continue;
if(q&(~a[i-2]))continue;
f[i%3][j][p]=max(f[i%3][j][p],f[(i-1)%3][p][q]+king[j]);
}
}
}
}
long long ans=0;
for(int j=0;j<(1<<m);j++){
for(int p=0;p<(1<<m);p++){
ans=max(f[n%3][j][p],ans);
}
}
cout<<ans;
}
例题4:地图放炮2
- 题目描述: 在 n × m n\times m n×m 的矩阵中放置若干个炮,使得炮与炮之间不会互相攻击到。能攻击到当且仅当两个炮之间有一个棋子。求合法放置的方案数。n,m<=100
- 问题分析:
例题5:地图摆马
- 题目描述: 在 X × Y X\times Y X×Y 的方阵中,放入任意个中国象棋中的马。求马之间不会互相攻击到的情况的方案数。 X ∈ [ 1 , 100 ] , Y ∈ [ 1 , 6 ] X\in[1,100],Y\in[1,6] X∈[1,100],Y∈[1,6]
- 问题分析: 主要是马脚很麻烦,对于相邻的两行 j , k j,k j,k ,考虑暴力去检查是否会互相攻击到。对于相邻的三行 j , k , p j,k,p j,k,p 也一样。
#include<bits/stdc++.h>
using namespace std;
long long f[3][65][65],mod=1e9+7,X,Y;
int check1(int x,int y){//吃到上一层
for(int i=2;i<Y;i++){
if(x&(1<<i)){
if(x&(1<<(i-1)))continue;
else if(y&(1<<(i-2)))return 1;
}
}
for(int i=0;i<Y-2;i++){
if(x&(1<<i)){
if(x&(1<<(i+1)))continue;
else if(y&(1<<(i+2)))return 1;
}
}
for(int i=2;i<Y;i++){
if(y&(1<<i)){
if(y&(1<<(i-1)))continue;
else if(x&(1<<(i-2)))return 1;
}
}
for(int i=0;i<Y-2;i++){
if(y&(1<<i)){
if(y&(1<<(i+1)))continue;
else if(x&(1<<(i+2)))return 1;
}
}
return 0;
}
int check2(int x,int y,int z){//吃到上上层
for(int i=1;i<Y;i++){
if(x&(1<<i)){
if(y&(1<<i))continue;
else if(z&(1<<(i-1)))return 1;
}
}
for(int i=0;i<Y-1;i++){
if(x&(1<<i)){
if(y&(1<<i))continue;
else if(z&(1<<(i+1)))return 1;
}
}
for(int i=1;i<Y;i++){
if(z&(1<<i)){
if(y&(1<<i))continue;
else if(x&(1<<(i-1)))return 1;
}
}
for(int i=0;i<Y-1;i++){
if(z&(1<<i)){
if(y&(1<<i))continue;
else if(x&(1<<(i+1)))return 1;
}
}
return 0;
}
int main() {
cin>>X>>Y;
if(X==1) {
cout<<(1<<Y);
return 0;
}
for(int j=0; j<(1<<Y); j++) {
for(int k=0; k<(1<<Y); k++) {
if(check1(j,k))continue;
f[2][j][k]=1;
}
}
for(int i=3; i<=X; i++) {//100*64*64*64
for(int j=0; j<(1<<Y); j++) {
for(int k=0; k<(1<<Y); k++) {
if(check1(j,k))continue;//吃到上一层
for(int p=0; p<(1<<Y); p++) {
if(check2(j,k,p))continue;//吃到上上层
f[i%3][j][k]=(f[i%3][j][k]+f[(i-1)%3][k][p])%mod;
}
}
}
}
long long ans=0;
for(int j=0; j<(1<<Y); j++) {
for(int k=0; k<(1<<Y); k++) {
ans=(ans+f[X%3][j][k])%mod;
}
}
cout<<ans;
}