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

题目及其大意

洛谷题目链接

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

状态表示

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

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

例如,下面的情况,绿色格子代表艹,白色则是没有种草的地方:
在这里插入图片描述
这个状态就可以表示为: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<iostream>
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);
	}
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: p109 [noip2004 提高组] 合并果子: 这道题目是一道经典的贪心算法题目,题目大意是给定n个果子,每个果子的重量为wi,现在需要将这n个果子合并成一个果子,每次合并需要消耗的代价为合并的两个果子的重量之和,求最小的代价。 我们可以使用贪心算法来解决这个问题,每次选择两个最小的果子进行合并,然后将合并后的果子的重量加入到集合中,重复这个过程直到只剩下一个果子为止。 这个算法的正确性可以通过反证法来证明,假设存在一种更优的合并方案,那么这个方案一定会在某一步将两个比当前选择的两个更小的果子进行合并,这样就会得到一个更小的代价,与当前选择的方案矛盾。 usaco06nov fence repair: 这道题目是一道经典的贪心算法题目,题目大意是给定n个木板,每个木板的长度为li,现在需要将这n个木板拼接成一块长度为L的木板,每次拼接需要消耗的代价为拼接的两个木板的长度之和,求最小的代价。 我们可以使用贪心算法来解决这个问题,每次选择两个最小的木板进行拼接,然后将拼接后的木板的长度加入到集合中,重复这个过程直到只剩下一个木板为止。 这个算法的正确性可以通过反证法来证明,假设存在一种更优的拼接方案,那么这个方案一定会在某一步将两个比当前选择的两个更小的木板进行拼接,这样就会得到一个更小的代价,与当前选择的方案矛盾。 ### 回答2: 题目描述: 有n个果子需要合并,合并任意两个果子需要的代价为这两个果子的重量之和。现在有一台合并机器,可以将两个果子合并成一堆并计算代价。问将n个果子合并成一堆的最小代价。 这个问题可以用贪心算法来解决,我们可以使用一个最小堆来存储所有果子的重量。每次从最小堆中取出两个最小的果子,将它们合并成为一堆,并将代价加入答案中,将新堆的重量加入最小堆中。重复以上步骤,直到最小堆中只剩下一堆为止。这样得到的代价就是最小的。 证明如下: 假设最小堆中的果子按照重量从小到大依次为a1, a2, ..., an。我们按照贪心策略,每次都将重量最小的两个果子合并成为一堆,设合并的过程为b1, b2, ..., bn-1。因此,可以发现,序列b1, b2, ..., bn-1必然是一个前缀和为a1, a2, ..., an的 Huffman 树变形。根据哈夫曼树的定义,这个树必然是最优的,能够得到的代价最小。 因此,使用贪心策略得到的答案必然是最优的,而且时间复杂度为O(n log n)。 对于[usaco06nov] fence repair g这道题,其实也可以用相同的思路来解决。将所有木板的长度存储在一个最小堆中,每次取出最小的两个木板长度进行合并,代价即为这两个木板的长度之和,并将合并后木板的长度加入最小堆中。重复以上步骤,直到最小堆中只剩下一块木板。得到的代价就是最小的。 因此,贪心算法是解决这类问题的一种高效、简单但有效的方法,可以应用于很多有贪心性质的问题中。 ### 回答3: 这两个题目都需要对操作进行模拟。 首先是合并果子。这个题目先将所有果子放进一个优先队列中。每次取出来两个果子进行合并,直到只剩下一个果子即为答案。合并的代价为两个果子重量之和。每次合并完之后再将新的果子放入优先队列中,重复上述过程即可。 再来看fence repair。这个题目需要用到贪心和并查集的思想。首先将所有板子的长度放入一个最小堆中,每次取出堆顶元素即为最短的板子,将其与其相邻的板子进行合并,合并的长度为这两块板子的长度之和。操作完之后再将新的板子长度放入最小堆中,重复上述过程直到只剩下一块板子。 关于合并操作,可以使用并查集来实现。维护每个板子所在的集合,每次操作时合并两个集合即可。 最后,需要注意的是题目中给出的整数都很大,需要使用long long来存储避免溢出。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值