区间博弈DP:通过推导使子区间成为必败态的两端边界石子数唯一解,O(n^2)判断全局胜负
OJ: roj
题目 ID: 1670
难度:省选/NOI-
标签:博弈论动态规划区间 DP
创建: 2026-10-01 01:10
更新: 2026-10-07 13:50
形式化题目
给定一排
正解
思路
由于每步只能从序列的最左端或最右端取石子,且石子数高达
1. 核心观察与状态定义
对于任意连续子序列
- 唯一性:如果存在两个不同的石子数
均使得左侧放该数量时为必败态,那么在面对数量为 的局面时,先手可以直接从该堆中取出 颗石子,将其转移到数量为 的局面。这意味着先手可以一步将一个必败态转移到另一个必败态,违背了博弈论中必败态的所有后继都是必胜态的基本定理。因此,使局面成为必败态的左侧石子数至多只有一个。 - 存在性:可以证明这样的石子数必定存在且非负。
基于此,我们定义:
:在区间 的最左侧添加一堆石子,使得整个新局面为先手必败态时,该堆石子的唯一数量。 :在区间 的最右侧添加一堆石子,使得整个新局面为先手必败态时,该堆石子的唯一数量。
2. 边界条件
当区间长度为
3. 状态转移推导
考虑如何利用区间
| 关系条件 | 转移逻辑解释 | |
|---|---|---|
| 由 |
||
| 左右两端石子数相同。先手无论取哪一端,后手都在另一端取相同数量;任一端被取光后留下的状态均在临界值之内,先手无法反制。 | ||
| 左右两端保持差值 |
||
| 与上述情况对偶,后手可通过将差值抵消或触发必败转移使先手落败。 | ||
| 当石子数同时超出两侧临界值时,只需保持左右两端石子数相等即可保持对称控制。 |
同理,推导
- 若
,则 ; - 若
或 ,则 ; - 若
,则 ; - 若
,则 。
4. 全局胜负判定
区间递推到区间
- 当
时,先手直接取完整堆石子即可获胜,结果为 。 - 当
时,原序列最左端的实际石子数为 : - 若
,则当前整个局面恰好是先手必败态,先手必输,输出 ; - 若
,先手必能将第一堆调整至 (若 ),或原局面本身是先手必胜态,输出 。
- 若
代码
Python 版:
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-07-05 21:47
# update_at: 2026-07-05 21:47
import sys
def next_end_state(x: int, l: int, r: int, is_left: bool) -> int:
"""根据子区间两侧必败态阈值及新堆石子数推导新端点的必败态阈值。"""
if x == (r if is_left else l):
return 0
if (x > l and x > r) or (x < l and x < r):
return x
return x - 1 if (r < x < l if is_left else l < x < r) else x + 1
def is_first_win(piles: list[int]) -> int:
"""判断当前石子序列是否存在先手必胜策略:1 为必胜,0 为必败。"""
n = len(piles)
if n == 1:
return 1
# L[i][j] 与 R[i][j] 表示给区间加左/右侧石子使其成为必败态所需的石子数
# 下标 1-based 便于对应原始区间 [i, j]
l_dp = [[0] * (n + 1) for _ in range(n + 1)]
r_dp = [[0] * (n + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
l_dp[i][i] = r_dp[i][i] = piles[i - 1]
for length in range(2, n):
for i in range(1, n - length + 2):
j = i + length - 1
l_dp[i][j] = next_end_state(piles[j - 1], l_dp[i][j - 1], r_dp[i][j - 1], is_left=True)
r_dp[i][j] = next_end_state(piles[i - 1], l_dp[i + 1][j], r_dp[i + 1][j], is_left=False)
target = next_end_state(piles[n - 1], l_dp[2][n - 1], r_dp[2][n - 1], is_left=True) if n > 2 else piles[n - 1]
return 0 if piles[0] == target else 1
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
cases = next(data, 0)
for _ in range(cases):
pile_count = next(data)
stones = [next(data) for _ in range(pile_count)]
out.append(str(is_first_win(stones)))
print('\n'.join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 01:40
* update_at: 2026-10-06 01:40
*/
#include <cstdio>
typedef long long ll;
const int MAXN = 1005;
ll a[MAXN]; // a[i]:第 i 堆石子的数量(下标从 1 开始)
ll Ldp[MAXN][MAXN]; // Ldp[i][j]:在区间 [i,j] 左侧加一堆石子,使整个新局面成为先手必败态所需的石子数
ll Rdp[MAXN][MAXN]; // Rdp[i][j]:在区间 [i,j] 右侧加一堆石子,使整个新局面成为先手必败态所需的石子数
// 在子区间的一端新增石子数为 x 的一堆,求该端对应区间 [i,j] 的必败阈值。
// is_left 为 1:新堆在左端,子区间为 [i,j-1],l/r 为其两侧阈值;
// is_left 为 0:新堆在右端,子区间为 [i+1,j],l/r 为其两侧阈值。
// 转移按 x 与两侧阈值 l、r 的大小关系分为五类;注意 x 恰好等于一侧阈值时,
// 取差 1 的临界值而非保持相等,这是本题最容易写错的边界。
ll next_end_state(ll x, ll l, ll r, int is_left) {
if (is_left) {
if (x == r) return 0; // 右端本身已是必败态,左侧无需再补石子
} else {
if (x == l) return 0; // 左端本身已是必败态,右侧无需再补石子
}
// 新堆石子数同时大于或同时小于两侧阈值:保持两端相等即可用对称模仿控制局面
if ((x < l && x < r) || (x > l && x > r)) return x;
// 夹在两侧阈值之间:按方向取差 1 的临界值
if (is_left) {
if (x >= l && x < r) return x + 1;
return x - 1;
} else {
if (x >= r && x < l) return x + 1;
return x - 1;
}
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%lld", &a[i]);
if (n == 1) { // 只有一堆时先手直接取完获胜
printf("1\n");
continue;
}
// 区间长度为 1 时,在任意一侧放上相同数量的石子即为必败态
for (int i = 1; i <= n; i++) Ldp[i][i] = Rdp[i][i] = a[i];
// 按区间长度从小到大递推两侧必败阈值
for (int len = 2; len <= n; len++) {
for (int i = 1; i + len - 1 <= n; i++) {
int j = i + len - 1;
Ldp[i][j] = next_end_state(a[j], Ldp[i][j - 1], Rdp[i][j - 1], 1);
Rdp[i][j] = next_end_state(a[i], Ldp[i + 1][j], Rdp[i + 1][j], 0);
}
}
// 把 a[1] 看作区间 [2,n] 左侧实际存在的堆:若与必败阈值相等则先手必败
if (a[1] == Ldp[2][n]) printf("0\n");
else printf("1\n");
}
return 0;
}复杂度
- 时间复杂度:
。状态数为 ,每个状态的转移仅涉及常数次大小比较与算术运算。对于 ,总计算量约为 次,在时限内非常充裕。 - 空间复杂度:
。使用二维数组记录 和 状态表,空间占用几十兆,满足内存限制。
总结
本题是典型的端点操作类博弈问题。面对极大的单堆石子数和无法直接套用 Nim 结论的限制,突破口在于:
- 建立基于区间的必败阈值定义(
和 ),并严格证明该阈值的唯一性与存在性; - 将博弈胜负的判定转化为对临界石子数的大力分类讨论,从而在
的区间 DP 中完成对博弈态的递推求解。