C. How Does the Rook Move?(1500)
#include <bits/stdc++.h>
using namespace std;
int dp[(int) 3e5+5]; // DP 数组,dp[i] 表示在 i×i 的子棋盘上继续游戏能得到的不同最终配置数
const int MOD = 1e9 + 7; // 模数
int main() {
cin.tie(0), cout.tie(0)->sync_with_stdio(0); // 加速 IO
int t;
cin >> t;
while (t--) {
int n, k;
cin >> n >> k;
int used = 0; // 已经占用的行/列数量(注意:占用的行列一定成对移除,除非是放在对角线)
for (int i = 0; i < k; i++) {
int r, c; cin >> r >> c;
// 如果 r == c,表示你下在了对角线上,电脑不镜像,只占用 1 行 + 1 列
// 如果 r != c,你和电脑各下一子,占用 2 行 + 2 列,但因为是成对出现,占用的总行/列数计为 2
// 实际上这里 used 记录的是被“彻底占用的行/列对数”(更准确说是被减少的自由行列数)
used += 2 - (r == c);
}
int m = n - used; // 剩余的自由行数(等价于剩余的自由列数,因为行列对称)
// 基础 DP
// dp[0] = 1:空棋盘只有一种最终状态
// dp[1] = 1:1×1 棋盘,只能放一个对角线 rook(类型 1 操作),之后结束
dp[0] = dp[1] = 1;
// 对于 i≥2 的棋盘,dp[i] = dp[i-1] + 2*(i-1)*dp[i-2]
// 推导:
// 情况 1:选择在 (i,i) 放一个 rook(类型 1),此时删除第 i 行和第 i 列,剩余 i-1×i-1 棋盘 → dp[i-1]
// 情况 2:选择在 (i,j)(j < i)放 rook(类型 2),计算机会在 (j,i) 放 rook,这删除第 i、j 行和第 i、j 列,剩余 i-2×i-2 棋盘
// j 有 (i-1) 种选择,而且我们也可以选择 (j,i) 先放(玩家先下对称位置),所以两种对称方式 → 2*(i-1) 种选择
// 每种选择下剩余 i-2 棋盘有 dp[i-2] 种最终配置
for (int i = 2; i <= m; i++)
dp[i] = (dp[i-1] + 2ll * (i-1) * dp[i-2] % MOD) % MOD;
cout << dp[m] << "\n"; // 输出剩余 m 行时的最终配置数
}
return 0;
}
思考
这是一个很好的问题——“什么情况下要用这种 DP?”
我们来看这类 DP 的共同特征,以及为什么在这里它适用。
DP 的使用场景
这类 DP 通常出现在组合计数问题中,并且满足以下特征:
什么情况下会想到这种 DP?
当你遇到以下情况时,可以尝试这种思路:
- 操作会减少问题的规模(每次操作后,剩余的自由行/列数减少 1 或 2)。
- 剩余部分的结构与原问题相同(棋盘仍是方阵,且行列对称)。
- 状态可以用一个整数表示(比如还剩多少个“自由行”)。
- 每次操作的方案数只依赖于当前规模,而不依赖于具体选了哪些行/列(因为对称性,选哪一行都一样)。
- 要求计算所有可能的最终配置数(组合计数)。