ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【luogu P6656】【LOJ 173】【模板】Runs(字符串)(Lyndon 串)

2022-07-23 09:33:56  阅读:176  来源: 互联网

标签:Runs run sta LOJ luogu mid int rm Ly


【模板】Runs

题目链接:luogu P6656 / LOJ 173

题目大意

给你一个字符串,要你求它所有的 Runs。

思路

本文也是参考着 command_block 大神的博客 进行学习的,只是书写一下自己的个人理解。

首先有一(亿)些关于 Lyndon 的知识建议先看看。

然后就开始讲了咯。
(文中许多东西建立在前面这篇文章的基础上,所以讲也不会再讲之前的内容)

定义

Run

一个三元组 \(r=(l,r,p)\) 是 \(\rm run\) 当且仅当:
字符串 \(s\) 中 \(l\sim r\) 形成的子串,\(p\) 是最小循环节,而且 \(2p\leqslant r-l+1\)。
而且这个串是不能延伸的,具体一点就是 \(s_{l-1}\neq s_{l+p-1},s_{r+1}\neq s_{r-p+1}\)。
然后一个字符串所有的 \(\rm run\) 就组成了 \(\rm Runs\)。

一个 run 的指数是 \(e_r=\dfrac{r-l+1}{p}\)
\(ρ_{\rm run}(n)\) 是长度为 \(n\) 的字符串至多有的 \(\rm run\) 的个数。
\(σ_{\rm run}(n)\) 是长度为 \(n\) 的字符串的 \(\rm run\) 的指数和的最大值。

Lyndon Root

这个东西一般是指一个 \(\rm run\) 的 Lyndon Root。
是一个在 \(\rm run\) 所在区间的子区间,而且长度为 \(p\),还是 Lyndon Word。
意思其实就是说是 \(\rm run\) 的循环节的最小表示的那个。

然后不难看出对于任意一个 \(r\) 至少有一个 Lyndon Root,而且每一个之间其实是相等的,只是位置不同。

Run

为了方便书写,我们设 Lyndon Word 为 \(\rm Ly\),Lyndon Root 为 \(\rm LyR\)。

首先有个性质,就是两个周期相同为 \(p\) 的 run 的交的长度是 \(<p\) 的。
因为如果 \(>p\) 我们可以在交中选一个循环节扩展,就矛盾了。
(性质 \(1\))


接着我们就要证明我们最重要的东西了:
\(ρ_{\rm run}(n)<n,σ_{\rm run}(n)\leqslant 3n-3\)

首先我们之前有一个说那个比较大小可以重载的(\(<_0,<_1\)),我们找找有关的性质。
然后因为有相反的,所有我们在前面比较完有一个空了的时候就要区别开来,所以我们会有一个占位符 \(\texttt{\textdollar}\),在 \(<_0\) 里面我们让 \(\texttt{\textdollar}\) 字典序最小,\(<_1\) 里面就是最大。


那我们就有这么一个小定义:
\({\rm Ly}_{s,f}(i)\) 为 \([i,j]\),其中 \(j\) 是 \(\max\{k|s[i,k]\) 是关于 \(<_f\) 的 \(\rm Ly \}\)
也就是在 \(<_f\) 意义下,起始位置为 \(i\) 的最长的 \(\rm Ly\) 子串。

然后有一个事情就是正反两个字典序只能有一个会找到长度大于 \(1\) 的 \(\rm Ly\)。
形式化就是:只有恰好一个 \(f\in\{0,1\}\) 使得 \({\rm Ly}_{s,f}(i)=[i,i],{\rm Ly}_{s,1-f}(i)=[i,j](j>i)\)
你就看后面第一个跟 \(s_i\) 不同的字符(因为有占位符所以必定存在),如果是在这个意义下是小于那就不能加上,所以只有自己的位置,否则至少也有那个不同的位置是可以作为 \({\rm Ly}\) 的。
(性质 \(2\))


然后就是对于一个 \({\rm run}\),设为 \(r=(l,r,p)\),找到 \(f\in\{0,1\}\) 使得 \(s_{r+1}<_fs_{r-p+1}\),那么 \(r\) 所有关于 \(<_f\) 的 \({\rm LyR}\)(设为 \([i,j]\)) 都跟 \({\rm Ly}_{s,f}(i)\) 相等。
因为是 \({\rm Ly}\) 循环节嘛,我们可以表示为 \(u^cu'\)(不用找 \({\rm Ly}\) 循环节),其中 \(u'\) 是 \(u\) 严格前缀(就是不能等于)

