P1063 · OFFICIAL SOLUTION

P1063 康神开播了 官方题解

Gioush OJ · P1063 康神开播了

【题意简述】

每次可以从当前字符串的左端或右端取出一个字符;若当前串形如 ABARABA^R,还可以把 AA 加入答案,同时删去左右两侧的 AAARA^R。求最终答案的字典序最小值。

【正解】

维护当前尚未处理的区间 [l,r][l,r]。若 SlSrS_l\ne S_r,所有合法操作接下来输出的第一个字符只能从两端产生,因此直接取较小的一端。

下面考虑 Sl=Sr=xS_l=S_r=x。设左端连续的 xx 长度为 LL,右端连续的 xx 长度为 RR;越过两段后遇到的字符分别为 a,ba,b。若整个区间都由 xx 构成,最优答案显然是

x(rl+1)/2+1.x^{\lfloor (r-l+1)/2\rfloor+1}.

否则只需比较 a,ba,bxx

  1. a>xa>xb>xb>x,分别使用前两类操作取完左右两端字符段,共输出 L+RL+Rxx
  2. a<xa<xb<xb<x,输出 min(L,R)\min(L,R)xx,并从两端各删去这么多个 xx
  3. 其余情况下 a,ba,b 分居 xx 两侧。若 a<ba<b,输出左侧的 LLxx 并删去左段;否则对称地处理右段。

预处理每个位置所在等值段的左右端点,就能在 O(1)\mathcal O(1) 时间取得 L,R,a,bL,R,a,b。每次循环至少删去一个字符,所以总复杂度为线性。

【正确性证明】

若两端字符不同,选择较大者会在答案的第一个不同位置立刻变劣,因此取较小端是唯一可能的最优选择。

若两端同为 xx,在遇到非 xx 字符前,任意方案产生的前缀都只含 xx。第三种操作中任意长度的 AA,都等价于连续执行 A|A|AA 为单字符的成对收缩:原操作要求中间的 BB 非空,所以逐字符收缩的每一步都合法;反向把这些收缩合并也显然合法。因此这里只需考虑“单端取出一个 xx”和“输出一个 xx 并成对删去两端 xx”。方案优劣由输出多少个 xx 后第一个实际输出的非 xx 字符决定。

  • a,b>xa,b>x 时,若使用一次成对收缩,可以把它拆成依次从两端各取一个 xx;这样在首次暴露大于 xx 的字符前会多得到一个 xx,字典序严格更小。因此最优方案不做成对收缩,而是分别取完两端的 xx,共输出 L+RL+Rxx
  • a,b<xa,b<x 时,首次暴露小于 xx 的字符前至少要输出 min(L,R)\min(L,R)xx,连续成对收缩恰好达到这个下界,所以输出 min(L,R)\min(L,R)xx 后从两端各删去相同数量;
  • a,ba,b 分居 xx 两侧时,第一个实际输出的非 xx 字符必须来自较小的一侧。即使较大侧的字符 bb 先成为端点,只要另一端仍为 xx,就有 x<bx<b,最优方案不会输出 bb;较小侧的 aa 成为端点后又有 a<ba<b,所以首个非 xx 输出仍为 aa。若较小字符来自左侧,至少要消耗左侧的 LLxx,而每次操作至多消耗一个左端 xx,故至少输出 LLxx;全部从左端取出恰好达到下界。期间若成对删去右端 xx,只会提前丢掉仍小于 bbxx,不会更优。右侧较小时完全对称。

若当前区间全部由 xx 构成,成对收缩能最大程度减少答案长度;但题目要求中间串 BB 非空,所以剩两个字符时不能继续收缩。奇数长度最终留下一个字符,偶数长度最终留下两个字符,答案长度均为 (rl+1)/2+1\lfloor (r-l+1)/2\rfloor+1

四种情况覆盖所有可能。每一步都得到所有合法方案中字典序最小的下一段,删去该段后问题仍与原问题同型;由归纳法,算法得到全局字典序最小答案。

【复杂度】

每个字符至多被预处理和删除各一次,时间复杂度为 O(S)\mathcal O(|S|),空间复杂度为 O(S)\mathcal O(|S|)

【参考代码】

#include <algorithm>#include <cstdio>#include <cstring>#include <string> const int MaxN = 500010; char S[MaxN];int Left[MaxN], Right[MaxN]; int main() {    int c, T;    scanf("%d%d", &c, &T);    while (T--) {        scanf("%s", S + 1);        int n = (int)strlen(S + 1);        Left[1] = 1;        for (int i = 2; i <= n; ++i)            Left[i] = S[i] == S[i - 1] ? Left[i - 1] : i;        Right[n] = n;        for (int i = n - 1; i >= 1; --i)            Right[i] = S[i] == S[i + 1] ? Right[i + 1] : i;         std::string Ans;        int l = 1, r = n;        while (l <= r) {            if (S[l] != S[r]) {                if (S[l] < S[r]) Ans.push_back(S[l++]);                else Ans.push_back(S[r--]);                continue;            }             char x = S[l];            int p = std::min(Right[l], r);            int q = std::max(Left[r], l);            if (p >= q) {                Ans.append((r - l + 1) / 2 + 1, x);                break;            }             int L = p - l + 1, R = r - q + 1;            char a = S[p + 1], b = S[q - 1];            if (a > x && b > x) {                Ans.append(L + R, x);                l = p + 1;                r = q - 1;            } else if (a < x && b < x) {                int d = std::min(L, R);                Ans.append(d, x);                l += d;                r -= d;            } else if (a < b) {                Ans.append(L, x);                l = p + 1;            } else {                Ans.append(R, x);                r = q - 1;            }        }        printf("%s\n", Ans.c_str());    }    return 0;}