状压DP
二进制操作
- 将第k位设为1
x = x | (1 << k)
- 将第k位设为0
x = x & -(1 << k)
- 查询第k位
x & (1 << k)
(x >> k) & 1
如果等于0,那么第k位是0,否则是1
状压dp
模板题:TSP问题(遍历每一个点)(\(n \leq 20\))
设\(f_{s,j}\) 表示已经走过集合s的点,且最后一个点在j节点的最短路径。
对于\(f_{s,j}\),枚举k,用\(f_{s,j}+edge_{j->k}\) 更新\(f_{s+k,k}\).
时间复杂度\(O(2^n\times n\times n)=4\times10^8\).
然后用二进制表示集合s = {1,3,4}:第1,3,4位设为1,其余设为0.
#include <algorithm>
#include <cstdio>
using std::min;
const int INF = 0x3FFFFFFF;
const int MAXN = 20;
int n, a[MAXN][MAXN], f[1 << MAXN][MAXN], ans;
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
scanf("%d", &a[i][j]);
for (int i = 0; i < (1 << n); i++)
for (int j = 0; j < n; j++)
f[i][j] = INF;
f[1][0] = 0;
for (int i = 1; i < (1 << n); i++) {
for (int j = 0; j < n; j++) {
if (!(i & (1 << j)))
continue;
for (int k = 0; k < n; k++) {
if (i & (1 << k))
continue;
int to = i | (1 << k);
f[to][k] = min(f[to][k], f[i][j] + a[j][k]);
}
}
}
ans = INF;
int s = (1 << n) - 1;
for (int j = 1; j < n; j++)
ans = min(ans, f[s][j] + a[j][0]);
printf("%d\n", ans);
return 0;
}
例题1 P1896 [SCOI2005]互不侵犯
设计状态\(f_{n,S}\):第n行的单行状态为S(状压)。
\(f_{n,S}=\sum f_{m,T}\),其中\(m=n-num[S]\),\(T\)为与\(S\)作为上下行的合法状态,\(num\)数组为\(S\)中放置国王的个数。
这时候我们发现时间不够,于是把单行内所有合法方案存起来,原先的\(S\)替换为合法状态的序号。
#include<algorithm>
#include<cstdio>
#include<iostream>
using namespace std;
typedef long long ll;
int n,b;
int s[210],num[210],tot;
ll f[11][210][101],ans;
int main() {
scanf("%d %d",&n,&b);
for(int i=0; i<(1<<n); i++) {
if(i&(i>>1)) continue;
int k=0;
for(int j=0; j<n; j++)
if(i&(1<<j)) k++;
s[++tot]=i;
num[tot]=k;
}
f[0][1][0]=1;
for(int i=1; i<=n; i++)
for(int j=1; j<=tot; j++)
for(int k=num[j]; k<=b; k++)
for(int t=1; t<=tot; t++) {
if(s[j]&s[t]) continue;
if((s[j]<<1)&s[t]) continue;
if((s[j]>>1)&s[t]) continue;
f[i][j][k]+=f[i-1][t][k-num[j]];
}
for(int i=1; i<=tot; i++) ans+=f[n][i][b];
printf("%lld\n",ans);
return 0;
}
例题2 P2704 [NOI2001] 炮兵阵地
预处理两行,把两行合作一个状态。
先把所有的在一行中合法状态记录下来,并赋予一个序号。
再把两行中合法状态记录下来,并赋予一个序号。
这里要注意数组记录的是序号还是状态。
\(f_{n,S}\) 表示第n行n-1,n的状态合起来为S。
\(f_{n,S}\)由\(f{n-1,T}\)更新,前提是当前行与\(T\)合法。
最后还要检验\(S,T\)在单行中的合法性(山丘)。
#include<algorithm>
#include<cstdio>
#include<iostream>
using namespace std;
int lm[102],s[200],num[200],s1[40000],s2[40000],num2[40000],tot,cnt1,v[200][200];
int n,m,f[102][40000];
int main() {
// freopen("data.in","r",stdin);
// freopen("data.out","w",stdout);
scanf("%d %d",&n,&m);
for(int i=1; i<=n; i++) {
char s[20];
scanf("%s",s);
for(int j=0; j<m; j++)
if(s[j]=='H') lm[i]|=1<<j;
}
for(int i=0; i<(1<<m); i++) {
if(i&(i<<1)||i&(i<<2)) continue;
s[++tot]=i;
int popc=0;
for(int j=i; j; j-=j&-j) popc++;
num[tot]=popc;
}
if(n==1) {
int ans=0;
for(int i=1; i<=tot; i++)
if(!(s[i]&lm[1])) ans=max(ans,num[i]);
printf("%d\n",ans);
return 0;
}
for(int i=1; i<=tot; i++)
for(int j=1; j<=tot; j++) {
if(s[i]&s[j]) continue;
s1[++cnt1]=i; s2[cnt1]=j;
num2[cnt1]=num[i]+num[j];
v[i][j]=cnt1;
}
for(int i=1; i<=cnt1; i++) {
if(s[s1[i]]&lm[1]) continue;
if(s[s2[i]]&lm[2]) continue;
f[2][i]=num2[i];
}
for(int i=3; i<=n; i++) {
for(int j=1; j<=tot; j++) {
if(s[j]&lm[i]) continue;
for(int k=1; k<=cnt1; k++) {
if(s[s1[k]]&s[j]) continue;
if(s[s2[k]]&s[j]) continue;
if(s[s1[k]]&lm[i-2]) continue;
if(s[s2[k]]&lm[i-1]) continue;
int cur=v[s2[k]][j];
f[i][cur]=max(f[i][cur],f[i-1][k]+num[j]);
}
}
}
int ans=0;
for(int i=1; i<=cnt1; i++) ans=max(ans,f[n][i]);
printf("%d\n",ans);
return 0;
}
例题3 P1441 砝码称重
用二进制枚举所有硬币使用情况。
然后使用bitset
统计即可。
#include<algorithm>
#include<bitset>
#include<cstdio>
#include<iostream>
#include<map>
using namespace std;
int n,m,a[21],f[1<<21],ans;
int main() {
scanf("%d %d",&n,&m);
for(int i=0; i<n; i++) scanf("%d",&a[i]);
for(int i=0; i<(1<<n); i++) {
int cnt=__builtin_popcount(i);
if(cnt==n-m) {
bitset<10000> S;
S[0]=1;
for(int j=0; j<n; j++)
if(i&(1<<j)) S=S|(S<<a[j]);
int siz=S.count()-1;
ans=max(ans,siz);
}
}
printf("%d\n",ans);
return 0;
}
例题4 P2831 [NOIP2016 提高组] 愤怒的小鸟
先把与\(i,j\)同在一条抛物线上的所有点记录下来在\(line_{i,j}\)里面。
把已经打掉的小鸟状压。
二进制枚举状态为\(S\),对于还没有打的小鸟枚举\(j\)。
然后再枚举没有打的小鸟\(k\)。
本题的思想是:把所有可以一起更新的预处理出来。
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
int t,n,m;
double x[20],y[20];
int f[1<<20],line[20][20];
const double eps=1e-9;
void equation(double &a,double &b,int i,int j) {
a=-(y[i]*x[j]-y[j]*x[i])/(x[j]*x[j]*x[i]-x[i]*x[i]*x[j]);
b=(y[i]*x[j]*x[j]-y[j]*x[i]*x[i])/(x[i]*x[j]*x[j]-x[j]*x[i]*x[i]);
}
int main() {
scanf("%d",&t);
for(; t; t--) {
memset(f,125,sizeof(f));
memset(line,0,sizeof(line));
scanf("%d %d",&n,&m);
for(int i=0; i<n; i++)
scanf("%lf %lf",&x[i],&y[i]);
if(n==1) {puts("1"); continue;}
for(int i=0; i<n; i++)
for(int j=0; j<n; j++) {
if(fabs(x[i]-x[j])<eps) continue;
double a,b;
equation(a,b,i,j);
if(a>-eps) continue;
for(int k=0; k<n; k++) {
if(fabs(a*x[k]*x[k]+b*x[k]-y[k])<eps)
line[i][j]|=1<<k;
}
}
f[0]=0;
for(int i=0; i<(1<<n); i++) {
for(int j=0; j<n; j++) {
if(!i&(1<<j)) continue;
f[i|(1<<j)]=min(f[i|(1<<j)],f[i]+1);
for(int k=0; k<n; k++)
f[i|line[j][k]]=min(f[i|line[j][k]],f[i]+1);
}
}
printf("%d\n",f[(1<<n)-1]);
}
return 0;
}
当然,还有比\(O(n^2 2^n)\)更快的\(O(n\cdot2^n)\)算法