2013寒假练习 1022 Mondriaan's Dream

本文介绍了一种使用状态压缩动态规划解决1*2砖块填充m*n矩阵的方法总数问题的思路和实现过程。通过将每行状态表示为二进制数,并利用记忆化搜索避免重复计算,实现了高效的解决方案。

地址:http://acm.bit.edu.cn/mod/programming/view.php?id=673

题意:求用1*2的砖填满m*n的矩阵的方法总数,对称的也算。

状态压缩DP。。由于接触的比较少是模仿别人AC代码写的。

首先可以简化这道题的关键之一是可以把每一行的状态记成101000(最多11位)之类的二进制表示,其中0表示该行的这一位要么是上一行的竖块占掉的,要么是这一行横放的占掉的,这两种都是不会影响下一行的情况。1表示这个位置放了一个竖块,所以会影响到下一行。那么,一旦确定了每一行的二进制表示,整个矩阵的放法也就唯一确定。(重要)。

我们设一个虚拟的第0行。那么该行的合法状态只有0一种。(即00000)每行可能的状态只有0到2^n-1(即从00..000到11....111),而根据上一行的状态可以推出下一行的合法状态,因为上一行和下一行状态合法(不冲突)需满足一定条件(见代码)。因此通过类似dfs的搜索可以从第0行推到第m行。第m行也是特殊的,其合法状态也只有0这一种。

如果第m-1行的状态和第m行的“0”不冲突,那么就返回1,否则返回0,然后层层返回回去,得到dfs(0,0)的答案。

但是这样有很多重复计算会超时,所以就用状态压缩的dp加记忆化搜索优化,即得出第k行状态为a的方法数的答案后,存入dp[a][k]中,下次就不用再递归搜索了。

参考的代码地址:http://www.cnblogs.com/goagain/archive/2012/12/05/2802772.html

我的代码:

#include<iostream>
#define ll long long
int m,n;
ll dp[(1<<11)+5][12];
bool judge(int a,int b)               //判断相邻两行,状态分别为a和b,是否合法
{
	bool ans=0;
	if(a&b) return false;               //若a与b不为0,说明两行在某相同位置均为1,不合法
	int k=a|b;                       //k中为0的位置即是下面那一行横放产生的0,必须满足连续偶数个出现
	for(int i=0;i<n;k/=2,i++)        //统计所有n位连续的0
	{ 
		if(k%2==0) ans=!ans;         //当前位是0, 0的奇偶计数器ans累加
        else                         //当前位是1
		{
			if(ans) return false;        //若连续的0奇数个,不合法
			else ans=0;              //否则继续计数(本行可省)
		}
	}
	return !ans;             //若最后剩下的0奇数个,不合法
}
ll dfs(int sta,int cen)              //记忆化搜索,参数为当前层数的状态及当前层数(层从1开始编号)
{
	if(cen==m-1) return judge(sta,0);
	if(dp[sta][cen]) return dp[sta][cen];
	for(int i=0;i<(1<<n);i++)
	{
		if(judge(sta,i)) dp[sta][cen]+=dfs(i,cen+1);
	}
	return dp[sta][cen];
}
int main()
{
	while(scanf("%d%d",&m,&n),m|n)
	{
		memset(dp,0,sizeof(dp));
		if(m*n%2==1) printf("0\n");
		else printf("%lld\n",dfs(0,0));  //(第“0”层状态一定为0)
	}
	return 0;
}


 

六、状压DP的优化技巧 6.1 预处理合法状态 很多问题中,大部分状态是不合法的,可以预先筛选: cpp vector valid_states; for (int state = 0; state < (1 << n); ++state) { if (check(state)) { // 检查state是否合法 valid_states.push_back(state); } } 6.2 滚动数组优化 当状态只依赖前一个阶段时,可以节省空间: cpp vector<vector> dp(2, vector(size)); // 只保留当前和上一个状态 int now = 0, prev = 1; for (int i = 1; i <= n; ++i) { swap(now, prev); for (auto& state : valid_states) { dp[now][state] = 0; // 清空当前状态 // 状态转移… } } 6.3 记忆化搜索实现 有时递归形式更直观: cpp int memo[1<<20][20]; // 记忆化数组 int dfs(int state, int u) { if (memo[state][u] != -1) return memo[state][u]; // 递归处理… return memo[state][u] = res; } 七、常见问题与调试技巧 7.1 常见错误 位运算优先级:总是加括号,如(state & (1 << i)) 数组越界:状态数是2ⁿ,不是n 初始状态设置错误:比如TSP中dp[1][0] = 0 边界条件处理不当:如全选状态是(1<<n)-1,不是1<<n 7.2 调试建议 打印中间状态:将二进制状态转换为可视化的形式 cpp void printState(int state, int n) { for (int i = n-1; i >= 0; --i) cout << ((state >> i) & 1); cout << endl; } 从小规模测试用例开始(如n=3,4) 使用assert检查关键假设 八、学习路线建议 初级阶段: 练习基本位操作 解决简单状压问题(如LeetCode 464、526题) 中级阶段: 掌握经典模型(TSP、棋盘覆盖) 学习优化技巧(预处理、滚动数组) 高级阶段: 处理高维状压(如需要同时压缩多个状态) 结合其他算法(如BFS、双指针) 九、实战练习题目推荐 入门题: LeetCode 78. Subsets(理解状态表示) LeetCode 464. Can I Win(简单状压DP) 中等题: LeetCode 526. Beautiful Arrangement LeetCode 691. Stickers to Spell Word 经典题: POJ 2411. Mondriaan’s Dream(棋盘覆盖) HDU 3001. Travelling(三进制状压) 挑战题: Codeforces 8C. Looking for Order Topcoder SRM 556 Div1 1000. LeftRightDigitsGame2 记住,掌握状压DP的关键在于: 彻底理解二进制状态表示 熟练运用位运算 通过大量练习培养直觉 希望这份超详细的教程能帮助你彻底掌握状压DP!如果还有任何不明白的地方,可以针对具体问题继续深入探讨。 请帮我转成markdown语法输出,谢谢
最新发布
08-13
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值