实战详解状压DP

本文详细介绍了状压DP的概念、解题思路和C++实现,包括前置知识、状态压缩的应用、模板题ACwing-蒙德里安的梦想和进阶题洛谷-P1896互不侵犯的解法,强调了在状态表示和合法状态筛选中的关键技巧。
摘要由CSDN通过智能技术生成

前置知识

1.二进制:根据二进制的总位数n,可以表示[0~2^n)的任意数

2.二进制运算:&即按位与,&中有0就为0;|即按位或,|中有1就为1;<<即将所有位向左移位,如1<<2表示1左移2位即2^2;>>为向右移位,与<<同理

3.DP:动态规划,建议先将简单的动态规划如背包先熟悉

概念与套路

1.状态压缩:简称状压,就是将多个状态用二进制来表示,而且通常可以排除掉一些不合法的状态从而降低时间复杂度

2.状压DP本质:本质上仍然是DP,而DP本质仍然是暴力,只不过可以用空间换时间减少重复运算的时间,比暴力好一点

3.状压DP通常是先预处理得到合法的状态,然后在循环中利用合法状态进行递推,而且经常性根据递推公式来初始化DP初值,初值也可能没有什么意义,仅仅只是方便得出递推的结果

模板题:ACwing-蒙德里安的梦想

在这里插入图片描述

解题思路

1.从题目给的例子开始分析,显然长方形要么竖放要么横放,也就是说只有两种状态,但对应到格子中,假如长方形竖放,这只会影响到当前列,而横放会影响到两列,所以这里可以看出不同列之间的限制关系

2.考虑到不同列之间的限制关系,而且长方形只有两种摆放状态,可以将横放长方形的第一个格子设为1,竖放的长方形的两个格子设为0,这样也很清楚横放长方形的第二个格子也一并设为0,这样对于同一列就存在二进制状态表示,这显然应该察觉这是一个状态压缩DP,因为它既拥有DP的递推关系,而且状态表示仅有0和1

3.在纸上模拟所有的状态,观察规律并挑出合法状态,假如N=3,显然存在8种状态,其中000,010,011,101,110是不合理的。总结规律可以发现,对于连续奇数个0是非法的,这很好理解,因为竖放必然是连续两个0;此外,针对于010,可以发现并非是连续奇数个0,但实际上用1隔开左右都是连续奇数个0,所以能够发现可以在统计0的个数时在1处结算;但这样000就没办法结算了,所以可以在判断完所有的位时进行判断结算

解题方法

1.状态定义:由于发现了第一列会对第二列造成影响,一直会影响到最后一列,所以定义dp[i][j]表示第i列j状态下的方案数

2.递推公式:dp[i][j] += dp[i-1][k],第i列状态j下的方案数必然是由前一列i-1状态j下的方案数得到,而且由于前一列可能有多个合法状态,所以需要累加

3.结果:由于最后一列必然不存在格子的状态为1,毕竟最后一列不可能横放长方形,所以结果是dp[n][0]

4.不同列之间的状态合并:对于二进制的状态合并,无非就是&和|两种,尽管在预处理中已经挑出了合法状态,但实际预处理也只挑出了单列的合法状态;对于相邻的两列需要通过合并为一列的方式来排除不合法状态,比如001和100,尽管在单列中这两个状态是合法的,但合并后就不合法,可以在纸上模拟一下就知道合并的意义在于判断是否有重叠

5.容易忽略的点:实际上,由于格子只有0和1两种状态,所以在没有摆放任何长方形的时候格子状态也默认为0,这也就意味着对于0既可以表示竖放的格子,也可以表示横放的第二个格子,还可以表示空格,所以在排除不合法状态时还需要&运算排除,这样就能考虑清楚所有情况了

C++代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2048;

int n,m;
bool st[N];
ll f[12][N];

//当前列的状态是否合法 
void isvalid(){
	for(int i=0; i< 1<<n; ++i){		//总共是0~2^n-1个状态,即<2^n 
		st[i] = true;
		int cnt= 0;					 //统计所有位的0的个数 
		for(int j=0; j<n; ++j){		//从低位到高位,第j位 
			if(i>>j & 1){			//将第j位移动到最低位和1按位与判断第j位是否是奇数 
				if(cnt & 1){		//对于1之间的连续0的个数为奇数,是不合法状态
					st[i] = false;
					break;
				}
			}
			else	cnt++;
		}
		if(cnt & 1)	st[i] = false;	//如果先前的1之间的0个数为偶数,但总0的个数为奇数,也是不合法的 
	}
}

