計蒜客 - 蒜廠工作手冊
蒜廠工作手冊,你聽說過麼?蒜頭君把蒜廠工作手冊全部摘抄了下來並把它變成了一個長度不超過 的字符串 ,蒜頭君還有一個包含 個單詞的列表,列表裏的 個單詞記爲 。他希望從 中刪除這些單詞。
蒜頭君每次在 中找到第一個出現的列表中的單詞,然後從 中刪除這個單詞。他重複這個操作直到 中沒有列表裏的單詞爲止。需要注意的是刪除一個單詞後,後面的緊跟着的字符和前面的字符連接起來可能再次出現列表中出現的單詞。並且蒜頭君注意到列表中的單詞不會出現一個單詞是另一個單詞子串的情況。
請你幫助蒜頭君輸出刪除後的 。
輸入格式
第一行輸入一個字符串 。
第二行輸入一個整數 。
接下來的 行,每行輸入一個字符串,第 行的字符串是 。
個字符串的長度和小於 。注意:輸入的字符串僅包含小寫字母。
oorjskorzorzzooorzrzrzr
2
orz
jsk
輸出格式
答案輸出一行,輸出操作後的 。
or
這道題的 main
函數非常簡單,首先初始化 AC 自動機,然後讀入所有的單詞,insert()
,最後 build()
即可。查找單詞自然有 search()
來完成。
int main() {
// 構造 AC 自動機
auto ac = new AC_Automaton();
ac->clear();
// 給定的文章,下標從 1 開始
char* t = (char*)malloc(sizeof(char) * MAXN);
scanf("%s", t + 1);
int n;
scanf("%d", &n);
// 讀入單詞,加入前綴樹
char* s = (char*)malloc(sizeof(char) * MAXN);
for (int i = 0; i < n; i++) {
// 字符串下標從 1 開始
scanf("%s", s + 1);
ac->insert(s);
}
// 根據前綴樹中的單詞構造失敗指針,即構造字典
ac->build();
// TODO answer
return 0;
}
現在的問題是,如何模擬字符串被刪除的過程。
不可能每次都是真的去操作一個字符串,移動、拼接,也不可能每次生成一個新的字符串。
有一個技巧非常常用,我們可以利用鍵盤上退格鍵(Backspace)的原理,來用一個棧來模擬這件事情。
比如說我現在在打字,照着工作手冊,一個字母一個字母地輸入,第一句話是 HelloWorld
,我就先輸入一個 H
,然後是 e
,然後是 l
……這個過程就是入棧的過程,每次往棧中壓一個字母。
突然我輸入到 o
的時候,此時棧頂元素是 o
,棧中已經有的字符串是 Hello
,我發現 llo
是單詞手冊中需要我刪除的一個單詞,這是第一次出現這個單詞,所以我在第一時間發現了這個問題(輸入到 o
之前我並不知道這件事),所以我要把這個單詞刪除,我按了 3 次退格鍵,相當於做了 3 次出棧操作,此時棧頂元素變成 e
。
我繼續往後輸入,輸入 W
,然後發現 eW
也是一個需要刪除的單詞,所以我又做了 2 次出棧。
從上面的過程可以發現,這個棧完全滿足題目的要求,刪除字符串後,後面的緊跟着的字符和前面的字符連接起來可能再次出現列表中出現的單詞,也可以順利解決。
// ans 爲字符串,cursor 爲當前光標的位置
char* ans = (char*)malloc(sizeof(char) * MAXN);
int cursor = 0;
// stack 爲模擬的棧,其每一個元素記錄一個前綴樹結點的編號,top 是棧頂指針
int* stack = (int*)_malloca(sizeof(int) * MAXN);
int top = 0;
// 初始從前綴樹根節點開始
int p = 0;
// 注意字符串下標從 1 開始
int l = strlen(ch + 1);
for (int i = 0; i < l; i++) {
ans[cursor] = ch[i]; // 輸入當前字符
cursor++; // 光標移動一位
p = child[p][ch[i] - 'a']; // 前綴樹節點往後移動一個,到該字符
stack[top] = p; // 保存進棧,以便後面處理
top++; // 棧頂指針
if (?) { // TODO
int length = ?; // 需要刪除的單詞的長度 // TODO
cursor -= length; // 回退那麼多個長度
top -= length; // 回退那麼多個長度
p = stack[top - 1]; // 取出當前的棧頂元素
}
}
上面這段代碼就是模擬棧的過程,應該很好理解。
接下來就要考慮,就是我們拿到棧中的結點 p
,可以幹什麼,可以利用哪些東西,來求出那個 length
。
只需要把這個問號填了,這道題就解決了。
首先我們需要知道每一個單詞的長度,即對於結點 p
,len[p]
記錄了字典中到這個結點爲止的單詞的長度。這個只需要在 insert()
的時候記錄一下就可以了。
另外,由於本題不像上一題一樣還需要記錄個數,所以可以把 sta[]
當做 boolean[]
來用,直接設置爲 true
,不做 sta[p]++
了。
那麼,這裏的 if
就可以是:如果 sta[p]
是 true
,說明字典中存在這個單詞,要進行刪除操作,於是模擬 Backspace 的過程,將光標和棧頂指針後移。
if (sta[p]) { // sta[p] 表示以結點 p 爲路徑的字符串是不是存在,如果存在,那麼按照題目意思就要刪除它,模擬 Backspace 的過程
int length = len[p]; // 需要刪除的單詞的長度
cursor -= length; // 回退那麼多個長度
top -= length; // 回退那麼多個長度
p = stack[top - 1]; // 取出當前的棧頂元素
}
只需要把這段 if
貼在上面那段代碼的 //TODO
位置,題目就完成了。
其他 AC 自動機的函數直接用模板,不需要修改,最後輸出 printf("%s\n", ac->solve(t))
。
#include <bits/stdc++.h>
const int MAXC = 26;
const int MAXN = 100007;
using namespace std;
int child[MAXN][MAXC], fail[MAXN], sta[MAXN], Q[MAXN];
int tot;
int len[MAXN];
/**
* AC 自動機
*/
struct AC_Automaton {
/**
* 清空
*/
void clear() {
memset(child, 255, sizeof(child));
memset(fail, 0, sizeof(fail));
tot = 0;
memset(sta, 0, sizeof(sta));
}
/**
* 插入單詞
* @param ch 單詞,該單詞下標從 1 開始
*/
void insert(char *ch) {
int p = 0, l = strlen(ch + 1);
for (int i = 1; i <= l; i++) {
if (child[p][ch[i] - 'a'] == -1) child[p][ch[i] - 'a'] = ++tot;
p = child[p][ch[i] - 'a'];
}
sta[p] = 1; // 以結點 p 的字符串是否存在,由於本題只需要是否存在,設置 true 就好了,像上一題還需要統計個數,可以改爲 sta[p]++,表示有多少個
len[p] = l; // 結點 p 的字符串的長度
}
/**
* 對插入了單詞的前綴樹構造失敗指針
*/
void build() {
int l = 0, r = 0;
for (int i = 0; i < MAXC; i++)
if (child[0][i] == -1)
child[0][i] = 0;
else
Q[++r] = child[0][i];
while (l < r) {
int p = Q[++l];
for (int i = 0; i < MAXC; i++)
if (child[p][i] == -1)
child[p][i] = child[fail[p]][i];
else {
fail[child[p][i]] = child[fail[p]][i];
Q[++r] = child[p][i];
}
}
}
/**
* 給定一個字符串,刪除所有存在在字典中的單詞,並返回
* @param ch 給定的字符串,該字符串下標從 1 開始
* @return 操作完成後的字符串
*/
char *solve(char *ch) {
// ans 爲字符串,cursor 爲當前光標的位置
char *ans = (char *) malloc(sizeof(char) * MAXN);
int cursor = 0;
// stack 爲模擬的棧,其每一個元素記錄一個前綴樹結點的編號,top 是棧頂指針
int *stack = (int *) malloc(sizeof(int) * MAXN);
int top = 0;
// 初始從前綴樹根節點開始
int p = 0;
// 注意字符串下標從 1 開始
int l = strlen(ch + 1);
for (int i = 1; i <= l; i++) {
ans[cursor] = ch[i]; // 輸入當前字符
cursor++; // 光標移動一位
p = child[p][ch[i] - 'a']; // 前綴樹節點往後移動一個,到該字符
stack[top] = p; // 保存進棧,以便後面處理
top++; // 棧頂指針
if (sta[p]) { // sta[p] 表示以結點 p 爲路徑的字符串是不是存在,如果存在,那麼按照題目意思就要刪除它,模擬 Backspace 的過程
int length = len[p]; // 需要刪除的單詞的長度
cursor -= length; // 回退那麼多個長度
top -= length; // 回退那麼多個長度
p = stack[top - 1]; // 取出當前的棧頂元素
}
}
ans[cursor] = '\0';
return ans;
}
} T;
int main() {
// freopen("in.txt", "r", stdin);
// 構造 AC 自動機
auto ac = new AC_Automaton();
ac->clear();
// 給定的文章,下標從 1 開始
char *t = (char *) malloc(sizeof(char) * MAXN);
scanf("%s", t + 1);
int n;
scanf("%d", &n);
// 讀入單詞,加入前綴樹
char *s = (char *) malloc(sizeof(char) * MAXN);
for (int i = 0; i < n; i++) {
// 字符串下標從 1 開始
scanf("%s", s + 1);
ac->insert(s);
}
// 根據前綴樹中的單詞構造失敗指針,即構造字典
ac->build();
// 執行操作,輸出結果
printf("%s\n", ac->solve(t));
return 0;
}
歡迎關注我的個人博客以閱讀更多優秀文章:凝神長老和他的朋友們(https://www.jxtxzzw.com)
也歡迎關注我的其他平臺:知乎( https://s.zzw.ink/zhihu )、知乎專欄( https://s.zzw.ink/zhuanlan )、嗶哩嗶哩( https://s.zzw.ink/blbl )、微信公衆號( 凝神長老和他的朋友們 )