← 全部文章

CF594E Cutting the Line 题解

本文目录(13 节)
  1. 题意
  2. 题解
  3. k=1k = 1
  4. k=nk = n
  5. k<nk < n
  6. k≥3k \ge 3
  7. k=2k = 2
  8. t1t_1 翻转,t2t_2 翻转
  9. t1t_1 不翻转,t2t_2 不翻转
  10. t1t_1 翻转,t2t_2 不翻转
  11. t1t_1 不翻转,t2t_2 翻转
  12. 总结
  13. 代码

CF594E Cutting the Line
做法来自 wucstdio 神仙的题解,表述经过了一定的修改,也许会好懂一些?

题意

  • 给定一个字符串 ss 和一个正整数 kk。
  • 你可以将 ss 分成至多 kk 段,并将每一段翻转或者不翻转。
  • 求最终能够得到的字典序最小的 ss。
  • ∣s∣≤5×106|s| \le 5 \times 10^6。

题解

令 n=∣s∣n = |s|。

k=1k = 1

这种情况需要特判,比较 ss 和 srs^r 哪个小即可。

k=nk = n

当 k=nk = n 时,没有分割数量的限制。

对于某一段,如果不翻转它,可以看成分割成单个字符后翻转。因此此时一定存在一个最优解,满足分割后的每一段都翻转。

将问题转化一下,等价于每次从 srs^r 中截取一个后缀拼接在当前字符串之后。

那么显然我们应该贪心地截取最小后缀,Lyndon 分解即可。

k<nk < n

当 kk 变小时,我们需要尽量节约一点分割次数。

因此对于相邻两次分割,我们给出两个原则:

  1. 如果截取的字符串相同,可以合并为一次。
  2. 如果截取的字符串都是回文串,可以合并为一次,然后对这一次进行翻转。

k≥3k \ge 3

对 srs^r 做 Lyndon 分解,设其最小后缀为 tt,出现在结尾次数为 cc。

根据刚才节约分割次数的原则 1,我们显然应该考虑截取 c×tc \times t。

但是因为原则 2,这样还并不是最优的。

由于截取的字符是最小后缀,因此如果它们为回文串,其长度必然为 11。

因此策略如下:

  • 如果 ∣t∣≠1|t| \ne 1,则截取 c×tc \times t,然后让 kk 减 11。
  • 如果 ∣t∣=1|t| = 1,且前一个 Lyndon 串的长度不为 11,则截取 tt,然后让 kk 减 11。
  • 如果 ∣t∣=1|t| = 1,且前一个 Lyndon 串的长度也为 11,则截取 tt,但不消耗 kk。

k=2k = 2

一种做法是枚举分割点,使用 SAM 实现 O(1)\mathcal O(1) 查询任意两个子串的 LCP,总时间复杂度为 O(n)\mathcal O(n)。

然而,这个做法过于暴力,实现不够优美,最重要的是,我也不会 SAM。

现在的问题是,有一个字符串 ss(原串的反串),你要截取一个后缀 t1t_1,剩下的部分为 t2t_2,对于 t1,t2t_1,t_2,你都可以选择翻转或不翻转,要使 t1+t2t_1 + t_2 最小。

分类讨论一下:

t1t_1 翻转,t2t_2 翻转

就是 srs^r。

t1t_1 不翻转,t2t_2 不翻转

相当于要求 ss 的最小表示,有多种方法,Lyndon 分解也可以求。

t1t_1 翻转,t2t_2 不翻转

从后往前枚举分割点 ii,假设当前最优分割点为 jj,相当于比较 (s[i:j−1])r+s[:i−1](s[i:j-1])^r + s[:i-1] 和 s[:j−1]s[:j-1] 的大小。

可以转化为两次询问:

  • (s[i:j−1])r(s[i:j-1])^r 和 ss 的 LCP 长度
  • ss 和 s[j−i+1:j−1]s[j-i+1:j-1] 的 LCP 长度

由于都是关于 ss 的询问,因此考虑使用 exkmp 求。

t1t_1 不翻转,t2t_2 翻转

设 ss 的 Lyndon 分解为 a1c1+a2c2+⋯+amcma_1^{c_1} + a_2^{c_2} + \cdots + a_m^{c_m},其中 a1>a2>⋯>ama_1 > a_2 > \cdots > a_m。

设 bi=aicib_i = a_i^{c_i},则 ss 的 Lyndon 分解也可以写成 b1+b2+⋯+bmb_1 + b_2 + \cdots + b_m。

设 Bi=bi+bi+1+⋯+bmB_i = b_i + b_{i+1} + \cdots + b_m。

