状压DP

状压

一般看到数据范围是16左右的时候想状压dp

解决两类问题:

集合问题

棋盘问题

O((n^4))

for(int s=0;s<(1<<n);s++)
    for(int t=0;t<(1<<n);t++)
        if(t&s==t)

O((n^3))

for(int s=0;s<(1<<n);s++)
    for(int t=s;t>0;t=(t-1)&s)

方格取数

#include <bits/stdc++.h>
using namespace std;
int a[19][1 << 17];
int f[19][1<<17];
int tot[1<<17];
int calc(int i,int x){
    int cnt = 1,res = 0;
    while(x){
        if(x & 1)  res += a[i][cnt];
        x /= 2;cnt++;
    }
    return res;
}
int main()
{
    ios::sync_with_stdio(false);
    int n;
    while(cin >> n){
        int cnt = 0,ans = 0;
        for(int i = 0;i < (1 << n);i++)   if((i & (i >> 1)) == 0) tot[++cnt] = i;
        for(int i = 1;i <= n;i++) for(int j = 1;j <= n;j++) cin >> a[i][j];
        for(int i = 1;i <= n;i++){
            for(int k = 1;k <= cnt;k++){
                int val = calc(i,tot[k]);
                for(int j = 1;j <= cnt;j++){
                    if((tot[j] & tot[k]) == 0){
                        f[i][k] = max(f[i][k],f[i - 1][j] + val);
                    }
                }
            }
        }
        for(int j = 1;j <= cnt;j++)  ans = max(ans,f[n][j]);
        cout << ans << endl;
    }
    return 0;

}

例1:互不侵犯

在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。

( 1 <=N <=9, 0 <= K <= N * N)

f{i}{j}{k}第 i 行、此行放什么状态(用 j 表示)、包括这一行 已经使用了的国王数 k 。

我们预先处理出每一个状态(sit[x])其中包含二进制下1的个数,以及此状态下这一行放的国王个数(gs[x])

#include<iostream> 
#include<cstdio> 
#include<cstring>
using namespace std;
typedef long long LL;
LL f[10][1<<10][100],sum;
int n,k,cnt;
int pre[1<<10],num[1<<10]; 
int lowbit(int x){//处理出每行的国王数
	int ans=0;
	while(x){
		ans+=(x&1);
		x>>=1;
	}
	return num[cnt]=ans;
}
int main(){
	scanf("%d%d",&n,&k);
	for(int i=0;i<(1<<n);i++){//预处理第一行
		if(!(i&(i<<1))&&!(i&(i>>1)))
		pre[++cnt]=i,f[1][cnt][lowbit(i)]=1;
	}
	for(int i=2;i<=n;i++){
		for(int j=1;j<=cnt;j++){//枚举当前行
			int x=pre[j];
			for(int s=1;s<=cnt;s++){//枚举上一行
				int y=pre[s];
				if((x&y) || (x&(y<<1)) || (x&(y>>1)))continue;
				for(int u=0;num[j]+u<=k;u++)
					f[i][j][num[j]+u]+=f[i-1][s][u];
			}
		}
	}
	for(int i=1;i<=cnt;i++)
		sum+=f[n][i][k];
	printf("%lld",sum);
	return 0;
} 

例2:Corn Fields G

#include<iostream> 
#include<cstring> 
#include<cstdio> 
using namespace std;
const int mod=100000000;
int n,m,ans=0;
int a[15][15];
int G[13];
int f[13][1<<17];
int h[1<<17];//判断合法 
int main(){
	scanf("%d%d",&m,&n);
	for(int i=1;i<=m;i++)
		for(int j=1;j<=n;j++){
			scanf("%d",&a[i][j]);
			G[i]=(G[i]<<1)+a[i][j];
		}
	for(int i=0;i<(1<<n);i++)
		h[i]=(!((i<<1)&i) && !((i>>1)&i));
	f[0][0]=1;
	for(int i=1;i<=m;i++)
		for(int j=0;j<(1<<n);j++)
			if(h[j]&&((j&G[i])==j))
				for(int k=0;k<(1<<n);k++)
					if(!(k&j))f[i][j]=(f[i][j]+f[i-1][k])%mod;
	for(int i=0;i<(1<<n);i++)
		ans=(ans+f[m][i])%mod;
	printf("%d
",ans);
	return 0;
}

例3:[USACO08NOV] Mixed Up Cows G

有N头奶牛(4 <= N <= 16),第i头奶牛的编号是Si,每头奶牛的编号都是唯一的。她们在挤奶的时候一定要排成混乱的队伍。在一只混乱的队 伍中,相邻奶牛的编号之差均超过K。比如当K = 1时,1, 3, 5, 2, 6, 4就是一支混乱的队伍, 而1, 3, 6, 5, 2, 4不是,因为6和5只差1。请数一数,有多少种队形是混乱的呢?

解: f {i}{j}表示以第i只奶牛为结尾的状态为j的队伍混乱的方案数是多少

然后枚举最后一头牛

#include<cstdio> 
#include<iostream> 
#include<cmath> 
using namespace std;
#define LL long long
LL n,m,a[18],f[18][1<<18],ans;
int main(){
	scanf("%lld%lld",&n,&m);
	for(LL i=1;i<=n;i++)scanf("%lld",&a[i]);
	for(LL i=1;i<=n;i++)
		f[i][1<<(n-i)]=1;
	for(LL j=1;j<(1<<n);j++)//注意:先枚举状态 
		for(LL i=1;i<=n;i++){
			if(!(j&(1<<(n-i))))continue;
			for(LL k=1;k<=n;k++){
				if(j&(1<<(n-k)))continue;
				if(abs(a[k]-a[i])>m)f[k][(1<<(n-k))|j]+=f[i][j];
			}
		}
	for(LL i=1;i<=n;i++)
		ans+=f[i][(1<<n)-1];
	printf("%lld",ans);
	return 0;
}

例4:题目

给出n个物品,体积为w[i],现把其分成若干组,要求每组总体积<=W,问最小分组。(n<=18)

#include<iostream> 
#include<cstdio> 
#include<cstring>
using namespace std;
typedef long long LL;
int n,m,w[20],f[1<<19],g[1<<19];
//f[i]就代表当前状态为i时最小组数 ,g[]来记录当前状态下剩余的体积
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&w[i]);
	memset(f,63,sizeof(f));
	f[0]=1;
	g[0]=m;
	for(int i=0;i<(1<<n);i++){
		for(int j=1;j<=n;j++){
			if(i&(1<<(j-1)))continue;
			if(g[i]>=w[j]){
				if(f[i|(1<<(j-1))]>f[i]){
					f[i|(1<<(j-1))]=f[i];g[i|(1<<(j-1))]=g[i]-w[j];
				}
				else if(f[i|(1<<(j-1))]==f[i])
				g[i|(1<<(j-1))]=max(g[i|(1<<(j-1))],g[i]-w[j]);
			}else if(g[i]<w[j]){
				if(f[i|(1<<(j-1))]>f[i]+1){
					f[i|(1<<(j-1))]=f[i]+1;
					g[i|(1<<(j-1))]=m-w[j];
				}
				else if(f[i|(1<<(j-1))]==f[i]+1) 
				g[i|(1<<(j-1))]=max(g[i|(1<<(j-1))],m-w[j]);
			}
		}
	}
	printf("%d",f[(1<<n)-1]);
	return 0;
} 

