[USACO06NOV]Corn Fields G 一道值得品味的经典状压dp

题目及其大意

洛谷题目链接

题目的大致意思就是在给定的n * m的矩形方阵内种草,有一些方块是不能种的,且种草需要满足一个条件,就是不能相邻的草方块,(上下左右),问给定的n * m的矩阵种草的方案数。(1 ≤ n,m≤ 12)

状态表示

这道题标准解法是使用状压dp,状压是状态压缩的简称,意思是将一个复杂的不好表示的状态通过算法压缩成为一个简单的,好表示的状态,便于运算和转移。通常情况是使用位运算来进行压缩和计算。

在这道题中,我们可以将一行的种草情况看作是一个状态,那么这个状态就由m个方块组成,有 2m种可能的情况,遍历起来非常的麻烦。一个朴素的压缩思想是使用哈希算法,将m个位置的种草情况用01来表示,1是种草了,0则是没有。这样,一行的状态就可以用一个m位的二进制数字
来表示。

例如,下面的情况,绿色格子代表艹,白色则是没有种草的地方:
[USACO06NOV]Corn Fields G 一道值得品味的经典状压dp_第1张图片
这个状态就可以表示为:010110101,对应的十进制数字为:181。

而前面提到的2m种情况,在该压缩算法下其实就是m位从全0到全1的过程,此时枚举这些情况只需要循环逐个遍历即可。

遍历所有的情况:

for(int i = 0;i < 1 << m;i++)//i < 1 << m 就表明i的上限就是m个1

状态处理

表示好了状态,后面就需要给出在转移状态时需要处理的相关问题的解决方案了。处理好这些问题,就可以开开心心的转移了。

判断左右相邻

想要判断某个状态是否存在左右相邻的情况,一个非常朴素的做法就是将数字一位一位的拆开,看看是否有相邻的1即可:

bool check(int k){
	int num = 0;	//上一位的状态
	for(int i = 0;i < m;i++){
		if(num & k & 1){
			return false;	//false表示这个装填不合法
		}
		num = k & 1;
		k >>= 1;
	}
	return true;	//该状态合法
}

这个方法的复杂度是O(m),但是考虑到m的范围,其实复杂度就是一个常数。但是我们总是希望有更优的解决方案,好在这道题确实有这样的方案。

考虑到检验是否存在两个1相邻,我们可以通过位运算的方式O(1)进行判定。具体的思路就是将原数左移或者右移一位,新的数字再与原数相与,如果存在相邻的1,那么结果一定不为零(1 & 1 = 1) 。这时我们的check函数就可以大大简化了:

bool check(int k){
	return k & (k << 1); //该写法与上面方法结果逻辑相反,非零是true代表不合法,反之亦然
}

判断上下相邻

当我们枚举了本行的状态j和上面一行的状态k,如何判断他们是否存在上下相邻的情况呢?

其实在解决上一个问题时就已经说出了该问题的解决方法,那就是使用与运算,如果存在上下相邻的情况,运算的结果不为零。所以判断合法的方式就是:k & j非零为不合法,零为合法。

判断非法方块

如何判断一个状态是否将艹都种到了合法的方块上了呢?其实做法还是使用与运算。

我们需要先将每行的地形表示成为一个二进制数。由于题目中允许种艹的地形是1,因此在计算的时候需要先取一下反。:

//mp[i]表示第i行的地形
for(int i = 0;i < n;i++){
	for(int j = 0,x;j < m;j++){
		mp[i] <<= 1;
		cin >> x;
		mp[i] |= !x;
	}
}

在枚举状态j时,需要先检验一下是否在该行地形下合法:

if(j & mp[i]) continue;//非零不合法

状态转移

解决完了问题,下面就可以开始转移了:

表示状态

我们使用数组dp[i][j]来表示第i行状态j的方案数

初始化

对于第一行,我们需要初始化,合法的状态初值为1否则为0:

for(int i = 0;i < 1 << m;i++){
	if(i & (i << 1) || i & mp[0])
		continue;
	dp[0][i] = 1;
}

转移状态

对于第i行(i ≠1),枚举所有的在该行合法状态j。对于每一个j,枚举上一行所有合法且不与j形成上下相邻的状态k

转移方程为:dp[i][j] = dp[i][j] + dp[i - 1][k]

for(int i = 1;i < n;i++){
	for(int j = 0;j < 1 << m;j++){
		if(j & (j << 1) || j & mp[i])
			continue;

		for(int k = 0;k < 1 << m;k++){
			if(k & (k << 1) || k & mp[i - 1] || k & j)
				continue;
			dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mo;	//mo取模数
		}
	}
}

统计答案

最终答案就是最后一行各状态的方案数总和。

long long ans = 0;
for(int i = 0;i < 1 << m;i++){
	ans = (ans + dp[n - 1][i]) % mo;
}
cout << ans;

完整代码

C++

#include
using namespace std;
int mp[20];
long long dp[20][1 << 13];
long long mo = 100000000;
int m,n;
int main(){
	cin >> n >> m;
	for(int i = 0;i < n;i++){
		for(int j = 0,x;j < m;j++){
			mp[i] <<= 1;
			cin >> x;
			mp[i] |= !x;
		}
	}
	
	for(int i = 0;i < 1 << m;i++){
		if(i & (i << 1) || i & mp[0])
			continue;
		dp[0][i] = 1;
	}
	
	for(int i = 1;i < n;i++){
		for(int j = 0;j < 1 << m;j++){
			if(j & (j << 1) || j & mp[i])
				continue;

			for(int k = 0;k < 1 << m;k++){
				if(k & (k << 1) || k & mp[i - 1] || k & j)
					continue;
				dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mo;
			}
		}
	}
	
	long long ans = 0;
	for(int i = 0;i < 1 << m;i++){
		ans = (ans + dp[n - 1][i]) % mo;
	}
	cout << ans;
}

Java

import java.util.Scanner;

public class Main {
	private static Scanner scan;

	public static void main(String[] args) {
		scan = new Scanner(System.in);
		
		int n = scan.nextInt();
		int m = scan.nextInt();
		long mo = 100000000;
		int[] mp = new int[n];
		long[][] dp = new long[n][1 << m];
		
		for(int i = 0;i < n;i++){
			for(int j = 0;j < m;j++){
				mp[i] <<= 1;
				mp[i] |= (1 - scan.nextInt());
			}
		}
		
		for(int i = 0;i < 1 << m;i++){
			if((i & (i << 1)) != 0 || (i & mp[0]) != 0)
				continue;
			dp[0][i] = 1;
		}
		
		
		for(int i = 1;i < n;i++){
			for(int j = 0;j < 1 << m;j++){
				if((j & (j << 1)) != 0 || (j & mp[i]) != 0)
					continue;

				for(int k = 0;k < 1 << m;k++){
					if((k & (k << 1)) != 0 || (k & mp[i - 1]) != 0 || (k & j) != 0)
						continue;
					dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mo;
				}
			}
		}
		
		long ans = 0;
		for(int i = 0;i < 1 << m;i++){
			ans = (ans + dp[n - 1][i]) % mo;
		}
		System.out.print(ans);
	}
}

你可能感兴趣的:(题解)