AtCoder ARC175F Append Same Characters 题解

更好的阅读体验

题意

传送门

给出由 n 个仅含小写英文字母的字符串组成的序列,你可以做下列两种操作:

  1. 选择一个小写英文字母,并在序列中 每个 字符串末尾添加这个字符;
  2. 交换序列中两个相邻的字符串。

你需要通过若干次操作使得字符串序列的字典序 单调不降,求最小操作次数。

2n3×105,字符串长度之和 3×105

思路

首先发现我们肯定会先做完操作 1 再做操作 2,并且操作 2 的次数等于序列的逆序对数,所以问题转化为在每个串后面加一个字符串 X,并最小化逆序对个数加 |X|

于是我们先计算出原序列的逆序对个数,然后再考虑 X 的影响。

考虑怎样的两个字符串 A,B 会被 X 影响(不妨设 |A||B|)。如果 AB 在前 min(|A|,|B|) 个字符就比出结果了,那么 X 显然是没用的。所以只有当 AB 的前缀的时候 X 才有用。

B 除去 A,B 的公共前缀部分后的字符串为 Y,那么 AX>BXX>YX。于是其实答案只和 X,Y 有关。

然后似乎就做不下去了。这时需要用到一些结论,这里先不给出证明,在文末统一给出证明(下同)。

引理 1: X<YXX<Y

有了这个之后就有了一个大致思路。我们把所有 Y 排序,那么一个 X 会影响到的 Y 就是一段前缀。我们对于每个 Y 统计它对逆序对的贡献,然后枚举一个前缀算答案就行了。

那么怎么对 Y 排序呢?这里需要用到另外的结论。

引理 2: X<YXY<YX

然后就可以很容易用 SA 做到 O(nlogn),或者用哈希做到 O(nlog2n)

然后考虑一个 Y 对逆序对的贡献到底是什么。对于一个原串 B,Trie 上它到根的链上所有的(终止)节点都可以作为 A。不过要注意字符串可以重复,所以一个点可能有多个字符串在这里终止。

由于 |si|3×105,所以我们可以枚举 Trie 上每个节点上的每个字符串,然后再枚举它到根的路径上每个点,这样的总复杂度是 |si|。假设我们枚举了一个字符串 si=B 和它到根路径上一个点 x,那么 x 上的所有字符串都可以作为 A 产生贡献。对于一个 A=sj,本来是 A<B,但如果加上 X(X>Y) 后大小关系会变成 AX>BX,此时如果 j<i 对答案的贡献为 +1(增加一个逆序对),否则(i>j)对答案的贡献为 1(减少一个逆序对)。所以对于 x 上的所有 A 我们只需要二分有多少个 A 的下标小于 i 并计算贡献就好了。

最后还一个问题就是,假如我们枚举了 Y1,Y2 并让 Y1<XY2,我们如何算出 |X| 的最小值呢?考虑对于有限的字符串来说,答案是简单的,就是 P<XQ|X|min=lcp(P,Q)+1。所以问题转化为 lcp(Y1,Y2)。这里又需要用一个结论。

引理 3: 如果 XY,那么 lcp(X,Y)=lcp(XY,YX)

然后就很容易用 SA 或哈希解决这个问题。具体来说就是把所有串拼在一起并用一个特殊字符隔开,然后就可以用 heightO(1) 比较两个串以及它们的(有限)拼接。

还有一点细节需要注意。比如 X 比所有 Y 都大的时候,要特殊处理,具体就是找到第一个非 z 的字符(如果没有就说明不可能找到一个 X>Y)。以及每个 Y 的贡献都是可能爆 int 的。

总时间复杂度为 O(n+|si|logn),如果用哈希就是 O(n+|si|log2n)

代码

代码
#include <bits/stdc++.h>

typedef long long LL;

const int N = 3e5 + 5;

int n;
int st[N], len[N];
int L;
char s[2 * N];

