状压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\)

\[f_{S\cup j}=min(f_S)+1 \]

然后再枚举没有打的小鸟\(k\)

\[f_{S\cup line_{j,k}}=min(f_S)+1 \]

本题的思想是:把所有可以一起更新的预处理出来。

#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)\)算法

posted @ 2022-08-13 13:30  s1monG  阅读(25)  评论(0)    收藏  举报