POJ 2411: Mondriaan's Dream

本文详细介绍了如何通过二进制枚举子集的方法优化骨牌覆盖问题的解决策略,包括基本算法原理、代码实现及具体应用案例。

题目链接:http://poj.org/problem?id=2411


题意:

用1*2的骨牌填充一个n*m的网格。

求方案数。


算法:

这道题用普通的状态压缩DP的话,打表也可以过。

也就是用一个m位二进制数,0表示上一行的这个位置未被覆盖,1表示被覆盖。

然后逐行转移。

不过我用朴素的状态压缩优化到死的时候,也就知道这题一定有别的解法。


先来介绍一种用二进制枚举子集的方法,是zkc同学之前教给我的。

假设有一个集合S,它的元素是0~(k-1)中的一部分。

用一个k位二进制数mask代表这个集合。

mask的第x位为1表示x在这个集合中,

否则x不在这个集合中。‘

那么下面这个语句可以不重不漏的枚举出S的所有子集

for(int mask1=mask; mask1>=0; mask1=(mask1-1)&mask)


原理也比较简单。

当某一次-1操作使得原本不该出现1的地方出现了1。

也就是说,出现了一个不合法的后缀(这个后缀肯定是一连串的1)

那么&操作就相当于是在合法的前缀不变的情况下,这些不合法的后缀“跨过去”了。

直观地看,就是把这些1“减去”了。

在这个过程中,一定没有漏掉任何合法的情形,

因为这些1不减去的话肯定是不合法的。


这道题与以上的情形正好相反。

当我们知道上一行的覆盖情况,那么上一行未被覆盖的格子下一行必定要覆盖,

也就是有些格子是必取的。

那么可以用下面这个语句枚举出下一行的所有情形。

for(int mask1=mask; mask1<(1<<m); mask1=(mask 1+1)|mask)


当然,要求除了与上一行相接的格子(竖直放置骨牌)外,

其它被覆盖的格子要是两两一组的。

这个判断一下就可以了。


代码如下:

#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<cmath>
#include<algorithm>
#include<queue>
#include<vector>
#include<stack>
#include<set>
#include<map>
#define INF 0x3f3f3f3f
using namespace std;

const int MAXN=11;
long long d[MAXN+1][MAXN+1][1<<MAXN];
int m;

bool judge(int mask)
{
    while(mask)
    {
        if(mask&1)
        {
            if((mask&3)!=3)
            {
                return false;
            }
            mask>>=2;
        }
        else
        {
            mask>>=1;
        }
    }
    return true;
}

int main()
{
    memset(d,0,sizeof(d));
    for(m=1; m<=11; m++)
    {
        d[0][m][(1<<m)-1]=1LL;
        for(int i=0; i<11; i++)
        {
            for(int mask1=0; mask1<(1<<m); mask1++)
            {
                int mask=(~mask1)&((1<<m)-1);
                for(int mask2=mask; mask2<(1<<m); mask2=(mask2+1)|mask)
                {
                    if(judge(mask2^mask))
                    {
                        d[i+1][m][mask2]+=d[i][m][mask1];
                    }
                }

            }
        }
    }
    int a,b;
    while(scanf("%d%d",&a,&b),(a||b))
    {
        printf("%I64d\n",d[a][b][(1<<b)-1]);
    }
    return 0;
}


PS:

在网上看到纳米兄的解题报告是用插头DP中的轮廓线解决的,深感不明觉厉。

不过最近确实事情太多,过一段时间好好的学习一下插头DP,再来重做这题。

六、状压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)-11<<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
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值