runs 相关理论——2025.6.27 鲜花

runs 相关理论

内臓ありますか(请问有内脏吗)
悟った気がしたけど 結局 元の木阿弥である
この世を俯瞰で見渡しても 肉体はジャンクフード食べる
因果応報はなく 全ては塞翁が馬である
何年経ってもいじめっこの君は 幸せそうだった
言っても無駄 言われても無駄 無気力 無自觉 無関心なほど
傷つくのを怖がった 愛さずに愛されたがった
因果応報はなく 全ては塞翁が馬である
天が見ていても 本当の邪悪は 邪悪のままだった
今日も皆さん やりたくないこと ばかりやってますか(はい!)
内心 舌打ちしながら ペコペコ頭 下げてますか(はい!)
君は善人ですか?(はい!)
やっぱり悪人ですか?(はい!)
関係ないけど 今夜は楽しい パーティー行きますか?
血も涙もないけど 優し気持ちも 足りないけど
バラバラの思想 ぶつかっても 皆んな
内臓ありますか? (はい!)
君がいなくなっても よくあることだ と割り切っても
身体が萎むくらい 泣いても みんな
内臓ありますか?
はい はい!
はい はい!
あっちが言うには被害者 でも こっちに言わせりゃ加害者
両成敗という言葉使って 思考停止の傍観者
因果応報はなく すべては塞翁が馬である
喧々諤々やっても結局は 結果がすべてだった?
今日もみなさん やりたくないことばかりやってますか はい
たいして何にもやっていないのにマージンとりますか はい
最近良いことあった? はい
やっぱり嫌なことあった? はい
関係ないけど 週末限定バーゲン行きますか
夢を見失うけど 探せば ほんの少しあるけど
最悪 売れば金になるけど みんな内臓ありますか はい
外面が綺麗でも 醜い内面が嫌いでも
生皮剥いだら おんなじだよ みんな
内臓ありますか
今日もみなさん やりたくないことばかりやってますか はい
今日もみなさん やりたくないことばかりやってますか はい
因果応報はなく すべては塞翁が馬である
関係ないけど 大きな声で叫んでくれますか
血も涙もないけど 優しい気持ちも足りないけど
バラバラの思想ぶつかっても みんな 内臓ありますか はい
君がいなくなっても よくあることだと割り切っても
身体が萎むくらい泣いても みんな 内臓ありますか はい
君の幸せ どんくらいで そしてトラウマこんくらいで
雨後のたけのこ背比べ 共感と嫌悪の雨あられ
外面が綺麗でも 醜い内面が嫌いでも
生皮剥いだら おんなじだよ みんな
内臓ありますか
はい はい
はい はい

很浅的一部分,模拟赛考了,所以写一下。

定义一个字符串 \(S\) 里的一个 run,指其内部一段两侧都不能扩展的周期子串,且周期至少完整出现两次。

容易发现一个 run 的每个长为 \(kp\) 的子串都是一个本原 \(k\) 次方串,并且每个本原 \(k\) 次方串至少位于一个 run 中。

更严一点的,每个本原 \(k\) 次方串都可以被上述方式唯一统计到。考虑互相包含的 runs,其外层的 \(p\) 必然严格大于内层,且其外层周期不是内层倍数(这里的倍数是指字符串的倍数,而不是长度)。于是一个最小周期为 \(p\) 的本原 \(k\) 次方串必然只会被周期为 \(p\) 的 run 统计恰好一次。

如何求 runs,一个简单的写法是用「优秀的拆分」中的调和级数分块,可以 \(\mathcal{O}(|S| \log |S|)\) 求出。同时其一个显然的个数下界是 \(\mathcal{O}(|S| \log |S|)\)

但这个个数下界实在是很松,考虑一个更严的下界。

我们发现,对于一个 run \((l, r, p)\),其一定包含 \(S[l\dots l + p - 1]\) 的所有循环位移,我们取出其中最小的循环位移,根据 lyndon 相关理论,其一定是一个 lyndon 串。

为避免不必要的前置知识,我们直接再这里补充需要的 lyndon 知识:

  1. lyndon 串是一个串,满足它所有真后缀都大于它。

  2. 当且仅当 \(s\) 的字典序严格小于它的所有非平凡的(非平凡:非空且不同于自身)循环同构串时,\(s\) 才是 Lyndon 串。

    证明,设 \(s\) 的后缀 \(t\) 小于 \(s\)

    \(t \le \left\lfloor\frac{s}2\right\rfloor\),则 \(s\) 可以表示为 \(tat\) 的形式,分讨 \(s,t\) 大小可证其必不可能是严格最小。

    \(t > \left\lfloor\frac{s}2\right\rfloor\),设 \(s = ta\),则 \(t\) 存在 \(a\) 的周期,设 \(a = xy, t = (xy)^kx\)\(s = (xy)^kxxy\),分讨 \(x,y\) 大小可证其必不可能是严格最小。

我们证明一下 \(s\) 能循环位移不超过 \(|s| - 1\) 次和 \(s\) 相等当且仅当其不是本原串。

考虑一个串 \(ab = ba\),其中 \(a,b\) 都是非空字符串且 \(|a| \le |b|\),我们有 \(a = b^k\),考虑归纳,将 \((ab)[|b| + 1\dots |a| + |b|]\) 归纳下去即可。

