1 条题解
-
0
问题描述
Tsovak 和 Narek 在玩一个游戏。有一个长度为 的数组 和一个 的矩阵 。
游戏规则:
- 两人轮流操作,Tsovak 先手
- 第 轮,玩家需要寻找数组元素
- 当前玩家选择一个格子 ,该格子的值必须等于
- 下一个玩家必须在子矩阵 到 中寻找
- 无法找到所需元素、剩余子矩阵为空、或数组结束时,当前玩家输
双方都采取最优策略,判断胜者。
关键观察
观察 1:数字范围限制
题目保证所有 和 满足:
这意味着所有数字都在 到 之间。
观察 2:重复数字的处理
重要结论:当数组 中某个数字出现第二次时,该位置的玩家必输。
证明: 设 是数字 在数组 中第一次和第二次出现的位置。
考虑玩家需要选择 的情况。最优策略是选择"最后"一个 (即选择后剩余子矩阵中不再包含任何 )。原因:
- 如果选择某个"内部"的 (记作 )能赢,那么在更大的矩阵中选择"最后"的 也能赢
- 如果选择 会输,意味着 后的子矩阵中存在一个必胜状态给对手,对手可以选择它而不受玩家选择的影响
因此,当玩家到达 时,他必然输,相当于数组 在这里结束。
结论:我们可以将数组 截断到每个数字最多出现一次。由于数字 ,截断后:
观察 3:博弈状态的简化
由于 ,我们可以用动态规划枚举所有可能的状态。
定义 表示:
- 当前需要寻找数组 的第 个元素
- 当前可用子矩阵的左上角为 (右下角固定为 )
- 当前轮到某玩家行动
- 值为 表示当前玩家有必胜策略, 表示必败
状态转移
对于状态 ,当前玩家有以下选择:
选择 1:跳过当前格子,向右移动
如果 且 ,则当前玩家可以获胜。
选择 2:跳过当前格子,向下移动
如果 且 ,则当前玩家可以获胜。
选择 3:选择当前格子
当 时,玩家可以选择这个格子:
情况 3.1:(这是最后一个元素)
- 选择后数组结束,对手无法继续
- 当前玩家获胜
情况 3.2: 且 且
- 选择后,对手需要在子矩阵 中寻找
- 当前玩家获胜当且仅当对手在 中必败
情况 3.3: 且( 或 )
- 选择后子矩阵为空,对手无法继续
- 当前玩家获胜
如果 ,则不能选择当前格子。
算法步骤
-
预处理数组 :
- 使用数组 记录每个数字第一次出现的位置
- 遍历 ,遇到重复数字时截断
-
动态规划计算:
- 从 到 , 到 逆序循环
- 对每个 到 计算
- 按照优先级:先判断能否通过移动获胜,再判断能否通过选择获胜
-
判断结果:
- 若 ,输出
"T"(Tsovak 获胜) - 否则输出
"N"(Narek 获胜)
- 若 ,输出
时间复杂度
- 预处理数组 :,由于 可忽略
- 动态规划:
- 所有测试用例的 之和 ,总时间复杂度可行
完整代码
#include <bits/stdc++.h> using namespace std; const int L = 305, N = 305, M = 305; int a[L], b[N][M]; int l, n, m; int ind[8]; // 记录每个数字第一次出现的位置 bool dp[8][N][M]; // dp[k][i][j] int main() { ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0); int t; cin >> t; while (t--) { cin >> l >> n >> m; // 读入数组 a for (int i = 1; i <= l; i++) { cin >> a[i]; } // 预处理:截断数组 a,使每个数字最多出现一次 for (int i = 1; i <= l; i++) { if (ind[a[i]]) { // 该数字已经出现过 l = i - 1; // 截断到当前位置之前 break; } ind[a[i]] = i; // 记录第一次出现的位置 } // 读入矩阵 b for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { cin >> b[i][j]; } } // 动态规划:从右下角往左上角计算 for (int i = n; i >= 1; i--) { for (int j = m; j >= 1; j--) { for (int k = 1; k <= l; k++) { // 优先级1:如果能通过向右移动获胜 if (j < m && dp[k][i][j + 1]) { dp[k][i][j] = true; continue; } // 优先级2:如果能通过向下移动获胜 if (i < n && dp[k][i + 1][j]) { dp[k][i][j] = true; continue; } // 优先级3:考虑选择当前格子 if (b[i][j] == a[k]) { if (k == l) { // 最后一个元素,选择后对手无法继续 dp[k][i][j] = true; } else if (i < n && j < m) { // 选择后,对手需要在子矩阵 (i+1, j+1) 中找 a[k+1] dp[k][i][j] = !dp[k + 1][i + 1][j + 1]; } else { // 选择后子矩阵为空,对手无法继续 dp[k][i][j] = true; } } else { // 不能选择当前格子,且无法通过移动获胜,则必败 dp[k][i][j] = false; } } } } // 输出结果 if (dp[1][1][1]) { cout << "T\n"; } else { cout << "N\n"; } // 清空 ind 数组,准备下一个测试用例 for (int i = 1; i <= l; i++) { ind[a[i]] = 0; } } return 0; }代码说明
-
数组大小: 足以处理 的情况
-
重复数字处理:
- 使用 数组记录每个数字( 到 )第一次出现的位置
- 遇到重复时立即截断
-
动态规划顺序:
- 外层循环 从 到
- 中层循环 从 到
- 内层循环 从 到
- 这种顺序保证了计算 时,、 和 都已经计算完毕
-
转移优先级:
- 先判断能否通过移动(向右或向下)获胜
- 再判断能否通过选择当前格子获胜
- 这是因为移动相当于"跳过"当前格子,如果存在必胜的移动,玩家一定选择它
-
边界处理:
- 当 时,选择最后一个元素直接获胜
- 当 或 时,选择后子矩阵为空,对手无法继续
总结
本题的核心技巧:
- 利用数字范围 的限制,通过观察重复数字的性质将问题规模缩小
- 使用从右下角到左上角的动态规划处理子矩阵博弈
- 状态转移时考虑"跳过"和"选择"两种操作
- 博弈问题的标准处理:当前玩家胜当且仅当存在一步使得对手进入必败态
- 1
信息
- ID
- 6260
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 2
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者