字串的基本問題,大部分都是實作問題
熟悉自己使用的語言工具以及技巧相當重要
不同程式語言的解法可以相差很多...
string s = "abcd";
// 反轉 "dcba"
reverse(s.begin(), s.end());
// 修改 "dcca"
s[2] = 'c';
s = "abcd"
# 反轉 "dcba"
s = s[::-1]
# 修改 "dcca"
s = s[:2]+'c'+s[3:]
# 常用方法
x = list(s)
x[2] = 'c'
s = ''.join(x)
Example
時間 02:23 = 兩個數字 + 冒號 + 兩個數字
日期 2/3 = 一或兩個數字 + 斜線 + 一或兩個數字
note : 這部分等於 "Formal Language" 這門選修課教的正規語言
Example
兩個數字 + 冒號 + 兩個數字
一或兩個數字 + 斜線 + 一或兩個數字
Example
數字 = 0 或 1 或 2 或 3 ... 或 9
Example
兩位數字 = 數字 + 數字
Example
\(\{a\}^*=\{\lambda, a, aa, aaa, \cdots\}\)
一些數字 = 數字* = \(\{0\sim 9\}^*\) = \(\{0,1,\cdots ,9, 00,01, \cdots\}\)
1. 寫對應的自動機 (根據規則,暴力解析)
2. 利用正規表達式,套函數處理 ( regex )
(有點有邊 : 可以出成圖論問題)
讀一個整數
讀一個字元
讀一個整數
#include <bits/stdc++.h>
using namespace std;
char buf[256];
int idx;
int getInt() {
int res = 0;
while (isdigit(buf[idx])) {
res = res * 10 + (buf[idx] - '0');
idx++;
}
return res;
}
int getChar() {
return buf[idx++];
}
int main() {
while (cin.getline(buf, 256)) {
idx = 0;
int hh = getInt();
int c = getChar();
int mm = getInt();
cout << "now : " << hh << ' ' << mm << endl;
}
}
1 | 2 | + |
---|
#include <bits/stdc++.h>
using namespace std;
int main() {
int hh, mm;
while (~scanf("%d:%d", &hh, &mm)) {
cout << "now : " << hh << ' ' << mm << endl;
}
}
char str[] = "12:30";
int hh, mm;
// 用法與 scanf 一樣
sscanf(str, "%d:%d", &hh, &mm);
int main() {
char str[] = "123 456 789";
char *ptr = str;
int val;
while ( sscanf(ptr, "%d", &val) == 1) {
cout << val << '\n';
// move ptr to next integer
ptr++; // space
while (isdigit(ptr[0])) ptr++;
}
}
輸出 :
123 456 789
一個討厭 strtok 的人,寫的字串分割
該程式的運行時間複雜度為何
int main() {
char str[] = "123 456 789";
char *ptr = str;
int val;
while (sscanf(ptr, "%d", &val) == 1) {
cout << val << '\n';
// move ptr to next integer
ptr++; // space
while (isdigit(ptr[0])) ptr++;
}
}
一個討厭 strtok 的人,寫的字串分割
該程式的運行時間複雜度為何
因為紅色部分,最多只會執行 str 那麼多的長度,所以整體下來是 \(O(n)\)
int main() {
char str[] = "123 456 789";
char *ptr = str;
while (sscanf(str, "%d", &val) == 1);
}
int
sscanf(const char *ibuf, const char *fmt, ...) {
// ...
ret = vsscanf(ibuf, fmt, ap);
// ...
}
int
vsscanf(const char *inp, /**/) {
// ...
int inr = strlen(inp);
// ...
}
int main() {
char str[] = "123 456 789";
char *ptr = str;
while ( sscanf(str, "%d", &val) == 1);
}
__sscanf (const char *s, const char *format, ...)
{
FILE *f = _IO_strfile_read (&sf, s);
}
_IO_strfile_read (_IO_strfile *sf, const char *string)
{
_IO_str_init_static_internal (sf, (char*)string, 0, NULL);
}
_IO_str_init_static_internal (_IO_strfile *sf, char *ptr /**/)
{
end = __rawmemchr (ptr, '\0');
}
RAWMEMCHR (const void *s, int c) {
// ...
return (char *)s + strlen (s);
}
int main() {
char str[] = "123 456 789";
char *ptr = str;
int val;
while ( sscanf(ptr, "%d", &val) == 1) {
cout << val << '\n';
// move ptr to next integer
ptr++; // space
while (isdigit(ptr[0])) ptr++;
}
}
當你以為你漂亮均攤的時候
sscanf 再搞你
所有常用的編譯器附帶的 sscanf 都會對 ptr 做 strlen
不管有沒有用到所有字元
自從去年發生 GTA 5 優化事件後,越來越多人知道這個小知識
Good Luck!
#include <bits/stdc++.h>
#include <regex>
using namespace std;
int main() {
int hh = 0, mm = 0;
string buf;
regex rule("([0-9]*):([0-9]*)");
smatch res;
while (getline(cin, buf)) {
regex_match(buf, res, rule);
hh = stoi(res[1]);
mm = stoi(res[2]);
cout << "now : " << hh << ' ' << mm << endl;
}
}
配對時間
時間 = 整數 + : + 整數
regex rule("([0-9]*):([0-9]*)");
整數是很多個 0-9 組成的東西 ,使用 [0-9]* 來表示
regex rule("([0-9]*):([0-9]*)");
smatch res;
regex_match(buf, res, rule);
// res[0] = "12:30"
hh = stoi(res[1]); // 12
mm = stoi(res[2]); // 30
res 是一個 smatch 的物件 (可以把它當作 vector 看)
res[0] 永遠存放被匹配的整體 (整個字串)
自訂的括號要從 1 開始拿 !
一個文章裡面有幾個單字 ?
單字 = 連續的英文字母
用任何你喜歡的方法寫吧 !
暴力 | 正規表達式 | scanf | |
---|---|---|---|
簡潔度 | 看自己能力 | 簡單,但要背很多語法 | 簡單通用,但遇到特殊格式無法處理 |
速度 | 看自己能力 | O(n) 常數稍大 | 跟 IO 一樣 |
#include <regex>
regex rule("([0-9]*):([0-9]*)");
smatch res;
regex_match(buf, res, rule);
// res[0] = "12:30"
hh = stoi(res[1]); // 12
mm = stoi(res[2]); // 30
import re
rule = re.compile(r'([0-9]*):([0-9]*)')
m = rule.match("12:30")
# m.group(0)= "12:30"
hh = int(m.group(1)); // 12
mm = int(m.group(2)); // 30
字典樹
如同一般的數字一樣,有些問題需要再一群字串中查詢特定的字串存不存在
但是不同於數字,數字默認可以 \(O(1)\) 比對資料
然而字串比較需要逐字元比對,需要花費更多的時間
因此需要設計一個更好的資料結構來加速比較的時間
\(\{"ant", "cat", "can"\}\)
a
c
a
n
t
\(\{"ant", "cat", "can","c"\}\)
a
c
a
n
t
struct node {
int hit = 0;
node *next[26] = {}; // a-z
};
using pnode = node *;
pnode insert(const char *s, pnode root) {
if (!root)
root = new node;
if (s[0] == '\0') {
root->hit++;
} else {
root->next[*s-'a']
= insert(s+1, root->next[*s-'a']);
}
return root;
}
一般而言,為了省空間,會把字母投射到 0~26 之類的位置。
設 s 是 const char * 的變數
s+1 : 取 s 去除第一個字元的後綴 (s[1:])
*s : 等於 s[0]
如果對二重指標熟的話,程式可以更簡單
void insert(const char *s, pnode *root) {
if (!*root)
*root = new node;
if (s[0] == '\0') {
(*root)->hit++;
} else {
insert(s+1, &(*root)->next[*s-'a']);
}
}
T=apple, S=app
Trie 其實很 月半... 空間用量要小心
#include <bits/stdc++.h>
using namespace std;
const int mod = 1'000'000'007;
struct node {
int hit = 0;
node* next[26] = {}; // a-z
};
using pnode = node *;
pnode insert(const char *s, pnode root) {
if (!root)
root = new node;
if (s[0] == '\0') {
root->hit++;
} else {
root->next[*s-'a'] = insert(s+1, root->next[*s-'a']);
}
return root;
}
int main() {
int k;
pnode root = new node;
string base, tmp;
cin >> base;
cin >> k;
while (k--) {
cin >> tmp;
root = insert(tmp.data(), root);
}
vector<int> dp(base.size() + 1, 0);
dp[0] = 1;
for (size_t i=1 ; i<=base.size() ; ++i) {
int len = 0;
pnode ptr = root;
const char *str = base.c_str() + (i-1);
while (*str) {
ptr = ptr->next[*str-'a'];
len++;
if (!ptr) break;
if (ptr->hit) {
dp[i-1+len] += dp[i-1];
dp[i-1+len] %= mod;
}
str++;
}
}
cout << dp.back() << '\n';
}
HDU4825
給一個數字集合,詢問數字 s 與集合內的哪一個數字 xor 起來最大
洛谷P4551
問一個有權重的樹,所有路徑中,權重 xor 最大值為何
KMP Algorithm
ex. 哪一些字串包含 "apple"
pineapple
application
thisisanapplepie
#include <bits/stdc++.h>
using namespace std;
int main() {
string T = "This is a string with an apple.";
string S = "apple";
if (auto res = T.find(S); res != string::npos) {
cout << "found " << S << " at " << res << '\n';
}
char Tc[] = "This is C-style string with an Egg.";
char Sc[] = "Egg";
if (auto res = strstr(Tc, Sc); res != nullptr) {
cout << "found " << Sc << " at " << res - Tc << '\n';
}
}
複雜度 : 規格不保證
最簡單方法 \(O(|T||S|)\)
// O(S*T)
for (int i=0;i<TSize-SSize+1 ++i)
if (strncmp(S, T, SSize) == 0)
return true;
return false;
/* Fast strstr algorithm with guaranteed linear-time performance.
Small needles up to size 3 use a dedicated linear search. Longer needles
up to size 256 use a novel modified Horspool algorithm. It hashes pairs
of characters to quickly skip past mismatches. The main search loop only
exits if the last 2 characters match, avoiding unnecessary calls to memcmp
and allowing for a larger skip if there is no match. A self-adapting
filtering check is used to quickly detect mismatches in long needles.
By limiting the needle length to 256, the shift table can be reduced to 8
bits per entry, lowering preprocessing overhead and minimizing cache effects.
The limit also implies worst-case performance is linear.
Needles larger than 256 characters use the linear-time Two-Way algorithm. */
char *
STRSTR (const char *haystack, const char *needle)
while (__len >= __n)
{
// Find the first occurrence of __elem0:
__first = traits_type::find(__first, __len - __n + 1, __elem0);
if (!__first)
return npos;
// Compare the full strings from the first occurrence of __elem0.
// We already know that __first[0] == __s[0] but compare them again
// anyway because __s is probably aligned, which helps memcmp.
if (traits_type::compare(__first, __s, __n) == 0)
return __first - __data;
__len = __last - ++__first;
}
return npos;
}
// O(S*T)
for (int i=0;i<TSize-SSize+1;++i)
if (strncmp(S, T, SSize) == 0)
return true;
return false;
線性匹配演算法
Knuth–Morris–Pratt Algorithm
A
P
P
L
E
ex. 哪一些字串包含 "apple"
pineapple
application
thisisanapplepie
The others (failed path)
\(\phi\)
bool test() {
char S[] = "apple";
char T[] = "thisisanapplepie";
int hit = 0;
int succ = strlen(S);
for (char c : T) {
if (c == S[hit]) hit++;
else hit = 0;
if (hit == succ) return true;
}
return false;
}
一個 字串 直直比過去就好 ?
bool test() {
char S[] = "apple";
char T[] = "thisisanapplepie";
int hit = 0;
int succ = strlen(S);
for (char c : T) {
if (c == S[hit]) hit++;
else hit = 0;
if (hit == succ) return true;
}
return false;
}
P
P
A
P
\(\phi\)
PPAPMAN
PPPAPMAN (失敗 !)
P | P | P | A | P | M |
P | P | A | P |
原來的方法遇到失敗時,直接重頭來過
漏掉了 "存在其他位置的配對依然正確" 的可能性
for (char c : T) {
if (c == S[hit]) hit++;
else hit = 0; // 不應該總是歸 0 !!
if (hit == succ) return true;
}
P | P | A | P |
P | P | A | P |
abaaba
abaaba............
abaabc
S:
T:
如果 \(R\) 中,存在一個不等於 \(R\) 的前綴等於後綴
abaab
abaab
abaaba............
abaabc
S:
T:
abaabc
把 \(S\) 往後移動 \(|R|\) 的距離也有機會產生匹配
abaaba............
abaabc
S:
T:
abaabc
一個字串可能有很多共同前後綴
abaaba?
abaaba
abaaba
為了要避免遺漏可能性,我們只能往前移動最少的步伐
也就是使用次長前後綴作為往前移動的根據
在發生錯誤返回跳回之前,我們希望可以檢查
當前 "已配對的部分" 是否能 "繼續利用"。
ABCDXXXABCD?
ABCDXXXABCD?
如果已配對的字串存在一個 開頭 等於 結尾 的部分
發生配對錯誤時,應該找到 次長的 開頭 等於 結尾 的部分來重新嘗試
最長的 開頭 = 結尾 就是字串本身
ABCDXXXABCDX
定義 \(dp[n]\) 表示:
字串 \(s_0s_1\cdots s_n\) 的次長前後綴 終點位置
此外定義 \(dp[0]=-1\),表示答案不存在
定義 \(dp[n]\) 表示:
字串 \(s_0s_1\cdots s_n\) 的次長前後綴 終點位置
也就是
字串 \(s_0s_1\cdots s_n\) 的開頭 \(dp[n]+1\) 個字與結尾 \(dp[n]+1\) 個字一樣
此外定義 \(dp[0]=-1\),表示答案不存在
0123456789
ABCDXXXABC
定義 \(dp[n]\) 表示:
字串 \(s_0s_1\cdots s_n\) 的開頭 \(dp[n]+1\) 個字與結尾 \(dp[n]+1\) 個字一樣
\(dp[9]=2\)
因為紅字處是一樣的
\(dp[3]=-1\)
不存在答案
此外定義 \(dp[0]=-1\),表示答案不存在
計算 \(dp[n]\) 有 \(3\) 種可能
01234567890
ABCXXXXABZA
1. 如果 \(dp[n-1]=-1\)
\(\Rightarrow\) 直接檢查 \(s[n]\) 與 \(s[0]\) 是否一樣即可
\(dp[9] = -1\)
\(dp[10]\) 直接比較字串開頭
計算 \(dp[n]\) 有 \(3\) 種可能
01234567890
ABCDXXXABCD
2. 如果 \(s[n]\) 等於 \(s[dp[n-1]+1] \)
\(\Rightarrow dp[n]=dp[n-1]+1\)
\(dp[9] = 2\) (紅橘)
\(dp[10]=dp[9]\) + 綠色
計算 \(dp[n]\) 有 \(3\) 種可能
01234567890
ABADXXXABAB
3. 如果 \(s[n]\) 不等於 \(s[dp[n-1]+1] \)
\(\Rightarrow\) 透過 \(s[0:dp[n-1]]\) 的次長前後綴繼續檢查
\(dp[9] = 2\)
\(dp[2] = 0\)
ABCABD.....ABCABA
\(dp[n-1] = 4\)
ABCAB
\(dp[4] = 2\)
橘色 = 橘色,紅色 = 紅色
ABCABD.....ABCABA
ABCABD.....ABCABA
如果比較 \(s[dp[n-1]+1]\) 失敗了
就遞迴比較 \(s[dp[dp[n-1]]+1]\)
void build(const char *s) {
dp[0] = -1;
for (int i=1; s[i] ; ++i) {
int len = dp[i-1];
while (last != -1) {
if (s[i] == s[len+1]) break;
else len = dp[len];
}
if (s[i] == s[len+1]) dp[i] = len + 1;
else dp[i] = -1;
}
}
len 表示目前已知最長的次長前後綴範圍
void build(const char *s) {
dp[0] = -1;
for (int i=1; s[i] ; ++i) {
int len = dp[i-1];
while (len != -1) {
if (s[i] == s[len+1]) break;
else len = dp[len];
}
if (s[i] == s[len+1]) dp[i] = len + 1;
else dp[i] = -1;
}
}
上程式的時間複雜度為和 ?
for (int i=1; s[i] ; ++i) {
int len = dp[i-1];
while (len != -1) {
if (s[i] == s[len+1]) break;
else len = dp[len];
}
if (s[i] == s[len+1]) dp[i] = len + 1;
else dp[i] = -1;
}
可以發現,在第 i 次迴圈
while 的執行次數不超過 dp[i-1] 次
for (int i=1; s[i] ; ++i) {
int len = dp[i-1];
while (len != -1) {
if (s[i] == s[len+1]) break;
else len = dp[len];
}
if (s[i] == s[len+1]) dp[i] = len + 1;
else dp[i] = -1;
}
可以發現,在第 i 次迴圈
while 的執行次數不超過 dp[i-1] 次
while 每執行一次,len 至少要 -1
for (int i=1; s[i] ; ++i) {
int len = dp[i-1];
while (len != -1) {
if (s[i] == s[len+1]) break;
else len = dp[len];
}
if (s[i] == s[len+1]) dp[i] = len + 1;
else dp[i] = -1;
}
可以發現,在第 i 次迴圈
while 的執行次數不超過 dp[i-1] 次
while 每執行一次,len 至少要 -1
len 最小只能是 -1
for (int i=1; s[i] ; ++i) {
int len = dp[i-1];
while (len != -1) {
if (s[i] == s[len+1]) break;
else len = dp[len];
}
if (s[i] == s[len+1]) dp[i] = len + 1;
else dp[i] = -1;
}
可以發現,在第 i 次迴圈
while 的執行次數不超過 dp[i-1] 次
while 每執行一次,len 至少要 -1
len 最小只能是 -1
len 只有在第 9 行會增加
for (int i=1; s[i] ; ++i) {
int len = dp[i-1];
while (len != -1) {
if (s[i] == s[len+1]) break;
else len = dp[len];
}
if (s[i] == s[len+1]) dp[i] = len + 1;
else dp[i] = -1;
}
可以發現,在第 i 次迴圈
while 的執行次數不超過 dp[i-1] 次
while 每執行一次,len 至少要 -1
len 最小只能是 -1
len 只有在第 9 行會增加
len 最多只會增加 \(O(n)\) 次
for (int i=1; s[i] ; ++i) {
int len = dp[i-1];
while (len != -1) {
if (s[i] == s[len+1]) break;
else len = dp[len];
}
if (s[i] == s[len+1]) dp[i] = len + 1;
else dp[i] = -1;
}
可以發現,在第 i 次迴圈
while 的執行次數不超過 dp[i-1] 次
while 每執行一次,len 至少要 -1
len 最小只能是 -1
len 只有在第 9 行會增加
len 最多只會增加 \(O(n)\) 次
因為扣除次數的不能比增加的次數多,
因此 while 的執行次數只能跟 len 增加的次數一樣多,
因此總複雜度為 \(O(n)\)
基本應用
CSES - String Matching
CSES - Finding Borders
找循環
CSES - Finding Periods
UVA 10298 - Power Strings
UVA 455 - Periodic Strings
Booth's algorithm (找 s 與 s 後綴 KMP dp 變化的關係)
CESE - Minimal Rotation
設一個字串 \(P\) 由 \(n\) 個 \(Q\) 串接
\(P=\underbrace{QQ\cdots QQ}_{n個}\)
那麼 P 的次長共同前後綴是
\(\underbrace{QQ\cdots QQ}_{n-1個}\)
把字串串接成 \(T' = S@T\),\(@\) 是不存在 S,T 中的一個字
然後做 DP ,如果過程中 DP 的值等於 \(|S|-1\) ,就找到了
ppap@pppapman
int dp[100];
void build(const char *s) {
dp[0] = -1;
for (int i=1; s[i] ; ++i) {
int last = dp[i-1];
while (last != -1) {
if (s[i] == s[last+1]) break;
else last = dp[last];
}
if (s[i] == s[last+1]) dp[i] = last + 1;
else dp[i] = -1;
}
}
bool find(const char *s, const char *t) {
int hit = -1;
for (int i=0; t[i] ; ++i) {
while (hit != -1) {
if (t[i] == s[hit+1]) break;
else hit = dp[hit];
}
if (t[i] == s[hit+1]) hit = hit + 1;
else hit = -1;
if (s[hit+1] == '\0') return true;
}
return false;
}
分開寫的版本,兩邊其實一樣,但找的那邊不用記錄 dp 的結果,直接放在 hit 。
Gusfield’s algorithm
\(Z[x]\) : 表示 \(s[x:]\) 與 \(s\) 共同前綴的長度
A | B | A | B | A | A | B |
---|---|---|---|---|---|---|
0 | 0 | 3 | 0 | 1 | 2 | 0 |
定義 \(Z[0] = 0\)
\(Z[2] = 3\) ,因為 \(s[2:] = \) ABAAB ,與整個字串 ABABAAB 開頭三個字一樣
然後做 Z algorithm ,如果過程中 Z algorithm 的值等於 \(|S|\) ,就找到了
這兩個演算法能做的事情差不多,但處理的方向不一樣,可以視需求來決定用哪一個
\(Z[x]\) : 表示 \(s[x:]\) 與 \(s\) 共同前綴的長度
A | B | A | B | A | A | B |
---|---|---|---|---|---|---|
0 | 0 | 3 | 0 | 1 | 2 | 0 |
定義 \(Z[0] = 0\)
Z Index 的演算法關鍵是 :
透過一對 L, R 來表示\(s[L, R]\) 字串開頭相同
A | B | A | B | A | A | B |
---|---|---|---|---|---|---|
0 | 0 | 3 | 0 | 1 | 2 | 0 |
L=2
R=4
Z Index 的演算法關鍵是 :
透過一對 L, R 來表示\(s[L, R]\) 字串開頭相同
那根據 \(i\) 是否在 \(L,R\) 之間,有 3 種可能
A | B | A | B | A | A | B |
---|---|---|---|---|---|---|
0 | 0 | 3 | 0 | 1 | 2 | 0 |
L=2
R=4
那這一個 \(L, R\) 一點用也沒有,暴力求答案 !
並且把 \(L, R\) 更新成暴力求出來的範圍
L=R=0;
for (int i=1; s[i];++i) {
if (R < i) {
while (s[z[i]] == s[i+z[i]])
z[i]++;
L = i;
R = i + z[i] - 1;
}
}
那 \(z[i]=z[i-L]\)
L
R
\(i\)
L
R
根據定義,紅色等於紅色
\(i\)
\(i-L\)
所以要算 \(Z[i]\),可以參考 \(Z[i-L]\)
那 \(z[i]=z[i-L]\)
L
R
根據定義,紅色等於紅色
\(i\)
\(i-L\)
所以要算 \(Z[i]\),可以參考 \(Z[i-L]\)
\(i+z[i-L]-1\)
根據 \(Z[i-L]\) 可以知道黃色的部分也相等
那 \(z[i]=z[i-L]\)
L
R
根據定義,紅色等於紅色
\(i\)
\(i-L\)
所以要算 \(Z[i]\),可以參考 \(Z[i-L]\)
\(i+z[i-L]-1\)
根據 \(Z[i-L]\) 可以知道黃色的部分也相等
如果黃色的最右邊 \(i+z[i-L]-1 < R\) 答案就能直接用了!
那 \(z[i]=z[i-L]\)
L
R
\(i\)
\(i-L\)
\(i+z[i-L]-1\)
因為黃色區塊一樣,因此前 \(R-i+1\) 個字已經判斷過了,不用重新判斷是否相等!
最後更新 L,R 的範圍 !
那暴力找 \(z[i]\) ,但是黃色區域不用檢查
void ZAlgo(const char *s) {
int L, R;
vector<int> z(strlen(s));
L=R=0;
for (int i=1; s[i]; ++i) {
if (R < i) {
z[i] = 0;
while (s[z[i]] == s[i+z[i]])
z[i]++;
L = i;
R = i + z[i] - 1;
} else if ( i + z[i-L] - 1 < R) {
z[i] = z[i-L];
} else {
z[i] = R - i + 1;
while (s[z[i]] == s[i+z[i]])
z[i]++;
L = i;
R = i + z[i] - 1;
}
}
}
可以發現 case 1,3 基本上是一樣的,只有起始點不一樣,所以可以合在一起寫 (只判斷case 2 )
void ZAlgo(const char *s) {
int L, R;
vector<int> z(strlen(s));
L=R=0;
for (int i=1; s[i]; ++i) {
if ( i <= R && i + z[i-L] - 1 < R) {
z[i] = z[i-L];
} else {
z[i] = max (0, R - i + 1);
while (s[z[i]] == s[i+z[i]])
z[i]++;
L = i;
R = i + z[i] - 1;
}
}
}
精簡版 Z Algorithm
此處運算有負數
R 或 i 的型態不可以是unsigned (ex. size_t)
void ZAlgo(const char *s) {
int L, R;
vector<int> z(strlen(s));
L=R=0;
for (int i=1; s[i]; ++i) {
if ( i <= R && i + z[i-L] - 1 < R) {
z[i] = z[i-L];
} else {
z[i] = max (0, R - i + 1);
while (s[z[i]] == s[i+z[i]])
z[i]++;
L = i;
R = i + z[i] - 1;
}
}
}
一樣有雙層迴圈,複雜度是多少呢 ?
void ZAlgo(const char *s) {
int L, R;
vector<int> z(strlen(s));
L=R=0;
for (int i=1; s[i]; ++i) {
if ( i <= R && i + z[i-L] - 1 < R) {
z[i] = z[i-L];
} else {
z[i] = max (0, R - i + 1);
while (s[z[i]] == s[i+z[i]])
z[i]++;
L = i;
R = i + z[i] - 1;
}
}
}
如果執行一次 while 迴圈,R 就會增加 1
R 最大只會是 \(n\)
因此 while 總共只會執行 \(O(n)\) 次
總時間複雜度是 \(O(n)\)
abac => #a#b#a#c#
雜湊
Raw
HashCode
Hash
Raw
HashCode
Hash
unordered_set<int> ust;
ust.insert(71);
ust.insert(22);
if (ust.find(22) == ust.end()) // true
cout << "22 is found\n";
ust.erase(71); // delete element
if (ust.find(71) == ust.end()) // false
cout << "71 is found\n";
可以在平均 O(1) 的時間
加入/刪除/調查元素
unordered_map<string, int> ump;
ump["one"] = 1;
ump["two"] = 2;
ump["three"] = 3;
//123
cout << ump["one"] << ump["two"] << ump["three"];
可以在平均 O(1) 的時間
加入/刪除/調查元素
struct custom_hash {
size_t operator()(uint64_t x) const {
// Add a random constant
const uint64_t FIXED_RANDOM = 71222;
return x + FIXED_RANDOM;
}
};
滾動雜湊
如果能先將字串透過萃取成一個 int , 那只需要 \(O(1\times\Sigma{|S|})\) 的時間
#define MAXN 1000000
#define prime_mod 1073676287
/*prime_mod 必須要是質數*/
typedef long long T;
char s[MAXN+5];
T h[MAXN+5];/*hash陣列*/
T h_base[MAXN+5];/*h_base[n]=(prime^n)%prime_mod*/
inline void hash_init(int len,T prime=0xdefaced){
h_base[0]=1;
for(int i=1;i<=len;++i){
h[i]=(h[i-1]*prime+s[i-1])%prime_mod;
h_base[i]=(h_base[i-1]*prime)%prime_mod;
}
}
Rabin fingerprint 具有滾動的性質,如果知道每一個前綴的 Hash,很容易可以推算中間任意一段的 Hash
\(S'=a_La_{L+1}a_{L+2}\dots a_R\)
求\(H(S',x)=a_Lx^{R-L}+a_{L+1}x^{R-L-1}+\dots+a_Rx^0\)
\(S'=a_La_{L+1}a_{L+2}\dots a_R\)
求\(H(S',x)=a_Lx^{R-L}+a_{L+1}x^{R-L-1}+\dots+a_Rx^0\)
\(S_R=a_1a_2a_3\dots a_R\)
\(H(S_R,x)=a_1x^{R-1}+a_2x^{R-2}+\dots+a_{L-1}x^{R-L+1}+a_{L}x^{R-L}+\dots+a_Rx^0\)
\(S_{L-1}=a_1a_2a_3\dots a_{L-1}\)
\(H(S_{L-1},x)=a_1x^{L-2}+a_2x^{L-3}+\dots+a_{L-1}x^{0}\)
\(S'=a_La_{L+1}a_{L+2}\dots a_R\)
求\(H(S',x)=a_Lx^{R-L}+a_{L+1}x^{R-L-1}+\dots+a_Rx^0\)
\(S_R=a_1a_2a_3\dots a_R\)
\(H(S_R,x)=a_1x^{R-1}+a_2x^{R-2}+\dots+a_{L-1}x^{R-L+1}+a_{L}x^{R-L}+\dots+a_Rx^0\)
\(S_{L-1}=a_1a_2a_3\dots a_{L-1}\)
\(H(S_{L-1},x)=a_1x^{L-2}+a_2x^{L-3}+\dots+a_{L-1}x^{0}\)
\(H(S',x)=H(S_R,x)-x^{R-L+1}H(S_{L-1},x)\)
inline T get_hash(int l,int r){
return (h[r+1]
-(h[l]*h_base[r-l+1])
%prime_mod+prime_mod)
%prime_mod;
}
有一本書,當作是字串 S
想問一個單字 T 在這本書裡面出現幾次
KMP Algorithm \(O(|S|+|T|)\)
剛剛的那一個
Rabin–Karp Algorithm \(O(|S|+|T|)\)
唬爛法
計算 S 所有前綴的雜湊 : \(O(S)\)
計算 T 的雜湊 : \(O(T)\)
檢查 S 中所有長度為 \(|T|\) 的字串,雜湊是否與 T 相同 : \(O(S)\)
雜湊可以把字串比對的複雜度從 \(O(n)\) 下降到 \(O(1)\)
讓暴力演算法複雜度下降
\(O(n)\) 到 \(O(\log n)\)
看起來可以 AC
人品太差
因為雜湊碰撞導致
WA
若 \(H(S,x)\) 中 \(x\) 夠大,就相當於把 \(S\) 編碼程 \(x\) 進位的整數
若 \(S\) 輸入是隨機的,且 \(x,p\) 互質
那碰撞率為 \(1/p\)
換句話說只跟取的餘數有關係
如果進行 \(k\) 次比較,希望有 90 % 的正確率, \(p\) 要多大?
如果進行 \(k\) 次比較,希望有 90 % 的正確率, \(p\) 要多大?
\((1-1/p)^k\geq 0.9\)
\(1-1/p\geq 0.9^{1/k}\)
\(p\geq 1/(1- 0.9^{1/k})\)
int 不夠大怎麼辦? 算多個雜湊一起比較!
根據有人推的近似公式 (待補證明)
\(p\geq 5 k^2\)
\(p\) 請勿選擇 \(2^n-1\) 的質數,容易被構造特殊測資
最常見 : INT_MAX \(2^{31}-1\)