状压dp

这个真的不会,连状压这种思想都不太会……

-----------------------------------------------------------------------------------------------------------

简单来说,状压就是把一段需要进行决策的状态用二进制0/1的方式变成一个数。

一般适用于数据规模小、状态多的情况。

进行状态转移就要用位运算了。

常规操作:

1.地图初始化

一般都是把每行的状态表示成一个数。

以炮兵阵地为例子:

for(int i=0;i<n;++i)
	for(int j=0;j<m;++j){
		cin>>c;
		map[i]<<=1; //二进制左移一位 
		if(c=='H') map[i]+=1; //如果是H标记为1
	}

2.dp状态设计

这个其实变化很多,一般都有 dp(i,j) 表示第i行状态为j时的最大方案数。

有时候需要滚动数组优化,这个也只能多做题了。

3.状态的转移

一般都是比较 原来的自己、前一个状态+增长,取较大值。

例题:P2704 [NOI2001] 炮兵阵地

模板题,适合状压入门。

做法:枚举状态,转移。

嗯?dp这么简单?

其实感觉dp最难的不在于某一部分,在于状态设计、初始化、状态转移、统计答案这些步骤合起来的思维难度。

 

#include<iostream>
using namespace std;
#define N 114514
#define M 1919810
#define ll long long
ll n,m;
char c;
ll map[N]; //二进制表示地图初始状态,每行用一个数表示
ll sum[N]; //状态为i时有多少个1
ll dp[1<<10][1<<10][3]; //dp(i,j,k) 表示第k-1行状态为i,第k行状态为j
//滚动数组优化,因为状态只涉及三行,就直接mod 3 
ll get(ll x){ //查找当前状态x中有多少个1
	ll tot=0;
	while(x){
		if(x&1) ++tot; //x二进制的最后一位& 1 
		x>>=1; //右移看下一位
	}
	return tot;
}
int main(){
	cin>>n>>m;
	for(int i=0;i<n;++i)
		for(int j=0;j<m;++j){
			cin>>c;
			map[i]<<=1; //二进制左移一位 
			if(c=='H') map[i]+=1; //如果是H标记为1
		}
	for(int i=0;i<1<<m;++i) //至多1<<m个状态
		sum[i]=get(i); 
	for(int i=0;i<1<<m;++i)
		if(!(i&map[0]||i&i<<1||i&i<<2)) //判非法条件初始化 
			dp[0][i][0]=sum[i]; //初始化dp数组 
	for(int i=0;i<1<<m;++i) //上一行状态 
		for(int j=0;j<1<<m;++j) //当前行             这里出错了,原来是i<<m和j<<m,牢记 
			if(!(i&j||i&map[0]||j&map[1]||i&i<<1||i&i<<2||j&j<<1||j&j<<1))
				dp[i][j][1]=sum[i]+sum[j];
	for(int i=2;i<n;++i) //正式dp过程
		for(int j=0;j<1<<m;++j){ //上一行 
			if(j&map[i-1]||j&j<<1||j&j<<2) continue;
			for(int k=0;k<1<<m;++k){ //当前行原来状态
				if(k&j||k&map[i]||k&k<<1||k&k<<2) continue;
				for(int l=0;l<1<<m;++l){ //上上行
					if(l&j||l&k||l&map[i-2]||l&l<<1||l&l<<2) continue;
					dp[j][k][i%3]=max(dp[j][k][i%3],dp[l][j][(i-1)%3]+sum[k]); //状态转移方程 
				}
			}
		}
	ll ans=0;
	for(int i=0;i<1<<m;++i)
		for(int j=0;j<1<<m;++j)
			ans=max(ans,dp[i][j][(n-1)%3]); //统计答案,取最后一行最大值
	cout<<ans;
	return 0;
}
//记得去括号 

虽然我是真的不想写dp,不过还是要练啊啊啊啊啊啊啊啊啊啊啊啊啊!

例题:P3694 邦邦的大合唱站队

看到M<=20,可以考虑对M状压,但怎么搞呢?

考虑将每个队伍所占有区间作为状态。

对于每一个乐队,一段区间中队员的站队情况便可看成一个状态。

可以预处理出每个乐队在完成排队后的区间长度(就是人数)。

怎么转移呢?

依次枚举乐队,把乐队的人右移到队列最后。

转移式呢,转移代价是什么呢?

提前处理了初始情况每个乐队在区间内出现人次的前缀和,那么转移代价就是 $num_j-(sum_{len,j}-sum_{len-sum_j,j})$

等一下,先放这里。

#include
using namespace std;
#define N 114514
#define M 1919810
#define ll long long
#define inf 1e9
ll n,m,a[N],l[N],sum[N][25];
ll f[1<<21];
int main(){
    cin>>n>>m;
    for (  int i=1;i<=n;++i){
        cin>>a[i];
        for (  int j=1;j<=m;++j) sum[i][j]=sum[i-1][j];   //一段区间内某个乐队的人数 
        l[a[i]]++,sum[i][a[i]]++;
    }
    memset (f,0x3f3f3f3f,  sizeof (f));
    f[0]=0;
    for (  int i=1;i<(1<

例题:P5005 中国象棋 - 摆上马

挺有趣的,这个1MB真TM有趣,于是就要用滚动数组优化了。

posted @ 2023-04-29 17:47  和蜀玩  阅读(15)  评论(0)    收藏  举报