[BZOJ2669][cqoi2012]局部极小值(状压DP+容斥)

可以发现一个很好的性质:
由于 1n4,1m7 1 ≤ n ≤ 4 , 1 ≤ m ≤ 7
因此一个合法的矩阵最多有 42×72=8 ⌈ 4 2 ⌉ × ⌈ 7 2 ⌉ = 8 个X,考虑将 1 1 n×m的数从小到大填入矩阵。状压DP。
定义状态: f[i][S] f [ i ] [ S ] 表示:
1 1 i的数已经填入矩阵。 S S 是一个用二进制数表示的集合,表示所有X格子的状态,某一位为1表示对应的格子已经被填入,否则为 0 0 表示没有被填入。在以上条件下的方案数。
转移就是填入一个数i+1。下面用图来分析一下。
下面,红色格子表示已经被填入的X格子,蓝色格子表示还未被填入的X格子:
这里写图片描述
显然,下面的阴影部分(蓝色格子周围 8 8 个方向的格子)在这一步是不能填数的,否则就破坏了性质:
这里写图片描述
因此i+1可以填在蓝色格子里,也可以填在阴影部分和蓝色格子之外的空格里。
其中,如果 i+1 i + 1 填在蓝色格子里,集合 S S 就会扩充。
因此有两种转移:
(1)填在蓝色格子(d为X格子的个数):

f[i+1][S+{j}]+=f[i][S] f [ i + 1 ] [ S + { j } ] + = f [ i ] [ S ]

其中 1jd 1 ≤ j ≤ d 并且 jS j ∉ S
(2)填在阴影和蓝色格子之外的空格里(下面 area a r e a 为这样的空格的个数,可以计算出):
f[i+1][S]+=f[i][S]×area f [ i + 1 ] [ S ] + = f [ i ] [ S ] × a r e a

最后结果为 f[n×m][{1,2,...,d}] f [ n × m ] [ { 1 , 2 , . . . , d } ]
但这个方程是假的,还需要解决的一个问题在于:
这个方程只能保证X格子是局部极小值, 但不能保证“.”格子不是局部极小值
因此考虑容斥原理:暴力 2... 2 . . . 枚举每个“.”格子,强制把这些格子变成X,然后每次都做一遍DP。看上去复杂度是爆炸的,但是实际上任意两个X格子都不能相邻。利用这一性质缩小枚举范围,就比较科学了。
答案的统计方法是:
最后答案 = = d个X格子的方案数 (d+1)个X格子的方案数 + + (d+2)个X格子的方案数 ... − . . .
复杂度 O(玄学) O() O ( 能 过 )
代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
using namespace std;
const int L = 6, R = 9, N = 30, C = (1 << 8) + 5, MX = 12345678;
int n, m, ans, dx[] = {-1, -1, 1, 1, 0, 0, -1, 1, 0}, dy[] = 
{-1, 1, -1, 1, -1, 1, 0, 0, 0}, f[N][C], area[C], iX[N], iY[N];
char s[L][R];
bool vis[L][R];
void solve(int cnt)
{
    int i, j, k, Cm, orz = 0;
    memset(f, 0, sizeof(f));
    f[0][0] = 1;
    For (i, 1, n) For (j, 1, m)
        if (s[i][j] == 'X') iX[++orz] = i, iY[orz] = j;
    Cm = 1 << orz;
    For (i, 0, Cm - 1)
    {
        memset(vis, 0, sizeof(vis));
        For (j, 1, orz)
        {
            if (!(((i >> j - 1)) & 1)) continue;
            For (k, 0, 8)
            {
                int tx = iX[j] + dx[k], ty = iY[j] + dy[k];
                if (tx >= 1 && tx <= n && ty >= 1 && ty <= m)
                    vis[tx][ty] = 1;
            }
        }
        area[i] = 0;
        For (j, 1, n) For(k, 1, m)
            if (!vis[j][k]) area[i]++;
    }
    For (i, 0, n * m - 1)
        For (j, 0, Cm - 1)
        {
            For (k, 1, orz)
                if (!((j >> k - 1) & 1))
                    f[i + 1][j | (1 << k - 1)] = (f[i + 1]
                        [j | (1 << k - 1)] + f[i][j]) % MX;
            f[i + 1][j] = (f[i + 1][j] + 1ll * (area[Cm - j - 1] - i)
                * f[i][j] % MX) % MX;
        }
    if (cnt & 1) ans = (ans - f[n * m][Cm - 1] + MX) % MX;
    else ans = (ans + f[n * m][Cm - 1]) % MX;
}
void dfs(int dep, int cnt)
{
    if (dep == n * m + 1) return solve(cnt);
    int i, x = (dep - 1) / m + 1, y = (dep - 1) % m + 1;
    bool flag = 1;
    For (i, 0, 8)
    {
        int tx = x + dx[i], ty = y + dy[i];
        if (tx >= 1 && tx <= n && ty >= 1 && ty <= m && s[tx][ty] == 'X')
            flag = 0;
    }
    dfs(dep + 1, cnt);
    if (!flag) return;
    s[x][y] = 'X'; dfs(dep + 1, cnt + 1);
    s[x][y] = '.';
}
int main()
{
    int i;
    cin >> n >> m;
    For (i, 1, n) scanf("%s", s[i] + 1);
    dfs(1, 0);
    cout << ans << endl;
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值