背单词
2023-01-10
暂无标签

Solution to「SCOI2016」背单词


题解 P3294。

我们看到字典树这个标签,下意识想到了前缀。

但是题目让我们求后缀,所以我们把所有字符串先 reverse 一下,将问题转化为前缀,方便描述。


我们发现,因为 \(y < x \le n\),所以三种情况的花费排序为 \(x-y<x<n\times n\)

不难发现,我们可以很简单地通过「把某单词的所有前缀单词放到它之前」消除开销为 \(n\times n\) 的情况。此时开销最劣情况下为 \(\sum\limits_{i=1}^ni < n\times n\)

接下来,问题在于怎样在满足「把某单词的所有前缀单词放到它之前」的条件下,最小化开销。不难发现,当我们去除第一种情况后,剩下的 \(x\)\(x-y\) 都包含 \(x\) 项,也就是说,我们只需要最小化 \(\sum -y\) 的值,也就是最大化 \(\sum y\) 的值。而 \(\sum x\) 为定值 \(\sum\limits_{i=1}^ni\)

关于处理方式:不难发现一个单词的最长前缀单词最多只有一个,但以该单词为最长前缀单词的单词可能有多个,联想到树。

我们将每个单词的最长前缀和该单词连边,会得到一个森林。

不妨先考虑问题的简化,假设有两条链(即,不会同时存在多个单词,使得它们的最长前缀单词相同),应该如何安排其顺序。

如图所示,考虑「同一条链均匀分散到序列中」和「同一条链集中在一起」两种有规律的安排方式,得到:

可以发现,第二种情况明显更优。

所以我们扩展猜想:对于某一单词,以它为前缀的所有单词必须紧随其后。该结论是可递归的。

继续处理细节部分。考虑两条链长度不同,应如何排列它们的位置?

下图展示了「前短后长」和「前长后短」两种方案:

可以发现,前短后长的方案更优。

扩展得到猜想,同一单词的所有子树中,大小更小的应在更前面。该结论是可递归的。


接下来对猜想进行证明。

假设现在已有一条链。我们只有把整条链连在一起,短单词放到长单词前面这一种方法。

然后新来了一个不属于这条链的节点,明显,因为节点不管放到哪里,其 \(y\) 值不变,但会改变已有链的 \(y\) 值,所以放到最前面最优。

又来了一个节点,和刚才新增的节点属于同一条链,一样,不管这个节点何去何从,其 \(y\) 不变,但放在越前面,后面的节点的 \(y\) 值增加得越多。

以此类推可得到结论,同一条链放到一起。短链放到长链前面也是比较好想的。假设有两条链,短链长度为 \(a\),长链长度为 \(b\),放置起点为 \(l\),则前短后长和钱长后短的 y 之和分别为 \(\dfrac{(l + l + a - 2) \times (a - 1) + (l + a + l + a + b - 2) \times (b - 1)} 2\)\(\dfrac{(l + l + b - 2) \times (b - 1) + (l + b + l + b + a - 2) \times (b - 1)} 2\)(运用等差数列求和公式),解不等式可得前短后长一定更优。

接下来是从链扩展到树。那么我们想到了什么?树链剖分。

我们化用树剖思想,以最轻边为剖分条件,将树划分为有顺序的链,按照上述方式排列其顺序即可。

此时我们记录下每个单词的直系父亲(即其最长前缀单词),在唯一序列中模拟计分操作,即可得到答案。


为了方便,可将所有入度为 \(0\) 的点与一个虚点相连,将森林转化为树。

注意 long long

namespace XSC062 {
using namespace fastIO;
using str = std::string;
const int maxn = 1e6 + 5;
const int maxm = 1e6 + 5;
str s[maxn];
int T[maxm][26];
int kaz[maxn], siz[maxn]; // kaz 即为 kazuha(?) 表示单词在最终序列中所处的位置
std::vector<int> g[maxn];
int n, l, res, cnt = 1, ti;
int tot[maxm], fa[maxn], deg[maxn];
inline void Insert(int k) { 
	int f = 1, len = s[k].length();
	for(int i = 0; i < len; ++i) {
		if (!T[f][s[k][i] - 'a']) 
			T[f][s[k][i] - 'a'] = ++cnt;
		f = T[f][s[k][i]-'a'];
	}
	tot[f] = k;
	return;
}
inline void Search(int k) {
	int f = 1;
	int len = s[k].length();
	for (int i = 0; i < len; ++i) {
		f = T[f][s[k][i] - 'a'];
		if (tot[f]) {
			if (tot[f] != k)
				fa[k] = tot[f]; // 记录当前最长前缀
		}
	}
	return;
}
inline void add(int x, int y) {
	++deg[y];
	g[x].push_back(y);
	return;
}
void DFS1(int x) { // 提前计算每个子树的 size
	siz[x] = 1;
	for (auto i : g[x]) {
		DFS1(i);
		siz[x] += siz[i];
	}
	return;
}
void DFS2(int x) {
	kaz[x] = ti++;  // 时间戳即为单词位置
	std::sort(g[x].begin(), g[x].end(),
		[&](int x, int y) { return siz[x] < siz[y]; }); // 按子树 size 排序
	for (auto i : g[x])
		DFS2(i);
	return;
}
int main() {
	scanf("%lld", &n);
	for (int i = 1; i <= n; ++i) {
		std::cin >> s[i];
		std::reverse(s[i].begin(), s[i].end());  // 后缀转前缀
		Insert(i);
		res += i;
	}
	for (int i = 1; i <= n; ++i) {
		Search(i);
		if (fa[i])
			add(fa[i], i); // 建图
	}
	for (int i = 1; i <= n; ++i) {
		if (!deg[i]) // 建虚点方便操作
			add(n + 1, i), fa[i] = n + 1;
	}
	DFS1(n + 1), DFS2(n + 1);
	for (int i = 1; i <= n; ++i)
		res -= kaz[fa[i]]; // 处理 y 值
	print(res);
	return 0;
}
} // namespace XSC062

一言 - Hitokoto