字串

失蹤

鬼轉一波

\(INDEX\)

  • Trie
  • KMP

這我

  • 20729 蔡嘉晉
  • 失蹤的世宗
  • judging

 

  • 因為大家都用之前的綽號講爛梗所以換綽號了
  • 但還是一樣爛
  • 這邊推薦大家叫我本名

 

  • 店言學術
  • 資訊校隊
  • 北市賽佳作

{Trie}

畢竟算是一棵樹對吧

字典樹

  • 字串匹配
  • 你有一個字典 裡面有很多字
  • 要怎麼找一個字有沒有出現過

 

  • 顯然是不可能全部跑一遍

 

  • 要怎麼在 \(O(|S|)\) 的時間跑完(?

剪枝(?

  • 如果正在比較的這兩個字串前綴不一樣
  • 就沒必要繼續比了
    • fdkjs & fdkew
    • 比完fdk都有可能是相同的
    • 但走道第四個字元的時候就不一樣了
    • 這樣就可以跳掉了

 

  • 所以要對每個字串逐字元比對
  • 還是太慢了

剪枝(?

  • 如果兩個字元前綴依樣
  • 他們甚至可以合併成一個節點一起比較欸
  • 等到出現不一樣的時候再分岔就好啦

TRIE

  • 每個節點代表一個前綴
  • 往下走一層代表為這個前綴
  • 多加一個字元
1
2 a
3 b
4 c
5 aa
6 ab
7 ba
8 ca
9 cb
10 cc
11 aba
12 caa
13 cab
14 cba
15 caaa

如何存節點

  • 對Node開一個struct

 

  • 用指標指向他的子傑點們
  • 因為你不知道你到底會有幾個節點
  • 所以這時候幫每個點存一個陣列的指標就很好往下走了

 

  • 複習一下struct跟指標一起出現的時候
struct Trie{
	Trie* c[26];//看你下面有幾種字元,也可以是不定長度的
    int cnt=0; //為了儲存這個節點有幾個字是結尾在這裡的
	Trie():cnt(0){
		memset(c, 0, sizeof(c));
	}
};
trie* root = new trie();

insert

  • 如果有下一個節點
  • 就走下去
  • 沒有就新創他
  • 如果走到底了就把那個節點cnt+=1
void insert(string s){
	trie* tmp = root;
	for(int i=s.size()-1; i>=0; i--){
		if(!tmp->c[(s[i]-'a')]){
			tmp->c[(s[i]-'a')] = new trie();
		}
		tmp=tmp->c[(s[i]-'a')];
	}tmp->cnt++;
}

query

  • 照這要找的字元走下去就好
bool query(string s){
	trie* tmp =root;
	for(int i=0; i<s.size(); i++){
		int eee=s[i]-'a';
		if(tmp->c[eee]){
			tmp=tmp->c[eee];
		}else{
			return false;
		}if(tmp->cnt&&i==s.size()-1){
        	return true;
		}
	}
    return false;
}

欸不是

  • 我用set存就好了啊

 

  • 但其實trie還有很多功能
  • 像是需要對每個字串長度進行比較時
    • 對sfkdlaj的所有前綴字串進行查詢
    • Trie可以做到\(O(N)\)
    • set需要\(O(N\ logN)\)

Trie+DP

  • CSES 1731
  • 給定一個字典
  • 裡面有很多字

 

  • 一個長字串可以有幾種用字典裡面組成的方法
  • 用推的
  • 當你算到第i個字元的時候
  • 把i當root開始跑字典樹
  • 如果跑到某個地方有這個字
  • 就把dp[i+k]+=dp[i]

CODE

  • TEXT
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define modd 1000000007
vector<int> dp(100007, 0);
struct trie{
	trie* c[26];
	int cnt=0;
	trie(){
		memset(c, 0, sizeof(c));
	}
};
trie* root = new trie();
 
void insert(string s){
	trie* tmp = root;
	for(int i=s.size()-1; i>=0; i--){
		if(!tmp->c[(s[i]-'a')]){
			tmp->c[(s[i]-'a')] = new trie();
		}
		tmp=tmp->c[(s[i]-'a')];
	}tmp->cnt++;
}
string ans;
void query(int e){
	trie* tmp =root;
	int tpe=e;
	while(tpe>=0){
		int eee=ans[tpe]-'a';
		if(tmp->c[eee]){
			tmp=tmp->c[eee];
		}else{
			break;
		}if(tmp->cnt){
			dp[tpe]+=dp[e+1];
			dp[tpe]%=modd;
		}tpe--;
	}
 
}
 
signed main(){
	cin>>ans;
	int n;cin>>n; 
	for(int i=0; i<n; i++){
		string t;cin>>t;
		insert(t);
	}
	dp[ans.size()]=1;
	for(int i=ans.size()-1; i>=0; i--){
		query(i);
	}cout<<dp[0];
}
SHARE CODE TO OTHERS

Trie ^ bitwise op

  • 字串匹配其實也不一定要匹配到完全一樣的
  • Trie就可以做到在一定程度下允許不同的字串
  • 或是找完全不同的東西
  • 如:
    • 給定一堆數字
    • 接下來有一連串的數字問他跟上面那堆數字哪個xor起來最大
    • 用數字的二進位表示建trie
    • 從頭往下走
    • 如果要問的數字這個位元是1就優先往0走 沒有就只能走1
    • 如果要問的數字這個位元是0就優先往1走 沒有就只能走0

今天時間蠻多的

大家現在去想辦法AC他

{KMP}

Knuth–Morris–Pratt 就三個人名

前綴函數 \(\pi\)

  • 對於一個字串
  • 定義其 \(\pi[i]\) 為 \(s[0...i]\) 中前後綴相同的子字串長度
    • \(\pi[i]=k\)  if  \(s[0...k]==s[i-k+1...i]\)
    • \(\pi[0]=0\)
    • 對於字串abcabcd
      • \(\pi[1]=0\)   ab無共同前後綴
      • \(\pi[2]=0\)   abc無共同前後綴
      • \(\pi[3]=1\)   abca中a=a
      • \(\pi[4]=2\)   abcab中ab=ab
      • \(\pi[5]=3\)   abcabc中abc=abc
      • \(\pi[6]=0\)   abcabcd無共同前後綴

怎麼算\(\pi\)函數?

  • Brute force
  • 對於\(pi[i]\)算每個字字串對他們的前後綴進行比較
  • \(O(N^3)\)

怎麼算\(\pi\)函數?

  • 優化
  • 現在算完了\(\pi[i]\) 要怎麼算\(\pi[i+1]\)?
  • \(max(\pi[i+1])=\pi[i]+1\)
    • iff \(s[i+1] == s[\pi[i]]\)
  • 但如果不同呢
  • 我要怎麼選下一個要比較的?

 

  • \(\pi[i]-1\)    !
  • why?
  • 動畫燒雞 我等下畫畫

實作

vector<int> prefix_function(string s) {
  int n = (int)s.length();
  vector<int> pi(n);
  for (int i = 1; i < n; i++) {
    int j = pi[i - 1];
    while (j > 0 && s[i] != s[j]) j = pi[j - 1];
    if (s[i] == s[j]) j++;
    pi[i] = j;
  }
  return pi;
}

字串匹配

  • 現在有兩個字串 要計算S在T中的哪些位置出現
  • 要怎麼用 \(\pi\) 函數算?
  • 對 \( S\#T \) 做 \(\pi\) 函數
  • # 代表一個不出現在T跟S中的字元
  • \(\pi\)函數中如果有任何一個值是S的長度
  • 代表T中出現了一次S!

字串

By cjtsai

字串

  • 74