状态压缩DP

简述

状态压缩的作用就是把 S S S编成数字,比如给 { 3 } \{3\} {3}编号 4 4 4,给 { 1 , 2 , 4 } \{1,2,4\} {1,2,4}编号 11 11 11等,便于存储集合信息。编码规则是 S = { x 1 , x 2 , … , x t } S=\{x_1,x_2,\dots,x_t\} S={x1,x2,,xt}则编码为 ∑ i = 1 t 2 x i − 1 \sum_{i=1}^t2^{x_i-1} i=1t2xi1。可以证明这种编码不同的 S S S之间没有冲突(这是最基本的要求);且用编码极易还原 S S S;更进一步,按编码顺序访问 S S S,则转移一定是从编码小的 S 1 S_1 S1到编码大的 S 2 S_2 S2

在编码和集合 S S S之间的转换用位运算更加高效。首先,位运算符的优先级低于加减(就如加减低于乘除一样)(因此优先级也低于逻辑运算符与(&&)或(||)但高于大于小于, w h y why why建议能加括号就加,不要干些不靠谱的活计 :):左移(<<),右移(>>),按位与(&),按位或(|),按位异或(^)。

取一个二进制数 n n n(其实就是普通的一个整数 n n n)的从右向左 k k k位的方法是(n>>k)&1,类比取十进制数的方法是(n/(10^k))%10一样。

状态压缩题目典型的特征就是数据范围很小,一般都在20左右。


P7859 [COCI2015-2016#2] GEPPETTO

直接枚举所有状态,判断该状态下包含的所有成员中是否任意两个都不冲突即可。

#include<bits/stdc++.h>
using namespace std;
int n,m;
long long a[410],b[410],f[1<<20];

int main()
{
	cin>>n>>m;
	for(int i=0;i<m;i++)
		cin>>a[i]>>b[i];
	int cnt=0;
	for(int i=0;i<(1<<n);i++)
	{
		int flag=1;
		for(int j=0;j<m;j++)
		{
			if((i>>(a[j]-1)&1)&&(i>>(b[j]-1)&1))
			{
				flag=0;
				break;
			}
		}
		if(flag) cnt++;
	}
	cout<<cnt;
	return 0;
}

蓝桥杯2021省赛A组E题 回路计数

(有21个点,1-21,编号互质的两点之间可通行,现从编号为1的点出发,问有多少种哈密顿回路的走法)

定义状态 f ( S , x ) f(S,x) f(S,x):不重复经过点集 S S S,当前所在点为 x x x的方案数;

状态转移:对状态 f ( S , x ) f(S,x) f(S,x),对任意 y ∉ S y \notin S y/S x x x y y y之间有连边的 y y y,可以转移到 f ( S ⋃ y , y ) f(S\bigcup y,y) f(Sy,y)。(换一句话说,前一个状态是达成后一个状态的策略之一);

#include<bits/stdc++.h>
using namespace std;
const int N=21,M=1<<N;
long long road[N][N],f[M][N];

int main()
{
	for(int i=0;i<N;i++)
	    for(int j=0;j<N;j++)
   	       if(__gcd(i+1,j+1)==1)
			    road[i][j]=1; 
	  f[1][0]=1;//集合为{1},停留在点1(在数组中处理将其减一故第二维为0)
	for(int i=0;i<(1<<N);i++)//集合状态
	    for(int j=0;j<N;j++)//走过当前集合且最后停留在点j
	        if(i>>j&1)//看j点有没有到(在不在编码i代表的集合S中),这里为真代表在
	            for(int k=0;k<N;k++)//要往点k走
	                if(!(i>>k&1))//k不在S中
	                	if(road[j][k])
							f[i|(1<<k)][k]+=f[i][j];
	long long ans=0;
	for(int i=0;i<N;i++)
		ans+=f[(1<<N)-1][i];//全集对应的编码为(1<<N)-1,这里i全循环一遍是因为教学楼1和所有教学楼都连边
	cout<<ans<<endl;
	return 0;
}

P4802 [CCO 2015]路短最

#include<bits/stdc++.h>
using namespace std;
int n,m;
int mp[20][20];
int f[1<<18][20];

int main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		int from,to,w;
		scanf("%d%d%d",&from,&to,&w);
		mp[from][to]=w;
	}
	memset(f,0x8f,sizeof(f));
	f[1][0]=0;
	for(int i=3;i<(1<<n);i+=2)//集合状态
	    for(int j=0;j<n;j++)//走过当前集合且最后停留在点j
	        if(i>>j&1)//看j点有没有到(在不在编码i代表的集合S中),这里为真代表在
	            for(int k=1;k<n;k++)//要往点k走
	                if(((i>>k)&1)&&mp[j][k])
						f[i][k]=max(f[i][k],f[i-(1<<k)][j]+mp[j][k]);
	int ans=0;
	for(int i=(1<<(n-1))+1;i<(1<<n);i+=2)//老老实实加括号~~~我debug1h的教训QAQ 
		ans=max(ans,f[i][n-1]);
	cout<<ans;
	return 0;
}

