算法自学__状态压缩动态规划

例1 炮兵阵地

所谓状态压缩, 就是用二进制的性质来表示状态.
比如说, 在本题中, 若某一行为HHPPHPPH, 则可以用二进制数11001001表示
若某一行为炮空炮炮空炮空空, 则可以用二进制数10110100表示

  • 定义状态dp[line][i][j]: line表示当前行的序号(实际上只需要0, 1, 2), i表示当前行的炮的排列, j表示上一行的炮的排列.
  • 定义数组sum[], 用以存储每一种炮的排列中包含的炮的总数
  • 状态转移: i为当前行的炮排列, j为上一行的炮排列, k为上上行的炮排列.
	dp[line%3][i][j]=max(dp[line%3][i][j],dp[(line-1)%3][j][k]+sum[i])
  • 我们在设计算法时, 对于i, j, k可以采用枚举法, 即遍历00...0 ~ 11...11的每一个取值, 此时显然i, j, k的组合并不一定符合题意. 我可以从如下角度判断:
    1. 炮排列是否满足地形条件: 若i&map[line] == 1则不符合, j, k情况类似
    2. 某一行中是否存在炮的左右距离过近的情况: 若i&(i<<1) == 1i&(i<<2) == 1则不符合条件, j, k情况类似
    3. 当前行的炮排列是否与前两行冲突: 若i&j == 1i&k == 1j&k == 1则不符合条件.
代码实现
#include<iostream>
using namespace std;

int map[105];
int dp[3][1<<10][1<<10];
int sum[1<<10];
int n,m,ans=-1;
char x;

int getSum(int x){
	int cnt=0;
	while(x!=0){
		if(x&1) cnt++;
		x>>=1;
	}
	return cnt;
}

int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++){
		for(int j=0;j<m;j++){
			cin>>x;
			map[i]<<=1;
			if(x=='H') map[i]+=1;	
		}
	}
	for(int i=0;i<(1<<m);i++) sum[i]=getSum(i);
	for(int i=0;i<(1<<m);i++){
		if(i&map[0]||i&(i<<1)||i&(i<<2)) continue;
		dp[0][i][0]=sum[i];
	}
	for(int i=0;i<(1<<m);i++){
		if(i&map[1]||i&(i<<1)||i&(i<<2)) continue;
		for(int j=0;j<(1<<m);j++){
			if(j&map[0]||j&(j<<1)||j&(j<<2)||j&i) continue;
			dp[1][i][j]=sum[i]+sum[j];
		}
	}
	for(int line=2;line<n;line++){
		for(int i=0;i<(1<<m);i++){
			if(i&map[line]||i&(i<<1)||i&(i<<2)) continue;
			for(int j=0;j<(1<<m);j++){
				if(j&map[line-1]||j&(j<<1)||j&(j<<2)||j&i) continue;
				for(int k=0;k<(1<<m);k++){
					if(k&map[line-2]||k&(k<<1)||k&(k<<2)||j&k||i&k) continue;
					dp[line%3][i][j]=max(dp[line%3][i][j],dp[(line-1)%3][j][k]+sum[i]);
				}
			}
		}
	}
	for(int i=0;i<(1<<m);i++){
		for(int j=0;j<(1<<m);j++){
			ans=max(ans,dp[(n-1)%3][i][j]);
		}
	}
	cout<<ans;
	return 0;
}

改进: 设置一个数组a[], 保存所有符合i&(i<<1) == 1i&(i<<2) == 1i, 避免大量的重复计算.

#include<iostream>
#include<algorithm>
using namespace std;

int n,m;
int graph[101];
int dp[3][65][65];
int sum[1<<10];
int a[65];
int cnt=0;
char c;
int _max=0;

int get_sum(int x){
	int cnt=0;
	while(x!=0){
		if(x&1) cnt++;
		x>>=1;
	}
	return cnt;
}
int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++){		//压缩地图 
		for(int j=1;j<=m;j++){
			cin>>c;
			if(c=='H') graph[i]|=1;
			if(j!=m) graph[i]<<=1;
		} 
	}
	for(int i=0;i<(1<<m);i++){
		if(i&i<<1||i&i<<2) continue;
		a[cnt++]=i;
		sum[i]=get_sum(i);
	} 
	for(int i=0;i<cnt;i++){
		if(a[i]&graph[0]) continue;
		dp[0][i][0]=sum[a[i]];
	} 
	for(int i=0;i<cnt;i++){
		if(a[i]&graph[1]) continue;
		for(int j=0;j<cnt;j++){
			if(a[j]&graph[0]||a[j]&a[i]) continue;
			dp[1][i][j]=sum[a[i]]+sum[a[j]];
		}
	}
	for(int line=2;line<n;line++){
		for(int i=0;i<cnt;i++){
			if(a[i]&graph[line]) continue;
			for(int j=0;j<cnt;j++){
				if(a[j]&graph[line-1]||a[i]&a[j]) continue;
				for(int k=0;k<cnt;k++){
					if(a[k]&graph[line-2]||a[i]&a[k]||a[j]&a[k]) continue;
					dp[line%3][i][j]=max(dp[line%3][i][j],dp[(line-1)%3][j][k]+sum[a[i]]);
				}
			}
		}
	}
	for(int i=0;i<cnt;i++){
		for(int j=0;j<cnt;j++){
			_max=max(_max,dp[(n-1)%3][i][j]);
		}
	}
	cout<<_max;
	return 0;
}