int main(){
	while(cin>>n>>m && n || m){
		isvalid();									//n每轮都在变,所以需要反复预处理 
		memset(f, 0, sizeof(f));
		f[0][0] = 1;
		for(int i=1;i<=m; ++i){						//第i列 
			for(int j=0; j< 1<<n; ++j){				//第i列的状态j 
				for(int k=0; k< 1<<n; ++k){			//第i-1列的状态k 
					if((j & k)==0 && st[j | k]){	//合并列时没有重叠1,也没有总的奇数个0即合法 
						f[i][j] += f[i-1][k];
					}
				}
			}
		}
		cout << f[m][0] << endl;
	}
}

进阶题:洛谷-P1896互不侵犯

在这里插入图片描述

解题思路

1.有了模板题的基础,这道题一眼就能看出来是状压DP,因为对于每个国王有且仅有放与不放两种状态,而且国王的状态与格子的状态是一一对应的,直接免去了分析,可以定义格子状态为1即放下国王,为0即没有放下国王

2.题目中非常直接地提到了国王之间的限制关系,先写出一行的合法状态,当N=3时,有8个状态,其中有000、001、010、100、101这5种合法状态

3.对于相邻的两行(习惯上是比较行,列实际也可以),观察不同的5种状态间的合并,但此时不能再依靠模板题的思路,直接使用&和|来判断已经无法囊括所有情况,不妨从题目的意思来表示;对于001和010,可以想到移位运算,只需移动一位再&==1即可知道上一行的国王的左下和右下不能放置国王;对于100和101,属于是上下国王相邻,显然直接&==1即可知道上下不能放置国王

解题方法

1.状态定义:最直接的想法就是定义dp[i][j][x]表示第0行-第i行第j个状态放置国王的数量x的方案数

2.递推公式:由于先前行与这一行的关系,dp[i][j][x] += dp[i-1][k][x-num[j]],这里其实可以类比背包DP,在这一行放下x个,那么在之前的行就得减少x个

3.预处理:这里由于国王的数量是有限的,所以预处理不仅要得到合法的状态,而且还要知道合法状态中国王的数量

4.初值:由于预处理已经得到了国王的合法状态数量,所以可以将dp[0][1][0]=1表示在第0行第1个状态放下放下国王数量为0的方案数为1,不用去深究为什么这样设置初值,仅仅只是方便后序的结果

5.结果:由于在最后一行已经放下所有国王,但实际并不知道国王放置的位置,所以还需要遍历所有的合法状态得到方案数,即dp[n][i][g]

C++代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 10, M = 1<<11;

ll num[M];
ll st[M]; 
ll dp[N][M][90];
int n,g,s0;

void isvalid(int m){
	s0=0;
    for(int i = 0; i < m; ++i){
        if((i & (i << 1)) || (i & (i >> 1))) continue;
        int cnt = 0;
        for(int j = 0; j < n; ++j){
            if(i & (1 << j)) cnt++;
        }
        st[++s0] = i; // 记录合法状态 
        num[s0] = cnt; // 记录状态i的国王数 
    }
}

int main(){
    scanf("%d %d", &n, &g);
    int m = 1 << n;
    isvalid(m);
    dp[0][1][0] = 1;
    for(int i = 1; i <= n; ++i){
        for(int j = 1; j <= s0; ++j){
            for(int k = 1; k <= s0; ++k){
                if(((st[j] & st[k]) == 0) && ((st[j] & (st[k] << 1)) == 0) && ((st[j] & (st[k] >> 1)) == 0)){	//合法判断 
                    for(int x = 0; x <= g; ++x){
                        if(x >= num[j]){
						 	dp[i][j][x] += dp[i - 1][k][x - num[j]]; // 类似背包 
							printf("dp[%d][%d][%d] += dp[%d][%d][%d]\n", i,j,x, i-1,k,x-num[j]); 
						}
                    }
                }
            }
            printf("\n");	//提交代码时注意删除
        }
    }
    ll res = 0;
    for(int i = 1; i <= s0; ++i){
        res += dp[n][i][g];
    }
    printf("%lld", res);
    return 0;
}

总结

1.针对状压DP,题目表现上有很明显的特征,基本是状态的二元性、数据范围比较小(通常是20以内),存在前后关系并且可以通过观察前后关系大致得出递推公式

2.状压DP思路上也比较模板化,通常是先预处理得出合法状态,并且根据题目要求做额外的操作,比如上面一道题需要统计合法状态的国王数量;然后在主循环中先遍历行或列,再枚举每个合法状态,然后根据题意表示合并行或列,再完成递推

3.结果上比较不明确,需要依据题意来表示

后话

本人才疏学浅,如文章有不合理的地方,希望大佬能在评论区留下宝贵的意见

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值