int sa[2 * N << 1], rk[2 * N << 1], tot[2 * N], tp[2 * N], ht[2 * N];
void get_SA() {
	int m = std::max('z', '#');
	auto basic_sort = [&]() {
		for(int i = 1; i <= m; i++) tot[i] = 0;
		for(int i = 1; i <= L; i++) tot[rk[i]]++;
		for(int i = 1; i <= m; i++) tot[i] += tot[i - 1];
		for(int i = L; i >= 1; i--) sa[tot[rk[tp[i]]]--] = tp[i];
	};
	for(int i = 1; i <= L; i++) rk[i] = s[i], tp[i] = i;
	basic_sort();
	for(int i = 1, p = 0; p < L; i <<= 1, m = p) {
		p = 0;
		for(int j = 1; j <= i; j++) tp[++p] = L - i + j;
		for(int j = 1; j <= L; j++) if(sa[j] > i) tp[++p] = sa[j] - i;
		basic_sort();
		for(int j = 1; j <= L; j++) tp[j] = rk[j];
		p = rk[sa[1]] = 1;
		for(int j = 2; j <= L; j++)
			rk[sa[j]] = (tp[sa[j - 1]] == tp[sa[j]] && tp[sa[j - 1] + i] == tp[sa[j] + i] ? p : ++p);
	}
	for(int i = 1, k = 0; i <= L; i++) {
		if(rk[i] == 1) { ht[rk[i]] = 0; continue; }
		if(k) k--;
		int j = sa[rk[i] - 1];
		while(i + k <= L && j + k <= L && s[i + k] == s[j + k]) k++;
		ht[rk[i]] = k;
	}
}

struct SparseTable {
	int go[21][2 * N];
	void init() {
		for(int i = 1; i <= L; i++) go[0][i] = ht[i];
		for(int j = 1; j <= 20; j++)
			for(int i = 1; i + (1 << j) - 1 <= L; i++)
				go[j][i] = std::min(go[j - 1][i], go[j - 1][i + (1 << (j - 1))]);
	}
	int get(int l, int r) {
		if(l == r) return std::min(L - sa[l] + 1, L - sa[r] + 1);
		if(l > r) std::swap(l, r);
		int k = 31 ^ __builtin_clz(r - l);
		return std::min(go[k][l + 1], go[k][r - (1 << k) + 1]);
	}
} ST;

int trie[2 * N][26], dep[2 * N], sz[2 * N];
std::vector<int> end[2 * N];
int ctrie;

struct String { int l, r; };
int comp(int i, int j, int k) {
	int lc = ST.get(rk[i], rk[j]);
	if(lc >= k) return 0;
	else return s[i + lc] < s[j + lc] ? -1 : 1;
}
bool operator<(String x, String y) {
	int lenx = x.r - x.l + 1, leny = y.r - y.l + 1;
	int ret = comp(x.l, y.l, std::min(lenx, leny));
	if(ret) return ret == -1;
	ret = comp(lenx > leny ? x.l + leny : y.l, leny > lenx ? y.l + lenx : x.l, std::max(lenx, leny) - std::min(lenx, leny));
	if(ret) return ret == -1;
	ret = comp(lenx > leny ? y.l : y.l + (leny - lenx), leny > lenx ? x.l : x.l + (lenx - leny), std::min(lenx, leny));
	return ret == -1;
}
int lcp(int i, int j, int k) {
	int lc = ST.get(rk[i], rk[j]);
	if(lc >= k) return -1;
	else return lc;
}
int lcp(String x, String y) {
	int lenx = x.r - x.l + 1, leny = y.r - y.l + 1;
	int ret = lcp(x.l, y.l, std::min(lenx, leny));
	if(ret != -1) return ret;
	ret = lcp(lenx > leny ? x.l + leny : y.l, leny > lenx ? y.l + lenx : x.l, std::max(lenx, leny) - std::min(lenx, leny));
	if(ret != -1) return ret + std::min(lenx, leny);
	ret = lcp(lenx > leny ? y.l : y.l + (leny - lenx), leny > lenx ? x.l : x.l + (lenx - leny), std::min(lenx, leny));
	if(ret != -1) return ret + std::max(lenx, leny);
	else return -1;
}

std::vector<int> stk;
std::vector<std::pair<String, LL>> vct;
void dfs(int x) {
	if(!end[x].empty()) {
		for(int y : stk) {
			int j = st[end[x].front()] + len[end[x].front()] - 1;
			vct.push_back({{j - (dep[x] - dep[y]) + 1, j}, 0});
			for(int i : end[x]) {
				auto it = std::lower_bound(end[y].begin(), end[y].end(), i);
				vct.back().second += it - end[y].begin();
				vct.back().second -= end[y].end() - it;
			}
		}
		stk.emplace_back(x);
	}
	for(int i = 0; i < 26; i++) if(trie[x][i]) dep[trie[x][i]] = dep[x] + 1, dfs(trie[x][i]);
	if(!end[x].empty()) stk.pop_back();
}

