★【动态规划】【状态压缩】【容斥原理】【ZJOI2009】多米诺骨牌

Description
  有一个n×m的矩形表格,其中有一些位置有障碍。现在要在这个表格内放一些1×2或者2×1的多米诺骨牌,使得任何两个多米诺骨牌没有重叠部分,任何一个骨牌不能放到障碍上。并且满足任何相邻两行之间都有至少一个骨牌横跨,任何相邻两列之间也都至少有一个骨牌横跨。求有多少种不同的放置方法,注意你并不需要放满所有没有障碍的格子。

Input
  第一行两个整数n;m。接下来n 行,每行m 个字符,表示这个矩形表格。其中字符“x” 表示这个位置有障碍,字符“.” 表示没有障碍。

Output
  一行一个整数,表示不同的放置方法数mod 19901013 的值。

Sample Input
3 3
...
...
...

Sample Output
2

Hint
两种放置方法分别为
112 411
4.2 4.2
433 332
注意这里的数字只用于区分骨牌,不同的排列并不代表不同的方案。

【数据范围】
对于40% 的数据,满足1 ≤ n;m ≤ 8。
对于90% 的数据,满足1 ≤ n;m ≤ 14。
对于100% 的数据,满足1 ≤ n;m ≤ 15。 


被这道题虐了一上午……

首先考虑一个更简单一些的情况:
若没有“任何相邻两行、列之间都至少有一个骨牌横跨”的限制,那么问题就相当简单了(直接一个裸的状压DP就好了);
若只有行(任意两行之间)的跨立没有列的跨立,当然也很简单(用插头DP实现,保证每行结束都至少有一个插头指向下一行)。

再回到原问题中。
一种想法就是增加状态的信息,在转移的时候将相邻各列的相连情况记录下来,解决了列的横跨问题;并且用上面所说的方法解决行的横跨问题。(下面附代码。)这样做的复杂度是O(n·m·4^m),显然要超时。
所以需要继续寻找其他办法。
通常,我们很容易计算某两行(列)之间一定没有骨牌横跨的情况(只需把原问题分解为几个小的部分再把分别的结果相乘即可),所以这令人想到了容斥原理!

首先一个预处理(用状压DP实现),算出ini[U][D][L][R]即在U <= i < D, L <= j < R的矩形中随便放骨牌的总数。
接下来再使用容斥原理计算最终结果。
我们先只考虑列,就拿样例来说:设|A|={1, 2列中有骨牌横跨的情况的总数},|B|={2, 3列中有骨牌横跨的情况的总数}。
则有:


那么,我们就可以枚举中间的分割线(即规定某些列不能被横跨),对于每一个容斥项(我们暂时把容斥原理公式中的每一项成为容斥项),又在行上进行动态规划(用一个g数组,令g[i]为这个容斥项中的前i行中每相邻两行都有骨牌横跨的总数),最后将结果正负交错地累加起来即可。
Accode:

#include <cstdio>
#include <cstdlib>
#define Add(a, b) ((a) += (b)) %= MOD

typedef long long int64;
const int maxN = 20, MOD = 19901013;
const int maxSTATUS = 1 << 16;

int64 g[maxN], ans, tmp;
char mp[maxN][maxN];
int f[maxN][maxN][maxSTATUS];
int ini[maxN][maxN][maxN][maxN], c[maxN];
int n, m, pst, ths;
int (*f0)[maxSTATUS], (*f1)[maxSTATUS];

