Lyndon分解和最小循環表示學習


做CF594E涉及的兩個知識點。以下字符串采用Python記法。

Lyndon分解

定義 $S$ 是Lyndon串,當且僅當對於任意有意義的正整數 $i$ 有 $S<S[i:]$.

定義 $S$ 的Lyndon分解是一個Lyndon串的序列 $s_1, s_2, \ldots, s_n$, 使得 $S=s_1s_2 \cdots s_n$ 並且 $s_1 \ge s_2 \ge \cdots \ge s_n$.

Lyndon分解存在且唯一。

不難發現,Lyndon分解可以這么得到:對於 $S$, 取最小的后綴 $S[i:]$, $S$ 的Lyndon分解,是在 $S[:i]$ 的Lyndon分解的最后加上一項 $S[i:]$ 得到的序列。

於是,Lyndon分解可以用后綴數組求,但是這樣太復雜了。

Lyndon分解的Duval算法:

設原字符串為 $S$. 逐個加入字符,設已能確定 $S[:i]$ 的Lyndon分解 $s_1, s_2, \ldots, s_n$ 為 $S$ 的Lyndon分解的前綴,再盡可能地擴充字符串 $S[i:k]$, 使得 $S[i:k]$ 具有Lyndon周期 $t$, 也就是說,$S[i:k]=(m \times t)u$, 其中 $m$ 是正整數,$t$ 是Lyndon串,$u$ 是 $t$ 的(可以為空的)真前綴。考慮加入字符 $S[k]$, 若 $k=|S|$ 則令 $S[k]=-\infty$.

  • 若 $S[k]<S[k-|t|]$, 則 $S[i:k+1]$ 就沒有Lyndon周期了。不過此時 $S[:i+m|t|]$ 的Lyndon分解已確定,即在 $S[:i]$ 的Lyndon分解末尾添加 $m$ 個 $t$, 取新參數 $i \gets i+n|t|$ 重來。
  • 若 $S[k]=S[k-|t|]$, 則 $t$ 也是 $S[i:k+1]$ 的Lyndon周期。
  • 若 $S[k]>S[k-|t|]$, 則 $S[i:k+1]$ 的Lyndon周期是其本身。

實現上我們只需要維護 $i, k$ 以及 $j=k-|t|$. 代碼鏈接

擴充 $S[i:k]$ 的過程中 $i+k\le2|S|$ 且隨擴充總輪數遞增,因此該算法時間 $O(|S|)$, 除去輸入輸出只需要 $O(1)$ 額外空間,是一個非常簡短而高效的算法。

最小循環表示

最小循環表示,也就是對於字符串 $S$, 求出最小的 $T_i=S[i:]S[:i]$.

我們維護兩個決策 $i, j$, 滿足 $i<j$ 並且 $[0, i)$ 和 $(i, j)$ 中的整數都不是最優決策。

初始時,設 $i=0, j=1$.

我們通過枚舉比較求出 $T_i$ 和 $T_j$ 的最長公共前綴 $k$.

  • 若 $T_i=T_j$, $T_i$ 即為最優解。這是因為 $S$ 以 $j-i$ 為循環節循環,而 $(i, j)$ 中任何整數都不是最優決策。
  • 若 $T_i<T_j$, 說明 $[j, j+k]$ 內的整數不是最優決策,因為它總比 $[i, i+k]$ 內的對應決策劣,因此令 $j \gets j+k+1$. 上述性質未改變。
  • 若 $T_i>T_j$, 同理令 $i \gets i+k+1$, 若此后 $i \ge j$ 則再令 $j \gets i+1$, 上述性質仍未改變。

當 $j \ge |S|$ 時,我們發現 $[0, i)$ 和 $(i, |S|)$ 中的整數都不可能為最小決策了,所以 $T_i$ 是唯一的最優解;否則重復此過程。

$i+j+k \le 3|S|$ 隨枚舉總次數遞增,因此該算法時間 $O(|S|)$, 除去輸入輸出只需要 $O(1)$ 額外空間,是一個非常簡短而高效的算法。

代碼:$n$($n \le 3\times10^5$)項整數序列的最小循環表示。

不過這份代碼由於在OJ上刷常數榜,不僅加入了輸入輸出優化,還破環為鏈以節省取模的時間,空間略大。

 1 #include <bits/stdc++.h>
 2 char r[1<<25], *rc=r, w[1<<25], *wc=w;
 3 int rd() {
 4     int a;
 5     bool b;
 6     unsigned char c;
 7     while(c=*rc++-48, c>9&c!=253);
 8     b=(c==253);
 9     for(a=b?0:c; (c=*rc++-48)<10; a=a*10+c);
10     return b?-a:a;
11 }
12 void wr(int v) {
13     char s[10], *p=s;
14     while(*p++=v%10+48, v/=10);
15     while(*wc++=*--p, p!=s);
16 }
17 const int N=3e5;
18 int n, x[2*N];
19 int main() {
20     int i, j, k;
21     fread(r, 1, 1<<25, stdin);
22     n=rd();
23     for(i=0; i<n; ++i) x[i]=rd();
24     memcpy(x+n, x, n*sizeof(int));
25     for(i=0, j=1, k=0; j<n&&k<n; ) {
26         int xi=x[i+k], xj=x[j+k];
27         if(xi==xj) ++k;
28         else if(xi<xj) j+=k+1, k=0;
29         else {
30             i+=k+1, k=0;
31             if(i>=j) j=i+1;
32         }
33     }
34     for(k=0; k<n; ++k) wr(x[i+k]), *wc++=' ';
35     wc[-1]='\n';
36     fwrite(w, 1, wc-w, stdout);
37     return 0;
38 }
最小循環表示


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM