1 条题解
-
0
你好!我是你的OI教练。很高兴带你攻克这道 GESP 七级的动态规划好题《纸牌游戏》。这道题考察的是线性动态规划(Linear DP)中的多维状态设计。
虽然题目描述有点长,但只要我们把它拆解成数学语言,就会发现它的逻辑其实很清晰。
1. 读题关键词:你的“雷达”响了吗?
读题时,请圈出以下决定解题方向的关键词:
- “小杨...全部计划告诉你”:
- 这意味着对手的出牌序列 是已知的,你可以针对性地应对。
- “换了 次牌...扣 分”:
- 核心痛点:扣的分数不是固定的,而是跟你累计换了多少次有关。第一次换扣 ,第二次换扣 ...
- 这提示我们在 DP 状态中,必须记录**“当前一共换了几次牌”**这个信息。
- “从第 2 轮开始...要么继续...要么记一次换牌”:
- 说明第 1 轮出什么牌是自由的,不算换牌。
- 是否产生罚分,取决于这一轮的牌和上一轮的牌是否相同。
- 这提示我们在 DP 状态中,必须记录**“上一轮出了什么牌”**。
- “最多获得多少分”:
- 最优解问题,符合无后效性,确定使用动态规划 (DP)。
2. 预备知识点
- 动态规划 (DP):理解状态定义、状态转移方程。
- 多维数组:本题需要三维数组来存储状态。
- 逻辑思维:分类讨论(换牌 vs 不换牌)。
3. 启发式教学:草稿纸上的推演
我们来定义 DP 的状态。根据刚才的关键词分析,我们需要知道:
- 现在是第几轮()
- 我现在出的是什么牌()—— 为了判断下一轮是否换牌。
- 我已经换了几次牌()—— 为了计算罚分。
状态定义: 表示:进行了前 轮游戏,第 轮我出的是牌 ,且截止目前一共换了 次牌,所能获得的最大净得分。
状态转移推导: 假设我们现在处于第 轮,想出牌 (假设 ),且总共换了 次。 这一轮的得分是 (根据我和小杨的牌计算)。
我怎么到达这个状态呢?看第 轮:
-
情况 A:上一轮我也出的 (0)
- 没有发生换牌。
- 所以我上一轮的状态应该是 (换牌次数还是 )。
- 转移:
-
情况 B:上一轮我出的不是 (比如 1 或 2)
- 发生了换牌!这是第 次换牌。
- 所以我上一轮的状态应该是 。
- 代价:因为是第 次换,要扣除 。
- 转移:$dp[i][j][k] = \max(\dots, dp[i-1][\text{other}][k-1] + S - b_k)$
初始状态(Base Case): 第 1 轮:
- 其他状态设为负无穷(不可达)。
目标: 最后一轮(),无论出什么牌,无论换几次,取最大值:
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. 关键点与易错点
- 数据类型:虽然 和 都在 ,但总分是累加的,最坏情况可能达到 级别,且可能有负分,必须使用
long long,并且INF要开得足够小(如-1e18)。 - 罚分下标:
b[1]代表第 1 次换牌。在代码中,如果状态转移是k-1 -> k,那么消耗的就是b[k]。 - 循环范围:
- (换牌次数)最大是 。
- 内层循环判断
k>=1才能从k-1转移过来。
- 初始化:因为要求最大值且可能为负,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
- 上传者