int main()
{
    freopen("domino.in", "r", stdin);
    freopen("domino.out", "w", stdout);
    scanf("%d%d\n", &n, &m);
    for (int i = 0; i < n; ++i) gets(mp[i]);
    for (int L = 0; L < m; ++L)
    for (int R = L + 1; R < m + 1; ++R)
    for (int U = 0; U < n; ++U)
    {
        int Lim = 1 << (R - L);
        for (int k = 1; k < Lim; ++k) f[U][L][k] = 0;
        f[U][L][0] = 1;
        for (int i = U; i < n; ++i)
        {
            for (int j = L; j < R; ++j)
            {
                f0 = &f[i][j];
                if (j < R - 1) f1 = &f[i][j + 1];
                else f1 = &f[i + 1][L];
                for (int k = 0; k < Lim; ++k) (*f1)[k] = 0;
                int x = R - j - 1;
                if (mp[i][j] == 'x')
                for (int k = 0; k < Lim; ++k)
                    Add((*f1)[k & ~(1 << x)], (*f0)[k]);
                else for (int k = 0; k < Lim; ++k)
                {
                    if (!((*f0)[k])) continue;
                    Add((*f1)[k & ~(1 << x)], (*f0)[k]);
                    if (j > L && mp[i][j - 1] - 'x'
                        && !(k & (2 << x)))
                        Add((*f1)[k | (3 << x)], (*f0)[k]);
                    if (i > U && mp[i - 1][j] - 'x'
                        && !(k & (1 << x)))
                        Add((*f1)[k | (1 << x)], (*f0)[k]);
                }
            }
            for (int k = 0; k < Lim; ++k)
                Add(ini[U][i + 1][L][R], (*f1)[k]);
        }
    }
    for (int S = 0; S < 1 << (m - 1); ++S)
    {
        int tot = 0; c[tot++] = 0;
        for (int i = 0; i < m - 1; ++i)
            if (S & (1 << i)) c[tot++] = i + 1;
        c[tot] = m;
        for (int D = 1; D < n + 1; ++D)
        for (int U = 0; U < D; ++U)
        {
            tmp = ini[U][D][c[0]][c[1]];
            for (int i = 1; i < tot; ++i)
                (tmp *= ini[U][D][c[i]][c[i + 1]]) %= MOD;
	//这里千万不能用减法代替除法取余,
	//否则后果自负(在时限1000s的情况下都能超时)。
            if (!U) g[D - 1] = tmp;
            else Add(g[D - 1], -tmp * g[U - 1]);
        }
        tmp = (tot & 1) ? g[n - 1] : -g[n - 1];
        Add(ans, tmp);
    }
    printf("%I64d\n", (ans + MOD) % MOD);
    return 0;
}

#undef Add
再贴一个朴素的状压代码:

#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <map>

using std::map; using std::make_pair;
const char fi[] = "domino.in";
const char fo[] = "domino_simple.out";
const int maxN = 20, MOD = 19901013;

map <int, int> pst, ths;
map <int, int>::iterator iter;
char mp[maxN][maxN];
int n, m;

inline void Add(int status, int &val)
{
    if (ths.find(status) == ths.end())
    {ths.insert(make_pair(status, val)); return;}
    int &tmp = ths[status];
    if ((tmp += val) >= MOD) tmp -= MOD;
    return;
}

int main()
{
    freopen(fi, "r", stdin);
    freopen(fo, "w", stdout);
    scanf("%d%d\n", &n, &m);
    for (int i = 0; i < n; ++i) gets(mp[i]);
    if (n < 3 || m < 3) {printf("0\n"); return 0;}
    for (int i = 0; i < n - 1; ++i)
    {
        bool flag = 1;
        for (int j = 0; j < m; ++j)
            flag &= (mp[i][j] == 'x' || mp[i + 1][j] == 'x');
        if (flag) {printf("0\n"); return 0;}
    }
    for (int j = 0; j < m - 1; ++j)
    {
        bool flag = 1;
        for (int i = 0; i < n; ++i)
            flag &= (mp[i][j] == 'x' || mp[i][j + 1] == 'x');
        if (flag) {printf("0\n"); return 0;}
    }
    ths.insert(make_pair(0, 1));
    for (int i = 0; i < n; ++i) for (int j = 0; j < m; ++j)
    {
        std::swap(pst, ths); ths.clear();
        int x = m - j - 1;
        for (iter = pst.begin(); iter != pst.end(); ++iter)
        {
            int val = iter -> second, Last = iter -> first;
            if (!val) continue;
            if (!j)
            {
                if (Last & 1) continue;
                else
                {
                    int tmp = Last & (((1 << m - 1) - 1) << m + 1);
                    ((Last &= (1 << m + 1) - 1) >>= 1) |= tmp;
                }
                if (i && !(Last & ((1 << m) - 1))) continue;
            }
            int wx = (Last >> x) & 3, Now = Last - (wx << x);
            if (mp[i][j] == 'x')
            {if (!wx) Add(Now, val); continue;}
            if (wx < 3) Add(Now, val);
            if (!wx)
            {
                if (j < m - 1 && mp[i][j + 1] - 'x')
                    Add(Now | (1 << x) | (1 << x + m), val);
                if (i < n - 1 && mp[i + 1][j] - 'x')
                    Add(Now | (2 << x), val);
            }
        }
    }
    if (ths.find(((1 << m - 1) - 1) << m + 1) == ths.end())
        printf("0\n");
    else printf("%d\n", ths[((1 << m - 1) - 1) << m + 1]);
    return 0;
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值