【算法】【线性动态规划】

0 阅读3分钟

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. 操作会减少问题的规模(每次操作后,剩余的自由行/列数减少 1 或 2)。
  2. 剩余部分的结构与原问题相同(棋盘仍是方阵,且行列对称)。
  3. 状态可以用一个整数表示(比如还剩多少个“自由行”)。
  4. 每次操作的方案数只依赖于当前规模,而不依赖于具体选了哪些行/列(因为对称性,选哪一行都一样)。
  5. 要求计算所有可能的最终配置数(组合计数)。

这类 DP 的常见形式

图片.png