int main() {
	scanf("%d", &n);
	st[1] = 1;
	LL base = 0;
	for(int i = 1; i <= n; i++) {
		scanf("%s", s + st[i]);
		len[i] = strlen(s + st[i]);
		s[st[i] + len[i]] = '#';
		st[i + 1] = st[i] + len[i] + 1;
		int now = 0;
		for(int j = st[i]; j <= st[i] + len[i] - 1; j++) {
			for(int k = s[j] - 'a' + 1; k < 26; k++) base += sz[trie[now][k]];
			if(!trie[now][s[j] - 'a']) trie[now][s[j] - 'a'] = ++ctrie;
			now = trie[now][s[j] - 'a'];
			sz[now]++;
		}
		for(int k = 0; k < 26; k++) base += sz[trie[now][k]];
		end[now].emplace_back(i);
	}
	L = st[n] + len[n] - 1;
	get_SA();
	ST.init();
	dfs(0);
	std::sort(vct.begin(), vct.end());
	LL ans = base, sum = 0;
	for(int i = 0; i < (int)vct.size() - 1; i++) {
		sum += vct[i].second;
		int val = lcp(vct[i].first, vct[i + 1].first);
		if(val != -1) ans = std::min(ans, base + sum + val + 1);
	}
	if(!vct.empty()) {
		sum += vct.back().second;
		int val = 0;
		while(vct.back().first.l + val <= vct.back().first.r && s[vct.back().first.l + val] == 'z') val++;
		if(vct.back().first.l + val <= vct.back().first.r) ans = std::min(ans, base + sum + val + 1);
	}
	printf("%lld\n", ans);
	return 0;
}

证明

下面的题解其实基本上是官方题解的翻译。

前置

可能需要了解一下 Border 理论和 period 相关的东西。(不用了解太多,知道 Border 和 period 的定义以及弱周期定理就行了,也可以先往下看,看到不懂的再到这篇文章里找)

引理 1

引理 1: X<YXX<Y

对于 Y 不是 X 的前缀的情况,此时 X>YXX>YX>Y

然后对于 YX 的前缀,则令 X=YX,那么 X>YXYX>Y2XX>YX,根据归纳法 X>YXX>YYX>YX>Y

引理 2

引理 2: X<YXY<YX

对于字符串 X,Yg=gcd(|X|,|Y|),下面三个结论都是等价的:

  1. X=Y
  2. g 同时是 X,Y 的循环节(且 X[0..g)=Y[0..g)
  3. XY=YX

其中 21 以及 23 是显然的。

12

根据中国剩余定理,只要 ij(modg),就一定可以找到 k 满足 ki(mod|X|)kj(mod|Y|),因为 gcd(|X|,|Y|)=g。所以 Xi=(X)k=(Y)k=Yj,又因为 i+gj(modg) 也成立,所以同理可得 Xi+g=Yj,所以 Xi=Xi+g,即 gX 的周期。Y 同理。

32

不妨设 |X||Y|。由于 XY=YX,所以 Y[0,|X|)=X=Y[|Y||X|,|Y|),故 XY 的 Border,即 |Y||X|Y 的循环节。同理,由于 XY=YX,所以 Y[|X|,|Y|)=Y[0,|Y||X|),故 Y[0,|Y||X|)Y 的 Border,即 |X|Y 的循环节。所以 |X||Y||X| 都是 Y 的循环节,根据 弱周期定理g=gcd(|X|,|Y||X|) 也是 Y 的循环节。

引理 3

引理 3: 如果 XY,那么 lcp(X,Y)=lcp(XY,YX)

由于 XY,由 引理 1 可得 XYYX。不妨设 |X||Y|

考虑 YX 的前缀的情况。因为 YX 的前缀,所以 XY 也是 X 的前缀。又由于 |X||Y|,所以 X 也是 Y 的前缀,然后可以得到 YXY2 的前缀,继而可以得到 YXY 的前缀。由于我们知道 XYYX,而 |XY|=|YX|,所以 XYYX 一定会在第 |X|+|Y| 位或之前比较出结果。而 XY 分别是它们两个的前缀,所以也一定会在同一个位置比较出结果。故 lcp(XY,YX)=lcp(X,Y)

然后考虑 Y 不是 X 的前缀的情况。我们不断把 Y 开头的 X 提出来,即找到最大的 n 使得 Y=XnY,那么此时 X 不是 Y 的前缀。又由于 Y=XnY 不是 X 的前缀,所以 Y 也不是 X 的前缀。因此 XY 一定会在第 min(|X|,|Y|) 位前比出结果,用于上面引理 1 类似的方法我们可以得到 lcp(X,YY)=lcp(X,Y),同理可得 lcp(X,Y)=lcp(XY,YX)。于是:

lcp(X,Y)=lcp(XnX,XnYY)=n|X|+lcp(X,YY)=n|X|+lcp(XY,YX)=lcp(Xn+1Y,XnYX)=lcp(XY,YX)

参考资料

https://www.luogu.com/article/d4y3zqqv
https://atcoder.jp/contests/arc175/editorial/9662

posted @   XxEray  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示