例2 互不侵犯

  • 定义状态dp[k][i][s]: 在仅考虑前k行的情况下, 第k行的国王排列方式为i, 前k行的国王总数为s情况数
  • 状态转移: 设上一行的状态为j, dp[k][i][s] = sum(dp[k-1][j][s-sum[i])
  • 类似例1, 我们同样需要枚举i, j的所有情况, 然后排除掉不和题目要求的情况(判断条件见代码)
代码实现
#include<iostream>
using namespace std;

long long dp[10][1<<9][100];
int n,m;
int sum[1<<9];
long long ans=0;

int getSum(int x){
	int cnt=0;
	while(x!=0){
		if(x&1) cnt++;
		x>>=1;
	}
	return cnt;
}


int main(){
	cin>>n>>m;
	for(int i=0;i<1<<n;i++){
		sum[i]=getSum(i);
	}
	for(int i=0;i<1<<n;i++){
		if(i&(i<<1)) continue;
		dp[0][i][sum[i]]++;
	}
	for(int k=1;k<n;k++){
		for(int i=0;i<1<<n;i++){
			if(i&(i<<1)) continue;	
			for(int j=0;j<1<<n;j++){
				if(j&(j<<1)||j&i||j&(i<<1)||j&(i>>1)) continue;
				for(int s=0;s<=m;s++){
					dp[k][i][s]+=dp[k-1][j][s-sum[i]];
				}
			}
		}
	}
	for(int i=0;i<1<<n;i++) ans+=dp[n-1][i][m];
	cout<<ans;
	return 0;
}

例3 愤怒的小鸟

题目分析
  • 设抛物线的表达式为y=ax^2+bx, 待定系数ab可通过两组x, y来确定.
  • 我们任选两只小鸟ij, 通过两只小鸟的坐标求出对应抛物线的a, b, 注意: a应该小于0, 然后我们遍历所有小鸟, 找出所有在这条抛物线上的小鸟. 由此, 我们得到了一个压缩的二进制数, 记录在line[i][j]中. 我们可以想象到, 在某些特殊的题目设定下, 有些小鸟可能不能与任何其他小鸟构成抛物线.
  • 我们将小鸟存活状态定义为i, 枚举i ∈ [00...00, 11...11], 定义start[]数组, start[i]表示第一个目前未被击中的小鸟的序号.
  • 状态转移: 对状态dp[i], 令j = start[i]. 为了避免重复计算, 我们可以仅更新经过j的状态. 于是dp[i|(1<<(j-1))]=min(dp[i|(1<<(j-1))],dp[i]+1), dp[i|line[j][k]]=min(dp[i|line[j][k]],dp[i]+1).
代码实现
#include<iostream>
#include<string.h>
#include<cmath>
using namespace std;

const double err=1e-8;

class BIRD{
public:
	double x;
	double y;
};
BIRD bird[20];
int line[20][20];
int dp[1<<18];
int start[1<<18];
int T, n, m;

void cal(double& a, double& b, int i, int j){
	double x1=bird[i].x, x2=bird[j].x;
	double y1=bird[i].y, y2=bird[j].y;
	a=(y1*x2-y2*x1)/(x1*x1*x2-x2*x2*x1);
	b=(y1*x2*x2-y2*x1*x1)/(x1*x2*x2-x2*x1*x1);
}

int getStart(int x){
	for(int j=1;j<=18;j++){
		if(!(x&(1<<(j-1)))) return j;
	}
}

int main(){
	cin>>T;
	for(int i=0;i<1<<18;i++) start[i]=getStart(i);
	for(int a=1;a<=T;a++){
		memset(bird,0,sizeof(bird));
		memset(dp,127,sizeof(dp));
		memset(line,0,sizeof(line));
		dp[0]=0; 
		cin>>n>>m;
		for(int i=1;i<=n;i++){
			cin>>bird[i].x>>bird[i].y;
		}	
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				if(fabs(bird[i].x-bird[j].x)<=err) continue;
				double a, b;
				cal(a,b,i,j);
				if(a>err) continue;
				for(int k=1;k<=n;k++){
					if(fabs(bird[k].y-a*bird[k].x*bird[k].x-b*bird[k].x)<err){
						line[i][j]|=1<<(k-1);
					}	
				}
			}
		}
		for(int i=0;i<1<<n;i++){
			int j=start[i];
			dp[i|(1<<(j-1))]=min(dp[i|(1<<(j-1))],dp[i]+1);
			for(int k=1;k<=n;k++){
				dp[i|line[j][k]]=min(dp[i|line[j][k]],dp[i]+1);
			}
		}
		cout<<dp[(1<<n)-1]<<'\n';
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值