显然,最优分割点一定在 Lyndon 分解的分割点上。

进一步地可以证明,t1t_1 一定是某个 BiB_i,因为如果不是,意味着分割点前后都是 aia_i,那么分割点移到 bib_i 头尾的分割点上一定有一种更优。

由于 Bi>Bi+1B_i > B_{i+1} 可知,除非 Bi+1B_{i+1} 是 BiB_i 的前缀,否则 t1=Bi+1t_1 = B_{i+1} 比 t1=Bit_1 = B_i 优。

因此我们取最后一个满足 BpB_p 不是 Bp−1B_{p-1} 的前缀的 pp,则最优分割点一定 ≥p\ge p。

对于 i≥pi \ge p,如果 t1=Bit_1 = B_i 比 t1=Bi−1t_1 = B_{i-1} 更优,由于 BiB_i 是 Bi−1B_{i-1} 的前缀,而 Bi−1B_{i-1} 又是 Bi−2B_{i-2} 的前缀,因此 t1=Bit_1 = B_i 比 t1=Bi−2t_1 = B_{i-2} 更优(如果这句话无法理解,可以利用下图辅助理解,图来自 wucstdio)。

CF594E Cutting the Line 题解:原文插图

换句话说,我们只需要找到最大的 q(>p)q(>p),满足 t1=Bqt_1 = B_q 比 t1=Bq−1t_1 = B_{q-1} 更优,则 t1=Bqt_1 = B_q 就是最优解。如果不存在 qq,则 t1=Bpt_1 = B_p 就是最优解。

暴力比较一次 t1=Bqt_1 = B_q 和 t1=Bq−1t_1 = B_{q-1} 是 O(∣Bq−1∣)\mathcal O(|B_{q-1}|) 的,因此这么做的时间复杂度为 O(∑i=pm−1∣Bi∣)\mathcal O(\sum_{i=p}^{m-1}|B_i|) 的,看起来很大。

但是,由于对于 i≥pi \ge p,Bi+1B_{i+1} 是 BiB_i 的前缀,又根据 Lyndon 分解的性质,可以得到 ∣bi∣>∣Bi+1∣|b_i| > |B_{i+1}|,因此 ∣Bi+1∣<∣Bi∣2|B_{i+1}| < \frac{|B_i|}2。

于是 O(∑i=pm−1∣Bi∣)=O(∣Bp∣)\mathcal O(\sum_{i=p}^{m-1}|B_i|) = \mathcal O(|B_p|),我们得到了一个优美的线性做法。

总结

回顾一下整个 O(n)\mathcal O(n) 算法流程:

  1. 特判 k=1k = 1 的情况。
  2. 将 ss 翻转,Lyndon 分解。
  3. 当 k≥3k \ge 3 时,设最后一个 Lyndon 串为 tt,出现次数为 cc,不断重复以下策略,直到 k=2k = 2:
    • 如果 ∣t∣≠1|t| \ne 1,则截取 c×tc \times t,然后让 kk 减 11。
    • 如果 ∣t∣=1|t| = 1,且前一个 Lyndon 串的长度不为 11,则截取 tt,然后让 kk 减 11。
    • 如果 ∣t∣=1|t| = 1,且前一个 Lyndon 串的长度也为 11,则截取 tt,但不消耗 kk。
  4. 当 k=2k = 2 时,设截取的后缀为 t1t_1,剩下的部分为 t2t_2,分类讨论:
    • t1t_1 翻转,t2t_2 翻转:srs^r。
    • t1t_1 不翻转,t2t_2 不翻转:ss 的最小表示。
    • t1t_1 翻转,t2t_2 不翻转:枚举分割点,exkmp 实现。
    • t1t_1 不翻转,t2t_2 翻转:找到最后一个满足 BpB_p 不是 Bp−1B_{p-1} 的前缀的 pp,再找到最大的 q(>p)q(>p),满足 t1=Bqt_1 = B_q 比 t1=Bq−1t_1 = B_{q-1} 更优,此时 t1=Bqt_1 = B_q 就是最优解,如果不存在 qq,则 t1=Bpt_1 = B_p 就是最优解。

代码

#define pc putchar
const int N = 5e6 + 7;
int n, k, r[N], l[N], t, z[N], p[N];
char s[N], ans[N], ss[N*2];

inline void Lyndon(char *s, int n, int *r, int *l, int &t) {
	t = 0;
	int i = 1;
	while (i <= n) {
		int j = i, k = i + 1;
		while (k <= n && s[j] <= s[k]) j = s[j] == s[k++] ? j + 1 : i;
		while (i <= j) i += k - j;
		r[++t] = i - 1, l[t] = k - j;
	}
}