你会发现它长得很像 Duval 算法里面的那个形式。
然后如果你用 \(1-f\),你会根据性质 \(2\) 发现都是一个字符,没有意义。

我们也发现这个 \(f\) 它这样在 \({\rm run}\) 中有特殊的地方,于是我们把这个 \(<_f\) 记为 \(r\) 的正序。
(性质 \(3\))


然后再设一个 \({\rm LyRs}(r)\) 表示 \(r\) 所有正序的 \({\rm LyR}\),除去 \(l\) 开头的(如果有)。
然后我们会发现 \(|{\rm LyRs}(r)|\geqslant \left\lfloor e_r-1\right\rfloor\geqslant 1\)
其实挺显然的?
首先第二个跟第三个那肯定没问题,毕竟你 \(2p\leq len\),那肯定 \(e_r\geqslant 2\) 了。
然后是第一个跟第二个,那 \(|{\rm LyRs}(r)|\) 就是看有多少个循环节,会发现跟 \(e_r\) 差不多。
但是由于可能会除去一个 \(l\) 开头的,所以要减一,而且可能跑不满一个新的循环节,所以是下取整。
(性质 \(4\))


对于两个不同的 \({\rm run}\)(\(r=(l,r,p),r'=(l',r',p')\)),它们的 \({\rm LyRs}\) 的左端点集合不会有交。
设左端点集合为 \({\rm Beg}(x)\),就是 \({\rm Beg}({\rm LyRs}(r))∩{\rm Beg}({\rm LyRs}(r'))=\varnothing\)。

考虑反证法,设存在且是位置 \(i\),那两个 \({\rm LyR}\) 设是 \([i,j],[i,j']\)。
然后设 \(<_f\) 是 \(r\) 的正序,然后你会发现 \({\rm Ly}_{s,f}(i)\) 就是两个的表示?
但是显然两个的这个值应该要不一样啊,不然就是同一个 \({\rm run}\) 了。
那只能是一个正序一个反序,所以另一个就是 \({\rm Ly}_{s,1-f}(i)\)。
那必然有一个 \([i,i]\),设 \(j=i\),就有 \(j'>i\)。(性质 \(2\))
那因为 \([i,j']\) 是 \({\rm Ly}\),所以自然 \(s_i\neq s_{j'}\),再从性质 \(3\) 会有 \(r\) 的循环节是 \(p=|[i,i]|=1\),\(r'\) 的循环节就是 \(p'=|[i,j']|=j-i+1\)。
那周期性一下:\(s_i=s_{i-1}=s_{i-1+(j-i+1)}=s_{j'}\),与 \(s_i=s_{j'}\) 矛盾。
所以就证好了。
(性质 \(5\))


不难通过性质 \(5\) 发现,最多每个左端点都被占了,再把 \(l\) 给去掉,所以我们可以证明:
\(ρ_{\rm run}(n)<n\)
再看看什么没用上,性质 \(4\)。
不难看出用到的是 \(|{\rm LyRs}(r)|\geqslant \left\lfloor e_r-1\right\rfloor\)。
下取整不行换一下 \(|{\rm LyRs}(r)|\geqslant \left\lfloor e_r-1\right\rfloor\geqslant e_r-2\)
推广到所有 \({\rm run}\):\(\sum\limits_{r\in {\rm runs}(s)}e_r-2\leqslant \sum\limits_{r\in {\rm runs}(s)}|{\rm LyRs}(r)|\leqslant n-1\)
再结合一下 \(|{\rm runs(s)}|\leqslant n-1\),把 \(-2\) 提出来:
\((\sum\limits_{r\in {\rm runs}(s)}e_r)-2(n-1)\leqslant n-1\)
\(σ_{\rm run}(n)-2n+2\leqslant n-1\)
\(σ_{\rm run}(n)\leqslant 3n-3\)
搞定!
(性质 \(6\))

Ex

\(ρ_{{\rm run},k}(n)<\dfrac{n}{k-1}\),\(ρ_{{\rm run},k}\) 表示 \(e_r\) 达到 \(k\) 的 \(\rm run\) 的个数。
挺显然的,就结合一下占位的想法看看就知道了。

计算

假设我们直接暴力找:
就是枚举周期 \(p\),然后在串中撒点(均匀撒),然后鸽笼(也许不需要)一下就知道一个大小为 \(p\) 的 \(\rm run\) 一定会穿过至少两个点。
那就相邻两个点之间向前向后求 \(\rm LCP\),如果覆盖范围能连接起来,就找到了一个。

当然你可以用性质 \(1\) 小小剪枝。

而且你会发现你枚举的周期不一定是最小周期,所以我们得排序,然后去重一下。

然后如果二分+Hash搞就是 \(n\log^2n\),后缀数组搞就是 \(n\log n\)。
感觉不优。


在介绍新的求法之前,不妨看看这么一个问题,如何快速求所有 \({\rm Ly}_s(i)\)。
考虑从那个合并的角度来看,从右往左扫,每次加进去一个字符(然后你用一个栈维护前面的 \({\rm Ly}\))。
然后不停的查看是否能合并,然后这个地方比大小我们就先二分+Hash找到不同的位置,然后用当前的 \(f\) 判断一下即可。


考虑用上一些性质,就我们可以从性质 \(2\) 发现,一个 \(\rm run\) 含有的 \({\rm Ly}\) 循环节是某个字典序下某个后缀最长 \({\rm Ly}\) 前缀。
而且两个不同的 \({\rm run}\) 的循环节肯定不同。
我们考虑直接枚举循环节,直接前后求 \({\rm LCP}\),这样循环节是 \(O(n)\) 的。

具体一点就是枚举 \(f,l\),然后由于我们求出了所有的 \({\rm Ly}_s(i)\),我们就 \({\rm LCP}\) 得到扩展,然后判一下是否能相交即可。
然后也是要去重。

然后由于后缀数组预处理要 \(n\log n\),不如使用二分+hash,好写一点。
这里我们就是使用了 \({\rm Ly}\) 串构造最小循环节,可以说是一种映射,所以就把复杂度降到了 \(O(n)\)。

代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define ull unsigned long long
#define di 131

using namespace std;

const int N = 1e6 + 100;
struct node {
	int l, r, p;
}ans[N];
char s[N];
int n, ly[N], sta[N], m, ans_num;
ull mi[N], hsh[N];

void Init() {
	mi[0] = 1;
	for (int i = 1; i <= n; i++) {
		mi[i] = mi[i - 1] * di;
		hsh[i] = hsh[i - 1] * di + s[i] - 'a'; 
	}
}

ull get(int l, int r) {
	return hsh[r] - hsh[l - 1] * mi[r - l + 1];
}

int getl(int x, int y) {
	int l = 0, r = min(x, y), re = 0;
	while (l <= r) {
		int mid = (l + r) >> 1;
		if (get(x - mid + 1, x) == get(y - mid + 1, y)) re = mid, l = mid + 1;
			else r = mid - 1;
	}
	return re;
}

int getr(int x, int y) {
	int l = 0, r = min(n - x + 1, n - y + 1), re = 0;
	while (l <= r) {
		int mid = (l + r) >> 1;
		if (get(x, x + mid - 1) == get(y, y + mid - 1)) re = mid, l = mid + 1;
			else r = mid - 1;
	}
	return re;
}

bool cmpS(int x, int y) {
	int sz = getr(x, y);
	return s[x + sz] < s[y + sz];
}

void Lyndon(bool op) {
	ly[n] = n;
	sta[0] = 0; sta[++sta[0]] = n + 1; sta[++sta[0]] = n;
	for (int i = n - 1; i >= 1; i--) {
		while (sta[0] > 1 && cmpS(i, sta[sta[0]]) == op) sta[0]--;
		sta[++sta[0]] = i;
		ly[i] = sta[sta[0] - 1] - 1;
	}
}

void slove(int x, int y) {
	int l = getl(x, y), r = getr(x, y);
	if (l + r >= y - x + 1) {
		ans[++m] = (node){x - l + 1, y + r - 1, y - x};
	}
}

bool cmp(node x, node y) {
	if (x.l != y.l) return x.l < y.l;
	if (x.r != y.r) return x.r < y.r;
	return x.p > y.p;
}

int main() {
	scanf("%s", s + 1); n = strlen(s + 1);
	Init();
	
	for (int op = 0; op <= 1; op++) {
		Lyndon(op);
		for (int i = 1; i < n; i++)
			slove(i, ly[i] + 1);
	}
	
	sort(ans + 1, ans + m + 1, cmp);
	for (int i = 1; i <= m; i++) {
		if (ans[i].l == ans[i - 1].l && ans[i].r == ans[i - 1].r) continue;
		ans_num++;
	}
	printf("%d\n", ans_num);
	for (int i = 1; i <= m; i++) {
		if (ans[i].l == ans[i - 1].l && ans[i].r == ans[i - 1].r) continue;
		printf("%d %d %d\n", ans[i].l, ans[i].r, ans[i].p);
	}
	
	return 0;
}

标签:Runs,run,sta,LOJ,luogu,mid,int,rm,Ly
来源: https://www.cnblogs.com/Sakura-TJH/p/luogu_P6656.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有