字串

Outline

  • Trie
  • KMP
  • Hash

Trie

Trie

  • 字典樹
  • 一種樹狀結構,可以儲純一堆字串
  • 因為字元種類很少,所以可以建成一棵樹
  • 當然也可以存 0-1 位元

每條邊表示要加的字元,每個節點代表從根開始走形成的字串。

h

he

hel

hell

hello 

k

ke

key

ken

+h

+e

+l

+l

+o

+k

+e

+y

+n

假設今天要加入 "bee", "yee", "yes", "yuhung", "zap", "zaps"

b

be

bee

y

ye

yee

yes

yu

yuh

yuhu

yuhun

yuhung

z

za

zap

zaps

+b

+e

+e

+y

+e

+e

+s

+u

+h

+u

+n

+g

+z

+a

+p

+s

Trie 實作

  • 簡單來說就是每個節點維護 26種字元下一個的位置

最長共同前綴

  • 給你 \(n\) 個字串,令\(lcp(i,j)\) 為字串 \(i,j\) 的前綴
  • 之後對字串 \(s_i\) 輸出\(max\{lcp(i,j)\}_{j<i}\)
  • \(1\leq n,S(\sum_{s_i})\leq 10^6\)

最長共同前綴

  • 把這些字串插入到字典樹吧,那這樣詢問時就只要按照字典樹依序走訪,直到沒有相同字源就停止。
  • 得到的就會是最長共同前綴
  • 插入、詢問時間複雜度會是 \(O(|s_i|)\) 
  • 總時間複雜度會是 \(O(S)\)
  • 空間複雜度會是 \(O(26 \cdot \max(s_i))\)

最長共同前綴

struct Node {
    vector<int> c;
    Node(): c(26, -1) {}
    int &operator()(char nxt) {
        return c[nxt - 'a'];
    }
};
vector<Node> trie;
int add(string &str, int now, int id) {
    //  cout<<now<<"now"<<id<<"id\n";
    if (id == -1) {
        id = tmp++;
    }

    if (now == str.size())
        return id;

    trie[id](str[now]) = add(str, now + 1, trie[id](str[now]));
    return id;
}
int search(string &str, int now, int id) {
    if (now == str.size())
        return now;

    char c = str[now];

    if (trie[id](c) != -1) {
        return search(str,now+1,trie[id](c));
    }
    return now+1;
}

LOJ 10050 The XOR Largest Pair

  • 給你一個序列 \(a_1, a_2, . . . , a_n\),求 \(\max\{a_i ⊕ a_j\}\)。
  • \(n ≤ 10^5、a_i < 2^{31}\)

LOJ 10050 The XOR Largest Pair

  • 思考一下對 \(x\) 的 xor 最大配對 \(a\) 有甚麼性質?
  • xor 是只有當兩個位元恰是 0,1 個一時才會是1,其餘為0
  • 要讓數字最大高位元要儘量是 1
  • 也就是說從第一個位元開始, \(a\) 要盡量和 \(x\) 恰好相反
  • Trie 搞定

LOJ 10050 The XOR Largest Pair

//Author: Woody
#include <bits/stdc++.h>
#define int long long
#define mp make_pair
#define eb emplace_back
#define rep(n) for(int i=0;i<n;i++)
#define rep2(n) for(int j=0;j<n;j++)
#define F first
#define S second
#define all(v) v.begin(),v.end()
#define SZ(x) (int)(x.size())
#define lowbit(x) (x&-x)
#define SETIO(s) ifstream cin(s+".in");ofstream cout(s+".out");
#define quick ios::sync_with_stdio(0);cin.tie(0);
using namespace std;
typedef pair<int, int> pii;
template <class t1, class t2>
inline const pair<t1, t2> operator + (const pair<t1, t2> &p1, const pair<t1, t2> &p2) {
    return pair<t1, t2>(p1.F + p2.F, p1.S + p2.S);
}
template <class t1, class t2>
inline const pair<t1, t2> operator - (const pair<t1, t2> &p1, const pair<t1, t2> &p2) {
    return pair<t1, t2>(p1.F - p2.F, p1.S - p2.S);
}
const int INF = 1e18;
const int N = 1e5 + 7;
const int D = 31;
string s[N];
string bin(int num) {
    string S;

    while (num > 0) {
        if (num & 1)
            S += "1";
        else
            S += "0";

        num >>= 1;
    }

    S += string(D - S.size(), '0');
    reverse(all(S));
    return S;
}
int tmp = 1;
struct Node {
    vector<int> c;
    Node(): c(2, -1) {}
    int &operator()(char nxt) {
        return c[nxt - '0'];
    }
};
vector<Node> trie;
int add(string &str, int now, int id) {
    //  cout<<now<<"now"<<id<<"id\n";
    if (id == -1) {
        id = tmp++;
    }

    if (now == str.size())
        return id;

    trie[id](str[now]) = add(str, now + 1, trie[id](str[now]));
    return id;
}
int search(string &str, int now, int id) {
    if (now == str.size())
        return 0;

    char c = '1' - str[now] + '0';

    if (trie[id](c) != -1) {
        return (1 << (D - now - 1)) + search(str, now + 1, trie[id](c));
    }

    return search(str, now + 1, trie[id](str[now]));
}
signed main() {
    quick
    trie.resize(2e6 + 7);
    int n;
    cin >> n;

    for (int i = 0; i < n; i++) {
        int a;
        cin >> a;
        s[i] = bin(a);
        //  cout<<s[i].size()<<"l\n";
        add(s[i], 0, 0);
    }//return 0;

    int ans = 0;

    for (int i = 0; i < n; i++) {
        int P = search(s[i], 0, 0);
        //  cout<<ans<<","<<P<<"P\n";
        ans = max(ans, P);
    }

    cout << ans << "\n";
    return 0;
}

