状态压缩动态规划—(附luogu官方题单解法

状态压缩动态规划—(附luogu官方题单解法

状态压缩,即将状态通过二进制存储,以将一个复杂的状态仅仅用一个int数字表示。

在动态规划中,有着很多状态的转化,面对一些不是那么复杂的题目,我们很容易使用一维数组二维数组或三维数组的下标来存储状态,但是如果状态很复杂有着三维甚至三维以上,再仅仅使用数组下表存储就有些不合适了,下面是luogu状态压缩及动态规划官方题单

题单链接

由简到难列出了几道状态压缩动态规划题的解法及思路

一、P1433

P1433

image-20210915144455849

这道题一开始看是没什么思路的,有点复杂,对着别人题解做的。

思路:

首先看数据范围。1=<n<=15,

为了求解这个问题,不妨将所有奶酪状态分为两种,已经被吃和没有被吃,用1和0来分别,那么对于每一种状态我们都能够使用一个15位(二进制)的数字来表示,数字的每一位是否为1依次对应1~15块奶酪是否被吃,那么我们所要求的就是所有奶酪被吃掉时(数字的所有位都为1)移动距离最短,那我们仅仅使用一个一维数组足够存储所有的状态吗?

依据题意,可以知道到吃奶酪是有顺序的,因此我们不妨再加一维来存储第一个(或最后一个)吃的奶酪的编号依次可以得到状态转移方程

//f[第一个奶酪下标][当前状态] 
//f[i][j] 从i出发,到状态j的最短距离
//i,j为奶酪编号(0~n-1) 
//s 状态 包含i、j  s-(1<<i)不包含i的状态
f[i][s] = min(f[i][s],dis[i][j] + f[j][s-(1<<i)]);

以上状态方程可以理解为从i出发,到达s中状态的距离求解。

我们可以遍历s中的每一个奶酪j,由于i到j再到s,取最小值,即完成状态的转移。

不妨捋一捋,遍历所有的状态,对于每个状态,求从满足条件的奶酪(包含在状态中的奶酪)出发到达状态的最短距离。

最后是两种写法(差不多其实)

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<string.h>
//luogu p1433 dp + 状态压缩
 
double dis(double a[],double b[]){
	return sqrt((a[0]-b[0])*(a[0]-b[0])+(a[1]-b[1])*(a[1]-b[1]));
}
double min(double a,double b ){
	return a>b?b:a;
}
double all[20][2],state[20][66000],dist[20][20],ans;
int main(){
	int n,i,j,k;
	
	all[0][0]=0,all[0][1]=0;
	scanf("%d",&n);
	for(i=1;i<=n;i++){
		scanf("%lf%lf",&all[i][0],&all[i][1]);
	}  
	for(i=0;i<=n;i++){
		for(j=0;j<=n;j++){
			dist[i][j]=dis(all[i],all[j]);
		}
	}
	
	memset(state,127,sizeof(state));
	for(i=1;i<=(1<<(n))-1;i++){//所有状态 
		for(j=1;j<=n;j++){//该状态所有可能的出发点 
			if(((1<<j-1))==i){//状态中仅仅包含该点 
			state[j][i] = 0;
			continue;
			}
			if(((1<<j-1)&i)==0){// 状态中不包含该点 
				continue;
			}
			for(k=1;k<=n;k++){
			if(j==k||(i&(1<<k-1))==0)continue;
			else{
			state[j][i] = min(state[j][i],dist[j][k]+state[k][i-(1<<j-1)]);
			}
			
			}
		}
	}
	ans = 1e20;
for(i=1;i<=n;i++){
	ans = min(ans,dist[0][i]+state[i][(1<<n)-1]);
//	printf("%.2f\n",state[i][(1<<n)-1]);
}
if(ans>1e10)ans =0;
	printf("%.2f",ans);



}




//
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<string.h>
double dis(double a[],double b[]){
	return sqrt((a[0]-b[0])*(a[0]-b[0])+(a[1]-b[1])*(a[1]-b[1]));
}
double min(double a,double b ){
	return a>b?b:a;
}
double all[20][2],state[20][66000],dist[20][20],ans;
int main(){
	int n,i,j,k;
	
	all[0][0]=0,all[0][1]=0;
	scanf("%d",&n);
	for(i=1;i<=n;i++){
		scanf("%lf%lf",&all[i][0],&all[i][1]);
	}  
	for(i=0;i<=n;i++){
		for(j=0;j<=n;j++){
			dist[i][j]=dis(all[i],all[j]);
		}
	}
	
	memset(state,127,sizeof(state));
	for(i=1;i<=(1<<(n+1))-1;i++){
		for(j=0;j<=n;j++){
			if((1<<j)==i){
			state[j][i] = 0;
			continue;
			}
			if(((1<<j)&i)==0){
				continue;
			}
			for(k=0;k<=n;k++){
			if(j==k||(i&(1<<k))==0)continue;
			state[j][i] = min(state[j][i],dist[j][k]+state[k][i-(1<<j)]);
			
			}
		}
	}

	printf("%.2f",state[0][(1<<(n+1))-1]);



}

二、P1441

P1441

image-20210915152721469

这道题有点跟上一道题难度差不多,不过多了点背包的应用,还采用了深度搜索,算是比较综合的一道题

首先当然是求出所有状态,然后就是对每个状态求最多测量种类

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int m,n;
int tf[30]={0}, all[30],ans =0,ret = 0,f[2100],count=0;

void dp(){//对状态求解
	int i,j,top= 0;
	ans =0;
	memset(f,0,sizeof(f));
	f[0] = 1;
	++count;
	for(i=0;i<n;++i){
		if(tf[i])continue;
		for(j = top;j>=0;j--){//背包
			if(f[j]&&!f[j+all[i]]){
				f[j+all[i]] = 1;
				top = j+all[i]>top?j+all[i]:top;
				++ans;
			}
		}
	}

	ret = ans>ret?ans:ret; 
}
void dfs(int cur,int rem){//深度搜索,查找出可能的状态
	if(rem>m||cur>n)return;
	if(rem==m&&cur==n){
		dp();return;}
	dfs(cur+1,rem);
	tf[cur] = 1;
	dfs(cur+1,rem+1);
	tf[cur] = 0;
}
int main(){

int i,j,k;
scanf("%d%d",&n,&m);
 for(i=0;i<n;i++){
 	scanf("%d",&all[i]);
 }
dfs(0,0);
printf("%d",ret);
}

三、P3694

P3694

与P1433那题类似,也与顺序有关,这里的顺序是每个队伍的顺序。由于每个队伍的人数是确定的,因此,确定了队伍的顺序就可求出相对应的出队数目

思路:遍历所有状态,对于每个状态,遍历这个状态中每一队为尾的出列情况

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define maxm 25
#define maxn 100005
int count[maxm]={0},pos[maxm][maxn]={0};
int ans[(1<<20)][2]={0}; 
int min(int a,int b){
	return a>b?b:a;
}

int mov_cnt(int sta,int type ){//包括 
	int ret = 0,cnt = count[type];
		if(cnt ==0)return ret;
if(sta==0)ret =cnt- pos[type][sta+cnt-1];
else {
	ret = pos[type][sta+cnt-1]-pos[type][sta-1];
	ret = cnt -ret;
}

	return ret;
	
}
int main(){
int n,m,i,type,j;
scanf("%d%d",&n,&m);
for(i=0;i<n;++i){//pos 从0开始 
	scanf("%d",&type);
	++count[type];
	for(j=1;j<=m;++j){
		pos[j][i] = count[j];
	}
}
for(i = 1;i<(1<<m);++i){
	ans[i][0] = maxn;
	for(j = 1;j<=m;j++){
		if((i&(1<<(j-1)))==0)continue;
		if(i==(1<<(j-1))){
			ans[i][0] = mov_cnt(0,j),ans[i][1] = count[j];
			break;
		}else{
			ans[i][0] =min(ans[i][0],ans[i-(1<<(j-1))][0]+mov_cnt(ans[i-(1<<(j-1))][1],j));
			ans[i][1] = ans[i-(1<<(j-1))][1] + count[j];
		}
		
	}
}
printf("%d",ans[(1<<m)-1][0]);
}

四、P1896 P2704 P1879

P1896
P2704
P1879
三体都是在二维平面上,给出一些限制条件,然后要求求出某个值。

对于这些题,我们可以把每一行的每一格子是否放入作为状态,在从上到下遍历所有。

一般来说某一行的选择除了本身有关外,还与上一行有关,这些都与题目的限制条件有关

image-20210915155012760

image-20210915155040385

image-20210915155102210

规则不同无非导致了限制条件的位运算不同。

值得一提的是 P2704 中某一行的选择还与前两行有关,但是这也没有增大太多难度,无非是再多开一维数组,将两层的状态都存起来。

还有就是滚动数组的使用,不难发现,某一行的选择与很可能仅仅与前一两行有关,因此数组可以循环使用以减小开销(MLE警告)上代码

//p1896 
#include<stdio.h>
#include<stdlib.h>
#define maxk 64
#define maxn 9
long long int f[maxn][(1<<maxn)+1][maxk+1]={0},tr[maxn]={0};//三维分别对应 行、行状态、已放入数目
int n,k;
int count_one(int a){
	int ret=0,i;
	for(i=0;i<=maxn;i++){
		ret +=(a%2);
		a = a>>1;
	}
	return ret;
}
int main(){
int i,j,l,cnt,m ;
long long int ans=0;
scanf("%d%d",&n,&k);
for(i=0;i<n;i++){//遍历行
	for(j=0;j<(1<<n);j++){//遍历该行所有状态
		
		if((((j<<1)&j)!=0)||(((j>>1)&j)!=0)){
			continue;
		}
		cnt = count_one(j);
		if(cnt>k)continue;
		if(i==0){
		f[i][j][cnt] = 1;
			continue;
		}
		for(l = 0;l<(1<<n);l++){// 遍历上一行所有状态
			if((l&j)!=0||((l<<1)&j)!=0||((l>>1)&j)!=0)continue;
			else {
				for(m = 0;m+cnt<=k;m++){
					f[i][j][m+cnt] +=f[i-1][l][m];
				}
			}
		}
	}
}

	for(j = 0 ;j<(1<<n);j++){
	//	printf("%d\n",f[i][j][k]);
		ans+=f[n-1][j][k];
	}

printf("%lld",ans);
}



//p2704
#include<stdio.h>
#include<stdlib.h>
#define maxn 105
#define maxm 10
char s[maxm]={0};
int n ,m,map[maxn]={0},sum[(1<<maxm)+1];
int state[3][(1<<maxm)][(1<<maxm)+1]={0};// 多出的一维来存储上一行状态
int count_one(int a){
	int ret=0,i;
	for(i=0;i<=maxn;i++){
		ret +=(a%2);
		a = a>>1;
	}
	return ret;
}
int max(int a,int b){
	return a>b?a:b;
	
}
int main(){
scanf("%d%d",&n,&m);
int i ,j,k,cnt,ret = 0,l;
for(i =0;i<(1<<m);i++){
	sum[i] = count_one(i);
}
for(i = 0 ;i<n;i++){
	scanf("%s",s);
	for(j = m-1 ;j>=0;j--){
		map[i] = (map[i]<<1);
		if(s[j]=='H'){
			map[i] += 1;
		}
	}
}

for(i = 0 ;i<n;i++){
	for(j = 0 ;j<(1<<m);j++){
		 state[i%3][j][1<<maxm]  = 0;
		cnt = sum[j];
		if(((j<<1)&(j))!=0||((j<<2)&(j))!=0||((j>>1)&(j))!=0||((j>>2)&(j))!=0||(j&map[i])!=0){
			state[i%3][j][1<<maxm] = -1;
			continue;
		}
		if(i == 0 ){
			state[i%3][j][0] +=cnt;
			ret = max(ret,cnt);
			continue;
		}
		
		for(k  = 0;k<(1<<m);k++){
			state[i%3][j][k] = 0;

			if(state[(i-1)%3][k][1<<maxm]==-1||(k&j)!=0)continue;

			if(i>1){
				for(l = 0 ;l<(1<<m);l++){
					if((l&j)!=0||(l&k)!=0||state[(i-2)%3][l][1<<maxm]==-1)continue;
					state[i%3][j][k] = max(state[i%3][j][k],state[(i-1)%3][k][l]+cnt);
				}		
			}
			else if( i ==1){
				state[i%3][j][k] = max(state[i%3][j][k],state[(i-1)%3][k][0]+cnt);	
			}
		ret  = max(ret,state[i%3][j][k]);	

		}

	}


}
printf("%d",ret);



}
#include<stdio.h>
#include<stdlib.h>
#define maxm 12
#define maxn 12
int n,m,map[maxn],sum[(1<<maxm)+1],row[maxm];
long long int count[2][(1<<maxm)+1],ret = 0;
int count_one(int a){
	int i,ret = 0;
	for(i = 0;i<=maxm+1;i++){
		ret += a%2;
		a/=2; 
	}
	return ret;
}
int main(){
int i,j,k;
scanf("%d%d",&n,&m);
for(i = 0 ;i<n;i++){
	map[i] = 0;
	for(j = 0;j<m;j++){
	scanf("%d",&row[j]);
	}
	for(j = m-1;j>=0;j--){
		map[i]=(map[i]<<1)+(1-row[j]); 
	}
}
//for(i =0;i<(1<<m);i++){
//	sum[i] = count_one(i);
//}
for(i = 0 ;i<n;i++){
	for(j =0;j<(1<<m);j++){ 
	count[i%2][j] = 0;
		if(((j<<1)&j)!=0||((j>>1)&j)!=0||(j&map[i]))continue;
		if(i==0){
		count[i%2][j] =1;
		continue;
		}
		for(k = 0 ;k<(1<<m);k++){
			if(((k<<1)&k)!=0||((k>>1)&k)!=0||(k&map[i-1])||(k&j)!=0)continue;
			count[i%2][j] += count[(i-1)%2][k];
		}
		count[i%2][j]%=100000000;
	}
}
for(j = 0;j<(1<<m);j++){
	ret += count[(n-1)%2][j];
	ret %=100000000;
}
printf("%lld",ret);



}

五、P3092

P3092

预处理+二分(避免tle)

计算每个状态能购买的最多数目。

#include<stdio.h>
#include<stdlib.h>
#define maxk 16
#define maxn 1000005
int worth[maxk],cost[maxn],k,n,countone[(1<<maxk)+1],sum[maxn] = {0};
int state[(1<<maxk)+1] = {0};//
long long int retu = 0;
int max(int a,int b){
	return a>b?a:b;
}
int count_one(int a){
	int i,ret =0;
	for(i = 0 ;i<maxk+1;i++){
		ret +=a%2;
		a/=2; 
	}
	return ret;
}
long long int calculate_left(int a){
	int i;
	long long int ret = 0;
	for(i = 0;i<k;i++){
		if((a&(1<<i))==0){
			ret +=worth[i];
		}
	}
	return ret;
}
int find_largestpos(int s,int w){
	int  start = s,end = n,mid ;
	while(start<=end){
		mid = (start+end)/2;
		
		 if(sum[mid]>w){
			end = mid -1;
			continue;
		}else if(sum[mid]<w){
			start = mid+1;
			continue;
		}else {
			return mid;
		}
	}
		return end;
	
}
int pos[(1<<maxk)+1];
 int main(){
int i,j,l,tmp,m;
long long int cc;
scanf("%d%d",&k,&n);
for(i = 0;i<maxk;i++){
	pos[(1<<i)] =  i;
}
for(i = 0 ;i<k;i++){
	scanf("%d",&worth[i]);
}
for(i =1;i<=n;i++){
	scanf("%d",&cost[i]);
	sum[i] = sum[i-1]+cost[i];
}

for(i = 1 ;i<(1<<k);i++){
	countone[i] = count_one(i);
	if(countone[i]==1){
		state[i] = find_largestpos(1,worth[pos[i]]);
		if(state[i]==n){
			retu = max(retu,calculate_left(i));
			
		}
		continue;
	}
	for(m= 0;m<k;m++){
		if(((1<<m)&i)==0)continue;
		j = ((1<<m)^i);
		if(state[j]==n){
			state[i] =n;
			break;
		}
		tmp = state[j];	
		state[i] = max(state[i],find_largestpos(tmp+1,sum[tmp]+worth[pos[i-j]]));	
		if(state[i]==n){
			retu = max(retu,calculate_left(i));
			
		}
	}

	
	
}
  
  if(state[(1<<k)-1]<n)printf("-1");
  else{
  	printf("%lld",retu);
  }


}

总结

花了三四天时间总算肝完了这个题单的,挺有趣的其实。总而言之,状态压缩可以说是dp的一种实现形式,能够处理更复杂的情况,难点在于如何存储状态以及状态转移方程的建立。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值