状压DP
通常把一类以一个集合内的元素信息作为状态、
且状态总数为指数级别的动态规划成为状态压缩DP。
通常具有以下两个特点:
- 数据规模的某一维或几维非常小(<25)。
- 具备DP的基本性质:最优子结构+无后效性。
具体解题模式:
- 【找状态】确定每行的M位二进制数中0、1的表示。
- 【存已知】存入时把初始每行的二进制状态变为一个十进制的数,便于数位操作。
- 【预处理】结合输入求出每行的所有满足可行性的M位二进制数。
- 【判边界】一般行列间的关系在起始行并不适用,要特殊处理第一行的状态。
- 【列方程】逐层枚举每行列的状态,判断行列关系,列状态转移方程。
目录
一. 位运算
1 位逻辑运算符
& (按位 “与”) and ; ^ (按位 “异或”);
| (按位 “或”) or ; ~ (按位 “取反”) 。
2 移位运算符
<<(左移) ; >>(右移) 。
3 优先级 【具体可见 这里】
位“与”、位“或”和位“异或”都是双目运算符,其结合性都是从左向右的。
优先级 高于逻辑运算符,低于比较运算符,且从高到低依次为&、^、| 。
二. 状态压缩典型例题
【例题1】洛谷 p1896 互不侵犯
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
/*【洛谷p1896】互不侵犯
国王能攻击到它上下左右、以及左上左下右上右下八个方向上一个格子。
在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。*/
//方案数DP+状压DP
//f[i][j][s]表示考虑到了第i行,已经放置了j个国王,上一行的放置情况为s。
//枚举这一行的放置情况t。要满足:t是合法的,并且可以与s作相邻行。
//f[i][j+count(t)][t]+=f[i-1][j][s];
//count(t)表示t的二进制表示下1的数量,就是这一行的国王数。
int n,m,cnt,MAX;
ll f[20][400][1000];
int can[1000],num[2000]; //行能到达的状态,每个状态的国王数
int sum1(int x){ //求出每行状态cnt对应的国王数
int ret=0; while(x) ret+=(x&1),x>>=1;
return num[cnt]=ret; //并保存为数组,随时取用
}
int main(){
int l,x,y; ll ans=0;
scanf("%d%d",&n,&m);
MAX=(1<<n)-1; //二进制状态总数
for(int i=0;i<=MAX;i++) //预处理,cnt为状态编号
if(!(i&(i<<1))) can[++cnt]=i,f[1][sum1(i)][cnt]=1;
for(int i=2;i<=n;i++)
for(int j=1;j<=cnt;j++){
x=can[j]; //枚举可能的方案
for(int k=1;k<=cnt;k++){
y=can[k]; //枚举下一行可能的方案
if((x&y)||(x&(y<<1))||(x&(y>>1))) continue; //不能相邻
for(int l=0;l<=m;l++)
f[i][num[j]+l][j]+=f[i-1][l][k];
//转移:f[i][count(t)+上行总国王数][状态]+=
// f[i-1][上行总国王数][上行状态];
}
}
for(int i=1;i<=cnt;i++) ans+=f[n][m][i]; //总方案数=最后一层的答案相加
printf("%lld",ans);
}
【例题2】洛谷 p2704 炮兵阵地(详情可见 这里)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
//用每个m位二进制数表示一行的状态:
//每个二进制数的第k位为1表示在第k列上放置部队。
/*【p2704】炮兵阵地
一个N*M的地图,每一格可能是山地(用“H” 表示),也可能是平原(用“P”表示)。
在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队)。
每个部队能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。
两支炮兵部队之间不能互相攻击,即任何炮兵部队都不在其他部队的攻击范围内。
在整个地图区域内最多能够摆放多少我军的炮兵部队。*/
//预处理出集合S,储存“相邻两个1的距离不小于3”的所有M位二进制数。
int sums[(1<<10)];
//sums[x]表示x的二进制表示中1的个数。(int)
//bool vaild[109][1<<11];
//vaild[i][x]表示满足预处理条件的情况下,x的二进制表示中,
//所有1的位置对应在地图第i行中,都是能使用的平原。(bool)
//↑↑↑【有障碍时】存储行数,判断该行某状态是否成立。
//也可以不使用,用a[109]记录每行的初始可行状态(是二进制数转化为十进制的数)。
//判断的时候,只用在每次循环时特判相对应的S&a[i]。
//int ff[109][1<<11][1<<11]; //2048*2048*109??? ---> 所以要用滚动数组啊qwq
//f[i][j][k]表示第i-1行的状态是j,第i行状态是k时,前i行最多放置的炮兵数。
int f[4][(1<<11)][(1<<11)]={0}; //【滚动数组】对于每一行,只用记录前两行的状态。
int n,m,a[109],anss=0; //a[109]记录每行的初始可行状态(是二进制数转化为十进制的数)
void reads(int &x){
int fx=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=fx;
}
int getsum(int S){ //当前状态 S 里面包含几个 1
int tot=0;
while(S) {if(S&1) tot++; S>>=1;}
return tot;
} //【坑点!!】这个一定要写成函数,要不然会TLE QAQ
int main(){
reads(n); reads(m); char ss;
for(int i=0;i<n;i++) //注意,状压DP中最好都用0开始
for(int j=0;j<m;j++){ //用a[i]记录每行的初始可行状态
cin>>ss; a[i]<<=1; //二进制数a[i]每次向前移一位
a[i]+=(ss=='H'?1:0); //记录障碍的位置
}
memset(sums,0,sizeof(sums));
for(int i=0;i<(1<<m);i++) //对于每个状态,记录每行放置的部队数
sums[i]=getsum(i); //初始化sum数组
for(int i=0;i<(1<<m);i++) //【预处理】初始化第一行(行数编号是0)
if(!(i&a[0] || (i&(i<<1)) || (i&(i<<2)))) //这些条件都不能成立
f[0][i][0]=sums[i]; //第一行要特殊判定(因为没有1-2=-1行)
for(int j=0;j<(1<<m);j++) //【预处理】初始化第二行(行数编号是1)
for(int k=0;k<(1<<m);k++) //j是上一行,k是这一行
if(!(j&k || j&a[0] || k&a[1] || (j&(j<<1)) || (j&(j<<2))
|| (k&(k<<1)) || (k&(k<<2)) ) )
f[1][j][k]=sums[k]+sums[j]; //第二行要特殊判定(因为没有2-2=0行)
for(int i=2;i<n;i++) //枚举行数
for(int j=0;j<(1<<m);j++){ //【预处理】“相邻两个1的距离不小于3”的所有M位二进制数
if(j&a[i-1] || (j&(j<<1)) || (j&(j<<2))) continue; //特判
for(int k=0;k<(1<<m);k++){ //j是上一行,k是这一行
if(k&a[i] || j&k || (k&(k<<1)) || (k&(k<<2))) continue; //特判
for(int FL=0;FL<(1<<m);FL++){ //FL是上上一行
if(FL&j || FL&k || FL&a[i-2] || (FL&(FL<<1)) || (FL&(FL<<2))) continue;
f[i%3][j][k]=max(f[i%3][j][k],f[(i-1)%3][FL][j]+sums[k]);
}
}
}
for(int j=0;j<(1<<m);j++)
for(int k=0;k<(1<<m);k++)
anss=max(anss,f[(n-1)%3][j][k]); //结束状态可以是最后一行(编号n-1)的任何状态
cout<<anss<<endl;
return 0;
}
【例题3】洛谷 p1879 玉米田
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;
/*【洛谷p1879】玉米田
一块长方形的牧场被划分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12)。
从中选择一些作为草地,但不会选择两块相邻的土地。
如果不考虑草地的总块数,一共有多少种选择方案?(0也是一种方案)。*/
//【状压DP】
//f[i][j]表示考虑到了第i行,这一行的放置情况为j的方案数。
//枚举上一行的放置情况k。要满足:k是合法的,并且可以与j作相邻行。
//转移方程:f[i][j]+=(sum)f[i-1][k];(方案数DP)
struct node{
int st[5019],num;
}a[18]; //a[i].st[k]即第i行的状态k
int n,m,f[18][1000]={0};
void get_state(int i,int t){ //第i行的初始状态t
int nums=0; //↓↓↓预处理出第i行的所有可行状态并储存
for(int j=0;j<(1<<m);j++)
if((j&(j<<1))||(j&(j>>1))||(j&t)) continue;
else a[i].st[++nums]=j; //新的可行状态
a[i].num=nums; //记录此行的可行状态数量
}
int main(){
ll ans=0; scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
int t=0,x; //用数t保存一行的状态
for(int j=1;j<=m;j++) cin>>x, t=(t<<1)+1-x;
//原题中1表示可以种草,0表示不适合种草,这里将它反过来(便于&)
get_state(i,t); //第i行中可以用的合法状态
}
for(int i=1;i<=a[1].num;i++) f[1][i]=1; //第一行特判
for(int i=2;i<=n;i++)
for(int j=1;j<=a[i].num;j++){
for(int k=1;k<=a[i-1].num;k++){
if(a[i].st[j]&a[i-1].st[k]) continue;
f[i][j]+=f[i-1][k];
}
}
for(int j=1;j<=a[n].num;j++)
ans=((ll)ans+f[n][j])%100000000;
cout<<ans<<endl; return 0;
}
【例题4】洛谷 p3092 没有找零
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;
/*【洛谷p3092】没有找零
约翰的钱包里有K个硬币,面值的范围是1..100,000,000。
约翰想按顺序买N个物品,第i个物品需要花费c(i)块钱,
在依次进行的购买N个物品的过程中,约翰可以随时停下来付款,
每次付款只用一个硬币,能支付购买的是从上一次支付后开始到现在的这些所有物品。
如果约翰支付的硬币面值大于所需的费用,他不会得到任何找零。
请计算出在购买完N个物品后,约翰最多剩下多少钱。如果无法完成购买,输出-1。 */
void reads(int &x){
int f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
int k,n,a[100001],sum[100001];/*物品数组和前缀和*/
int c[17],pre[17],f[(1<<16)+10];/*在某个状态下购买的物品数*/
int ans=-1,tot;
int main(){
int k,n; reads(k); reads(n);
for(int i=1;i<=k;i++){
reads(c[i]); tot+=c[i];
}
for(int i=1;i<=n;i++){
reads(a[i]); sum[i]=sum[i-1]+a[i];
}
pre[1]=1; for(int i=2;i<=k;i++) pre[i]=pre[i-1]<<1;
//↑↑↑ 初始化:单独选择每个物品对应的状态
for(int i=0;i<=(1<<k)-1;i++) //循环每个状态,更新f[i]
for(int j=1;j<=k;j++) //转移:选择j号硬币
if(pre[j]&i){ //状态中包含这个硬币
int cnt=f[i^pre[j]]; //该状态不用第j个硬币时,可以购买的物品数
cnt=upper_bound(sum+1,sum+n+1,sum[cnt]+c[j])-sum; //判断第j个硬币能带来的状态转移
//sum[cnt]是原先买物品达到i^b[j]状态花的总钱数
//用upper_bound寻找第一个大于sum[cnt]+c[j]的物品的编号
f[i]=max(f[i],cnt-1); //cnt-1是已买的物品数量,用来更新状态f[i]
}
for(int i=0;i<=(1<<k)-1;i++) //循环每个状态,求最多剩余的钱
if(f[i]==n){ //枚举买全部的n个物品的所有状态
int cnt=0; //花费的硬币总价值
for(int j=1;j<=k;j++) //枚举状态中使用的硬币
if(i&pre[j]) cnt+=c[j]; //计算花费的硬币总价值
ans=max(ans,tot-cnt); //用ans记录最多剩余的钱数
}
cout<<ans<<endl; return 0; //ans初始化为-1,直接输出
}
【例题5】旅行商(TSP / NPC问题)
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;
/*【洛谷p1523】旅行商(NPC难题简化版)
现在笛卡尔平面上有n(n<=1000)个点,每个点的坐标为(x,y)(为整数)。
任意两点之间相互到达的代价为这两点的欧几里德距离,
现要你编程求出最短bitonic tour(双调旅程)。
即从最左点开始,严格地从左到右直至最右点,然后严格地从右到左直至出发点。 */
void reads(int &x){
int f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
int dist[50][50],n;
int dp[1<<12][20];
int main(){
while(scanf("%d",&n)!=EOF&&n!=0){
n++;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
scanf("%d",&dist[i][j]);
for(int k=0;k<n;k++)
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
if(dist[i][j]>dist[i][k]+dist[k][j])
dist[i][j]=dist[i][k]+dist[k][j];
for(int i=0;i<(1<<12);i++)
for(int j=0;j<20;j++) dp[i][j]=INF;
dp[0][0]=0;
for(int i=1;i<(1<<n);i++)
for(int j=0;j<n;j++)
if(((1<<j)&i))
for(int k=0;k<n;k++)
if(!(i-(1<<j)) || ((1<<k)&(i-(1<<j))) )
dp[i][j]=min(dp[i][j],dp[i-(1<<j)][k]+dist[k][j]);
cout<<dp[(1<<n)-1][0]<<endl;
}
return 0;
}
//以上是点少的时候的状压解法,洛谷的题目是不能过的
//旅行商问题,不过要求了只能单向走,所以可以简化。
//双向的问题转化为:假设有两个人一起从西往东走,走过的点不能重复。
//f[i][j]表示第一个人走到i,第二个人走到j的最短路径。
//能够发现,第j+1个点不是被第一个人走,就是被第二个人走。
//方程1:f[i][j+1]=min{f[i][j]+d[j][j+1]}
//方程2:f[j][j+1]=min{f[i][j]+d[i][j+1]}
struct node{
double x,y;
}a[1019];
double d[1019][1019],f[1019][1019];
bool cmp(node a,node b){ return a.x<b.x; }
double dist(node a,node b){
return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
int main(){
int n; reads(n);
for(int i=0;i<n;++i)
scanf("%lf%lf",&a[i].x,&a[i].y);
sort(a,a+n,cmp);
for(int i=0;i<n;++i)
for(int j=i+1;j<n;++j){ //预处理+初始化
d[i][j]=dist(a[i],a[j]); f[i][j]=1e30;
}
f[0][1]=d[0][1]; //dp起点
for(int i=0;i<n;++i)
for(int j=i+1;j<n;++j){ //j+1点不是被第一个人走,就是被第二个人走
f[i][j+1]=min(f[i][j+1],f[i][j]+d[j][j+1]); //被j走
f[j][j+1]=min(f[j][j+1],f[i][j]+d[i][j+1]); //被i走
}
double ans=1e30;
for(int j=0;j<n-1;++j) //当一个人走到终点时,枚举另一个人的最后一步
ans=min(ans,f[j][n-1]+d[j][n-1]);
printf("%.2lf\n",ans);
return 0;
}
【例题6】洛谷 p3052 摩天大楼的奶牛
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;
/*【洛谷p3052】摩天大楼的奶牛
给出n个物品,体积分别为w[i],现把其分成若干组,
要求每组总体积<=W,问最小分组。(n<=18)。 */
/*【分析】状压DP
f[i][j]表示已经分了i组,n个物品的选择状态是j时,当前组中的min体积。
如果存在d[i][j]这种方案,我们枚举不属于状态j的物品k。
那么就可以转移到d[i][j&(1<<k)]或者d[i+1][j&(1<<k)]
从前往后搜索,得到第一个存在j=2^n-1的方案即可。 */
void reads(int &x){
int fx=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=fx;
}
int f[20][(1<<20)];
int n,m,w[20];
int main(){
reads(n); reads(m);
for(int i=0;i<n;i++) reads(w[i]);
for(int i=0;i<=n;i++) //分组数
for(int j=0;j<=(1<<n)-1;j++) //每个状态
f[i][j]=0x3f3f3f3f; //初始化为+∞
for(int i=0;i<=n;i++) f[1][1<<i]=w[i]; //边界:第1组放任意一个物品
for(int i=0;i<=n;i++) //组数从0~n
for(int j=0;j<=(1<<n)-1;j++)
if(f[i][j]!=0x3f3f3f3f) //如果该状态存在【存在性DP】
for(int k=0;k<n;k++){ //枚举不属于状态j的物品k
if((j&(1<<k))!=0) continue; //k属于j,舍去 ↓↓↓判断是否更新f[i][j|(1<<k)]
if(f[i][j]+w[k]<=m) f[i][j|(1<<k)]=min(f[i][j|(1<<k)],f[i][j]+w[k]);
else f[i+1][j|1<<k]=w[k]; //多存一组
//cout<<"i="<<i<<" j="<<j<<" f[i][j]="<<f[i][j]<<endl;
}
for(int i=0;i<=n;i++) //组数从小到大
if(f[i][(1<<n)-1]!=0x3f3f3f3f){ //只要有满足全选的状态的方案
printf("%d\n",i); break; //输出组数并退出
}
return 0;
}
【例题7】洛谷 p3622 动物园
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;
/*【洛谷p3622】动物园
每个小朋友站在大围栏圈的外面,可以看到连续的 5 个围栏。
你得到了所有小朋友喜欢和害怕的动物信息。
当下面两处情况之一发生时,小朋友就会高兴:
1.至少有一个他害怕的动物被移走
2.至少有一个他喜欢的动物没被移走
输出一个数,表示最多可以让多少个小朋友高兴。 */
/*【分析】【状压DP】【环形问题】
考虑到不同的小朋友看见的围栏范围可能相同,要预处理num[pos][s]:
表示从第pos个开始的五个围栏移走状态为s(全满则为15)时,满意的人数。
f[i][s]表示枚举到第i个围栏且[i,i+5]的围栏移走状态为s时的最多满意人数。
则f[i][s]可以由第i-1个围栏移走和不移走两种状态转移得来:
f[i][s]=max(f[i−1][(s&15)<<1],f[i−1][(s&15)<<1∣1])+num[i][s];
注意:在dp之前先枚举前五个的状态state。避免环形问题。
那么此时必须满足s=state才是有效状态,更新答案。 */
void reads(int &x){
int fx=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=fx;
}
const int maxn=50010;
int n,m,ans,f[maxn][40],num[maxn][40];
void input(){
int E,a,b,l,d,t;
reads(n); reads(m);
for(int i=1;i<=m;i++){
reads(E);reads(a);reads(b);
l=d=0; //分别记录不喜欢的和喜欢的对应的状态
for(int j=1;j<=a;j++) //不喜欢的,相应位置上为1
{ reads(t); t=(t-E+n)%n; l|=1<<t; }
for(int j=1;j<=b;j++) //喜欢的,相应位置上为1
{ reads(t); t=(t-E+n)%n; d|=1<<t; }
for(int j=0;j<32;j++)
if((j&l)||(~j&d)) //j为要选择移走的状态
num[E][j]++; //从E开始的5个围栏移走状态为s时,满意的人数
}
}
int main(){
input();
for(int i=0;i<32;i++){
memset(f[0],128,sizeof(f[0])); //极小值
f[0][i]=0; //起点
for(int j=1;j<=n;j++) //枚举围栏数
for(int s=0;s<32;s++) //移走状态,从上一位置移走和不移走两种状态转移而来
f[j][s]=max(f[j-1][(s&15)<<1],f[j-1][(s&15)<<1|1])+num[j][s];
if(ans<f[n][i]) ans=f[n][i]; //更新ans
}
printf("%d\n",ans);
return 0;
}
——时间划过风的轨迹,那个少年,还在等你。