我们称取出的最小的循环位移是这个 run 的 lyndon 根。

我们考虑到在第一个 lyndon 根处统计这个 run,设其是 \(S[i\dots i + p - 1]\)

具体的,我们发现对于 \(j > i\)\(S\) 的后缀 \(S[j\dots |S|]\) 中,可能小于 \(S[i\dots |S|]\) 的最小的 \(j = i + p\),考虑 run 循环的性质,此时满足 \(S[r + 1] > S[r - p + 1]\),我们直接以 \(i\) 和第一个比 \(S[i\dots |S|]\) 小的 \(S[k\dots |S|]\)\(k\) 组成的二元组 \((i, k)\) 进行扩展即可。

如何统计 \(S[r + 1] < S[r - p + 1]\) 的呢?我们人为翻转一下字典序,认为 \(a > b,b > c\)(注意空串大于所有串),然后再做即可。容易发现不存在 \(S[r + 1] = S[r - p + 1]\) 的。

这证明了 runs 个数是 \(\mathcal{O}(n)\) 的且给出了另一种求 runs 的方式。

实现的时候用 SA-IS + \(\mathcal{O}(n)-\mathcal{O}(1)\) st 表即可 \(\mathcal{O}(n)\)。当然也直接暴力二分 hash 加单调栈做到小常数 \(\mathcal{O}(n\log n)\)

给出 hash 实现:

Code
/*
ulimit -s 128000 && clear && g++ % -o %< -O2 -std=c++14 -DLOCAL -Wall -Wextra && time ./%< && size %<
ulimit -s 128000 && clear && g++ % -o %< -O2 -std=c++14 -DLOCAL -Wall -Wextra -fsanitize=address,undefined -g && time ./%< && size %<
echo && cat in_out/out.out && echo
*/
#include <bits/stdc++.h>
using namespace std;
using llt = long long;
using llf = long double;
using ull = unsigned long long;
#define endl '\n'
#ifdef LOCAL
FILE *InFile = freopen("in_out/in.in", "r", stdin), *OutFile = freopen("in_out/out.out", "w", stdout);
#endif
 
const int N = 1e6 + 3, Bs = 2333;

namespace HS{
	ull pw[N];
	struct INIT{ INIT(){ pw[0] = 1; for(int i = 1; i <= N - 3; ++i) pw[i] = pw[i - 1] * Bs; } } Initer;
	struct Hs{
	private:
		ull o[N];
	public:
		void In(char *s, int l){ for(int i = 0; i < l; ++i) o[i + 1] = o[i] * Bs + s[i]; }
		ull operator()(int l, int r){ return o[r] - o[l - 1] * pw[r - l + 1]; }
	};
} using HS::Hs;

Hs hs; char s[N]; int n;
vector<tuple<int, int, int>> aas;

int main(){
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> s + 1, n = strlen(s + 1), hs.In(s + 1, n);
	auto Add = [](int a, int b){
		if(s[a] != s[b]) return ;
		int l = 1, r = a - 1, cl, cr;
		while(l <= r){
			int mid = (l + r) >> 1;
			if(hs(a - mid, a) == hs(b - mid, b)) l = mid + 1;
			else r = mid - 1;
		}
		cl = a - r, l = 1, r = n - b;
		while(l <= r){
			int mid = (l + r) >> 1;
			if(hs(a, a + mid) == hs(b, b + mid)) l = mid + 1;
			else r = mid - 1;
		}
		cr = b + r; int p = b - a;
		if(cl > a - p && (cr - cl + 1) / p > 1) aas.emplace_back(cl, cr, p);
	};
	auto Cmp = [](int a, int b){
		int l = 0, r = n - b;
		while(l <= r){
			int mid = (l + r) >> 1;
			if(hs(a, a + mid) == hs(b, b + mid)) l = mid + 1;
			else r = mid - 1;
		}
		if(l > n - b) return false;
		else return s[a + l] < s[b + l];
	};
	stack<int> stk;
	for(int i = n; i; --i){
		while(!stk.empty() && Cmp(i, stk.top())) stk.pop();
		if(!stk.empty()) Add(i, stk.top());
		stk.emplace(i);
	}
	while(!stk.empty()) stk.pop();
	for(int i = n; i; --i){
		while(!stk.empty() && !Cmp(i, stk.top())) stk.pop();
		if(!stk.empty()) Add(i, stk.top());
		stk.emplace(i);
	}
	sort(aas.begin(), aas.end());
	cout << aas.size() << endl;
	for(auto [l, r, p] : aas) cout << l << ' ' << r << ' ' << p << endl;
}

考虑更进一步的理论:对于所有的 runs \((l, r, p)\),有 \(\sum \frac{r - l + 1}p \sim \mathcal{O}(n)\),证明就是上面的过程,我们每个 \(\frac{r - l + 1}p\) 其实都可以拆到 \((i, k)\) 上去,仔细分析可以的得到 \(|runs| < n,\sum \frac{r - l + 1}p < 3n\)

P





posted @ 2025-06-27 14:20  xrlong  阅读(48)  评论(1)    收藏  举报

Loading