字符串
Trie树
UVA - 1401 - Remember the Word - Trie+DP组合 ,UVA - 11732 - “strcmp()” Anyone? - Trie
# define reset ( A) memset ( A, 0 , sizeof ( A) )
const int maxnode = . . . ;
const int maxc = . . . ;
struct Trie {
int ch[ maxnode] [ maxc] , val[ maxn] , sz;
void init ( ) { sz = 1 ; val[ 0 ] = 0 ; reset ( ch[ 0 ] ) ; }
int id ( char c) { return c - 'a' ; }
void insert ( char * s, int v) {
int p = 0 ;
for ( int i = 0 ; s[ i] ; i++ ) {
int c = id ( s[ i] ) ;
if ( ! ch[ i] [ c] ) {
val[ sz] = 0 ; reset ( ch[ sz] ) ;
ch[ i] [ c] = sz;
}
p = ch[ i] [ c] ;
}
val[ p] = v;
}
int query ( char * s, . . . ) { . . . }
} trie;
KMP
KMP主要思路是计算模式串每个位置的失配位置fail[i]
,换句话说fail[i]
表示字符串s[0,...,i-1]
的最长前缀与后缀相同的长度(长度小于i
),fail[i] = max{k:s[0,...,k-1] = s[i-k,...,i-1], k < i}
,生成方法运用DP+贪心的方法,通过第fail[i]
求出fail[i+1]
。
练习题:UVA - 1328 - Period - KMP求解字符串的最小周期长度 ,这道题给出了KMP的重要性质:
当(i-fail[i])|i
时(a|b
表示a整除b),i-fail[i]
为它的最小周期长度。
const int maxn = 1e6 + 10 ;
struct KMP {
int fail[ maxn] ;
void getfail ( char * P) {
fail[ 0 ] = fail[ 1 ] = 0 ;
for ( int i = 1 ; P[ i] ; i++ ) {
int j = fail[ i] ;
while ( j && P[ i] != P[ j] ) j = fail[ j] ;
fail[ i+ 1 ] = ( P[ i] == P[ j] ) ? j+ 1 : 0 ;
}
}
void find ( char * T, char * P) {
getfail ( P) ;
for ( int i = 0 , j = 0 ; T[ i] ; i++ ) {
while ( j && T[ i] != P[ j] ) j = fail[ j] ;
if ( T[ i] == P[ j] ) j++ ;
if ( ! P[ j] ) printf ( "%d\n" , i- j+ 2 ) ;
}
}
} kmp;
Aho-Corasick Automaton
AC自动机就是Trie树和KMP算法的结合,将KMP算法中的失配跳转,转化为失配边跳转。
struct AhoCorasickAutomaton {
int ch[ maxnode] [ 26 ] , sz, val[ maxnode] ;
void init ( ) { sz = 1 , val[ 0 ] = 0 , reset ( ch[ 0 ] ) ; }
int id ( char c) { return c - 'a' ; }
void insert ( char * s, int v) {
int p = 0 ;
for ( int i = 0 ; s[ i] ; i++ ) {
int c = id ( s[ i] ) ;
if ( ! ch[ p] [ c] ) {
val[ sz] = 0 , reset ( ch[ sz] ) ;
ch[ p] [ c] = sz++ ;
}
p = ch[ p] [ c] ;
}
if ( val[ p] ) repr[ v] = val[ p] ;
else val[ p] = v, repr[ v] = v;
}
int q[ maxnode] , tail, last[ maxnode] , fail[ maxnode] , cnt[ maxnode] ;
void getfail ( ) {
tail = - 1 ; fail[ 0 ] = 0 ;
for ( int c = 0 ; c < 26 ; c++ ) {
int v = ch[ 0 ] [ c] ;
if ( v) q[ ++ tail] = v, last[ v] = fail[ v] = cnt[ v] = 0 ;
}
for ( int head = 0 ; head <= tail; head++ ) {
int u = q[ head] ;
for ( int c = 0 ; c < 26 ; c++ ) {
int v = ch[ u] [ c] ;
if ( ! v) { ch[ u] [ c] = ch[ fail[ u] ] [ c] ; continue ; }
q[ ++ tail] = v;
int p = fail[ u] ;
while ( p && ! ch[ p] [ c] ) p = fail[ p] ;
fail[ v] = ch[ p] [ c] ;
last[ v] = val[ fail[ v] ] ? fail[ v] : last[ fail[ v] ] ;
cnt[ v] = 0 ;
}
}
}
void find ( char * T) {
getfail ( ) ;
for ( int i = 0 , j = 0 ; T[ i] ; i++ ) {
int c = id ( T[ i] ) ;
j = ch[ j] [ c] ;
if ( val[ j] ) cnt[ j] ++ ;
else if ( last[ j] ) cnt[ last[ j] ] ++ ;
}
getcnt ( ) ;
}
void getcnt ( ) {
for ( int i = tail; i >= 0 ; i-- ) {
int u = q[ i] ;
cnt[ last[ u] ] += cnt[ u] ;
}
for ( int i = 0 ; i < sz; i++ ) if ( val[ i] ) ans[ val[ i] ] = cnt[ i] ;
}
} ac;
后缀数组
UVA - 11107 - Life Forms - 多文本串查找最大公共(>n/2)模式串 ,HDU - 6704 - K-th occurrence - 后缀数组RMQ+二分+可持久化线段树
后缀数组 sa[i]
:表示排名为i
的后缀编号,排名数组rk[i]
:表示编号为i
的后缀的排名,二者互为反函数,sa[rk[i]]=rk[sa[i]]=i
。通过倍增的思路,每次求解每个后缀的前缀前缀长度为1 , 2 , ⋯ , 2 k 1,2,\cdots,2^k 1 , 2 , ⋯ , 2 k 的排名数组r k k rk_k r k k ,当2 k ⩾ n 2^k\geqslant n 2 k ⩾ n 时,r k k = r k rk_k = rk r k k = r k 。
利用字符串的序关系可以拆分的性质(可以基数排序):如果要比较字符串s , t s,t s , t 的大小关系s ∼ t s\sim t s ∼ t ,取前缀长度k k k ,则只需比较二元组( s [ 0... k ) , t [ 0... k ) ) ∼ ( s [ k . . . n ) , t [ k . . . n ) ) (s[0...k),t[0...k))\sim (s[k...n), t[k...n)) ( s [ 0 . . . k ) , t [ 0 . . . k ) ) ∼ ( s [ k . . . n ) , t [ k . . . n ) ) ,所以可以通过倍增将字符串的比较转化为对r k k rk_k r k k 和r k k − k rk_k-k r k k − k (表示将r k k rk_k r k k 数组中的每个元素想做平移k k k 位)对应位组成的二元组进行排序。
又由于字符的值域大小有限,所以可以通过基数排序对二元组进行排序,于是可以得到时间复杂度为O ( n log n ) \mathcal{O}(n\log n) O ( n log n ) 求解后缀数组的算法,空间大小为O ( 4 n ) \mathcal{O}(4n) O ( 4 n ) ,分别由两个辅助数组t[],t2
、一个桶c[]
、后缀数组sa[]
构成。
高度数组 height[i]
:表示排名为i
和i-1
的最长公共前缀(Longest Common Prefix, LCP),也就是sa[i-1],s[i]
的LCP长度,用途是求出两个任意两个后缀j,k
的LCP,不妨令rk[j]<rk[k]
,利用height[]
可以转化为区间最小值问题:求height[rk[j]+1],height[rk[j]+2],...,height[rk[k]]
的最小值。
求解height[]
可以利用一个优美的性质:height[rk[i]] >= height[rk[i-1]]-1
,注意分辨我们的height[rk[i]]
是rk[i],rk[i]-1
的LCP。解释也非常容易:主要利用到了后缀i-1
和i
只相差了开头的一位,首先考虑height[rk[i-1]]
,由height
性质告诉我们i-1
与k:=sa[rk[i-1]-1]
的LCP为height[rk[i-1]]
,而i
就是i-1
去掉第一个字符后的结果,所以我们也将k
去掉第一个字符得到k+1
,我们发现i
与k+1
的LCPheight[rk[i-1]]-1
一定是小于等于i
与sa[rk[i]-1]
的LCP的(这可以由k+1=sa[rk[i-1]-1]+1
的排名一定小于等于rk[i]
得到,由于k
的排名小于i-1
的排名,所以k+1
的排名小于i
的排名),所以height[rk[i]] >= height[rk[i-1]]-1
。
const int maxn = 1e6 + 10 ;
# define resetn ( A, n) memset ( A, 0 , sizeof ( A[ 0 ] ) * n)
struct SA {
char * s;
int n, sa[ maxn] , t[ maxn* 2 ] , t2[ maxn* 2 ] , c[ maxn] ;
void init ( char * T) { s = T; n = strlen ( s) ; }
void build_sa ( int m = 256 ) {
int i, * x = t, * y = t2;
resetn ( c, m) ;
for ( i = 0 ; i < n; i++ ) c[ x[ i] = s[ i] ] ++ ;
for ( i = 1 ; i < m; i++ ) c[ i] += c[ i- 1 ] ;
for ( i = n- 1 ; i >= 0 ; i-- ) sa[ -- c[ x[ i] ] ] = i;
for ( int k = 1 ; k < n; k <<= 1 ) {
int p = 0 ;
for ( i = n- 1 ; i >= n- k; i-- ) y[ p++ ] = i;
for ( i = 0 ; i < n; i++ ) if ( sa[ i] >= k) y[ p++ ] = sa[ i] - k;
resetn ( c, m) ;
for ( i = 0 ; i < n; i++ ) c[ x[ i] ] ++ ;
for ( i = 1 ; i < m; i++ ) c[ i] += c[ i- 1 ] ;
for ( i = n- 1 ; i >= 0 ; i-- ) sa[ -- c[ x[ y[ i] ] ] ] = y[ i] ;
swap ( x, y) ;
p = 1 ; x[ sa[ 0 ] ] = 0 ;
for ( i = 1 ; i < n; i++ )
x[ sa[ i] ] = ( y[ sa[ i] ] == y[ sa[ i- 1 ] ] && y[ sa[ i] + k] == y[ sa[ i- 1 ] + k] ) ? p- 1 : p++ ;
if ( p == n) break ;
m = p;
}
}
int rank[ maxn] , height[ maxn] ;
void get_height ( ) {
for ( int i = 0 ; i < n; i++ ) rank[ sa[ i] ] = i;
for ( int i = 0 , k = 0 ; i < n; i++ ) {
if ( k) k-- ;
if ( rank[ i] ) {
int j = sa[ rank[ i] - 1 ] ;
while ( s[ i+ k] == s[ j+ k] ) k++ ;
}
height[ rank[ i] ] = k;
}
}
} sa;
ababa aabaaaab aaaa
sa: 5 3 1 4 2 sa: 4 5 6 1 7 2 8 3 sa: 4 3 2 1
height sorted height sorted height sorted
0 a 0 aaaab 0 a
1 aba 3 aaab 1 aa
3 ababa 2 aab 2 aaa
0 ba 3 aabaaaab 3 aaaa
2 baba 1 ab
2 abaaaab
0 b
1 baaaab
Hash
将字符串后缀以 x x x 进制的形式表示出来的值称为Hash值,例如12345在 10 10 1 0 进制下每一位的Hash值就是(下标从 0 0 0 开始):
H ( 0 ) = 54321 , H ( 1 ) = 4321 , . . . , H ( 4 ) = 5 H(0) = 54321, H(1) = 4321, ...,H(4) = 5
H ( 0 ) = 5 4 3 2 1 , H ( 1 ) = 4 3 2 1 , . . . , H ( 4 ) = 5
也就是说 H ( i ) = H ( i ) + H ( i + 1 ) ⋅ x = s ( N − 1 ) x N − i + 1 + s ( N − 2 ) x N − i + ⋯ + s ( i ) H(i) = H(i) + H(i+1)\cdot x = s(N-1)x^{N-i+1}+s(N-2)x^{N-i}+\cdots+s(i) H ( i ) = H ( i ) + H ( i + 1 ) ⋅ x = s ( N − 1 ) x N − i + 1 + s ( N − 2 ) x N − i + ⋯ + s ( i ) ,取出以 i i i 开头长度为 L L L 的 H ( i , L ) = H ( i ) − H ( i + L − 1 ) ⋅ x L = s ( i + L ) x L − 1 + ⋯ + s ( i ) H(i, L) = H(i) - H(i+L-1)\cdot x^{L} = s(i+L)x^{L-1}+\cdots+s(i) H ( i , L ) = H ( i ) − H ( i + L − 1 ) ⋅ x L = s ( i + L ) x L − 1 + ⋯ + s ( i ) ,我们可以通过以下方法判断子串是否相同:
H ( i , L ) = H ( j , L ) ⟺ s ( i , . . . , i + L − 1 ) = s ( j , . . . , j + L − 1 ) H(i,L) = H(j,L)\iff s(i,...,i+L-1)=s(j,...,j+L-1)
H ( i , L ) = H ( j , L ) ⟺ s ( i , . . . , i + L − 1 ) = s ( j , . . . , j + L − 1 )
其实并非必须要用后缀来表示Hash值,也可以类似地用前缀表示Hash值。
typedef unsigned long long ULL;
struct StrHash {
ULL n, H[ maxn] , xp[ maxn] , x = 2027 ;
StrHash ( ) { xp[ 0 ] = 1 ; for ( int i = 1 ; i < maxn; i++ ) xp[ i] = xp[ i- 1 ] * x; }
void init ( char * s) {
n = strlen ( s) ; H[ n] = 0 ;
for ( int i = n- 1 ; i >= 0 ; i-- ) H[ i] = H[ i+ 1 ] * x + s[ i] ;
}
ULL hash ( int l, int r) { return H[ l] - H[ r+ 1 ] * xp[ r- l+ 1 ] ; }
} shash;
char s[ maxn] ;
int rank[ maxn] , hash[ maxn] ;
int check ( int L) {
int mx = 0 , tot = 0 ;
for ( int i = 0 ; i < n- L+ 1 ; i++ ) {
rank[ i] = i;
hash[ i] = shash. hash ( i, i + L - 1 ) ;
}
std:: sort ( rank, rank+ n- L+ 1 , [ ] ( int & a, int & b) { return hash[ a] == hash[ b] ? a < b : hash[ a] < hash[ b] ; } ) ;
for ( int i = 0 ; i < n- L+ 1 ; i++ ) {
if ( i == 0 || hash[ rank[ i] ] != hash[ rank[ i- 1 ] ] ) tot = 0 ;
mx = std:: max ( mx, ++ tot) ;
}
return mx;
}
Manacher
Manacher算法用于求解最大回文串长度,方法也是DP,思路与KMP相似,避免使用已访问过的字符,从而达到线性复杂度,首先由于回文串有奇偶问题,如果原串长度为L L L ,则进行以下填补,使得最后得到的串长度为2 L + 2 2L+2 2 L + 2 :
\text{原串}:12212321\\
\text{填补后}:$\#1\#2\#2\#1\#2\#3\#2\#1\#
\text{}
T
$
#
1
#
2
#
2
#
1
#
2
#
3
#
2
#
1
#
f
1
1
2
1
2
5
2
1
4
1
2
1
6
1
2
1
2
1
设 f ( i ) f(i) f ( i ) 表示填补后的串 T T T ,以 T ( i ) T(i) T ( i ) 为回文中心的最大回文串半径(半径包括回文中心),不难发现,原串的对应位置 S ( i / 2 − 1 ) S(i/2-1) S ( i / 2 − 1 ) 处的原最大回文串长度就是 f ( i ) − 1 f(i)-1 f ( i ) − 1 ,可以理解为将所有左侧的字符向右移一位,填补#的位置,并删去左侧最左端的#,即可得到原最大回文串长度。
假设当前的最大回文串右端点为 r r r ,对应的回文中心为 c c c ,考虑当前要求解的 f ( i ) f(i) f ( i ) ,设 i i i 关于 c c c 的对称点为 j j j ,于是可分以下几个情况(思路就是最大化利用 f ( j ) f(j) f ( j ) 的回文串长度):
所以可以通过 f ( j ) f(j) f ( j ) 的值对 f ( i ) f(i) f ( i ) 进行下界估计,然后再向右尝试对 f ( i ) f(i) f ( i ) 进行延拓:
const int maxn = 1e5 + 10 ;
struct Manacher {
char s[ maxn<< 1 ] ;
int n, f[ maxn<< 1 ] ;
void init ( char * T) {
n = 0 ;
s[ n++ ] = '$' ; s[ n++ ] = '#' ;
for ( int i = 0 ; T[ i] ; i++ ) s[ n++ ] = T[ i] , s[ n++ ] = '#' ;
}
void build ( ) {
int c = 0 , r = 0 ;
for ( int i = 0 ; i < n; i++ ) {
int & p = f[ i] ;
p = r > i ? std:: min ( f[ 2 * c- i] , r- i) : 1 ;
while ( s[ i- p] == s[ i+ p] ) p++ ;
if ( i + p > r) r = i + p, c = i;
}
}
} mc;
后缀自动机
定义 :设 p , q p,q p , q 为SAM中的节点,用 S ( p ) S(p) S ( p ) 表示节点 p p p 对应的字符串集合,l e n ( p ) len(p) l e n ( p ) 为所有字符串集合中最长的字符串长度,用 E ( p ) E(p) E ( p ) 表示节点 p p p 对应的 e n d p o s endpos e n d p o s 集合,n e x t ( p , c ) next(p,c) n e x t ( p , c ) 表示节点 p p p 向字符 c c c 在DAG图上的边,l i n k ( p ) link(p) l i n k ( p ) 为节点 p p p 的后缀链接。
引理1 :定义一下等价关系 S 1 ∼ S 2 ⟺ E ( S 1 ) = E ( S 2 ) S_1\sim S_2\iff E(S_1)=E(S_2) S 1 ∼ S 2 ⟺ E ( S 1 ) = E ( S 2 ) ,则 ∀ S 1 , S 2 ∈ S ( p ) \forall S_1,S_2\in S(p) ∀ S 1 , S 2 ∈ S ( p ) 有 S 1 ∼ S 2 S_1\sim S_2 S 1 ∼ S 2 。
引理2 :记 l i n k ( q ) = p link(q) = p l i n k ( q ) = p ,则 E ( q ) ⊂ E ( p ) E(q)\subset E(p) E ( q ) ⊂ E ( p ) ,且在后缀链接树中 p p p 的子节点两两不交,即(集合的不交并 ∩ \cap ∩ 记为 + + + )
∑ l i n k ( q ) = p E ( q ) ⊂ E ( p ) \sum_{link(q) = p}E(q)\subset E(p)
l i n k ( q ) = p ∑ E ( q ) ⊂ E ( p )
引理3 :从SAM中的起点沿DAG边达到 p p p 的左右路径对应的字符串,构成 S ( p ) S(p) S ( p ) ,即 n e x t ( p , c ) = q next(p,c) = q n e x t ( p , c ) = q ,则 S ( p ) + c ⊂ S ( q ) S(p)+c\subset S(q) S ( p ) + c ⊂ S ( q ) (S ( p ) + c S(p)+c S ( p ) + c 表示将 S ( p ) S(p) S ( p ) 中的每个字符串后缀都加上字符 c c c ),且 l e n ( p ) + 1 ⩽ l e n ( q ) len(p)+1\leqslant len(q) l e n ( p ) + 1 ⩽ l e n ( q ) 。
template < const int maxn>
struct SuffixAutomaton {
std:: map< char , int > next[ maxn] ;
int link[ maxn] , len[ maxn] , sz, last;
void init ( ) { sz = 0 ; last = new_node ( ) ; }
int new_node ( ) {
link[ sz] = - 1 ; len[ sz] = 0 ;
next[ sz] . clear ( ) ;
return sz++ ;
}
void insert ( char c) {
int p = last, cur = new_node ( ) ;
len[ last = cur] = len[ p] + 1 ;
while ( p != - 1 && ! next[ p] . count ( c) ) next[ p] [ c] = cur, p = link[ p] ;
if ( p == - 1 ) { link[ cur] = 0 ; return ; }
int q = next[ p] [ c] ;
if ( len[ p] + 1 == len[ q] ) { link[ cur] = q; return ; }
int nq = new_node ( ) ;
next[ nq] = next[ q] ;
link[ nq] = link[ q] , len[ nq] = len[ p] + 1 , link[ q] = link[ cur] = nq;
while ( p != - 1 && next[ p] [ c] == q) next[ p] [ c] = nq, p = link[ p] ;
}
void build ( char * s) { while ( * s) insert ( * s++ ) ; }
} ;
桶排序
对len
数组进行从小到大排序,得到的即是DAG
图的拓扑序,也是后缀链接树的深度从小到大的DFS序(用于求解endpos
大小和在DAG
图上进行DP都非常好用):
int c[ maxn] , la[ maxn] ;
void toposort ( ) {
resetn ( c, 0 , sz) ;
for ( int i = 0 ; i < sz; i++ ) c[ len[ i] ] ++ ;
for ( int i = 1 ; i < sz; i++ ) c[ i] += c[ i- 1 ] ;
for ( int i = sz- 1 ; i >= 0 ; i-- ) la[ -- c[ len[ i] ] ] = i;
for ( int i = 1 ; i < sz; i++ ) endpos[ la[ i] ] += endpos[ link[ la[ i] ] ] ;
广义SAM
HDU - 4436 - str2int - E3 - 广义SAM模板题(只用到DAG图)
广义SAM,就是一个SAM中同时插入多个字符串,其实方法很简单,只需在每次插入新串前重置last = 0
,插入串的字符c
时,判断是否当前插入的节点已经在SAM中有对应节点,如果已有则将last
直接转移过去,否则类似创建nq
节点,从q
节点中分裂出后缀长度小于等于len[p]+1
部分的子串,除了不用将link[cur]
设置为nq
其他与之前完全一致,这里引入split
函数,只需要对insert(char c)
函数进行修改:
int split ( int c, int p, int q, int cur = - 1 ) {
int nq = new_node ( ) ;
copy ( next[ nq] , next[ q] ) ;
link[ nq] = link[ q] ; len[ nq] = len[ p] + 1 ; link[ q] = nq;
if ( cur != - 1 ) link[ cur] = nq;
while ( p != - 1 && next[ p] [ c] == q) next[ p] [ c] = nq, p = link[ p] ;
return nq;
}
void insert ( char c) {
c = id ( c) ; int p = last, np = next[ p] [ c] ;
if ( np) {
if ( len[ p] + 1 == len[ np] ) last = np;
else last = split ( c, p, np) ;
return ;
}
int cur = new_node ( ) ;
len[ last = cur] = len[ p] + 1 ;
while ( p != - 1 && ! next[ p] [ c] ) next[ p] [ c] = cur, p = link[ p] ;
if ( p == - 1 ) { link[ cur] = 0 ; return ; }
int q = next[ p] [ c] ;
if ( len[ p] + 1 == len[ q] ) { link[ cur] = q; return ; }
split ( c, p, q, cur) ;
}
倍增求子串对应的节点
为了求文本串T
的某个子串T[l,...,r]
在后缀链接树的节点位置,即T[l,...,r]
所处的endpos节点。
方法:记录下每个字符串结束位置,例如T[0,...,r]
对应的SAM节点记为pos[r]
,然后在后缀链接树上从pos[r]
出发,在树上倍增找祖先节点u
中满足len[u]>=r-l+1
最浅 的节点,这就可以用树上倍增解决了(类似倍增求LCA):
int jump[ maxn] [ 18 ] ;
void build_jump ( ) {
for ( int i = 0 ; i < sz; i++ ) jump[ i] [ 0 ] = link[ i] ;
for ( int j = 1 ; ( 1 << j) < sz; j++ )
for ( int i = 0 ; i < sz; i++ )
if ( jump[ i] [ j- 1 ] == - 1 ) jump[ i] [ j] = - 1 ;
else jump[ i] [ j] = jump[ jump[ i] [ j- 1 ] ] [ j- 1 ] ;
}
int p = pos[ r] ;
for ( int i = 17 ; i >= 0 ; i-- ) {
int q = jump[ p] [ i] ; if ( q == - 1 ) continue ;
if ( len[ q] >= r- l+ 1 ) p = q;
}
维护endpos集合
K-th occurrence - SAM倍增 + 线段树合并
有些题目需要实际维护endpos集合,比如求出某个节点的endpos集合中第k大的元素(就是该节点对应的字符串集合在原串中第k次出现的位置),我们考虑每个节点u
对应一颗权值线段树(之所以称为权值线段树,是因为他直接统计每个值域的值在该节点内出现的次数),然后根据len
数组从大到小的顺序(后缀链接树从深到浅),将u
的线段树合并到link[u]
的线段树中即可。
TNode* merge ( TNode * p1, TNode * p2) {
if ( ! p1 || ! p2) return p1 ? p1 : p2;
TNode & p = * new_node ( p1-> l, p1-> r) ;
p. ls = merge ( p1-> ls, p2-> ls) ;
p. rs = merge ( p1-> rs, p2-> rs) ;
pushup ( p) ; return & p;
}
时间复杂度计算
向lnc同学请教了下总算明白了时间复杂度就是稳定的O ( n log n ) \mathcal{O}(n\log n) O ( n log n ) ,根据上述代码,我们发现线段树合并算法满足以下性质:合并两颗线段树的时间复杂度为两颗线段树并的节点数 ,首先给出以下两个结论:
假设初始有n n n 颗线段树,每颗线段树的叶结点数目均为n = 2 h − 1 n = 2^h-1 n = 2 h − 1 ,第i i i 颗线段树的初始节点数为a i a_i a i 。
对于任意的一种合并方法,将n n n 颗线段树两两合并最后得到一颗线段树,其时空复杂度均为∑ i = 1 n a i \sum_{i=1}^na_i ∑ i = 1 n a i 。
假设初始时每颗线段树都是两两不交的单点,即a i = log n = h − 1 a_i = \log n = h-1 a i = log n = h − 1 ,则合并所有线段树的时空复杂度为( h − 2 ) 2 h − 1 + 1 = n log n 2 + 1 = O ( n log n ) (h-2)2^{h-1}+1 = n\log \frac{n}{2} + 1 = \mathcal{O}(n\log n) ( h − 2 ) 2 h − 1 + 1 = n log 2 n + 1 = O ( n log n ) 。
我们证明第一个结论:只需发现,每个线段树的结构都是相同的(叶结点有n n n 个),考虑线段树上每个节点的创建次数,由于线段树合并的性质,该节点的创建次数一定不超过该节点在所有的初始线段树中的出现次数 (反证法很容易证明),所以合并的时间复杂度就是∑ i = 1 n a i \sum_{i=1}^na_i ∑ i = 1 n a i ,由于每一次合并都会开一个新的节点,所以时空复杂度相同。
第二个结论是第一个结论的特例,也就是基于第一问求出每个非叶子节点合并时会被创建多少次,如下图所示
根据上述结果,我们还可以给出线段树合并的开的节点数具体应该是n log n / 2 n\log n/2 n log n / 2 ,n n n 为节点数目,再加上初始时的所有线段树的节点数目n log 2 n n\log 2n n log 2 n ,于是总的节点数目应该开到2 n log n 2n\log n 2 n log n ,也就是n = 1 0 5 n=10^5 n = 1 0 5 ,线段树大概要开到3.6 × 1 0 6 3.6\times 10^6 3 . 6 × 1 0 6 。
因为可能重复在同一个位置创建节点,将之前节点覆盖时会产生内存泄漏,所以从数组中取新的节点可以避免该问题。
struct TNode {
TNode * ls, * rs; int l, r, sum;
TNode ( ) { }
void init ( int l, int r) { this -> l = l, this -> r = r; ls = rs = nullptr ; sum = 0 ; }
void update ( int k) { sum += k; }
} ;
template < const int maxn, const int LOGN= 18 >
struct SegmentTree {
TNode t[ 2 * maxn * LOGN] ;
int sz;
void init ( ) { sz = 0 ; }
TNode* new_node ( int l, int r) { t[ sz] . init ( l, r) ; return & t[ sz++ ] ; }
void pushup ( TNode & p) {
if ( ! p. ls || ! p. rs) p. sum = p. ls ? p. ls-> sum : p. rs-> sum;
else p. sum = p. ls-> sum + p. rs-> sum;
}
void update ( TNode * & p, int l, int r, int k) {
if ( ! p) p = new_node ( l, r) ;
if ( l == r) { p-> update ( 1 ) ; return ; }
int mid = ( l+ r) >> 1 ;
if ( k <= mid) update ( p-> ls, l, mid, k) ;
else update ( p-> rs, mid+ 1 , r, k) ;
pushup ( * p) ;
}
TNode* merge ( TNode * p1, TNode * p2) {
if ( ! p1 || ! p2) return p1 ? p1 : p2;
TNode & p = * new_node ( p1-> l, p1-> r) ;
p. ls = merge ( p1-> ls, p2-> ls) ;
p. rs = merge ( p1-> rs, p2-> rs) ;
pushup ( p) ; return & p;
}
int query ( TNode & p, int k) {
if ( p. l == p. r) return p. l;
int mid = ( p. l+ p. r) >> 1 ;
if ( p. sum < k) return - 1 ;
if ( p. ls) if ( p. ls-> sum >= k) return query ( * p. ls, k) ;
else k -= p. ls-> sum;
return query ( * p. rs, k) ;
}
} ;
void toposort ( ) {
. . .
for ( int i = sz- 1 ; i >= 1 ; i-- ) {
int v = la[ i] , u = link[ v] ;
rt[ u] = seg. merge ( rt[ u] , rt[ v] ) ;
}
}