算法专题——状态压缩型动态规划


状态压缩DP的特点

​ 一般的DP是将…………,而状态压缩DP则是将状态本身进行压缩(一般是压缩成二进制的模式),然后整个放进状态方程里,在转移方程中状压DP与其他DP并没有什么大的区别,压入的状态就是可以理解为一种状态,类比背包问题中的拿与不拿两种状态,涂色问题中的涂红黄蓝三种颜色的三种状态,只不过状压DP中的压入的状态的状态数量一般会比较多,一般是一千起步之类的(具体看要压入东西的位数)

​ 状压DP中要压入的东西的每一位通常仅有两种状态,如有与无,拿与不拿。以下面的题目为例,矩阵中的一行便是要压入的一行,而一行中每一位(这里体现为每一格)只有放与不放炮兵两种状态。于是n个这样的格子拼在一起,那便组成了一行二进制数,进而可以进行压缩,放进状态方程中。

​ 为什么通常是两种状态呢,可不可以是三种状态,四种状态?仅我浅显的观点认为,之所以是01两种状态而不是三种四种状态,是因为计算机内部存储数据的格式全都是用的二进制,计算机内部也有配套的按位运算符进行相对应的计算,所以两种状态,显然更容易处理状态与状态之间的限制与制约,更容易计算。


例题


炮兵阵地


题面:

算法专题——状态压缩型动态规划


分析:

​ 这道题长得就很像要状压的亚子,我们将一行进行状态压缩,规定0为没有放炮兵,1为有炮兵,那么由于一行最多不会超过10格(上面图片没有截到),所以状压十分合理。即可以写成dp[i][sta]的亚子,表示第i行状态为sta时可以得到的最多炮兵放置数目,但是由于一个炮兵会影响后面的两行,即当前行的状态即受前面一行的影响,也受前面第二行的影响,所以中应该将2行的状态压入状态方程中,表示为dp[i][sta1][sta]

​ 接下来便是开始推导递推方程,当前行的状态会受到前面两行的状态影响以及当前是否有山地的影响,稍微想想之后就可以得到转移方程了。

for (int i = 3; i <= n; i++) {
	for (int Now = 0; Now < end; Now++) {
        if (检查当前状态是否和地形,自己本身冲突) continue; //冲突了,下一个
        for (int L = 0; L < end; L++) {
            if (检查当前状态是否与上一行状态冲突) continue; //冲突了,下一个
            for (int LL = 0; LL <= end; LL++) {
                if (检查当前状态是否与上一行状态冲突 || 检查上一行状态是否与上上一行状态冲突) continue;	//冲突了,下一个
                dp[i][Now][L] = max(dp[i][Now], dp[i - 1][L][LL] + sum[Now]); //sum[Now]存储Now状态有多少个1,需要预处理
            }
        }
    }
}

​ 由于空间限制,需要开滚动数组,开三行就足够了。


摆上马


题面:

算法专题——状态压缩型动态规划


分析:

​ 本质上和炮兵阵地是差不多的,但是要更多的考虑特殊状态,这道题中体现为撇马脚

​ 显然,可以直接得到转移方程,dp[i][N][L]表示第i行,当前行状态为N,上一行状态为L,的最大方案数。

​ 我们先得到递推方程,检测是否合理的方法先用函数封装起来。

for(int i = 3; i <= x; ++i) {
    for(int j = 0; j < end; ++j) {
        for(int k = 0; k < end; ++k) {
            if(j与k冲突)	continue;
            for(int s = 0; s < end; ++s) {
            	if(s与k冲突||s与j冲突)	continue;
                dp[i][j][k] += dp[i-1][k][s];
                dp[i][j][k] %= mod;
            }
        }
    }
}

​ 检测冲突函数的讨论:递推方程中,当前行不会受到同一行马的制约,会受到上一行马的制约(会撇马脚),会受到上上一行马的制约(会撇马脚),所以我们可以得到检测当前行与上一行是否会冲突的函数Check1(摘自题解)

bool Ch1(int k1,int k2,bool I)
{
	int T= (~(((k1>>1)&k1)>>1)) , K ; K =(T&k2);
	if((k1>>2)&K) return 0;
	T= (~(((k1<<1)&k1)<<1)) ; K=(T&k2);
	if((k1<<2)&K) return 0;
	if(I) return 1;
	  else return 1&Ch1(k2,k1,1);
}

同时得到Check2函数(摘自题解)

bool Ch2(int k1,int k2,int k3,bool I)
{
	int T= (~((k1&k2)>>1)) , K; K=(T&k3);
	if((k1>>1)&K) return 0;
	T= (~((k1&k2)<<1)); K=(T&k3);
	if((k1<<1)&K) return 0;
	if(I) return 1;
	  else return 1&Ch2(k3,k2,k1,1);
}

一双木棋Chess


题面:

算法专题——状态压缩型动态规划


分析:

​ 由于下子必须要左边有棋子,上边也有棋子,所以没下一步棋得到的旗面都一定是一个这样的形状,又棋盘并不是很大,即使对于每一步都进行搜索也不会超时,所以我们将棋盘可能出现的每一个状态都压入状态方程中国,那么要怎么压?,那么如何让搜索的过程不会进行重复计算。

​ 其中一个办法是轮廓线的方法,见下面引用(来自题解)

那么,可以证明,任何时候,棋盘上的棋子都是一个连续且单调的的左上三角形,所有我们可以用一个轮廓线来表示三角形的右下边界,这样就可以表达棋盘的状态了

不妨用 11 表示竖着的轮廓边,00 表示横着的轮廓边。
从左下角开始,任何一个轮廓线都可以表达为长度为 n + m,且拥有n个1,m个0的0101串

令 SS 表示一条从左下到右上的轮廓线,令 f[S] 表示这个轮廓线的状态距离游戏结束还能得多少分,可以得到边界条件 f[11⋯1100⋯00]=0,最终的答案便是 f[00⋯0011⋯11]

​ 上面已经提到,将棋盘可能出现的每一个状态压入状态方程之后可以得到f[i],表示当前棋盘状态距离结束最多还可以获得的分数。

​ 至于转移方程,即怎么从一个棋盘状态转移到另一个棋盘状态,可以使用记忆化搜索,直接看代码吧(摘自题解。

int dfs(int sta, bool who, int n, int m) {
	if (~f[sta]) return f[sta];
	f[sta] = who ? -oo : oo;
	int x = n, y = 0;						//从左下角填到右上角
	for (int i = 0; i < n + m - 1; i++) {
		if (sta >> i & 1) x--; else y++;	
		if ((sta >> i & 3) != 1) continue; // 可以转移的部分一定是01交界处,否则跳过
		int nxt = sta ^ (3 << i);	//每一次转移的本质在于01的位置交换(轮廓线变换的数据化表达
		if (who) 
			f[sta] = max(f[sta], dfs(nxt, who ^ 1, n, m) + a[x][y]);
		else 	
			f[sta] = min(f[sta], dfs(nxt, who ^ 1, n, m) - b[x][y]);
	}	
	return f[sta];			
}
\\答案为 dfs((1 << n) - 1, 1, n, m) 
上一篇:2021牛客暑期多校训练营2 - F - Girlfriend ( 球体积相交 )


下一篇:【NOIP2013中秋节模拟】表白(love) 题解