状压dp
当题目中的某一项的值极小(小于等于\(20\))时考虑使用状压\(dp\)
状压\(dp\)是二维时一般是
\(dp[i][s]\)表示进行到第\(i\)步且第\(i\)步的状态为\(s\)时的\(dp\)值
状压\(dp\)由于是二进制,多与位运算的操作有关
以下是一些常见的位运算操作:
\(1.s\&(1<<i)\) 判断第\(i\)位是否为\(1\)
\(2.s=s | (1<<i)\)把第i位设为\(1\)
\(3.s=s\&(~(1<<i))\)把第\(i\)位设为\(0\)
\(4.s=s\)^\((1<<i)\)把第\(i\)位的值取反
\(5.s=s\&(s-1)\)把\(s\)二进制下最靠右的一个\(1\)去掉
\(6.for(s0=s;s0;s0=(s0-1)\&s)\)枚举\(s\)的子集
\(7.x\&-x\)取出最低位的\(1\)
例题
互不侵犯 洛谷P1896
\(f[i][k][s]\)表示前\(i\)行放了\(k\)个国王,当前行状态为s的方案数
#include <iostream>
#include <cstdio>
using namespace std;
int n,King,tot=0;
int s[1050],pd[1050];
int num[15];
long long ans=0;
long long f[10][105][1050];
void Deal_first()
{
for(int i=0;i<(1<<n);i++)
{
int cnt=0,x=i;
pd[i]=1;
while(x!=0)
{
num[++cnt]=x%2;
x/=2;
}
for(int j=1;j<cnt;j++)
{
if(num[j]==num[j+1]&&num[j]==1)
{
pd[i]=0;break;
}
if(num[j]==1)s[i]++;
}
if(pd[i]==1)if(num[cnt]==1)s[i]++;
}
}
int main()
{
scanf("%d%d",&n,&King);tot=(1<<n)-1;
Deal_first();
for(int i=0;i<=tot;i++)if(pd[i]==1)f[1][s[i]][i]=1;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=tot;j++)
{
for(int k=0;k<=tot;k++)
{
if(!pd[j])continue;if(!pd[k])continue;
if(((j|(j<<1)|(j>>1))&k)!=0)continue;
for(int t=King;t>=s[j];t--)f[i][t][j]+=f[i-1][t-s[j]][k];
}
}
}
for(int i=0;i<=tot;i++)
{
if(pd[i])ans+=f[n][King][i];
}
printf("%lld",ans);
return 0;
}
愤怒的小鸟 洛谷P2831
显然,因为原点加两个点即可确定抛物线,故抛物线的数量最多为\(n^2\)条,又因为\(n<=18\),
故考虑用状压\(dp\),枚举抛物线,复杂度为\(O(T\times n^2\times2^n)\)
但考虑到每个猪都要被打,所以可以从小到大枚举\(S\),每次每次打掉编号最小的还没消灭的猪,
由于包含该猪的抛物线只有\(O(n)\)种,所以时间复杂度为\(O(n*2^n)\)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
#define lld long double
#define eps 1e-8
int dp[1<<19],line[20][20];
lld x[20],y[20];
int n,m;
int lowbit(int x)
{
return x&(-x);
}
int main()
{
int t;scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
lld z=eps;
memset(dp,0x3f3f3f3f,sizeof(dp));
memset(line,0,sizeof(line));
dp[0]=0;
for(int i=1;i<=n;i++)
{
//scanf("%lf%lf",&x[i],&y[i]);//double用这个
cin>>x[i]>>y[i];//long double用这个
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(fabs(x[i]-x[j])<eps)continue;
lld a=y[j]/(x[j]*(x[j]-x[i]))-y[i]/(x[i]*(x[j]-x[i]));
lld b=(y[i]-a*x[i]*x[i])/x[i];
if(a>-eps)continue;
for(int k=1;k<=n;k++)
if(fabs(a*x[k]*x[k]+b*x[k]-y[k])<eps)line[i][j]|=(1<<(k-1));
}
}
for(int i=0;i<(1<<n);i++)
{
int x=-1;
for(int j=1;j<=18;j++)
{
if((i&(1<<(j-1)))==0)
{
x=j;break;
}
}
if(x==-1)continue;
dp[i|(1<<(x-1))]=min(dp[i|(1<<(x-1))],dp[i]+1);
for(int j=1;j<=n;j++)dp[i|line[x][j]]=min(dp[i|line[x][j]],dp[i]+1);
}
printf("%d\n",dp[(1<<n)-1]);
}
return 0;
}
玉米田
题意:一块\(n\times m\)的牧场,有的方格可以种草,有的不能种,且任意种草的方格不能有公共边,求方案数
\(1<=n,m<=12\)
\(m\)极小,故可以用状压\(dp\)
设\(f[i][j]\)表示前i行符合条件且第\(i\)行的状态为\(j\)的方案数
预处理\(cnt[i]\)表示在不管其它行的情况下第\(i\)行符合条件的状态数,\(g[i][j]\)表示第i行符合条件的第j个状态
在转移时需满足\(g[i-1][j]\&g[i][k]==0\)
#include <iostream>
#include <cstdio>
using namespace std;
#define mol 100000000
int n,m;
int a[15][15];
int h[15],cnt[15];
int g[15][1<<15],f[15][1<<15];
void read()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
scanf("%d",&a[i][j]);
}
}
for(int i=1;i<=n;i++)
{
int sum=0;
for(int j=1;j<=m;j++)
{
sum=sum*2+a[i][j];
}
h[i]=sum;
}
}
void Deal_first()
{
for(int i=1;i<=n;i++)
{
for(int j=0;j<=(1<<m)-1;j++)
{
int pd=1;
if((j|h[i])==h[i])
{
if(((j&(j<<1))==0)&&((j&(j>>1))==0))pd=0;
if(!pd)g[i][++cnt[i]]=j;
}
}
}
for(int i=1;i<=cnt[1];i++)f[1][g[1][i]]=1;
}
int main()
{
read();
Deal_first();
for(int i=2;i<=n;i++)
{
for(int j=1;j<=cnt[i-1];j++)
{
for(int k=1;k<=cnt[i];k++)
{
if((g[i-1][j]&g[i][k])==0)
{
f[i][g[i][k]]=(f[i][g[i][k]]+f[i-1][g[i-1][j]])%mol;
}
}
}
}
int ans=0;
for(int i=1;i<=cnt[n];i++)ans=(ans+f[n][g[n][i]])%mol;
printf("%d\n",ans);
return 0;
}
炮兵阵地
题意:在\(n \times m\)的网格地图中部署炮兵部队,只能在平原地形上部署,每支炮兵部队能攻击沿横向左右各两格,沿纵向上下各两格,求最多能部署多少支炮兵部队。
玉米田的变式题。
因为前面两行都会对当前行产生影响,故\(dp\)要多记录一维,但是\(f[110][1024][1024]\)会炸空间
但发现每一行合法的状态最多\(59\)种,故只需用\(f[110][70][70]\)来记录
预处理\(cnt[i]\)表示在不管其它行的情况下第i行符合条件的状态数,\(g[i][j]\)表示第i行符合条件的第j个状态
我一开始把状态编号和状态搞混了,调错调了半天。
#include <iostream>
#include <cstdio>
using namespace std;
int n,m;
char ch[110][15];
int h[110],cnt[110];
int g[110][1<<12];
int num[1<<12]={};
int f[110][70][70];
void read()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%s",ch[i]+1);
}
void Deal_first()
{
for(int i=1;i<=n;i++)
{
int sum=0;
for(int j=1;j<=m;j++)
{
if(ch[i][j]=='P')sum=sum*2+1;
else sum=sum*2;
}
h[i]=sum;
}
for(int i=1;i<=(1<<m)-1;i++)
{
int cnt=0;
for(int j=1;j<=m;j++)
{
if((i&(1<<(j-1)))!=0)cnt++;
}
num[i]=cnt;
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<=(1<<m)-1;j++)
{
if((j|h[i])==h[i])//
{
if((((j<<2)&j)==0)&&(((j<<1)&j)==0)&&(((j>>2)&j)==0)&&(((j>>1)&j)==0))g[i][++cnt[i]]=j;
}
}
}
for(int i=1;i<=cnt[1];i++)
{
for(int j=1;j<=cnt[2];j++)
{
if((g[1][i]&g[2][j])==0)f[2][j][i]=num[g[1][i]]+num[g[2][j]];
}
}
}
int main()
{
read();
Deal_first();
for(int i=3;i<=n;i++)
{
for(int j=1;j<=cnt[i];j++)
{
for(int k=1;k<=cnt[i-1];k++)
{
if((g[i][j]&g[i-1][k])==0)
{
for(int t=1;t<=cnt[i-2];t++)
{
if(((g[i][j]&g[i-2][t])==0)&&((g[i-1][k]&g[i-2][t])==0))
{
f[i][j][k]=max(f[i][j][k],f[i-1][k][t]+num[g[i][j]]);
}
}
}
}
}
}
int ans=0;
for(int j=1;j<=cnt[n];j++)
{
for(int k=1;k<=cnt[n-1];k++)
{
if((g[n][j]&g[n-1][k])==0)
{
ans=max(ans,f[n][j][k]);
}
}
}
printf("%d\n",ans);
return 0;
}
动物园
题意:
动物园的围栏呈环状排列
每个小朋友可以看到从他所在位置开始数的连续5个围栏,每个小朋友有他害怕或喜欢的动物。
当下面两处情况之一发生时,小朋友就会高兴:
- 至少有一个他害怕的动物被移走
- 至少有一个他喜欢的动物没被移走
你可以选择去移走一些围栏,使得最多的小朋友高兴
围栏数为\(N (1≤N≤10 000)\),小朋友的个数为\(C (1≤C≤50 000)\)
经过观察发现围栏数极小,故可以用状压\(dp\)
设\(f[i][j]\)表示第i个小朋友可以看到的动物被移走的状态为\(j\)
预处理\(s[i][j]\)表示第i个位置的所有小朋友可以看到的动物被移走的状态为j时会高兴的小朋友数量
先枚举第n个小朋友的状态,再进行\(dp\)
\(dp\)方程为
for(int j=1;j<=n;j++)
{
for(int k=0;k<32;k++)
f[j][k]=max(f[j-1][(k&15)<<1],f[j-1][((k&15)<<1)|1])+s[j][k];
}
整道题的代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m;
int s[10100][35];
int f[10100][35];
int ans=0;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int e,f,l;scanf("%d%d%d",&e,&f,&l);
int x=0,y=0;
for(int j=1;j<=f;j++)
{
int xx;scanf("%d",&xx);
x=x|(1<<((xx-e+n)%n));
}
for(int j=1;j<=l;j++)
{
int xx;scanf("%d",&xx);
y=y|(1<<((xx-e+n)%n));
}
for(int j=0;j<32;j++)
{
s[e][j]+=((x&j)||(y&((~j)&31)));
}
}
for(int i=0;i<32;i++)
{
memset(f,-0x3f3f3f3f,sizeof(f));
f[0][i]=0;
for(int j=1;j<=n;j++)
{
for(int k=0;k<32;k++)f[j][k]=max(f[j-1][(k&15)<<1],f[j-1][((k&15)<<1)|1])+s[j][k];
}
ans=max(ans,f[n][i]);
}
printf("%d\n",ans);getchar();
return 0;
}
学校食堂
同学们排队在食堂吃饭,每个同学想吃的菜都有一个特定的口味值T[i],每个同学的菜所需的时间为T[i]^T[j](T[j]表示食堂做的前一道菜的口味值),每个同学有一定的忍耐值B[i],表示他最多允许他身后的B[i]个人先拿到饭菜。
求在满足所有人的忍耐值的前提下,学校食堂做完这些菜最少需要多少时间。
对于100%的数据,满足\(1 ≤ N ≤ 1,000,0 ≤ Ti ≤ 1,000,0 ≤ Bi ≤ 7。\)
我们通过观察发现\(B[i]\)的值极小,考虑状压\(dp\)
这道题最大的一个难点就是\(dp\)方程怎么列
首先我们考虑至少有一维是[i]且前i-1个人的菜已经做完了,肯定还有一维[j]用于状态压缩表示这个人和他身后的B[i]个人拿到菜的情况,又因为做菜时间与上一个拿到饭菜的人有关,所以再记录一维\([k]\)表示上一个拿到饭菜的人相对第i个人的位置,经过分析可得k的范围是\([-8,7]\)。(作为数组下标时需整体加8)
所以\(f[i][j][k]\)表示前i-1个人已经拿到饭菜了,第\(i\)个人到第\(i+B[i]\)个人拿到饭菜的情况为\(j\),上一个拿到饭菜的人为第\(i+k\)个人
考虑状态方程如何转移:
如果\(j\&1==1\),那么\(f[i][j][k]\)可以等价地转移到\(f[i+1][j>>1][k-1]\)
如果\(j\&1==0\),枚举下一个拿到饭菜的人,注意用\(B[i]\)来限制枚举的范围
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n;
int T[1010],B[1010];
int f[1070][1<<10][20];
int main()
{
int Case;scanf("%d",&Case);
while(Case--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d%d",&T[i],&B[i]);
memset(f,0x3f3f3f3f,sizeof(f));
f[1][0][7]=0;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=(1<<8)-1;j++)
{
for(int k=-8;k<=7;k++)
{
if(f[i][j][k+8]>=1e9)continue;
if(j&1)f[i+1][j>>1][k+7]=min(f[i+1][j>>1][k+7],f[i][j][k+8]);
else
{
int cnt=1e9;
for(int t=0;t<=7;t++)
{
if(i+t>cnt)break;
if((j>>t)&1)continue;
cnt=min(cnt,i+t+B[i+t]);
f[i][j|(1<<t)][t+8]=min(f[i][j|(1<<t)][t+8],f[i][j][k+8]+((i+k>0)?(T[i+k]^T[i+t]):0));
}
}
}
}
}
int ans=1e9;
for(int i=-8;i<=0;i++)ans=min(ans,f[n+1][0][i+8]);
printf("%d\n",ans);
}
return 0;
}