1 条题解

  • 0
    @ 2025-12-9 12:07:44

    你好!我是你的OI教练。很高兴带你攻克这道 GESP 七级的动态规划好题《纸牌游戏》。这道题考察的是线性动态规划(Linear DP)中的多维状态设计

    虽然题目描述有点长,但只要我们把它拆解成数学语言,就会发现它的逻辑其实很清晰。


    1. 读题关键词:你的“雷达”响了吗?

    读题时,请圈出以下决定解题方向的关键词:

    1. “小杨...全部计划告诉你”
      • 这意味着对手的出牌序列 c1,,cNc_1, \dots, c_N已知的,你可以针对性地应对。
    2. “换了 tt 次牌...扣 b1++btb_1 + \dots + b_t 分”
      • 核心痛点:扣的分数不是固定的,而是跟你累计换了多少次有关。第一次换扣 b1b_1,第二次换扣 b2b_2...
      • 这提示我们在 DP 状态中,必须记录**“当前一共换了几次牌”**这个信息。
    3. “从第 2 轮开始...要么继续...要么记一次换牌”
      • 说明第 1 轮出什么牌是自由的,不算换牌。
      • 是否产生罚分,取决于这一轮的牌上一轮的牌是否相同。
      • 这提示我们在 DP 状态中,必须记录**“上一轮出了什么牌”**。
    4. “最多获得多少分”
      • 最优解问题,符合无后效性,确定使用动态规划 (DP)

    2. 预备知识点

    • 动态规划 (DP):理解状态定义、状态转移方程。
    • 多维数组:本题需要三维数组来存储状态。
    • 逻辑思维:分类讨论(换牌 vs 不换牌)。

    3. 启发式教学:草稿纸上的推演

    我们来定义 DP 的状态。根据刚才的关键词分析,我们需要知道:

    1. 现在是第几轮ii
    2. 我现在出的是什么牌j{0,1,2}j \in \{0, 1, 2\})—— 为了判断下一轮是否换牌。
    3. 我已经换了几次牌kk)—— 为了计算罚分。

    状态定义dp[i][j][k]dp[i][j][k] 表示:进行了前 ii 轮游戏,第 ii 轮我出的是牌 jj,且截止目前一共换了 kk 次牌,所能获得的最大净得分

    状态转移推导: 假设我们现在处于第 ii 轮,想出牌 jj(假设 j=0j=0),且总共换了 kk 次。 这一轮的得分是 SS(根据我和小杨的牌计算)。

    我怎么到达这个状态呢?看第 i1i-1 轮:

    • 情况 A:上一轮我也出的 jj (0)

      • 没有发生换牌。
      • 所以我上一轮的状态应该是 dp[i1][j][k]dp[i-1][j][k](换牌次数还是 kk)。
      • 转移:dp[i][j][k]=max(,dp[i1][j][k]+S)dp[i][j][k] = \max(\dots, dp[i-1][j][k] + S)
    • 情况 B:上一轮我出的不是 jj (比如 1 或 2)

      • 发生了换牌!这是第 kk 次换牌。
      • 所以我上一轮的状态应该是 dp[i1][other][k1]dp[i-1][\text{other}][k-1]
      • 代价:因为是第 kk 次换,要扣除 bkb_k
      • 转移:$dp[i][j][k] = \max(\dots, dp[i-1][\text{other}][k-1] + S - b_k)$

    初始状态(Base Case): 第 1 轮:

    • dp[1][0][0]=score(0,c1)dp[1][0][0] = \text{score}(0, c_1)
    • dp[1][1][0]=score(1,c1)dp[1][1][0] = \text{score}(1, c_1)
    • dp[1][2][0]=score(2,c1)dp[1][2][0] = \text{score}(2, c_1)
    • 其他状态设为负无穷(不可达)。

    目标: 最后一轮(NN),无论出什么牌,无论换几次,取最大值: max(dp[N][j][k])\max(dp[N][j][k])


    4. 标准代码 (NOIP C++14)

    /**
     * 题目:P10111 [GESP202312 七级] 纸牌游戏
     * 考点:线性动态规划、多维状态设计
     * 时间复杂度:O(N^2),空间复杂度 O(N^2)
     */
    
    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <cstring>
    
    using namespace std;
    
    // 数据范围 N <= 1000
    // 分数可能很大或者是负数(罚分),用 long long
    // INF 设为一个极小的数
    const long long INF = -1e18;
    const int MAXN = 1005;
    
    // dp[i][j][k]
    // i: 当前轮数 (1~N)
    // j: 当前出的牌 (0, 1, 2)
    // k: 累计换牌次数 (0 ~ N-1)
    long long dp[MAXN][3][MAXN];
    
    int n;
    int a[MAXN]; // 获胜/平局的基础分
    int b[MAXN]; // 换牌罚分,b[k] 表示第 k 次换牌的代价
    int c[MAXN]; // 对手出的牌
    
    // 辅助函数:计算单轮得分
    long long calc_score(int my_card, int opp_card, int score_val) {
        if (my_card == opp_card) return score_val; // 平局
        // 赢的情况: 1>0, 2>1, 0>2
        if ((my_card == 1 && opp_card == 0) ||
            (my_card == 2 && opp_card == 1) ||
            (my_card == 0 && opp_card == 2)) {
            return 2LL * score_val;
        }
        return 0; // 输了
    }
    
    void solve() {
        cin >> n;
        for (int i = 1; i <= n; i++) cin >> a[i];
        // 注意 b 只有 n-1 个,下标 1 表示第 1 次换牌
        for (int i = 1; i < n; i++) cin >> b[i]; 
        for (int i = 1; i <= n; i++) cin >> c[i];
    
        // 1. 初始化 DP 数组为负无穷
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j < 3; j++) {
                for (int k = 0; k <= n; k++) {
                    dp[i][j][k] = INF;
                }
            }
        }
    
        // 2. 初始状态:第 1 轮
        // 第 1 轮不算换牌,所以 k=0
        for (int j = 0; j < 3; j++) {
            dp[1][j][0] = calc_score(j, c[1], a[1]);
        }
    
        // 3. 状态转移
        for (int i = 2; i <= n; i++) { // 从第 2 轮开始
            for (int j = 0; j < 3; j++) { // 枚举我这一轮出的牌
                long long current_score = calc_score(j, c[i], a[i]);
    
                // 枚举目前为止可能的换牌次数 k
                // 第 i 轮最多换 i-1 次
                for (int k = 0; k < i; k++) {
                    
                    // 情况 A: 不换牌,上一轮也是 j
                    // 此时 k 不变,且要求上一轮状态有效
                    if (dp[i-1][j][k] != INF) {
                        dp[i][j][k] = max(dp[i][j][k], dp[i-1][j][k] + current_score);
                    }
    
                    // 情况 B: 换牌,上一轮是 prev (prev != j)
                    // 此时这一轮是第 k 次换牌,说明上一轮是 k-1 次
                    // 前提:k >= 1
                    if (k >= 1) {
                        for (int prev = 0; prev < 3; prev++) {
                            if (prev == j) continue;
                            if (dp[i-1][prev][k-1] != INF) {
                                dp[i][j][k] = max(dp[i][j][k], dp[i-1][prev][k-1] + current_score - b[k]);
                            }
                        }
                    }
                }
            }
        }
    
        // 4. 统计答案
        long long ans = INF;
        for (int j = 0; j < 3; j++) {
            for (int k = 0; k < n; k++) {
                ans = max(ans, dp[n][j][k]);
            }
        }
        
        cout << ans << endl;
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(0);
        solve();
        return 0;
    }
    

    5. 关键点与易错点

    1. 数据类型:虽然 aia_ibib_i 都在 10610^6,但总分是累加的,最坏情况可能达到 10910^9 级别,且可能有负分,必须使用 long long,并且 INF 要开得足够小(如 -1e18)。
    2. 罚分下标b[1] 代表第 1 次换牌。在代码中,如果状态转移是 k-1 -> k,那么消耗的就是 b[k]
    3. 循环范围
      • kk(换牌次数)最大是 N1N-1
      • 内层循环判断 k>=1 才能从 k-1 转移过来。
    4. 初始化:因为要求最大值且可能为负,DP 数组初始值不能是 0,必须是负无穷。

    6. 数据生成器 (1.in - 10.in)

    /**
     * P10111 数据生成器
     * 生成 1.in/1.out ~ 10.in/10.out
     */
    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <algorithm>
    #include <string>
    #include <cstdlib>
    #include <ctime>
    
    using namespace std;
    
    // ------------------- 标准题解逻辑 (用于生成 .out) -------------------
    const long long INF = -1e18;
    long long dp[1005][3][1005];
    
    long long get_score_gen(int my, int opp, int val) {
        if (my == opp) return val;
        if ((my == 1 && opp == 0) || (my == 2 && opp == 1) || (my == 0 && opp == 2)) return 2LL * val;
        return 0;
    }
    
    long long solve_std(int n, const vector<int>& a, const vector<int>& b, const vector<int>& c) {
        for (int i = 0; i <= n; i++) 
            for (int j = 0; j < 3; j++) 
                for (int k = 0; k <= n; k++) 
                    dp[i][j][k] = INF;
    
        for (int j = 0; j < 3; j++) 
            dp[1][j][0] = get_score_gen(j, c[1], a[1]);
    
        for (int i = 2; i <= n; i++) {
            for (int j = 0; j < 3; j++) {
                long long s = get_score_gen(j, c[i], a[i]);
                for (int k = 0; k < i; k++) {
                    // 不换牌
                    if (dp[i-1][j][k] != INF) 
                        dp[i][j][k] = max(dp[i][j][k], dp[i-1][j][k] + s);
                    // 换牌
                    if (k >= 1) {
                        for (int prev = 0; prev < 3; prev++) {
                            if (prev == j) continue;
                            if (dp[i-1][prev][k-1] != INF) 
                                dp[i][j][k] = max(dp[i][j][k], dp[i-1][prev][k-1] + s - b[k]);
                        }
                    }
                }
            }
        }
        long long ans = INF;
        for (int j = 0; j < 3; j++) 
            for (int k = 0; k < n; k++) 
                ans = max(ans, dp[n][j][k]);
        return ans;
    }
    // ------------------------------------------------------------------
    
    int randInt(int l, int r) {
        return rand() % (r - l + 1) + l;
    }
    
    void makeData(int id) {
        string inName = to_string(id) + ".in";
        string outName = to_string(id) + ".out";
        ofstream fin(inName);
        ofstream fout(outName);
    
        int N;
        // 梯度数据
        if (id <= 3) N = randInt(5, 15);
        else if (id <= 6) N = randInt(50, 100);
        else N = randInt(900, 1000);
    
        vector<int> a(N + 1), b(N), c(N + 1);
    
        // 生成 a (分数)
        for (int i = 1; i <= N; i++) a[i] = randInt(0, 10000);
        if (id >= 9) for(int i=1; i<=N; i++) a[i] = randInt(100000, 1000000); // 大数据
    
        // 生成 b (罚分)
        for (int i = 1; i < N; i++) {
            if (id == 4) b[i] = 0; // 特判:无罚分
            else if (id == 5) b[i] = 1000000; // 特判:超大罚分,逼迫不换牌
            else b[i] = randInt(0, 5000); 
        }
    
        // 生成 c (对手出牌)
        for (int i = 1; i <= N; i++) c[i] = randInt(0, 2);
    
        // 写入输入
        fin << N << endl;
        for (int i = 1; i <= N; i++) fin << a[i] << (i == N ? "" : " ");
        fin << endl;
        for (int i = 1; i < N; i++) fin << b[i] << (i == N - 1 ? "" : " ");
        fin << endl;
        for (int i = 1; i <= N; i++) fin << c[i] << (i == N ? "" : " ");
        fin << endl;
    
        // 写入输出
        fout << solve_std(N, a, b, c) << endl;
    
        fin.close();
        fout.close();
        cout << "Generated Case " << id << " (N=" << N << ")" << endl;
    }
    
    int main() {
        srand(time(0));
        for (int i = 1; i <= 10; i++) makeData(i);
        return 0;
    }
    
    • 1

    信息

    ID
    14432
    时间
    1000ms
    内存
    32MiB
    难度
    5
    标签
    递交数
    1
    已通过
    1
    上传者