KMP (Knuth-Morris-Pratt)

  • 給字串 \(T\) 和目標字串 \(S\)
  • 要怎麼知道 \(T\) 出現幾次 \(S\) 和出現位置
  • \(O(|S||T|)\) 枚舉?
  • \(T\) = ababbaabababbaababbabaa
  • \(S\)=ababbaababbabaa
  • 但其實有很多次不需比
  • 當知道第一次是匹配前十個的情況下
  • 下一次一定從對齊四個繼續試試看
  • 目標字串有特殊性質

Failure function

  • \(F(i)=\begin{cases} \max\{k: P[0,k-1]=P[i-k+1,i] ,k < i+1 \} & {if \ i \ \neq 0 \ and \ k \ exists} \\ 0 & {otherwise} \\ \end{cases}\)
  • 其實就是位置 \(x\) 的次長前後綴

KMP

  • 先假設有了 \(F(x)\) , 那要怎麼做匹配?
  • \(F(x)\) 存的是前面的共同長度,因此當位置 \(x+1\) 配對失敗時,可以把 \(x+1\) 和 \(F(x)\) 位置貼齊繼續匹配

KMP

  • 實際匹配過程如下:
  • 當位置 \(P_i \neq T_{pos} , pos=F(pos-1)\)
  • 持續進行直到等號成立,成立後則往下一位置繼續看
  • 若匹配完全則成功,當作匹配失敗繼續找

KMP

int KMP(string a,string b){
	vector<int> f=buildf(b);
	int ans=0;
	int n=sz(a);
	int m=sz(b);
	int pos=0;
	rep(i,0,n-1){
		while(a[i]!=b[pos]&&pos){
			pos=f[pos-1];
		}
		if(a[i]==b[pos]) pos++;
		if(pos>=m){
			ans++;
			pos=f[pos-1];
		}
	}
	return ans;
}

KMP

  • 時間複雜度呢 (\(O(n^2)\)?)
  • 雖然pos持續往回的迴圈時間不好估
  • 但可以保證 \(F(x)<x\), 因此會遞減
  • 但只有在配對成功時會增加
  • 時間複雜度 \(O(n)\)

KMP

  • 有了 \(F(x)\) 我們會做匹配
  • 那要如何建 \(F(x)\)
  • 假設已算完 \(F(0) \sim F(x-1)\)
  • 當 \(P[x]=P[F(x-1)+1]\) \(F(x)=F(x-1)+1\)
  • 反之則代表配對失敗,那就和前面一樣,從 Failure function 繼續往前找
  • 和匹配 code 很類似

KMP

vector<int> buildf(const string&p){
	int n=sz(p);
	vector<int> f(n);
	int pos=0;
	rep(i,1,n-1){
		while(p[i]!=p[pos]&&pos){
			pos=f[pos-1];
		}
		if(p[i]==p[pos]) pos++;
		f[i]=pos;
	}
	return f;
}
int KMP(string a,string b){
	vector<int> f=buildf(b);
	int ans=0;
	int n=sz(a);
	int m=sz(b);
	int pos=0;
	rep(i,0,n-1){
		while(a[i]!=b[pos]&&pos){
			pos=f[pos-1];
		}
		if(a[i]==b[pos]) pos++;
		if(pos>=m){
			ans++;
			pos=f[pos-1];
		}
	}
	return ans;
}

Rolling Hash

字串匹配的唯一選擇

Rolling Hash

  • \(H(s)=\sum_s(s_i \cdot p^{n-i}) \pmod{M}\)
  • \(p\) 通常選字元數量以上的質數,或是隨機選
  • \(M\) 會是 int 範圍內的質數
  • ex:
    • \(10^9+7,998244353,2147483647,19260817\)

Rolling Hash

  • \(H(s)=\sum_s(s_i \cdot p^{n-i}) \pmod{M}\)
  • 可以想像成倒三角形

區間詢問呢

\(H_{l\sim r}=H_r-H_l\cdot p^{r-l}\)

