1 条题解
-
0
思路与算法
单堆大小为x的SG值记为g[x],g[0]=0。合法取数来自给定递增集合B,因此g[x]=mex({g[x-b] | b在B中且b<=x})。按x从小到大计算到最大初始堆大小A。后继数至多M,SG值至多M,用M+1大小的时间戳数组记录后继值。
设所有堆SG异或为z。若z=0,输出NO。否则输出YES,按堆编号从小到大、允许取数从小到大枚举第一步。选取第一个满足z xor g[A_i] xor g[A_i-b]=0的方案,输出堆编号与b,立即结束。
正确性说明
单堆每步至少减少一颗石子,递推图无环。按石子数归纳,较小后继的SG均已正确,取mex便得到当前SG。不同堆互不影响,正常玩法下整体SG等于各堆SG异或;为0时所有合法后继非零,为非零时存在合法后继归零。故NO与YES判断正确。
从第i堆取b时,其他堆不变,新的异或恰为z xor g[A_i] xor g[A_i-b]。枚举条件因此准确筛出所有能够转移到必败态的第一步。非零局面必存在此类方案。外层编号递增,内层取数递增,第一个符合条件的方案先最小化堆编号,再最小化取数,满足输出要求。
复杂度
令A为最大初始堆大小。分配和初始化SG数组O(A),标记数组O(M),读取堆与取数O(N+M)。每个x标记至多M个后继,mex至多M,共O(AM)。异或O(N),策略搜索最坏O(NM)。总时间O(A+M+AM+NM+N),额外空间O(A+M+N)。
易错点
- 只允许从给定集合中取数,不能套普通Nim的直接石子数异或。
- 只枚举b<=当前堆大小的合法操作;若所有b都更大,SG为0。
- YES之后必须输出获胜策略,NO后不输出第二行。
- 编号从1开始,先比较编号再比较取数。输入已经递增,找到第一步立即返回。
- 标记值使用当前x作为时间戳,避免反复清空数组;mex最大为M,数组需要M+1格。
参考代码(C++11)
#include <algorithm> #include <iostream> #include <vector> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; vector<int> a(n); int limit = 0; for (int &x : a) { cin >> x; limit = max(limit, x); } int m; cin >> m; vector<int> b(m); for (int &x : b) cin >> x; vector<int> sg(limit + 1), seen(m + 1, -1); for (int x = 1; x <= limit; ++x) { for (int t : b) { if (t > x) break; seen[sg[x - t]] = x; } while (seen[sg[x]] == x) ++sg[x]; } int sum = 0; for (int x : a) sum ^= sg[x]; if (!sum) { cout << "NO\n"; return 0; } cout << "YES\n"; for (int i = 0; i < n; ++i) for (int t : b) { if (t > a[i]) break; if ((sum ^ sg[a[i]] ^ sg[a[i] - t]) == 0) { cout << i + 1 << ' ' << t << '\n'; return 0; } } }
- 1