Loading

「刷题记录」国王(状压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;
}
posted @ 2022-09-11 09:22  yi_fan0305  阅读(107)  评论(0编辑  收藏  举报