P1433 吃奶酪

跟上上一题类似。

#include<bits/stdc++.h>
using namespace std;
int n;
double f[1<<15][16],x[17],y[17];
double dis(int a,int b)
{
	return sqrt((x[a]-x[b])*(x[a]-x[b])+(y[a]-y[b])*(y[a]-y[b]));
}

int main()
{
	cin>>n;
	for(int i=0;i<n;i++)
		cin>>x[i]>>y[i];
	memset(f,127,sizeof(f));
	for(int i=0;i<n;i++) f[1<<i][i]=0;
	for(int i=0;i<1<<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			if((i>>j)&1)
			{
				for(int k=0;k<n;k++)
				{
					if(!(i>>k&1||j==k))
					f[i|(1<<k)][k]=min(f[i|(1<<k)][k],f[i][j]+dis(j,k));
				}
			}
		}
	}
	double ans=0x3ffffff;
	for(int i=0;i<n;i++)
	{
		f[(1<<n)-1][i]+=dis(i,17);
		ans=min(ans,f[(1<<n)-1][i]);
	}
	printf("%.2lf",ans);
	return 0;
}

P5911 [POI2004]PRZ

首先要知道怎么枚举一个集合的子集:

for (int x = S; x; x = (x-1)&S)

若S=1011,则x分别为:1011, 1010, 1001, 1000, 0011, 0010, 0001。

观察过程:

#include<bits/stdc++.h>
using namespace std;

#define inf 0x3ffffff
#define MAXN (1<<16)+1
int w, n, c[17], t[17];
int T[MAXN], C[MAXN], f[MAXN];

