1 条题解
-
0
题解:Good Game
题目分析
问题核心
给定一个长度为 (N) 的由 'A' 和 'B' 组成的字符串,判断是否能通过一系列操作移除所有字符。每次操作可选择 2 或 3 个连续且相同的字符 移除,移除后剩余字符拼接重新编号。若可全部移除,输出操作序列;否则输出 (-1)。
关键观察
- 字符分组:连续相同的字符可视为一个“块”。例如字符串
ABAABBBAA可分为块序列[A(1), B(1), A(2), B(3), A(2)](括号内为块的长度)。设块的总数为 (n),则问题转化为对这 (n) 个块的“合法消除”(块的消除依赖于相邻块的合并,本质是块序列的奇偶性与长度特性)。 - 块的关键属性:定义 (a[i] = 1) 表示第 (i) 个块的长度 (>1)(可独立作为 2/3 个字符移除),(a[i] = 0) 表示块长度 (=1)(无法独立移除,需与相邻块合并)。
- 可消除的充要条件:块序列需满足特定的奇偶性和“可合并性”,核心是避免出现无法配对消除的孤立单长度块。
数据范围挑战
- (N \leq 10^6),需设计 (O(N)) 或 (O(n)) 复杂度算法((n \leq N) 为块数),避免递归或暴力枚举。
算法设计
核心思路
- 预处理:字符分组:将原始字符串按连续相同字符分组,得到块序列及属性数组 (a[i])(块长度是否 >1)。
- 可行性判断(check 函数):分两种情况判断块序列是否可全部消除:
- 块数 (n) 为奇数:需存在一个“中心区域”,可通过合并消除所有块。
- 块数 (n) 为偶数:通过并查集维护可合并的块区间,判断是否存在无法消除的“断点”。
- 构造操作序列(solve 函数):若可行,按块序列的结构(奇数/偶数块数),通过“对称合并”或“分段消除”构造操作,将块转化为 2/3 长度的连续字符并记录操作位置。
- 操作序列转换:将块的消除操作映射回原始字符串的位置(利用块的起始编号),并拆分为 2/3 长度的具体操作。
具体步骤**
1. 预处理:字符分组
void read() { string s; cin >> n >> s; int cnt = 0; for (int i = 0; i < n; i++) { if (i == 0 || s[i] != s[i-1]) pid[++cnt] = i + 1; // 块的起始位置(原始字符串编号,1-based) b[cnt]++; // 块的长度 } swap(n, cnt); // n 变为块数 for (int i = 1; i <= n; i++) a[i] = (b[i] > 1); // 块属性:长度>1 标记为1 }- 输出:块数 (n)、块长度数组 (b)、块起始位置数组 (pid)、属性数组 (a)。
2. 可行性判断(check 函数)
情况 1:块数 (n) 为奇数
核心是存在一个“中心块”,其左右两侧的单长度块((a[i]=0))可通过合并消除:
- 找到最左和最右的“长块”((a[i]=1)),记为 (l) 和 (r)。
- 若 (r - l - 1 \geq n/2)(中心区域足够大,可容纳所有单长度块的合并),则可行;否则不可行。
情况 2:块数 (n) 为偶数
需确保块序列无“断点”(即不存在无法合并消除的单长度块区间):
- 用并查集合并相邻的单长度块((a[i]=0) 且 (a[i+1]=0)),维护可合并区间的大小。
- 预处理前缀数组 (pre[i]):表示前 (i) 个块组成的前缀是否“可消除”。
- 预处理后缀数组 (suf[i]):表示后 (n-i+1) 个块组成的后缀是否“可消除”。
- 若存在某个位置 (i) 使得前缀 (pre[i]) 和后缀 (suf[i+1]) 均不可消除,则存在断点,不可行;否则可行。
3. 构造操作序列(solve 函数)
情况 1:块数 (n) 为奇数
以块序列的中心为基准,通过“对称合并”将单长度块与相邻长块合并,转化为可消除的 2/3 长度:
- 找到中心区域的左右边界 (l)(最左长块)和 (r)(最右长块)。
- 先处理中心右侧的块,再处理左侧的块,将合并后的块长度记录为操作对象。
情况 2:块数 (n) 为偶数
找到断点 (pmid),将块序列分为左右两段(([1, pmid]) 和 ([pmid+1, n])),分别对两段调用 solve 函数构造操作。
4. 操作序列转换(out 函数)
将块的合并长度拆分为 2/3 的操作(优先用 2,剩余用 3,或反之),并映射回原始字符串的起始位置:
- 例如,合并后某块的长度为 5,可拆分为“2+3”或“3+2”,对应的操作是原始起始位置 + 长度 2 或 3。
代码关键模块解析
1. 并查集(FaC 类)
用于合并相邻的单长度块,维护可合并区间的大小,辅助可行性判断:
struct FaC { int fa[N], sz[N]; void init() { for (int i=1; i<=n; i++) fa[i]=i, sz[i]=1; } int find(int x) { return fa[x]==x ? x : fa[x]=find(fa[x]); } void merge(int x, int y) { // 合并两个块 x=find(x), y=find(y); if (x!=y) fa[y]=x, sz[x]+=sz[y]; } int getsiz(int x) { return sz[find(x)]; } // 获取合并后的区间大小 };2. 可行性判断(check 函数)
bool check() { if (n & 1) { // 块数为奇数 int l=0, r=n+1; for (int i=1; i<=n/2+1; i++) if (a[i]) l=i; // 最左长块 for (int i=n; i>=n/2+1; i--) if (a[i]) r=i; // 最右长块 return (r-l-1 >= n/2); // 中心区域足够大 } else { // 块数为偶数 F.init(); for (int i=1; i<=n; i++) { if (i>1 && a[i]==0 && a[i-1]==0) F.merge(i-1, i); if (i&1) pre[i] = (F.getsiz((1+i)/2) < i/2 || a[(1+i)/2]==1); } F.init(); for (int i=n; i>=1; i--) { if (i<n && a[i]==0 && a[i+1]==0) F.merge(i+1, i); if ((n-i+1)&1) suf[i] = (F.getsiz((n+i)/2) < (n-i+1)/2 || a[(n+i)/2]==1); } for (int i=1; i<n; i++) if (i&1) { if (pre[i] && suf[i+1]) { pmid=i; return 0; } // 存在断点,不可行 } return 1; } }- 奇数块数:通过左右长块的位置判断中心区域是否足够。
- 偶数块数:通过前缀/后缀数组判断是否存在断点,若存在则记录断点位置 (pmid)。
3. 操作构造(solve 函数)
以奇数块数为例,对称合并块并记录操作:
void solve(int ql, int qr) { int len=qr-ql+1, l=ql-1, r=qr+1, mid=(ql+qr)>>1; for (int i=ql; i<=mid; i++) if (a[i]) l=i; // 左长块 for (int i=qr; i>=mid; i++) if (a[i]) r=i; // 右长块 // 处理右侧块 for (int c=0; c<mid-l; c++) { if (c==0) ans.pb(mp(b[r], pid[r])); else ans.pb(mp(b[r-c]+b[r+c], pid[r-c])); } // 合并块,更新长度 int fl=r-(mid-l), fr=r+(mid-l), j=l; if (l!=mid) b[fr] += b[fl]; // 处理左侧块 for (int c=0; c<l-ql+1; c++) { if (c==0) ans.pb(mp(b[l], pid[l])); else ans.pb(mp(b[l-c]+b[j], pid[l-c])); j++; if (l!=mid && j==fl) j=fr; } }- 核心:将单长度块与相邻长块合并,转化为可消除的长度(≥2),并记录合并后的块长度和原始起始位置。
4. 操作转换(out 函数)
将合并后的块长度拆分为 2/3 的操作:
void out() { for (pii p : ans) { int id=p.sec, w=p.fir; while (w) { if (w&1) pans.pb(mp(3, id)), w-=3; // 奇数剩余用3 else pans.pb(mp(2, id)), w-=2; // 偶数用2 } } cout << pans.size() << '\n'; for (pii p : pans) cout << p.sec << ' ' << p.fir << '\n'; }- 例如,长度 5 拆分为 2+3,长度 4 拆分为 2+2,长度 3 直接输出。
时间复杂度与空间复杂度
- 时间复杂度:(O(N))。字符分组为 (O(N)),可行性判断和操作构造均为 (O(n))((n \leq N)),操作转换为 (O(K))((K) 为操作次数,(K \leq N))。
- 空间复杂度:(O(N))。存储块信息、并查集、操作序列等数组,均为线性空间。
关键结论
- 块序列转化:将原始字符串转化为块序列,将问题简化为块的合并与消除,大幅降低复杂度。
- 奇偶性判断:块数的奇偶性决定了判断逻辑,奇数块数依赖中心区域,偶数块数依赖断点检测。
- 操作构造技巧:通过对称合并和分段处理,确保所有块都能转化为 2/3 长度的可消除单元,同时映射回原始位置保证操作的正确性。
- 字符分组:连续相同的字符可视为一个“块”。例如字符串
- 1
信息
- ID
- 5420
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者