「刷题记录」国王(状压DP)
第一道状压 DP 题 夏令营学长讲了一晚上我也没听明白
题目传送门:国王
分析
这道题似乎和八皇后很像,但由于国际象棋中规则不同,国王只能吃掉周围 \(8\) 个格的棋子,所以也是有所不同的
我们一步一步来
首先,同一行允许有多个国王,前提是这些国王不紧挨着
其次,同一列允许有多个国王,前提也是这些国王不紧挨着
再然后,就是一个国王的四个角上不能有其他国王,同时国王总数不能超过 \(k\)
现在,我们已经找到规则了
状压部分
一个位置放国王,在二进制上就是 \(1\),否则就是 \(0\)
二进制数 \(0101\) 代表第一个位置放国王,第三个位置放国王,第二和第四个位置不放
我们二进制的放国王顺序是从右往左,因为二进制每次加 \(1\)都是从最右边开始加的,因此顺序也就设为从右往左
DP 部分
先设置状态
\(dp(i, j, h)\):第 \(i\) 行,第 \(j\) 种情况,放了 \(h\) 个棋子
\(need_i\):情况 \(i\) 需要多少个棋子
转移方程式:\(dp(i, j, h) += dp(i - 1, s, h - need_j)\)
\(s\):\(s\) 情况下,\(j\) 情况是合法的
代码
先预处理出每一种状态
all = (1 << n) - 1; // 每一个格都放的情况
for (int i = 0; i <= all; ++i) { // 枚举所有的情况
int tmp = i;
while (tmp) {
need[i] += (tmp & 1); // 统计这种情况需要多少个国王
tmp >>= 1;
if (need[i] > k)
break;
// 一个小优化,最多放k个国王,如果已经大于k个,则这种情况一定不会选,直接退出即可
}
}
判断情况是否合法,即判断左右是否相邻
for (int i = 0; i <= all; ++i) {
can[i] = !((i << 1) & i); // 判断左右是否紧挨着
}
然后预处理出第一行的合法情况
for (int i = 0; i <= all; ++i) {
if (can[i] && need[i] <= k)
dp[1][i][need[i]] = 1;// 预处理出第一行
}
然后,枚举每一行的每一种合法情况,再枚举上一行的合法情况,判断当前这一行的这种情况对于上一行来说是否合法
for (int i = 2; i <= n; ++i) { // 枚举当前行
for (int j = 0; j <= all; ++j) { // 枚举当前行的每一种情况
if (can[j]) {
for (int h = 0; h <= all; ++h) { // 枚举上一行的每一种情况
if (!can[h])
continue;
if (j & h) // 判断上下是否紧挨着
continue;
if ((j << 1) & h) // 判断当前国王的位置是否是上一行国王位置的右下角
continue;
if ((j >> 1) & h) // 判断当前国王的位置是否是上一行国王位置的左下角
continue;
for (int l = k; l >= need[j]; --l) { // 枚举国王个数
dp[i][j][l] += dp[i - 1][h][l - need[j]];
}
}
}
}
}
完整代码:
点击查看代码
/*
date: 2022.9.10
worked by yi_fan0305
*/
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
int n, k, all;
ll dp[15][1010][100];
// 一维 行数 二维 状态 三维 国王个数
int need[1010], can[1010];
// need 第i种情况所需的国王个数 can 第i种情况是否合法
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
int main() {
n = read();
k = read();
all = (1 << n) - 1; // 每一个格都放的情况
for (int i = 0; i <= all; ++i) { // 枚举所有的情况
int tmp = i;
while (tmp) {
need[i] += (tmp & 1); // 统计这种情况需要多少个国王
tmp >>= 1;
if (need[i] > k)
break;
// 一个小优化,最多放k个国王,如果已经大于k个,则这种情况一定不会选,直接退出即可
}
}
for (int i = 0; i <= all; ++i) {
can[i] = !((i << 1) & i); // 判断左右是否紧挨着
}
for (int i = 0; i <= all; ++i) {
if (can[i] && need[i] <= k)
dp[1][i][need[i]] = 1;// 预处理出第一行
}
for (int i = 2; i <= n; ++i) { // 枚举当前行
for (int j = 0; j <= all; ++j) { // 枚举当前行的每一种情况
if (can[j]) {
for (int h = 0; h <= all; ++h) { // 枚举上一行的每一种情况
if (!can[h])
continue;
if (j & h) // 判断上下是否紧挨着
continue;
if ((j << 1) & h) // 判断当前国王的位置是否是上一行国王位置的右下角
continue;
if ((j >> 1) & h) // 判断当前国王的位置是否是上一行国王位置的左下角
continue;
for (int l = k; l >= need[j]; --l) { // 枚举国王个数
dp[i][j][l] += dp[i - 1][h][l - need[j]];
}
}
}
}
}
ll ans = 0;
for (int i = 0; i <= all; ++i) { // 统计答案
ans += dp[n][i][k];
}
printf("%lld\n", ans);
return 0;
}
朝气蓬勃 后生可畏