##[$AcWing$ $1321$. 取石子](https://www.acwing.com/problem/content/description/1323/) **[参考题解](https://www.cnblogs.com/ZJXXCN/p/11068490.html)** ### 一、题目描述 $Alice$ 和 $Bob$ 两个好朋友又开始玩取石子了。 游戏开始时,有 $N$ 堆石子排成一排,然后他们轮流操作($Alice$ 先手),每次操作时从下面的规则中任选一个: - 从某堆石子中取走一个; - 合并任意两堆石子。 不能操作的人输。 $Alice$ 想知道,她是否能有必胜策略。 **输入格式** 第一行输入 $T$,表示数据组数。 对于每组测试数据,第一行读入 $N$; 接下来 $N$ 个正整数 $a_1,a_2,⋯,a_N$ ,表示每堆石子的数量。 **输出格式** 对于每组测试数据,输出一行。 输出 $YES$ 表示 $Alice$ 有必胜策略,输出 $NO$ 表示 $Alice$ 没有必胜策略。 **数据范围** $1≤T≤100,1≤N≤50,1≤a_i≤1000$ **输入样例:** ```cpp {.line-numbers} 3 3 1 1 2 2 3 4 3 2 3 5 ``` **输出样例:** ```cpp {.line-numbers} YES NO NO ``` ### 二、博弈论总结 必胜态 $\Rightarrow$ 选择合适方案 $\Rightarrow$ 必败态 必败态 $\Rightarrow$ 选择任何路线 $\Rightarrow$ 必胜态 ### 三、简单情况 为什么会想到讨论简单情况呢?我们来思考一下:如果某一堆石子只有$1$个,随着我们执行拿走$1$个的操作,它的堆就没了,这样石子个数变了,堆数也变了,两个变量,问题变复杂了,我们上来就想难题,怕是搞不定。 既然这样,我们就思考一下 **子空间** :只考虑所有个数大于等于$2$的那些堆,其它可能存在石子数等于$1$的,等我们想明白这个简单问题再研究扩展的事,由易到难。 同时,我们需要思考博弈的胜负与什么因素相关呢?因为只有两种操作:**拿走一个石子、合并两堆**,很显然,**两个关键因素:石子个数、堆数** 同时,两个操作同一时间只能执行一个,所以可以理解为拿走一个石子对结果影响一下,合并两堆石子对结果也是影响一下,初步考虑应该堆个数与石子总数的加法关系相关。 **子空间:当每堆的石子个数都是大于等于$2$时**
设$b$ = 堆数 + 石子总数 - $1$
结论:$b$是奇数⟺先手必胜,$b$是偶数⟺先手必败
**证明:** 1、边界:当我们只有一堆石子且该堆石子个数为$1$个时,$b=1$,先手必胜。 2、当$b$为奇数,一定可以通过某种操作将$b$变成偶数 * 如果堆数大于$1$,合并两堆让$b$变为偶数 * 如果堆数等于$1$,从该堆石子中取出一个就可以让$b$变为偶数 3、当$b$为偶数,无论如何操作,$b$都必将变为奇数 * 合并两堆,则$b$变为奇数 * 从某一堆中取走一个石子: * 若该堆石子个数大于$2$,则$b$变为奇数,且所有堆石子数量严格大于$1$ * 若该堆石子个数等于$2$,取一个石子后,$b$变为奇数,该堆石子个数变为$1$个,此时就再是子空间范围内了,因为出现某堆的石子个数为$1$,而不是每一堆都大于等于$2$了!需要继续分类讨论: #### 特殊情况 此时为了保证所有堆的石子个数大于$1$,**足够聪明的对手** 可以进行的操作分为两类: ① 如果只有这一堆石子,此时 **对手必胜** ② 如果有多堆石子,可以将这一个石子合并到其他堆中,这样每对石子个数都大于$1$ **$Q$:对手为什么一定要采用合并的操作,而不是从别的堆中取石子呢?** 我来举两个简单的栗子: * **只有一堆石子** 石子个数是$2$个。你拿走一个,对手直接拿走另一个,游戏结束,**对手赢了**!你也是足够聪明的,你会在这种情况下这么拿吗?不能吧~,啥时候可能遇到这个情况呢?就是你被 **逼到** 这个场景下,也就是一直处于必败态! * **两堆石子** 每堆石子个数是$2$个。**我是先手**,可以有两种选择: (1)、从任意一堆中拿走$1$个, 现在的局面是$\{2,1\}$ $$\large 后手选择(对手) \Rightarrow \left\{\begin{matrix} 从2中取一个 & \Rightarrow \{1,1\} & \Rightarrow \large \left\{\begin{matrix} 先手合并 \Rightarrow \{2\}& 剩下一个一个取,先手胜 \\ 先手后手一个一个取 \Rightarrow 先手败 & \end{matrix}\right. \\ 从1中取一个& \Rightarrow \{2,0\} & 剩下一个一个取,先手败\\ 合并两堆 & \Rightarrow \{3\} & 剩下一个一个取,先手胜 \\ \end{matrix}\right. $$ 指望对手出错我才有赢的机会,人家要是聪明,我就废了! 我是先手,我肯定不能把自己的命运交到别人手中!我选择合并两堆,这样我保准赢! (2)、把两堆直接合并,现在的状态$\{4\}$ 这下进入了我的套路,你取吧,你取一个,我也取一个;你再取一个,我也再取一个,结果,没有了,**对手必败**。 上面的例子可能不能描述所有场景,我现在$b$是奇数,我在必胜态,我不会让自己陷入到$b$可能是偶数的状态中去,如果我选择了 - 合并操作减少$1$个堆 - 拿走操作减少$1$个石子 都会把$b-1$这个偶数态给对方 我不会傻到一个操作,即可能造成堆也变化,让石子个数也变化,这样就得看对方怎么选择了,而他还那么聪明,我不能犯这样的错误。 ### 四、本题情况 本题中可能存在一些堆的石子个数等于$1$: * 假设有$a$堆石子,其中每堆石子个数为$1$ * 剩余堆的石子个数都严格大于$1$ 根据这些数量大于$1$的堆的石子可以求出上述定义出的$b$,我们使用$f(a, b)$表示此时先手必胜还是必败,因为博弈论在本质上是可以递推的,我们可以想出起点,再想出递推关系,就可以递推得到更大数据情况下的递推值,也就是博弈论本质上是$dp$。
相关疑问 $Q1:$**情况**$3$**为什么是两个表达式?** 答: ①当右侧存在时,合并左边两堆石子,则右侧多出一堆石子,并且,石子个数增加$2$,也就是$b+=3$ ②当右侧一个都没有的时候,左边送来了一堆,两个石子,按$b$的定义,是堆数+石子个数$-1=2$,即$b+=2$ $Q2$:**为什么用一个奇数来描述简单情况的状态,而不是用偶数呢?** 答:因为要通过递推式进行计算,最终的边界是需要我们考虑的: - 如果用奇数,那么边界就是$b=1$,表示只有$1$堆,石子数量只有$1$个,此时当然必胜。 - 如果用偶数,比如边界是$b=0$,表示目前$0$堆,$0$个石子,这都啥也没有了,还必胜态,不符合逻辑,说不清道不明。 - 那要是不用$b=0$做边界,用$b=2$呢?表示只有$1$堆,石子数量只有$1$个,这个应该也是可以,但没有再仔细想了。 $Q3:$**情况**$2$**从右边取一个石子,如果此时右侧存在某一堆中石子个数是$2$,取走$1$个后,变成了$1$,不就是右侧减少了一个堆,减少了两个石子,即$b-=3$;同时,此堆石子个数变为$1$,左侧个数$a+=1$,为什么没有看到这个状态变化呢?** 答:这是因为聪明人不会从右侧某个石子数量大于$2$的堆中取走石子! 看一下 **讨论简单情况** 中第$3$点后面的 **特殊情况**: - 如果右侧只有一堆,石子数量为$2$,拿走$1$个,剩$1$个,一堆一个,对方必胜,此为必败态 - 如果右侧大于一堆,某一堆只有$2$个石子,拿走$1$个,剩$1$个,对手足够聪明,会采用右侧两堆合并的办法,此时 石子数量减$1$,堆数减$1$,对$b$的影响是减$2$,对$b$的奇偶性没有影响,换句话说,如果你现在处在必败态,你这么整完,还是必败态 ### 五、时间复杂度 这里因为$a$最大取$50$,$b$最大取$50050$,因此计算这些状态的计算量为$2.5×10^6$,虽然有最多$100$次查询,但是这些状态每个只会计算一遍,因此不会超时。 ### 六、实现代码 ```cpp {.line-numbers} #include using namespace std; const int N = 55, M = 50050; // M 包括了 50 * 1000 + 50个石子数量为1的堆数 int f[N][M]; int dfs(int a, int b) { int &v = f[a][b]; if (~v) return v; // 简单情况: 即所有堆的石子个数都是严格大于1,此时a是0 if (!a) return v = b % 2; // 奇数为先手必胜,偶数为先手必败 // 一般5个情况 + 1个特殊情况 // 特殊情况: 如果操作后出现b中只有一堆,且堆中石子个数为1 // 那么应该归入到a中,并且b为0 // 以下所有情况,如果能进入必败态,先手则必胜! if (b == 1) return dfs(a + 1, 0); // 情况1:有a,从a中取一个 if (a && !dfs(a - 1, b)) return v = 1; // 情况2, 4:有b,从b中取1个(石子总数 - 1) or 合并b中两堆(堆数 - 1), if (b && !dfs(a, b - 1)) return v = 1; // 情况3:有a >= 2, 合并a中两个 // 如果b的堆数不为0, a - 2, b + 1堆 + 2个石子(只需要加delta) ====> b + 3 // 如果b的堆数为0, a - 2, 0 + 2个石子 + 1堆 - 1 ====> b + 2 if (a >= 2 && !dfs(a - 2, b + (b ? 3 : 2))) return v = 1; // 情况5:有a,有b, 合并a中1个b中1个, a - 1, b的堆数无变化 + 1个石子(只加delta) if (a && b && !dfs(a - 1, b + 1)) return v = 1; // 其他情况,则先手处于必败状态 return v = 0; } int main() { memset(f, -1, sizeof f); int T, n; cin >> T; while (T--) { cin >> n; int a = 0, b = 0; for (int i = 0; i < n; i++) { int x; cin >> x; if (x == 1) a++; // b != 0时 加1堆 + 加x石子 = 原来的 + x + 1 (其实就是区别一开始的时候) // 当b != 0时, 我们往后加的delta // b == 0时 加1堆 + 加x石子 = 0 + 1 + x - 1 = x // 注意操作符优先级 else b += b ? x + 1 : x; } // 1 为先手必胜, 0为先手必败 if (dfs(a, b)) puts("YES"); else puts("NO"); } return 0; } ```