inline void k1() {
	for (int i = 1; i <= n; i++) {
		if (s[n+1-i] == s[i]) continue;
		if (s[n+1-i] < s[i]) reverse(s + 1, s + n + 1);
		break;
	}
	for (int i = 1; i <= n; i++) pc(s[i]);
}

inline void k3() {
	for (int i = r[t-1] + 1; i <= r[t]; i++) pc(s[i]);
	k -= l[t] != 1 || l[t-1] != 1, --t;
}

inline void upd(char *s) {
	for (int i = 1; i <= n; i++) {
		if (s[i] > ans[i]) return;
		if (s[i] < ans[i]) break;
	}
	for (int i = 1; i <= n; i++) ans[i] = s[i];
}

inline void upd1() {
	reverse(s + 1, s + n + 1);
	upd(s);
	reverse(s + 1, s + n + 1);
}

inline void upd2() {
	for (int i = 1; i <= n; i++) ss[i] = ss[i+n] = s[i];
	int i = 1, o;
	while (i <= n) {
		o = i;
		int j = i, k = i + 1;
		while (k <= n * 2 && ss[j] <= ss[k]) j = ss[j] == ss[k++] ? j + 1 : i;
		while (i <= j) i += k - j;
	}
	upd(ss + o - 1);
}

inline void Z(char *s, int n, int *z) {
	for (int i = 1; i <= n; i++) z[i] = 0;
	z[1] = n;
	for (int i = 2, l = 0, r = 0; i <= n; i++) {
		if (i <= r) z[i] = min(z[i-l+1], r - i + 1);
		while (i + z[i] <= n && s[i+z[i]] == s[z[i]+1]) ++z[i];
		if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
	}
}

inline void exkmp(char *s, int n, char *t, int m, int *z, int *p) {
	Z(t, m, z);
	for (int i = 1; i <= n; i++) p[i] = 0;
	for (int i = 1, l = 0, r = 0; i <= n; i++) {
		if (i <= r) p[i] = min(z[i-l+1], r - i + 1);
		while (i + p[i] <= n && s[i+p[i]] == t[p[i]+1]) ++p[i];
		if (i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
	}
}

inline void upd3() {
	for (int i = 1; i <= n; i++) ss[i] = s[i];
	reverse(ss + 1, ss + n + 1);
	exkmp(ss, n, s, n, z, p);
	reverse(p + 1, p + n + 1);
	int j = n;
	for (int i = n - 1; i; i--)
		if (p[j-1] < j - i) {
			if (s[j-1-p[j-1]] < s[p[j-1]+1]) j = i;
		} else if (z[j-i+1] < i - 1) {
			if (s[z[j-i+1]+1] < s[j-i+1+z[j-i+1]]) j = i;
		}
	reverse(s + 1, s + j);
	reverse(s + 1, s + n + 1);
	upd(s);
	reverse(s + 1, s + n + 1);
	reverse(s + 1, s + j);
}

inline void upd4() {
	int p = t + 1;
	while (--p > 1) {
		bool ok = 1;
		for (int i = r[p-1] + 1, j = r[p-2] + 1; ok && i <= r[p]; i++, j++)
			if (s[i] != s[j]) ok = 0;
		if (!ok) break;
	}
	int q = p;
	while (++q <= t)
		for (int i = r[q-1], j = r[q-2] + 1 + n - i; i > r[q-2]; i--, j++) {
			if (s[i] == s[j]) continue;
			if (s[i] < s[j]) p = q;
			break;
		}
	reverse(s + r[p-1] + 1, s + n + 1);
	reverse(s + 1, s + n + 1);
	upd(s);
	reverse(s + 1, s + n + 1);
	reverse(s + r[p-1] + 1, s + n + 1);
}

inline void k2() {
	for (int i = 1; i <= n; i++) ans[i] = s[i];
	upd1(), upd2(), upd3(), upd4();
}

int main() {
	rds(s, n), rd(k);
	if (k == 1) return k1(), 0;
	reverse(s + 1, s + n + 1);
	Lyndon(s, n, r, l, t);
	while (k >= 3 && t) k3();
	if ((n = r[t])) k2();
	for (int i = 1; i <= n; i++) pc(ans[i]);
	return 0;
}

标签:exKMP、Lyndon 分解、最小表示法、贪心

评论

    填写昵称即可留言。评论经站主审核后公开,请勿提交私人信息。

    正在连接评论服务…