int main() 
{
	scanf("%d %d",&w,&n);
	for (int i=0;i<n;i++) 
		scanf("%d %d",&t[i],&c[i]);
	for (int i=1;i<(1<<n);i++) 
	{
		for(int j=0;j<n;j++)
		{
			if((1<<j)&i)
			{
				C[i]+=c[j];
				T[i]=max(T[i],t[j]);
			}
		}
		f[i]=inf;
	}
	for(int i=1;i<(1<<n);i++) 
	{
		cout<<"母集合:";
		int t=i;
		while(t) 
		{
			cout<<t%2;
			t/=2;
		}
		cout<<endl;
		for (int j = i; j >= 0; j = i & (j - 1)) 
		{
			cout<<" 子集:" ;
			int temp1=j;
			int temp2=i^j;
			while(temp1) 
			{
				cout<<temp1%2;
				temp1/=2;
			}
			cout<<endl;
			cout<<" 补集:";
			while(temp2) 
			{
				cout<<temp2%2;
				temp2/=2;
			}
			cout<<endl;
			cout<<" f[i]:" <<f[i]<<' '<<"f[j]:"<<f[j]<<" + "<<"T[i^j]"<<T[i^j]<<endl;
			if (C[i ^ j] <= w) f[i] = min(f[i], f[j] + T[i ^ j]);
			if (j == 0) break;
		}
		cout<<"最后f[i]:" <<f[i]<<endl<<endl;
	}
	printf("%d\n",f[(1<<n)-1]);
	return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最终代码:

#include<bits/stdc++.h>
using namespace std;

#define inf 0x3ffffff
#define MAXN (1<<16)+1
int w, n, c[17], t[17];
int T[MAXN], C[MAXN], f[MAXN];
//C[i]:当i这些人为一队时的总重量;
//T[i]:i些人为一队时里面最慢的那个人要的时间

int main() 
{
	scanf("%d %d",&w,&n);
	for(int i=0;i<n;i++) 
		scanf("%d %d",&t[i],&c[i]);
	for(int i=1;i<(1<<n);i++)//枚举所有的状态 
	{
		for(int j=0;j<n;j++)//枚举每一位 
		{
			if((1<<j)&i)//该状态选中了枚举的该位 
			{
				C[i]+=c[j];//更新i状态的重量 
				T[i]=max(T[i],t[j]);//更新i状态的时间(所有成员所耗的时间中最大的) 
			}
		}
		f[i]=inf;//求最小值,初始化为无穷大 
	}
	for(int i=1;i<(1<<n);i++)//枚举所有状态 
	{
		for(int j=i;j>=0;j=i&(j-1)) //假设i为母集,则j为i的一个子集 
		{
			if(C[i^j]<=w) f[i]=min(f[i],f[j]+T[i^j]);
			//i^j可以得到i中除了j以外的成员(补集),如果补集算出的重量小于限额(T[i^j]==f[i^j] ) ,可以尝试更新f[i]
			//也可以用下面这个状态转移方程,道理类似 
			//f(C[j]<=w) f[i]=min(f[i],f[i^j]+T[j]); 
			if(j==0) break;//这句必不可少,少了会挂掉 
		}
	}
	printf("%d\n",f[(1<<n)-1]);//f[(1<<n)-1]满员状态 
	return 0;


P2622 关灯问题II

#include<bits/stdc++.h>
using namespace std;
int f[1<<10],n,m,tab[110][11];
//f[状态]:达到该状态的最小步数 
int main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
		for(int j=0;j<n;j++)
			scanf("%d",&tab[i][j]);
	memset(f,0x3f,sizeof(f));
	f[(1<<n)-1]=0; //0步达到全开状态 
	for(int i=(1<<n)-1;i>=0;i--)
	{
		for(int k=1;k<=m;k++)//枚举在该状态下按下的按钮,算出会达到的状态 
		{
			int temp=i;
			for(int j=0;j<n;j++) 
			{
				if(tab[k][j]==-1&&!(i&(1<<j))) temp^=(1<<j);
				if(tab[k][j]==1&&(i&(1<<j))) temp^=(1<<j);
			}
			f[temp]=min(f[temp],f[i]+1);//状态转移 
		}
	}
	if(f[0]==0x3f3f3f3f) cout<<-1;
	else cout<<f[0];
	return 0;	
}

P1896 [SCOI2005]互不侵犯

#include<bits/stdc++.h>
using namespace std;
int N,K;
long long f[1<<9][10][85];
//三个状态参数分别为:每行的状态;行数;已经用掉的国王数,f用于记录方案数 
//不开long long WAWA两声
 int state[1000],num[1000];
//state:储存预处理出的合法状态数
//num:记录每个合法状态中1的个数 

int main()
{
	cin>>N>>K;
	int cnt=0;
	for(int s=0;s<1<<N;s++)//枚举行内的所有状态 
	{
		if((s>>1|s<<1)&s) continue;//判断国王们是否相邻,如果相邻,行内就会起冲突,不合法 
		state[cnt++]=s;
		int cnt2=0;
		for(int i=0;i<N;i++)
			if(s>>i&1) cnt2++;
		num[s]=cnt2;
	}//预处理出一些可能的排序,并记录这些排序中1的个数,这样不用每行都枚举所有状态并记录每种状态中1的个数 
	//for(int i=0;i<cnt;i++)
	//	cout<<state[i]<<' '<<num[state[i]]<<endl;
	f[0][0][0]=1;//初始的第0行(会被第1行调用 ):一个国王都不放的方案数只有一种 
	for(int line=1;line<=N;line++)//枚举第一行到第n行 
	{
		for(int i=0;i<cnt;i++)//枚举当前行的状态 
		{
			for(int j=0;j<cnt;j++)//枚举前一行的状态 
			{
				if((state[j]<<1|state[j]|state[j]>>1)&state[i]) continue;
				//前一行有国王的位置的左下方、正下方、右下方不能安置国王 
				for(int p=0;p<=K;p++)//枚举前一行国王的个数,希望找到对应f[state[j]][line-1][p]的方案数 
					f[state[i]][line][p+num[state[i]]]+=f[state[j]][line-1][p];
					//在当前行新增了num[state[i]个国王,状态转移 
			}
		}
	}
	long long ans=0;
	for(int i=0;i<cnt;i++)
		ans+=f[state[i]][N][K];//求出到第n行时所有状态下符合条件的方案总数 
	cout<<ans;
	return 0;
}

P1879 [USACO06NOV]Corn Fields G

#include<bits/stdc++.h> 
using namespace std;
int n,m,num[15],mp[13][1<<12];
// mp[行标][状态编号]=状态 
const int mod=100000000;
long long f[1<<12][13];
//f[状态][行标]=方案数 
int state[1<<12],cnt;

int main()
{
	cin>>m>>n;
	for(int s=0;s<1<<n;s++)
	{
		if((s<<1)&s) continue;
		state[s]=1;
	}//预处理出合法状态(1的位置不相邻)
	for(int i=1;i<=m;i++)
	{
		int temp=0;
		for(int j=1;j<=n;j++)
		{
			int x;
			scanf("%d",&x);
			temp<<=1;
			temp+=x;
		}
		for(int j=temp;j;j=(j-1)&temp)//枚举temp的子集 
			if(state[j]) mp[i][num[i]++]=j;
		mp[i][num[i]]=0;
	}//预处理出每行的合法状态,第i行的合法状态有num[i]个 
	f[0][0]=1;//第0行全为0,这是一种方案 
	for(int line=1;line<=m;line++)//枚举行标 
	{
		for(int i=num[line];i>=0;i--)//枚举当前行状态 
		{
			for(int k=num[line-1];k>=0;k--)//枚举前一行状态 
			{
				if(!(mp[line][i]&mp[line-1][k]))//当前行状态和前一行妆台在所有位置都不冲突 
				{
					//cout<<"mp[line][i]:"<<mp[line][i]<<" mp[line-1][k]:"<<mp[line-1][k]<<endl;
					//cout<<"f[mp["<<line-1<<"]["<<k<<"]]["<<line-1<<"]:"<<f[mp[line-1][k]][line-1]<<endl;
					f[mp[line][i]][line]+=f[mp[line-1][k]][line-1];
					f[mp[line][i]][line]%=mod;
				}
			} 
			//cout<<"f[mp["<<line<<"]["<<i<<"]]["<<line<<"]:"<<f[mp[line][i]][line]<<endl;
		}
	}
	long long ans=0;
	for(int i=0;i<=num[m];i++)//求和最后一行的所有行状态对应的方案数 
		ans=(ans+f[mp[m][i]][m])%mod;
	cout<<ans;
	return 0;
}


附:一只题单

QAQ
在这里插入图片描述

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

春弦_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值