小国王(30-30)

177 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 30 天,点击查看活动详情

今天继续进行状态压缩DP的学习。

题目描述

在 n×nn×n 的棋盘上放 kk 个国王,国王可攻击相邻的 8 个格子,求使它们无法互相攻击的方案总数。

输入格式

共一行,包含两个整数 nn 和 kk

输出格式

共一行,表示方案总数,若不能够放置则输出 0。

数据范围

1n101≤n≤10
0kn20≤k≤n^2

输入样例:

3 2

输出样例:

16

题目分析

这是一道状态压缩的问题。

首先考虑暴力做法,即按列枚举每个格子的状态,并于前一列比较,判断是否出错。

单论枚举格子状态复杂度便为 O(2n2)O(2^{n^2}),可知已经超时。

现在我们从动态规划的角度分析,定义 f[i][k][j] 表示前 i 列已经放置了 k 个国王,且第 i 列国王排布状态为 j 的方案数。

此处的排布状态为当前列按行数从小到大以 1/0 表示 放/不放 的二进制排布。

假设第 i 列状态为 a,第 i-1 列状态为 b,若 a&b==0 && check(a|b) 我们便认为其是一种有效的转移状态。 (check 函数在代码中解释,即为判断二进制排布中有无连续的 1)。

转移方程为 f[i][k][a] += f[i-1][k-cnt(a)][b] (cnt 函数为统计二进制排布中 1 的数目)

初始化 f[0][0][0] = 1,遍历每一个 if[i][0][0]

由于合法状态远不到上限,复杂度远小于 O(nk2n2n)O(nk2^n2^n)

Accept代码 O(nk2n2n)O(\ll nk2^n2^n)

#include <bits/stdc++.h>

using namespace std;

const int N = 12, M = 1 << 10, K = 110;

int n, m;
long long f[N][K][M];
vector<int> sta, head[M];
int cnt[M];

bool check(int x)
{
    for (int i = 0; i < n; i ++)
        if (1 << i & x && 1 << i + 1 & x)
            return false;
    return true;
}

int count(int x)
{
    int ans = 0;
    for (int i = 0; i < n; i ++)
        if (1 << i & x) ans ++;
    return ans;
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < 1 << n; i ++)
        if (check(i)) 
        {
            cnt[i] = count(i);
            sta.push_back(i);
        }
        
    for (int i = 0; i < sta.size(); i ++)
        for (int j = 0; j < sta.size(); j ++)
        {
            int a = sta[i], b = sta[j];
            if (!(a & b) && check(a | b))
                head[a].push_back(b);
        }
    
    f[0][0][0] = 1;
    for (int i = 1; i <= n + 1; i ++)
        for (int k = 0; k <= m; k ++)
            for (int j = 0; j < sta.size(); j ++)
                for (auto b : head[sta[j]])
                {
                    int a = sta[j];
                    if (k >= cnt[a]) f[i][k][a] += f[i - 1][k - cnt[a]][b];
                }
    
    cout << f[n + 1][m][0];
    return 0;
}