【字符串】Lyndon 分解

发布时间 2024-01-02 22:13:39作者: The_Last_Candy

题目描述

定义 Lyndon Word:\(s\) 是 Lyndon Word 当且仅当 \(s\) 是其所有后缀中最小的一个串。

给定字符串 \(s\) ,请把这个字符串分成若干个子串,使得每个子串都是 Lyndon Word 。并且从左到右每个字符串都大于等于下一个。

最后输出每一个子串右端点异或和。

\(1 \leq n \leq 5 \times 10^6\)

算法描述

此处使用时间复杂度 \(\Theta(n)\) ,空间复杂度 \(\Theta(1)\) (不记录划分方案)的方法:Duval 算法

首先可以证明 Lyndon 分解存在且唯一。

我们使用 \(i,j,k\) 三个指针, \([1,i - 1]\) 代表前面已经定好的串;\([i,k]\) 代表正在划分的串,\([k + 1,n]\) 代表没有加入的串。

我们断言,\([i,k]\) 既然没有被确定,一定会形如 \(uuu\dots v\) (后面解释),其中 \(u\) 是一个定长子串,\(v\) 是一个没有确定的子串,其中最后一个字符 (也就是 \(k\)) 和 \(u\) 的相应位置不一样,前面都一样。此时 \(j\) 就指向这一位置:

image

首先我们要知道一件事情:像 \(uuu\) 这样并列相等的串一定要被分开,因为 \(uu\) 并不是一个 Lyndon 串。(后缀 \(u < uu\)

也就是说, \(j\) 就是 \(u\) 这样的串中和 \(k\) ”同步匹配“ 的指针,现在我们来讨论 \(j,k\) 的大小关系:

  • \(s_j < s_k\) :容易发现如果将 \(k\) 分出去,就不满足 "前面大于后面" 这个性质,所以 \([i,k]\) 要分在一起。所以从 \(k + 1\) 开始,\([i,k]\) 整体变成了一个串 \(u'\),将指针 \(j = i\)

  • \(s_j > s_k\) :发现如果将 \(k\) 和前面的串划分成一个,就会出现后缀比整体小的情况,所以确定前面所有 \(u\) 的划分结果,将这些 \(u\) 划分出去,具体方法是将 \(i += (k - j)\) 直到大于 \(j\) 为止。

  • \(s_j = s_k\) :没有问题,继续匹配下一个 ,\(j = j + 1,k = k + 1\)

由此还可以得出每次划分时 \((k - j)\) 就是串长,\(i\) 就是开头位置。因为每次 \(j\) 左移至 \(i\) 时中间的部分都划进了一个串。

由此解答前面的问题,只有在 \(k - j\) 等于某个值的时候,对比到很多次的 \(s_j = s_k\)\([j,k]\) 像一个滑动窗口一样右移,以至于 \(j\) 已经超过了原来 \(k\) 的位置,就会形成一个周期 \(u\)

具体在实现代码的时候,采用枚举 \(i\) 的方法,对于每个 \(i\) ,从 \(j = i,k = i + 1\) 开始扫描,扫到 \(s_j > s_k\) 为止,然后移动 \(i\)

时间复杂度 \(\Theta(n)\) ,证明详见 OI-Wiki。

#include<bits/stdc++.h>
using namespace std;
const int N = 5e6 + 5;
char s[N];
int n;
vector <int> rt;
int main()
{
	scanf("%s",s + 1);
	n = strlen(s + 1);
	int i = 1,j,k;
	while(i <= n)
	{
		for(j = i,k = i + 1;k <= n && s[j] <= s[k];)
		{
			if(s[j] == s[k]) j++,k++;
			else j = i,k++;
		}
		for(int len = k - j;i <= j;i += (k - j)) rt.push_back(i + len - 1);
	}
	int res = 0;
	for(auto in : rt) res ^= in;
	cout<<res;
	return 0;
}

用处

目前蒟蒻作者只知道这个可以解决一个串的最小表示法问题:

我们知道每一个 Lyndon 子串小于所有后缀,所以从每个子串的开头一定是最优的,然而我们又知道前面的子串大于等于后面的子串,所以直接取最后一个子串开头即可。