Birthday Attack

  • 一個房間至少要有 \(n\) 個人,存在兩個人是同一天生日機率 \(>50\%\)
  • \(n=23\)
  • 當 \(n\) 個人均勻從 \(1\sim N\) 選一個數字
  • 存在兩人選擇數字相同 \(p(n)\sim 1-1/exp(n^2/2N)\) 

Birthday Attack

  • 一個房間至少要有 \(n\) 個人,存在兩個人是同一天生日機率 \(>50\%\)
  • \(n=23\)
  • 當 \(n\) 個人均勻從 \(1\sim N\) 選一個數字
  • 存在兩人選擇數字相同 \(p(n)\sim 1-1/exp(n^2/2N)\) 

避免碰撞

  • \(n=10^5, M=10^9+7\) 
  • \(p(n)=1-1/exp(n^2/2M) \sim 99\%\) ?
  • 既然一個模數不夠,那就兩個
  • 求字串 \(S\) 的最長迴文
  • \(1 \leq |S| \leq 10^6\)
  • 迴文的性質就是從中間切開後左右兩字串是相等的
  • 也就是 \(Hash_{pref}(L)=Hash_{suff}(R)\)
  • 所以可以暴力枚舉中間點,剩下要怎麼找到最長的左右相等字串?
  • 二分搜
#pragma GCC optimize("O3,unroll-loops")
#pragma GCC target("avx,popcnt,sse4,abm")
#include<bits/stdc++.h>
#define int long long
#define quick ios::sync_with_stdio(0);cin.tie(0);
#define rep(x,a,b) for(int x=a;x<=b;x++)
#define repd(x,a,b) for(int x=a;x>=b;x--)
#define lowbit(x) (x&-x)
#define sz(x) (int)(x.size())
#define F first
#define S second
#define all(x) x.begin(),x.end()
#define mp make_pair
#define eb emplace_back
using namespace std;
typedef complex<int> P;
#define X real()
#define Y imag()
typedef pair<int,int> pii;
void debug(){
    cout<<"\n";
}
template <class T,class ... U >
void debug(T a, U ... b){
    cout<<a<<" ",debug(b...);
}
const int N=1e6+7;
const int INF=1e18;
pii operator + (pii a,pii b){
	return mp(a.F+b.F,a.S+b.S);
}
pii operator + (pii a, int b){
	return mp(a.F+b,a.S+b);
}
pii operator - (pii a,pii b){
	return mp(a.F-b.F,a.S-b.S);
}
pii operator * (pii a,int k){
	return mp(a.F*k,a.S*k);
}
pii operator * (pii a,pii b){
	return mp(a.F*b.F,a.S*b.S);
}
pii operator % (pii x,pii y){
	return mp(x.F%y.F,x.S%y.S);
}
bool operator == (pii a,pii b){
	return a.F==b.F&&a.S==b.S;
}
const pii Mod={1234567891,998244353};
int p;
pii pw[N];
pii pref[N];
pii suff[N];
pii q(int l,int r,bool pf){
	if(pf){
		return (pref[r]-pref[l-1]*pw[r-l+1]%Mod+Mod)%Mod;
	}
	return (suff[l]-suff[r+1]*pw[r-l+1]%Mod+Mod)%Mod;
}
bool ok(int posl,int pos,int pos2,int posr,int n){
	if(posl<=0||posr>n) {return false;}
	return q(posl,pos,true)==q(pos2,posr,false);
}
bool ok(int ln,int pos,int pos2,int n){
	return ok(pos-ln,pos,pos2,pos2+ln,n);
}
int qry(int pos,int pos2,int n){
	int l=0;
	int r=n+1;
	while(l+1<r){
		int mid=(l+r)>>1;
		if(ok(mid,pos,pos2,n)) l=mid;
		else r=mid;
	}
	return l;
}
signed main(){
	quick
	string s;
	cin>>s;
	int n=sz(s);
	s=' '+s;
	mt19937 rand;
	p=rand()%17+29;
	pw[0]=mp(1,1);
	rep(i,1,n){
		pw[i]=pw[i-1]*p%Mod;
	}
	rep(i,1,n){
		pref[i]=(pref[i-1]*p+(s[i]-'a'+1))%Mod;
	}
	repd(i,n,1){
		suff[i]=(suff[i+1]*p+(s[i]-'a'+1))%Mod;
	}
	int L,R;
	L=R=0;
	int ans=0;
	rep(i,1,n){
		int l1=qry(i,i,n);
		if(l1*2+1>ans){
			ans=l1*2+1;
			L=i-l1;
			R=i+l1;
		}
		if(i+1<=n&&s[i]==s[i+1]){
			int l2=qry(i,i+1,n);
			if(l2*2+2>ans){
				ans=l2*2+2;
				L=i-l2;
				R=i+l2+1;
			}
		}
	}
	rep(i,L,R) cout<<s[i];cout<<"\n";
 
	return 0;
}

字串演算法

By yuhung94

字串演算法

  • 162