例5:排列

给一个数字串 s和正整数 d, 统计 s 有多少种不同的排列能被 d 整除(可以有前导 0)。

100% 的数据满足:s 的长度不超过 10,1≤d≤1000,1≤T≤15。(多组数据)

解:

状压 f { i }{ j }第一维表示状态,第二维表示余数

/*
最关键:f[i|(1<<(j-1))][(k*10+a[j])%d]+=f[i][k];
细节:如1223序列在状态枚举时
0101 和 0011统计了两次,但其实都是一种排序(排序重复不计)所以用vis来去重(每次重置0)
*/
#include<iostream> 
#include<cstdio> 
#include<cstring>
using namespace std;
typedef long long LL;
bool vis[15];
int T,d,f[1<<11][1005],a[15];
char s[15];
int main(){
	scanf("%d",&T);
	while(T--){
		memset(f,0,sizeof(f));
		scanf("%s%d",s+1,&d);
		int n=strlen(s+1);
		for(int i=1;i<=n;i++)a[i]=s[i]-'0';
		f[0][0]=1;
		for(int i=0;i<(1<<n);i++){
			memset(vis,0,sizeof(vis));
			for(int j=1;j<=n;j++){
				if(i&(1<<(j-1)))continue;
				if(!vis[a[j]]){
					vis[a[j]]=1;
					for(int k=0;k<d;k++)
						f[i|(1<<(j-1))][(k*10+a[j])%d]+=f[i][k];
				}
			}
		}
		printf("%d
",f[(1<<n)-1][0]);
	}
	
	return 0;
}

例6: 奖励关

#include <cstdio>
#include <iostream>
using namespace std;
int k,n,s[1<<16],p[20],ss;
double f[105][1<<16];
inline int read() {
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
int main() {
	k=read();n=read();	
	for(int i=1;i<=n;i++) {
		p[i]=read();
		while(1) {
			ss=read();
			if(ss==0)break;
			s[i]|=(1<<(ss-1));
		} 
	}
	for(int i=k;i>=1;i--){
		for(int state=0;state<(1<<n);state++) {
			for(int j=1;j<=n;j++) {
				if((s[j]&state)!=s[j]) f[i][state]+=f[i+1][state]; //不满足
				else  f[i][state]+=max(f[i+1][state],f[i+1][state|1<<(j-1)]+p[j]);
			}
			f[i][state]/=n;
		}
	}
	printf("%.6lf
",f[1][0]);		
	return 0;
}

例7:奇怪的道路

好吧看到一开始想是状压,然后看到n<=30放弃了,,,反正考试5选3就去调别的了。。。

好了看题解正解

#include<iostream>
#include<cstdio>
using namespace std;
const int Mod = 1000000007;
int n,m,k;
long long f[35][35][1<<10][10];
signed main(){
	scanf("%d%d%d",&n,&m,&k);
	f[1][0][0][0]=1;
	for(int i=1;i<n;i++){
		for(int j=0;j<=m;j++){
			for(int s=0;s<(1<<(k+1));s++){
				for(int p=0;p<k;p++){
					if(!f[i][j][s][p]) continue;
					(f[i][j][s][p+1]+=f[i][j][s][p])%=Mod;//不连边
					if (j<m && i-k+1+p>0) (f[i][j+1][s^(1<<k)^(1<<p)][p]+=f[i][j][s][p])%=Mod;//连边,影响k和p奇偶性
					if ((s&1)==0) (f[i+1][j][s>>1][0]+=f[i][j][s][k])%=Mod;//当前点连的是奇数条边,下一个点向当前连边
				}
			}
		}
	}
	printf("%lld",f[n][m][0][0]);
	return 0;
}
原文地址:https://www.cnblogs.com/ke-xin/p/13509571.html