KMP
引入
問題
定義 input string s[1 ~ n] (1-base)
令 f[i] : s[1~i] 的 「次長」共同前後綴長度
性質1
s[1~i] 的所有共同前後綴長度 :
最長 i
第 2 長 f[i]
第 3 長 f[f[i]]
第 4 長 f[f[f[i]]]
…
性質2
f[i] - 1 一定是 s[1 ~ (i-1)] 的一個共同前後綴長度,但不一定是最長
⇒ 想要找 f[i] : 從 s[1~(i-1)] 共同前後綴去找
實作
code
vector < int > kmp ( string s ) { // 1-based string
int n = s . size ();
vector < int > f ( n , -1 );
for ( int i = 1 ; i < n ; i ++ ) {
int w = f [ i - 1 ];
while ( w >= 0 && s [ w + 1 ] != s [ i ]) {
w = f [ w ];
}
f [ i ] = w + 1 ;
}
return f ;
}
例題
CSES - String Matching
給一個長度 \(n\) 的字串 \(S\) 和一個長度 \(m\) 的字串 \(T\) ,問 \(T\) 在 \(S\) 內出現幾次
\(n,m\le 10^6\)
思路
new_str = target + "$" + str
f = build_f(new_str), 看幾個 f[i] = target.size()
fail link dp
CF 432 D. Prefixes and Suffixes
給一個字串 s,問對於每個 s 的共同前後綴,在 s 出現幾次
\(1\le |s| \le 10^5\)
思路
根據性質 2,也就是當 f(i) 出現時,f(f(i)) 也會出現,我們可以推得一個 dp 轉移 :
for (int i = n ~ 1)
cnt[i]++;
cnt[f[i]] += cnt[i]
CF 955 D. Scissors
給一個字串 s,你可以拿兩段 s 中長度為 k 的 substring,再將他們兩個拼起來。問有沒有可能使裡面包含 substring t,若有可能輸出兩段分別的開頭 index
\(2\le |t| \le 2\times k\le |s|\le 5\times 10^5\)
思路
令 pre[i]: t[1:i] 首次出現位置,存結尾位置,suf[i]: t[i:m] 最後出現位置,存開頭位置,我們就只要枚舉 i,看是否符合 pre[i] < suf[i] 即可
至於 pre, suf 要怎麼建立呢 ? 我們可以利用 kmp 看 s 的每一項與 t 的次長共同前後綴,然後再用 fail link dp 轉移。或是也可以用 rolling hash + two pointer
code
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
using namespace std ;
const int INF = 1e9 ;
vector < int > kmp ( string s ) { // 1-based string
int n = s . size ();
vector < int > f ( n , -1 );
for ( int i = 1 ; i < n ; i ++ ) {
int w = f [ i - 1 ];
while ( w >= 0 && s [ w + 1 ] != s [ i ]) {
w = f [ w ];
}
f [ i ] = w + 1 ;
}
return f ;
}
vector < int > make_pre ( int k , string s , string t ) {
int n = s . size ();
int m = t . size ();
auto f = kmp ( "$" + t + "$" + s ); // $aaaa$baabaab
//
if ( * max_element ( f . begin (), f . end ()) >= m ) { // 不用分兩段就是好的
cout << "Yes" << '\n' ;
int pos = max_element ( f . begin (), f . end ()) - f . begin ();
pos -= m + 1 ;
pos = max ( pos , 2 * k );
cout << pos - 2 * k + 1 << ' ' << pos - k + 1 << '\n' ;
exit ( 0 );
}
vector < int > pre ( m + 1 , INF ); // pre[i]: t[1:m] 第一次出現位置
for ( int i = m + 1 + k ; i < n + m + 2 ; i ++ ) {
int len = f [ i ];
pre [ len ] = min ( pre [ len ], i - m - 1 );
}
// fail link dp
// 長度i 的位置出現的地方,也同時會有長度 f[i] 出現
for ( int i = m ; i >= 1 ; i -- ) {
int j = f [ i ];
pre [ j ] = min ( pre [ j ], pre [ i ]);
}
return pre ;
}
int main () {
cin . tie ( 0 );
cin . sync_with_stdio ( 0 );
int n , m , k ;
string s ;
string t ;
cin >> n >> m >> k ;
cin >> s >> t ;
string rs ( s . rbegin (), s . rend ());
string rt ( t . rbegin (), t . rend ());
auto pre = make_pre ( k , s , t ); // pre[i]: t[1:i] 首次出現位置,存結尾位置
auto suf = make_pre ( k , rs , rt ); // suf[i]: t[i:m] 最後出現位置,存開頭位置
reverse ( suf . begin (), suf . end ());
for ( int i = 0 ; i <= m ; i ++ ) {
suf [ i ] = n + 1 - suf [ i ];
}
/*
cout << s << '\n';
cout << t << '\n';
for (int i = 0; i <= m; i++) {
cout << pre[i] << '\t' << suf[i] << '\n';
}
*/
bool good = false ;
for ( int i = 0 ; i <= m ; i ++ ) {
if ( i <= k && m - i <= k && pre [ i ] < suf [ i ]) {
cout << "Yes" << '\n' ;
cout << pre [ i ] - k + 1 << ' ' << suf [ i ] << '\n' ;
/*
cout << s.substr(pre[i] - k, k) << ' ' << s.substr(suf[i] - 1, k)
<< '\n';
*/
good = true ;
break ;
}
}
if ( good == false ) {
cout << "No" << '\n' ;
}
return 0 ;
}
kmp+dp
利用 dp(i, j),代表對於 s[1, i],次長共同前後綴為 t[1, j]。
轉移的部分 dp(i + 1, k) = dp(i, j) + ...,其中 k 就是 s[1, i+1] 的次長共同前後綴會是 t[1, k]。這部分可以利用 kmp 預處理 t 的失敗函數,轉移過程直接從 w = j 去找次長共同前後綴(詳見 CSES - Required Substring 的代碼)
CSES - Required Substring
給一個長度 \(m\) 的字串 t,問有幾個長度為 \(n\) 的字串 s 包含 substring t
\(1\le n\le 100,1\le m\le 100,\) s, t 由字母 A–Z 組成
思路
dp(i, j) = s[1, i] 的最長匹配為 t[1, j] 的方案數
dp(i + 1, k) += dp(i, j) 其中 t[1, k] 為 s[1, i] + c 後的最長匹配
答案為 total - dp(n, 0~(m - 1))
code
#include <bits/stdc++.h>
#define int long long
#define pb push_back
#define mk make_pair
#define F first
#define S second
#define ALL(x) x.begin(), x.end()
using namespace std ;
using pii = pair < int , int > ;
const int INF = 2e18 ;
const int M = 1e9 + 7 ;
int fpow ( int a , int b ) {
int ret = 1 ;
a %= M ;
while ( b != 0 ) {
if ( b & 1 ) ret = ( ret * a ) % M ;
a = ( a * a ) % M ;
b >>= 1 ;
}
return ret ;
}
vector < int > kmp ( string s ) {
int n = s . size ();
vector < int > f ( n , -1 );
for ( int i = 1 ; i < n ; i ++ ) {
int w = f [ i - 1 ];
while ( w >= 0 && s [ w + 1 ] != s [ i ]) {
w = f [ w ];
}
f [ i ] = w + 1 ;
}
return f ;
}
signed main () {
int n ;
cin >> n ;
string t ;
cin >> t ;
int m = t . size ();
t = "$" + t ;
vector < int > f = kmp ( t );
vector < vector < int >> dp ( n + 1 , vector < int > ( n + 1 , 0 ));
dp [ 0 ][ 0 ] = 1 ;
for ( int i = 0 ; i < n ; i ++ ) {
for ( int j = 0 ; j < m ; j ++ ) {
for ( int k = 0 ; k < 26 ; k ++ ) {
int w = j ;
while ( w >= 0 && t [ w + 1 ] != ( 'A' + k )) w = f [ w ];
w += 1 ;
dp [ i + 1 ][ w ] = ( dp [ i + 1 ][ w ] + dp [ i ][ j ]) % M ;
}
}
}
int sum = 0 ;
for ( int j = 0 ; j < m ; j ++ ) {
sum = ( sum + dp [ n ][ j ]) % M ;
}
// total - fail
cout << ( fpow ( 26 , n ) - sum + M ) % M << endl ;
}
2015 北市賽 pD. 猜謎遊戲 (Guess)
給字串 s 跟 t,問最少要刪幾個 s 中的字元,讓 t 不是 s 的 substring
\(|s| \le 100,|t| \le 1000,\) s, t 由字元 A 與 B 組成
思路
code
#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define pb push_back
#define mk make_pair
#define F first
#define S second
#define ALL(x) x.begin(), x.end()
using namespace std ;
const int INF = 2e18 ;
vector < int > KMP ( string s ) { // 1-based string
int n = s . size () - 1 ;
vector < int > F ( n + 1 , -1 );
for ( int i = 1 ; i <= n ; i ++ ) {
int w = F [ i - 1 ];
while ( w >= 0 && s [ w + 1 ] != s [ i ]) w = F [ w ];
F [ i ] = w + 1 ;
}
return F ;
}
signed main () {
string s , t ;
cin >> t >> s ;
int n = s . size ();
int m = t . size ();
t = "$" + t ;
s = "$" + s ;
vector < int > f = KMP ( t );
vector < vector < int >> dp ( n + 1 , vector < int > ( m + 1 , INF ));
dp [ 0 ][ 0 ] = 0 ;
for ( int i = 0 ; i < n ; i ++ ) {
for ( int j = 0 ; j < m ; j ++ ) {
dp [ i + 1 ][ j ] = min ( dp [ i + 1 ][ j ], dp [ i ][ j ] + 1 );
int w = j ;
while ( w >= 0 && t [ w + 1 ] != s [ i + 1 ]) w = f [ w ];
w += 1 ;
dp [ i + 1 ][ w ] = min ( dp [ i + 1 ][ w ], dp [ i ][ j ]);
}
}
cout << * min_element ( dp [ n ]. begin (), dp [ n ]. begin () + m ) << '\n' ;
}
CF 1303 E. Erase Subsequences
給 s, t,至多能從 s 中選出兩個 subsequence,問是否能組成 t
\(|t| \le |s| \le 400\)
思路
我們很明顯可以想出來一步:枚舉 t 在哪裡拆開,然後將 t 轉化為 t1 + t2,再判斷 s 中能不能拆出 t1, t2 就好了。 那麼問題轉化為了 s 中能不能拆出來的問題了。發現可能需要 dp 解。
很顯然的狀態是: dp(i, j, k) 表示 s 的前 i 位能不能拆出 t1 的前 j 位和 t2 的前 k 位,因為狀態量過大我們就考慮優化這個狀態,dp(i, j) 表示 s 的前 i 位拆出 t1 的前 j 位,最多再拆出 t2 的前多少位。
當 s[i + 1] = t1[j + 1] 時,dp(i, j) → dp(i + 1, j + 1)
當 s[i + 1] = t2[dp(i, j) + 1] 時,dp(i, j) + 1 → dp(i + 1, j)
任何情況下,dp(i, j) → dp(i + 1, j)
複雜度 O(n) * O(n ^ 2) = O(n ^ 3)
code
#include <bits/stdc++.h>
using namespace std ;
const int MAXN = 805 ;
int dp [ MAXN ][ MAXN ];
bool check ( string s , string t1 , string t2 ) {
memset ( dp , -1 , sizeof ( dp ));
dp [ 0 ][ 0 ] = 0 ;
s = "$" + s ;
t1 = "$" + t1 ;
t2 = "$" + t2 ;
for ( int i = 0 ; i < s . size (); i ++ ) {
for ( int j = 0 ; j < t1 . size (); j ++ ) {
if ( dp [ i ][ j ] == -1 ) continue ;
dp [ i + 1 ][ j ] = max ( dp [ i + 1 ][ j ], dp [ i ][ j ]);
if ( s [ i + 1 ] == t1 [ j + 1 ]) dp [ i + 1 ][ j + 1 ] = max ( dp [ i + 1 ][ j + 1 ], dp [ i ][ j ]);
if ( s [ i + 1 ] == t2 [ dp [ i ][ j ] + 1 ]) dp [ i + 1 ][ j ] = max ( dp [ i + 1 ][ j ], dp [ i ][ j ] + 1 );
}
}
if ( dp [ s . size () - 1 ][ t1 . size () - 1 ] == t2 . size () - 1 ) {
return true ;
}
return false ;
}
bool solve ( string s , string t ) {
for ( int i = 0 ; i <= t . size (); i ++ ) {
string t1 , t2 ;
for ( int j = 0 ; j < i ; j ++ ) {
t1 = t1 + t [ j ];
}
for ( int j = i ; j < t . size (); j ++ ) {
t2 = t2 + t [ j ];
}
if ( check ( s , t1 , t2 )) {
return true ;
}
}
return false ;
}
int main () {
int n ;
cin >> n ;
while ( n -- ) {
string s , t ;
cin >> s >> t ;
if ( solve ( s , t )) {
printf ( "YES \n " );
} else {
printf ( "NO \n " );
}
}
return 0 ;
}
資料