ZJOI 2009 取石子游戏题解

\(0.\) 说明

本文作者:Altwilio 希望写的题解不要再被小破网站爬了。
本题解给出所有情况下保证先手必败的具体方案。

\(1.\) 题意

有一排 \(n\) 堆石子,两人轮流从最左或最右一堆取若干石子,不能取的人输。

问对于给定的初始局面,是否有先手必胜策略。

\(2.\) 思路

假设当前 \(i\)\(j\) 堆已经固定。 如 [?] [i j] [?][?] 代表未固定。

\(left_{i,j}\) 表示我在 \(i\) 左边放多少个棋子先手必败。

结论是 \(left_{i,j}\) 存在且唯一。

使用反证法:假设有两个取值 \(a < b\) 使得先手必败,那么当先手取 \(b - a\) 个时,留给后手 \(a\) 个,后手必败先手就不必败了。所以 \(left_{i,j}\) 存在且唯一。

同理可以求出 \(right_{i,j}\) 表示我在 \(j\) 右边放多少个棋子先手必败。

则当 \(left_{2,n} = a_1\) 时,先手没有必胜策略。这句话就是字面意思,当在 \(a_2\) 左边也就是 \(a_1\)\(left_{2,n}\) 是先手必败。

接下来是 \(\textbf{重点}\), 分析 \(left_{i,j}\) 如何递推,不分析 \(right_{i,j}\) 因为对称。

假设当前状态为 [?] [i j-1] [j(x)],第 \(j\) 堆的大小为 \(x\)。则需要求 [?] 等于什么可以使状态 [?] [i j-1] [j(x)] 必败。

再定义一个 \(L\) 代表使状态 [?] [i j-1] 必败的 [?] 值,\(R\) 代表使状态 [i j-1] [?] 必败的 [?] 值。

然后就是大幅分类讨论啦!

情况1

\(R = x\)

结论 [0] [i j-1] [x]

在状态 [?] [i j-1] [x]\(R = x\),则 \(left_{i,j} = 0\)

情况2

\(x < L, x < R\)

结论 [x] [i j-1] [x]

在状态 [x] [i j-1] [x]\(x < L\)\(x < R\),先手取完后,后手只要在另一堆取相同的个数即可,最后这两堆肯定会存在先手取完后有一堆为 \(0\) 的情况,则状态为 [0] [i j-1] [y]\(0 < y ≤ x\)。而此时的必败态只有 \(L\)\(R\),而 \(y<L,y<R\) 所以此时后手不必败,先手必败。

情况3.1

\(L>R\) 则有 \(R < x ≤ L\)

结论 [x-1] [i j-1] [x]

先手取右边:

先手把右边取到 \(R\)。后手把左边取完,先手面临必败状态。

先手把右边取到 \(x < R\)。后手把左边取到与右边相等,则转换为 情况2,先手必败。

先手把右边取到 \(x > R\)。后手把左边取到比右边小 \(1\),则右边必然会到达 情况1 或 情况2。先手必败。

先手取左边:

先手把左边取到 \(x ≥ R\)。后手把右边取到 \(x + 1\),则转换为 情况3.1。

先手把左边取到 \(x < R\)。后手把右边取到 \(R\),则转换为 情况2。

则只要 左边 \(≥R\), 右边 \(≥R+1\), 后手保证左边比右边少一个,一旦左边或者右边有一个 \(<R\),后手就保证左右两边一样多,就可以保证先手必败。

情况3.2

\(R > L\) 则有 \(L ≤ x < R\)

结论 [x+1] [i j-1] [x]

如果先手把左边取到 \(≥L+1\) 时,后手就把右边取到 \(≥L\),后手永远保证左边比右边多 \(1\)

如果先手把左边取到 \(L\) 时,后手就把右边取完,先手必败。

如果先手把左边或右边取到 \(<L\),后手就保证左右两边一样多,先手必败。

则后手保证左边比右边多一个,一旦左边或者右边有一个 \(<L\),后手就保证左右两边一样多,就可以保证先手必败。

情况4

\(x > L\)\(x > R\)

结论 [x] [i j-1] [x]

意味着只要 \(x>L\)\(x>R\),左边和右边取一样多就行。

\(L>R\) 时。

先手取完后 \(>L\),后手保证左右两边相同。

一旦先手把某一边个数取到 \((R,L]\) 后手保证左边比右边少一个,转换为 情况3.1。

一旦先手把某一边个数取到 \([R,L)\) 后手保证右边比左边多一个。

一旦先手把某一边个数取到 \((,R)\) 后手保证右边和左边一样多。

\(R>L\) 时,对称。

先手取完后 \(>R\),后手保证左右两边相同。

一旦先手把某一边个数取到 \((L,R]\) 后手保证右边比左边少一个,转换为 情况3.2。

一旦先手把某一边个数取到 \([L,R)\) 后手保证左边比右边多一个。

一旦先手把某一边个数取到 \((,L)\) 后手保证右边和左边一样多。

\(3.\) 代码

const int N = 1010;
int n, a[N], l[N][N], r[N][N];

signed main(){
    int T; read(T);
    while(T --){
        read(n);
        for(int i = 1; i <= n; i ++) read(a[i]);
        for(int len = 1; len <= n; len ++){
            for(int i = 1; i + len - 1 <= n; i ++){
                int j = i + len - 1; // 左端点 i 右端点 j
                // 补充若区间只有一个堆,应取 [x][x][x]
                if(len == 1) l[i][j] = r[i][j] = a[i];
                else{
                    int L = l[i][j - 1], R = r[i][j - 1], X = a[j]; // 情况1
                    if(R == X) l[i][j] = 0; //情况2 情况4
                    else if(X < L && X < R || X > L && X > R) l[i][j] = X; //情况3.1 情况4.1
                    else if(L > R) l[i][j] = X - 1; //情况3.2 情况4.2
                    else l[i][j] = X + 1;
                    //以下对称
                    L = l[i + 1][j], R = r[i + 1][j], X = a[i];
                    if(L == X) r[i][j] = 0;
                    else if(X < L && X < R || X > L && X > R) r[i][j] = X;
                    else if(R > L) r[i][j] = X - 1;
                    else r[i][j] = X + 1;
                }
            }
        }
        if(n == 1) puts("1");
        else print(l[2][n] != a[1]), puts("");
    } 
    return 0;
}
posted @ 2022-07-26 16:58  Altwilio  阅读(81)  评论(0编辑  收藏  举报