写在前面的话:我的能力也有限,错误是在所难免的!因此如发现错误还请指出一同学习!
索引
(难度由题目自身难度与本周做题情况进行分类,仅供新生参考!)
零、基础知识过关
一、easy:01、06、07、08、09、10、11、16
二、medium:02、03、04、05、12、14、15、17、18
三、hard:13、19、20、21、22、23
四、神仙题:24
终于还是来字符串了,本周的题目基本上都是 KMP、Trie 字典树以及 AC自动机的题目。
KMP: O ( n ) O(n) O(n) 线性时间内实现原串与模式串之间的匹配。该算法的核心在于理解 n e x t next next 数组的含义以及作用,要想不只停留在做板子题就必须要把 n e x t next next 数组吃透了,忘记了一次就再重新学习一次,直到完全掌握。
我第一次了解 KMP 算法是在 B 站的 三哥讲解KMP算法中文字幕版 学习到的,我还记得我至少看了 3 遍才有一点理解了,所以大家学习的时候如果一次没懂那肯定是很正常的,多看几遍就慢慢有感觉了!而且以后肯定是会忘记的,忘了就复习!
Trie 树:也称之为字典树,因为这颗树上每个节点都代表着一个字符。使用字典树最常见的作用就是对字符串进行快速的插入与查询。
推荐网站:看动画轻松理解「Trie树」
AC 自动机:别想多了,这个东西不是用来自动帮你 AC 题目的。如果简单得来说,KMP 是用来在原串中查询是否出现过某个模式串,那么 AC自动机就是用来在原串中查询出现过多少个不同的模式串。该算法也是基于 Trie字典树进行的,在字典树上构建一个类似 n e x t next next 数组的 f a i l fail fail 指针数组来实现快速地查询。
推荐网站:(无图)AC自动机总结 (有图)AC自动机 算法详解(图解)及模板
1001:亲和串(KMP)
题意:给两个字符串 S 1 S1 S1 和 S 2 S2 S2,问是否能通过 S 1 S1 S1 循环移位使得 S 2 S2 S2 包含在 S 1 S1 S1 中。
范围: ∣ S 1 ∣ 、 ∣ S 2 ∣ ≤ 1 e 5 |S1|、|S2| \le 1e5 ∣S1∣、∣S2∣≤1e5
分析:问 S 1 S1 S1 循环移位后是否能够得到 S 2 S2 S2,相当于是把两个 S 1 S1 S1 拼接起来的字符串中是否包含 S 2 S2 S2。考虑到数据范围,暴力不可取,于是上 K M P KMP KMP,唯一需要注意的就是 S 2 S2 S2 的长度不能超过 S 1 S1 S1 的长度。
Code:
#include
using namespace std;
const int MAXN = 2e5 + 10;
int Next[MAXN];
void getNext(string T, int len)
{
int i = 0, j = -1;
Next[0] = -1;
while (i < len)
{
if (j == -1 || T[i] == T[j])
{
Next[++i] = ++j;
}
else
{
j = Next[j]; // 若字符不相同,则j值回溯
}
}
return;
}
int indexKMP(string S, string T, int pos, int lenS, int lenT)
{
int i = pos;
int j = 0;
getNext(T, lenT); // 对串T作分析,得到next数组
while (i < lenS && j < lenT) // 若i小于S的长度且j小于T的长度时循环继续
{
if (j == -1 || S[i] == T[j]) // 两字母相等则继续,与朴素算法相比增加了 j = -1 判断
{
i++;
j++;
}
else // 指针后退重新开始匹配
{
j = Next[j]; // j退回合适的位置,i值不变
}
}
if (j >= lenT)
{
return 1;
}
else
{
return 0;
}
}
int main()
{
string s1, s2;
while (cin >> s1 >> s2)
{
// 肯定不满足要求,退出
if (s1.length() < s2.length())
{
cout << "no" << endl;
continue;
}
// 否则的话进行拼接KMP检查是否能够得到
s1 = s1 + s1;
if (indexKMP(s1, s2, 0, s1.length(), s2.length()))
{
cout << "yes" << endl;
}
else
{
cout << "no" << endl;
}
}
return 0;
}
1006:Simpsons’ Hidden Talents(KMP)
题意:给两个字符串 S 1 S1 S1 和 S 2 S2 S2,求两个字符串的最长公共前后缀长度。
范围: ∣ S 1 ∣ 、 ∣ S 2 ∣ ≤ 50000 |S1|、|S2| \le 50000 ∣S1∣、∣S2∣≤50000
分析:KMP 裸题,把两个字符串进行拼接求 N e x t Next Next 数组即可。需要注意的是求出来的长度不能超过单个字符串的长度。
Code:
#include
using namespace std;
const int MAXN = 1e5 + 10;
int Next[MAXN];
void getNext(string T, int len)
{
int i = 0, j = -1;
Next[0] = -1;
while (i < len)
{
if (j == -1 || T[i] == T[j])
{
Next[++i] = ++j;
}
else
{
j = Next[j]; // 若字符不相同,则j值回溯
}
}
return;
}
int main()
{
string str1, str2;
while (cin >> str1 >> str2)
{
string s = str1 + str2;
getNext(s, s.length());
int ans = Next[s.length()];
// 0则直接输出0,不用输出字符串
if (ans == 0)
{
cout << 0 << endl;
}
else
{
// 注意不能超过单个字符串的长度
if (ans > str1.length())
ans = str1.length();
if (ans > str2.length())
ans = str2.length();
cout << str1.substr(0, ans) << " " << ans << endl;
}
}
return 0;
}
1007:统计难题(Trie树)
第三周字符串场 1005,详见博客
1008:Immediate Decodability(Trie树)
题意:给一些字符串,问是否存在某个字符串是另外一个字符串的前缀。
范围:未明确指出
分析:也是 Trie 树的裸题了,在插入的时候判断是否经过其他的字符串即可。
Code:
#include
using namespace std;
const int MAXN = 100 + 10;
int trie[MAXN][2]; //数组形式定义字典树,值存储的是下一个字符的位置
int last[MAXN];
int pos = 1;
char str[MAXN];
int flag;
void Insert(char word[]) //在字典树中插入某个单词
{
int i;
int c = 0;
for (i = 0; word[i]; i++)
{
int n = word[i] - '0';
if (trie[c][n] == 0) //如果对应字符还没有值
trie[c][n] = pos++;
else if (i == strlen(word) - 1) // 如果对应字符有值,并且该串已经处理完,说明跟之间的字符串出现重叠
flag = 1;
c = trie[c][n];
if (last[c]) // 如果遇到了其他串的终结符,也说明出现了重叠
flag = 1;
}
last[c] = 1;
}
int main()
{
int kase = 1;
while (~scanf("%s", str))
{
// 遇到9则判断答案
if (strcmp(str, "9") == 0)
{
cout << "Set " << kase++;
if (flag)
{
cout << " is not immediately decodable" << endl;
}
else
{
cout << " is immediately decodable" << endl;
}
// 注意清空
memset(trie, 0, sizeof(trie));
memset(last, 0, sizeof(last));
flag = 0;
pos = 1;
continue;
}
Insert(str);
}
return 0;
}
1009:Phone List(Trie树)
题意:给 N N N 个不同的数字串 S [ i ] S[i] S[i],问是否存在某个数字串是另外一个数字串的前缀。
范围: 1 ≤ N ≤ 10000 , ∣ S [ i ] ∣ < = 10 1 \le N \le 10000~,~|S[i]| <= 10 1≤N≤10000 , ∣S[i]∣<=10
分析:跟 1008 几乎是一模一样的问题,只需要稍微修改一点地方,比如输入、数组大小等。
Code:
#include
using namespace std;
const int MAXN = 1e5 + 10;
int trie[MAXN][10]; //数组形式定义字典树,值存储的是下一个字符的位置
int last[MAXN]; // 用来标记终结符
int pos = 1;
char str[MAXN];
int flag;
void Insert(char word[]) //在字典树中插入某个单词
{
int i;
int c = 0;
for (i = 0; word[i]; i++)
{
int n = word[i] - '0';
if (trie[c][n] == 0) //如果对应字符还没有值
trie[c][n] = pos++;
else if (i == strlen(word) - 1)
flag = 1; // 同上题
c = trie[c][n];
if (last[c])
flag = 1;
}
last[c] = 1;
}
int main()
{
int T;
cin >> T;
while (T--)
{
// 注意清空
memset(trie, 0, sizeof(trie));
memset(last, 0, sizeof(last));
pos = 1;
flag = 0;
int n;
cin >> n;
for (int i = 0; i < n; i++)
{
cin >> str;
Insert(str);
}
if (flag)
{
cout << "NO" << endl;
}
else
{
cout << "YES" << endl;
}
}
return 0;
}
1010:单词数(set)
题意:给一篇文章,统计其中不同单词的数量。
范围:未明确指出
分析:直接上集合 set,最后输出集合的大小即可。
Code:
#include
using namespace std;
set<string> Set; // set容器自动去重
int main()
{
string str;
while (getline(cin, str) && str != "#")
{
Set.clear();
stringstream ss(str); // 使用stringstream可以起到分割字符串的作用
string s;
while (ss >> s)
{
Set.insert(s); // 只保留不同的字符串
}
cout << Set.size() << endl;
}
return 0;
}
1011:What Are You Talking About(map)
第三周字符串场 1016:详见博客
1016:Keywords Search(AC自动机)
题意:给 N N N 个模式串 S [ i ] S[i] S[i],再给出一个查询串 S t r Str Str,问 S t r Str Str 中出现了多少个模式串。
范围: N ≤ 10000 , ∣ S [ i ] ∣ ≤ 50 , ∣ S t r ∣ ≤ 1000000 N \le 10000~,~|S[i]| \le 50~,~|Str| \le 1000000 N≤10000 , ∣S[i]∣≤50 , ∣Str∣≤1000000,字符串只包含小写字母
分析:AC自动机板子题。
Code:
#include
using namespace std;
struct Trie
{
int next[500010][26], fail[500010], end[500010];
int root, L;
int newnode()
{
for (int i = 0; i < 26; i++)
{
next[L][i] = -1;
}
end[L++] = 0;
return L - 1;
}
void init()
{
L = 0;
root = newnode();
}
// 往树中插入字符串
void insert(char buf[])
{
int len = (int)strlen(buf);
int now = root;
for (int i = 0; i < len; i++)
{
if (next[now][buf[i] - 'a'] == -1)
{
next[now][buf[i] - 'a'] = newnode();
}
now = next[now][buf[i] - 'a'];
}
end[now]++;
}
// 构造fail指针
void build()
{
queue<int> Q;
fail[root] = root;
for (int i = 0; i < 26; i++)
{
if (next[root][i] == -1)
{
next[root][i] = root;
}
else
{
fail[next[root][i]] = root;
Q.push(next[root][i]);
}
}
while (!Q.empty())
{
int now = Q.front();
Q.pop();
for (int i = 0; i < 26; i++)
{
if (next[now][i] == -1)
{
next[now][i] = next[fail[now]][i];
}
else
{
fail[next[now][i]] = next[fail[now]][i];
Q.push(next[now][i]);
}
}
}
}
// 查询字符串是否存在
int query(char buf[])
{
int len = (int)strlen(buf);
int now = root;
int res = 0;
for (int i = 0; i < len; i++)
{
now = next[now][buf[i] - 'a'];
int temp = now;
while (temp != root)
{
res += end[temp];
end[temp] = 0;
temp = fail[temp];
}
}
return res;
}
};
char buf[1000010];
Trie ac;
int main()
{
int T;
int n;
scanf("%d", &T);
while (T--)
{
scanf("%d", &n);
ac.init();
for (int i = 0; i < n; i++)
{
scanf("%s", buf);
ac.insert(buf);
}
ac.build();
scanf("%s", buf);
printf("%d\n", ac.query(buf));
}
return 0;
}
1002:Count the string(Next数组)
题意:给一个长度为 N N N 且只包含小写字母的字符串 S S S,问其所有前缀在串中出现次数的总和。
范围: 1 ≤ N ≤ 2 e 5 1 \le N \le 2e5 1≤N≤2e5
分析:根据数据范围可知暴力不可取,因为涉及到字符串的匹配问题,因此想到 K M P KMP KMP 以及 N e x t Next Next 数组。 N e x t [ i ] Next[i] Next[i] 表示前 i i i 个字符所组成的字符串的最大前后缀匹配的长度。考虑字符串 a b c a b c abcabc abcabc,那么 N e x t Next Next 数组为 [-1, 0, 0, 0, 1, 2, 3], n e x t [ 6 ] = 3 next[6] = 3 next[6]=3,说明 a b c a b c abcabc abcabc 前后匹配了 3 3 3 个字符 a b c abc abc,此时对答案贡献了 1 1 1 个 a a a, 1 1 1 个 a b ab ab , 1 1 1 个 a b c abc abc。可以发现通过 N e x t Next Next 数组我们就可以快速统计所有前缀对答案的贡献,需要注意统计的时候要统计非递增点,比如上面的例子中计算了 N e x t [ 6 ] Next[6] Next[6],则不需要统计 N e x t [ 5 ] Next[5] Next[5],因为答案已经包含在 N e x t [ 6 ] Next[6] Next[6] 中了。另外每个前缀都出现了一次,所以答案需要加上 N N N。
详见代码。
Code:
#include
using namespace std;
const int MAXN = 2e5 + 10;
const int MOD = 10007;
int n;
int Next[MAXN];
void getNext(string T, int len)
{
int i = 0, j = -1;
Next[0] = -1;
while (i < len)
{
if (j == -1 || T[i] == T[j])
{
Next[++i] = ++j;
}
else
{
j = Next[j]; // 若字符不相同,则j值回溯
}
}
return;
}
int main()
{
int T;
cin >> T;
while (T--)
{
cin >> n;
string str;
cin >> str;
getNext(str, n);
int ans = n % MOD + Next[n] % MOD;
for (int i = 1; i < n; i++)
{
// 到“峰点”才统计答案
if (Next[i] + 1 != Next[i + 1])
{
ans = ans % MOD + Next[i] % MOD;
}
}
cout << ans % MOD << endl;
}
return 0;
}
1003:Cyclic Nacklace(Next数组)
题意:给一个只包含小写字母的字符串 S S S,问至少需要往左边或者右边加上多少个字符才能让该字符串成为一个子字符串 s u b sub sub 周期循环 k k k 次形成的字符串。
范围: 3 ≤ ∣ S ∣ ≤ 1 e 5 3 \le |S| \le 1e5 3≤∣S∣≤1e5
分析:题目说可以往左边或者右边进行加字符,但是两边实际上是等价的,因此我们只考虑往字符串右侧加字符。我们需要考虑该字符串中重复的字符串连续出现了多少次,以及还需要多少个字符来满足要求。使用 N e x t Next Next 失配数组我们可以知道前面有多少个与当前字符串前缀相同的字符串,考虑最后一个字符的 N e x t Next Next 值:
① N e x t [ l e n − 1 ] = 0 Next[len-1] = 0 Next[len−1]=0,表示该字符串没有公共的前后缀,那么就是最坏的情况,我们需要重新加上该整个字符串来满足要求。
② N e x t [ l e n − 1 ] = k Next[len-1] = k Next[len−1]=k,此时有长度为 k k k 的公共前后缀,如图我们只需要添加 l e n − 2 k len-2k len−2k 长度的字符即可。
Notice:有毒吧,用cin、cout又有问题,scanf、printf就没问题
Code:
#include
using namespace std;
const int MAXN = 1e5 + 10;
char str[MAXN];
int Next[MAXN];
void getNext(string T, int len)
{
int i = 0, j = -1;
Next[0] = -1;
while (i < len)
{
if (j == -1 || T[i] == T[j])
{
Next[++i] = ++j;
}
else
{
j = Next[j]; // 若字符不相同,则j值回溯
}
}
return;
}
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
memset(Next, 0, sizeof(Next));
scanf("%s", str);
int n = strlen(str);
getNext(str, n);
int len = n - Next[n];
if (Next[n] == 0)
{
printf("%d\n", n);
}
else if (n % len == 0)
{
printf("0\n");
}
else
{
printf("%d\n", len - n % len);
}
}
return 0;
}
1004:Period(Next数组)
题意:给一个长度为 N N N 的字符串 S S S,判断该字符串的每个前缀是否是周期字符串,即由一个子字符串循环 k > 1 k > 1 k>1 次形成。
范围: 2 ≤ S ≤ 1 e 6 2 \le S \le 1e6 2≤S≤1e6
分析:根据上一题的结论我们知道 i − N e x t [ i ] i-Next[i] i−Next[i] 表示当前以 S [ i ] S[i] S[i] 结尾的字符串的循环节长度 L L L,若 i % L = = 0 i\%L == 0 i%L==0 则说明满足题意。
Code:
#include
using namespace std;
const int MAXN = 1e6 + 10;
int Next[MAXN];
void getNext(string T, int len)
{
int i = 0, j = -1;
Next[0] = -1;
while (i < len)
{
if (j == -1 || T[i] == T[j])
{
Next[++i] = ++j;
}
else
{
j = Next[j]; // 若字符不相同,则j值回溯
}
}
return;
}
int main()
{
int n;
int kase = 1;
while (cin >> n, n)
{
string str;
cin >> str;
getNext(str, n);
cout << "Test case #" << kase++ << endl;
for (int i = 2; i <= n; i++)
{
// i-next[i]现在也算是一种套路了,求当前串的最小循环串
int len = i - Next[i];
if (Next[i] > 0 && i % len == 0)
{
cout << i << " " << i / len << endl;
}
}
cout << endl;
}
return 0;
}
1005:剪花布条(不可重复kmp计数)
题意:给一个字符串 S S S 以及一个模式串 P P P,问 S S S 中最多能够分出多少个 P P P 来。
范围: ∣ S ∣ 、 ∣ P ∣ ≤ 1000 |S|、|P| \le 1000 ∣S∣、∣P∣≤1000
分析:板子题,稍微修改一点。在 K M P KMP KMP 匹配的过程中当匹配的长度到达模式串的长度时,答案++,不进行 N e x t Next Next 回溯,而是重新开始匹配。
详见代码。
Code:
#include
using namespace std;
const int MAXN = 1000 + 10;
int Next[MAXN];
void getNext(string T, int len)
{
int i = 0, j = -1;
Next[0] = -1;
while (i < len)
{
if (j == -1 || T[i] == T[j])
{
Next[++i] = ++j;
}
else
{
j = Next[j]; // 若字符不相同,则j值回溯
}
}
return;
}
int KMP_Count(string p, int m, string str, int n)
{
//x是模式串,y是主串
int i, j;
int ans = 0;
getNext(str, n);
i = j = 0;
while (i < n)
{
while (-1 != j && str[i] != p[j])
{
j = Next[j];
}
i++, j++;
if (j >= m)
{
// j要重新开始计数,i多走了一步,退回来
ans++;
j = -1;
i--;
}
}
return ans;
}
int main()
{
string s1, s2;
while (cin >> s1 && s1 != "#")
{
cin >> s2;
cout << KMP_Count(s2, s2.length(), s1, s1.length()) << endl;
}
return 0;
}
1012:(拆分字符串+trie树)
题意:给一些字符串,问其中帽子字符串的数量,帽子字符串定义为由其他给出的两个字符串组成的字符串。
范围:字符串数量不超过 50000 50000 50000
分析:这道题目数据范围没有给清楚,但是简单的做法就是可以通过的。先把所有的字符串保存到 trie 树中,再遍历一次所有字符串,将每个字符串每个位置断开在树中查找两侧的字符串是否都存在,是则输出。
Code:
#include
using namespace std;
const int MAXN = 1e5 + 10;
int trie[MAXN][26]; //数组形式定义字典树,值存储的是下一个字符的位置
int pos = 1;
int last[MAXN];
string str[MAXN];
int flag;
void Insert(string word) //在字典树中插入某个单词
{
int i;
int c = 0;
int len = word.length();
for (i = 0; i < len; i++)
{
int n = word[i] - 'a';
if (trie[c][n] == 0) //如果对应字符还没有值
trie[c][n] = pos++;
c = trie[c][n];
}
last[c] = 1;
}
int Find(string word)
{
int i;
int c = 0;
int len = word.length();
for (i = 0; i < len; i++)
{
int n = word[i] - 'a';
if (trie[c][n] == 0)
return 0;
c = trie[c][n];
}
return last[c];
}
int main()
{
string s;
int n = 0;
while (cin >> s)
{
str[n++] = s;
Insert(s);
}
for (int i = 0; i < n; i++)
{
s = str[i];
int len = s.length();
// 暴力拆分检查两侧字符串是否都存在
for (int j = 1; j < len; j++)
{
string s1 = s.substr(0, j), s2 = s.substr(j);
if (Find(s1) && Find(s2))
{
cout << s << endl;
break; // 别忘了退出!
}
}
}
return 0;
}
1014:Intelligent IME(Trie树)
题意:在九宫格输入法中,给出用户的 N N N 个数字组合串,再给出 M M M 个小写字母串,问这些字母串能够通过九宫格中用户的某个数字组合打出来。
范围: 1 ≤ N , M ≤ 5000 1 \le N, M \le 5000 1≤N,M≤5000,字符串长度不超过 6 6 6
分析:题意也比较清晰,需要进行字符串的快速查询,上 Trie。根据输入的数字序列进行建树,然后再用符号串在树上匹配,匹配成功就计数。
Code:
#include
using namespace std;
const int MAXN = 1e5 + 10;
int trie[MAXN][10]; //数组形式定义字典树,值存储的是下一个字符的位置
int pos = 1;
int last[MAXN], num[MAXN];
string alph[8] = {"abc", "def", "ghi", "jkl", "nmo", "pqrs", "tuv", "wxyz"};
void Insert(string word, int id) //在字典树中插入某个单词
{
int i;
int c = 0;
int len = word.length();
for (i = 0; i < len; i++)
{
int n = word[i] - '0';
if (trie[c][n] == 0) //如果对应字符还没有值
trie[c][n] = pos++;
c = trie[c][n];
}
last[c] = id; // 以编号标记终结符
}
// 查找该字符所对应的数字
int id(char ch)
{
for (int i = 0; i < 8; i++)
{
if (alph[i].find_first_of(ch) != string::npos)
return i + 2;
}
return 0;
}
int Find(string word)
{
int i;
int c = 0;
int len = word.length();
for (i = 0; i < len; i++)
{
int n = id(word[i]);
if (trie[c][n] == 0)
return 0; // 不存在则返回0
c = trie[c][n];
}
return last[c];
}
int main()
{
int T;
cin >> T;
while (T--)
{
memset(trie, 0, sizeof(trie));
memset(last, 0, sizeof(last));
memset(num, 0, sizeof(num));
pos = 1;
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
string s;
cin >> s;
Insert(s, i);
}
for (int i = 0; i < m; i++)
{
string s;
cin >> s;
int ans = Find(s);
if (ans)
num[ans]++;
}
for (int i = 1; i <= n; i++)
{
cout << num[i] << endl;
}
}
return 0;
}
1015:Flying to the Mars(思维+Map)
第四周 1008:详见博客
1017:病毒侵袭(AC自动机)
题意:给 N N N 个模式串 S [ i ] S[i] S[i],再给 M M M 个查询串 S t r [ i ] Str[i] Str[i],问每个查询串中包含了多少个模式串,输出模式串的编号。最后输出匹配到模式串的查询串数量。
范围: 1 ≤ N ≤ 500 , 1 ≤ M ≤ 1000 , 20 ≤ ∣ S [ i ] ∣ ≤ 200 , 7000 ≤ ∣ S t r [ i ] ∣ ≤ 10000 1 \le N \le 500~,~1 \le M \le 1000~,~20 \le |S[i]| \le 200~,~7000 \le |Str[i]| \le 10000 1≤N≤500 , 1≤M≤1000 , 20≤∣S[i]∣≤200 , 7000≤∣Str[i]∣≤10000,字符串字符都是 A S C I I ASCII ASCII 码可见字符
分析:显明是 AC 自动机,我们需要做的是在 Trie 树上每个节点加上标记 e n d end end,如果该结点是某个模式串的最后一个字符,那么 e n d end end 就等于该模式串的编号。这样用查询串上树上匹配的时候某个节点匹配成功且 e n d end end 非 0 0 0,说明匹配成功了一个模式串,记录下编号,最终的答案数量增加。
Notice:需要注意本题中的字符不都是小写字母,可以把之间 26 26 26 的地方都改成 130 130 130
Code:
#include
using namespace std;
const int MAXN = 1e5 + 10;
struct Trie
{
// 题目说了是ASCII可见字符,数组要开大
int next[MAXN][130], fail[MAXN], end[MAXN];
set<int> Set;
int root, L;
int newnode()
{
for (int i = 0; i < 130; i++)
{
next[L][i] = -1;
}
end[L++] = 0;
return L - 1;
}
void init()
{
L = 0;
root = newnode();
}
void insert(char buf[], int id)
{
int len = (int)strlen(buf);
int now = root;
for (int i = 0; i < len; i++)
{
// 数组开到130那buf[i]就不需要-'a'了
if (next[now][buf[i]] == -1)
{
next[now][buf[i]] = newnode();
}
now = next[now][buf[i]];
}
end[now] = id;
}
void build()
{
queue<int> Q;
fail[root] = root;
for (int i = 0; i < 130; i++)
{
if (next[root][i] == -1)
{
next[root][i] = root;
}
else
{
fail[next[root][i]] = root;
Q.push(next[root][i]);
}
}
while (!Q.empty())
{
int now = Q.front();
Q.pop();
for (int i = 0; i < 130; i++)
{
if (next[now][i] == -1)
{
next[now][i] = next[fail[now]][i];
}
else
{
fail[next[now][i]] = next[fail[now]][i];
Q.push(next[now][i]);
}
}
}
}
void query(char buf[])
{
int len = (int)strlen(buf);
int now = root;
for (int i = 0; i < len; i++)
{
now = next[now][buf[i]];
int temp = now;
while (temp != root)
{
// 加入set集合
if (end[temp])
{
Set.insert(end[temp]);
}
temp = fail[temp];
}
}
}
};
char buf[1000010];
Trie ac;
int main()
{
int n, m;
scanf("%d", &n);
ac.init();
for (int i = 0; i < n; i++)
{
scanf("%s", buf);
ac.insert(buf, i + 1);
}
ac.build();
scanf("%d", &m);
int cnt = 0;
for (int i = 0; i < m; i++)
{
scanf("%s", buf);
ac.Set.clear(); // 清空再使用
ac.query(buf);
// 如果匹配成功则输出
if (ac.Set.size())
{
cout << "web " << i + 1 << ":";
// set中有序
for (auto x : ac.Set)
{
cout << " " << x;
}
cout << endl;
cnt++;
}
}
cout << "total: " << cnt << endl;
return 0;
}
1018:病毒侵袭持续中(AC自动机)
题意:给 N N N 个模式串 S [ i ] S[i] S[i],再给出一个查询串 S t r Str Str,问查询串中出现了哪些模式串,出现了多少次。
范围: 1 ≤ N ≤ 1000 , 1 ≤ S [ i ] ≤ 50 , ∣ S t r ∣ ≤ 2 e 6 1 \le N \le 1000~,~1\le S[i] \le 50~,~|Str| \le 2e6 1≤N≤1000 , 1≤S[i]≤50 , ∣Str∣≤2e6
分析:在上题的代码上稍微修改即可,增加数组 n u m num num 保存某个编号的模式串出现次数,当匹配成功时则对应 n u m + + num++ num++。
Code:
// mid
// ac自动机
// 字符串计数
#include
using namespace std;
const int MAXN = 1e5 + 10;
int n, m;
char buf[2000010], str[1010][100];
struct Trie
{
int next[MAXN][130], fail[MAXN], end[MAXN];
int root, L;
int newnode()
{
for (int i = 0; i < 130; i++)
{
next[L][i] = -1;
}
end[L++] = 0;
return L - 1;
}
void init()
{
L = 0;
root = newnode();
}
void insert(char buf[], int id)
{
int len = (int)strlen(buf);
int now = root;
for (int i = 0; i < len; i++)
{
if (next[now][buf[i]] == -1)
{
next[now][buf[i]] = newnode();
}
now = next[now][buf[i]];
}
end[now] = id;
}
void build()
{
queue<int> Q;
fail[root] = root;
for (int i = 0; i < 130; i++)
{
if (next[root][i] == -1)
{
next[root][i] = root;
}
else
{
fail[next[root][i]] = root;
Q.push(next[root][i]);
}
}
while (!Q.empty())
{
int now = Q.front();
Q.pop();
for (int i = 0; i < 130; i++)
{
if (next[now][i] == -1)
{
next[now][i] = next[fail[now]][i];
}
else
{
fail[next[now][i]] = next[fail[now]][i];
Q.push(next[now][i]);
}
}
}
}
void query(char buf[])
{
int num[1010];
memset(num, 0, sizeof(num));
int len = (int)strlen(buf);
int now = root;
for (int i = 0; i < len; i++)
{
now = next[now][buf[i]];
int temp = now;
while (temp != root)
{
if (end[temp])
{
// 对应编号数量增加
num[end[temp]]++;
}
temp = fail[temp];
}
}
for (int i = 1; i <= n; i++)
{
if (num[i])
{
printf("%s: %d\n", str[i], num[i]);
}
}
}
};
Trie ac;
int main()
{
while (~scanf("%d", &n))
{
ac.init();
for (int i = 1; i <= n; i++)
{
scanf("%s", str[i]);
ac.insert(str[i], i);
}
ac.build();
scanf("%s", buf);
ac.query(buf);
}
return 0;
}
1013:T9(Trie树+dfs)
题意:模拟九宫格输入法,给出 w w w 个该用户输入单词的频率 p [ i ] p[i] p[i],再给出该用户输入的数字串,对于该数字串,输出用户每按下一个数字当前最可能要输出的字符串。
范围: 0 ≤ w ≤ 1000 , 1 ≤ p [ i ] ≤ 100 , 0 \le w \le 1000~,~1 \le p[i] \le 100~,~ 0≤w≤1000 , 1≤p[i]≤100 , 字符串长度不超过 100 100 100。
分析:首先题目涉及到对一些模式串的快速查询,所以需要在字典树 Trie 上进行,其次我们需要对每个用户的输入数字串进行逐位分析,分析出每输入一个数字时最可能的字符串,可能性是由 p [ i ] p[i] p[i] 决定的,因此需要在树上加上权值,每次查询的时候在树上进行 dfs,记录下每一层的最优解,即输入每一位数字时当前最可能的解。
细节很多,详见代码。
Code:
#include
using namespace std;
const int MAXN = 1e5 + 10;
int trie[MAXN][26]; //数组形式定义字典树,值存储的是下一个字符的位置
int num[MAXN]; // num表示各个节点的总权值
int pos = 1;
string str, ans[MAXN]; // ans保存每一步的答案
map<char, string> mp; // 数字到字母的映射
void Insert(string word, int w) //在字典树中插入某个单词
{
int i;
int c = 0;
int len = word.length();
for (i = 0; i < len; i++)
{
int n = word[i] - 'a';
if (trie[c][n] == 0) //如果对应字符还没有值
trie[c][n] = pos++;
c = trie[c][n];
num[c] += w; // 累计权值
}
}
int Find(string word)
{
int i;
int c = 0;
int len = word.length();
for (i = 0; i < len; i++)
{
int n = word[i] - 'a';
if (trie[c][n] == 0)
return 0;
c = trie[c][n];
}
return num[c]; // 返回权值
}
// 已经构成的串保存在s中
void dfs(string s)
{
int len1 = s.length(), len2 = str.length();
if (len1 > len2) // 大于所求串,退出
return;
int res1 = Find(s), res2 = Find(ans[len1]);
if (!res1 && len1) // 确实没找到,退出
return;
if (res1 > res2)
{
ans[len1] = s; // 保存每一步的最优解
}
string temp = s;
// 尝试该数字对应的每个字母
for (int i = 0; i < mp[str[len1]].length(); i++)
{
temp += mp[str[len1]][i];
dfs(temp);
temp = s; // 注意回溯
}
}
int main()
{
mp['2'] = "abc";
mp['3'] = "def";
mp['4'] = "ghi";
mp['5'] = "jkl";
mp['6'] = "mno";
mp['7'] = "pqrs";
mp['8'] = "tuv";
mp['9'] = "wxyz";
int T;
cin >> T;
int kase = 1;
while (T--)
{
// 注意清空
memset(trie, 0, sizeof(trie));
memset(num, 0, sizeof(num));
pos = 1;
int n;
cin >> n;
for (int i = 0; i < n; i++)
{
string s;
int w;
cin >> s >> w;
Insert(s, w);
}
cout << "Scenario #" << kase++ << ":" << endl;
int m;
cin >> m;
for (int i = 0; i < m; i++)
{
cin >> str;
string temp;
int len = str.length();
// 清空答案
for (int j = 0; j <= len; j++)
ans[j].clear();
dfs(temp);
// 输出答案
for (int i = 1; i < len; i++)
{
if (ans[i].length())
cout << ans[i] << endl;
else
cout << "MANUALLY" << endl;
}
cout << endl;
}
cout << endl;
}
return 0;
}
1019:考研路茫茫——单词情结(AC自动机+矩阵快速幂)
题意:给出 N N N 个小写字母组成的词根,现在问长度不超过 L L L,只由小写字母组成,且至少包含一个词根的单词数量,答案需要 % 2 64 \% 2^{64} %264。
范围: 0 < N < 6 , 0 < L < 2 31 0 < N < 6~,~0 < L < 2^{31} 0<N<6 , 0<L<231,字符串长度不超过 5 5 5
分析:顶不住了,看了半天题解还是没有吃透,终点就是在于矩阵的构造,有兴趣的同学可以尝试理解一下!
HDU 2243 AC自动机->DP->附矩阵乘法板子
Code:
// hard
// ac自动机+矩阵快速幂
// https://www.cnblogs.com/WABoss/p/5168429.html
#include
#include
#include
using namespace std;
int tn, ch[33][26], fail[33];
bool flag[33];
void insert(char *s)
{
int x = 0;
for (int i = 0; s[i]; ++i)
{
int y = s[i] - 'a';
if (ch[x][y] == 0)
ch[x][y] = ++tn;
x = ch[x][y];
}
flag[x] = 1;
}
void init(int x)
{
memset(fail, 0, sizeof(fail));
queue<int> que;
for (int i = 0; i < 26; ++i)
{
if (ch[0][i])
que.push(ch[0][i]);
}
while (!que.empty())
{
int x = que.front();
que.pop();
for (int i = 0; i < 26; ++i)
{
if (ch[x][i])
que.push(ch[x][i]), fail[ch[x][i]] = ch[fail[x]][i];
else
ch[x][i] = ch[fail[x]][i];
flag[ch[x][i]] |= flag[ch[fail[x]][i]];
}
}
}
void init()
{
memset(fail, 0, sizeof(fail));
queue<int> que;
for (int i = 0; i < 26; ++i)
{
if (ch[0][i])
que.push(ch[0][i]);
}
while (!que.empty())
{
int now = que.front();
que.pop();
for (int i = 0; i < 26; ++i)
{
if (ch[now][i])
que.push(ch[now][i]), fail[ch[now][i]] = ch[fail[now]][i];
else
ch[now][i] = ch[fail[now]][i];
flag[ch[now][i]] |= flag[ch[fail[now]][i]];
}
}
}
struct Mat
{
unsigned long long m[66][66];
int n;
};
Mat operator*(const Mat &m1, const Mat &m2)
{
Mat m = {0};
m.n = m1.n;
for (int i = 0; i < m.n; ++i)
{
for (int j = 0; j < m.n; ++j)
{
for (int k = 0; k < m.n; ++k)
m.m[i][j] += m1.m[i][k] * m2.m[k][j];
}
}
return m;
}
int main()
{
int n, l;
char str[6];
while (~scanf("%d%d", &n, &l))
{
tn = 0;
memset(ch, 0, sizeof(ch));
memset(flag, 0, sizeof(flag));
while (n--)
{
scanf("%s", str);
insert(str);
}
init();
Mat se = {0}, sm = {0};
se.n = sm.n = 2;
for (int i = 0; i < 2; ++i)
se.m[i][i] = 1;
sm.m[0][0] = 26;
sm.m[0][1] = 1;
sm.m[1][1] = 1;
n = l;
while (n)
{
if (n & 1)
se = se * sm;
sm = sm * sm;
n >>= 1;
}
unsigned long long tot = se.m[0][1] * 26;
Mat te = {0}, tm = {0};
te.n = tm.n = tn + 1 << 1;
for (int i = 0; i < te.n; ++i)
te.m[i][i] = 1;
for (int i = 0; i <= tn; ++i)
{
tm.m[i + tn + 1][i + tn + 1] = tm.m[i][i + tn + 1] = 1;
}
for (int i = 0; i <= tn; ++i)
{
if (flag[i])
continue;
for (int j = 0; j < 26; ++j)
{
if (flag[ch[i][j]])
continue;
++tm.m[i][ch[i][j]];
}
}
Mat tmp = tm;
tmp.n = tn + 1;
n = l;
while (n)
{
if (n & 1)
te = te * tm;
tm = tm * tm;
n >>= 1;
}
Mat tmp2;
tmp2.n = tn + 1;
for (int i = 0; i <= tn; ++i)
{
for (int j = tn + 1; j < te.n; ++j)
tmp2.m[i][j - tn - 1] = te.m[i][j];
}
tmp = tmp * tmp2;
unsigned long long res = 0;
for (int i = 0; i <= tn; ++i)
{
res += tmp.m[0][i];
}
printf("%llu\n", tot - res);
}
return 0;
}
1020:Wireless Password(AC自动机+状压DP)
题意: 有 M M M 个小写字母字符串,现在问至少选择其中 K K K 个字符串构成长度为 N N N 的字符串方案数为多少?答案 % 20090717 \%20090717 %20090717,选择的字符串可以重叠。
范围: 1 ≤ N ≤ 25 , 0 ≤ K ≤ M ≤ 10 1 \le N \le 25~,~0 \le K \le M \le 10 1≤N≤25 , 0≤K≤M≤10,字符串长度不超过 10 10 10
分析:由于选择的字符串前后缀是可以重叠的,因此使用 AC自动机 来处理会比较方便。对于计算方案数的问题,暴力不可取,考虑进行 DP,在 AC自动机 上跑 DP 也是常见的套路了。本道题 DP 数组是三维的, d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k] 前面两维就是常见的处理到字符串前 i i i 位且处于自动机上 j j j 结点,本题特殊的就在于第三维 k k k,因为我们需要记录下这 M M M 个字符串出现的情况,总不能开个 M M M 维数组来记录吧, 因此考虑状态压缩,把这 M M M 个字符的选择情况压缩成长度为 M M M 的二进制数。因此 DP 数组大小为 N ∗ M ∗ 10 ∗ 2 10 N*M*10*2^{10} N∗M∗10∗210,可以接受。
所以我们处理完 AC自动机 之后,对于自动机上的每一个结点,更新其所能到达结点的 dp 值并且更新二进制串,最后遍历一遍所有 i = = N i==N i==N 的 dp 点,如果二进制串中 1 1 1 的数量小于 k k k 则忽略,否则更新答案。
细节很多,详见代码。
Code:
#include
using namespace std;
const int MAXN = 100 + 10;
const int MOD = 20090717;
int n, m, k;
char buf[100];
int dp[30][MAXN][1 << 10]; // 表示10位的二进制需要2^10的空间,即1<<10
struct Trie
{
int next[MAXN][26], fail[MAXN], end[MAXN];
int root, L;
int newnode()
{
for (int i = 0; i < 26; i++)
{
next[L][i] = -1;
}
end[L++] = 0;
return L - 1;
}
void init()
{
L = 0;
root = newnode();
}
void insert(char buf[], int id)
{
int len = (int)strlen(buf);
int now = root;
for (int i = 0; i < len; i++)
{
if (next[now][buf[i] - 'a'] == -1)
{
next[now][buf[i] - 'a'] = newnode();
}
now = next[now][buf[i] - 'a'];
}
end[now] = (1 << id); // 二进制对应位上改成1
}
void build()
{
queue<int> Q;
fail[root] = root;
for (int i = 0; i < 26; i++)
{
if (next[root][i] == -1)
{
next[root][i] = root;
}
else
{
fail[next[root][i]] = root;
Q.push(next[root][i]);
}
}
while (!Q.empty())
{
int now = Q.front();
Q.pop();
end[now] |= end[fail[now]]; // // 与后缀所包含的字符串的二进制进行结合
for (int i = 0; i < 26; i++)
{
if (next[now][i] == -1)
{
next[now][i] = next[fail[now]][i];
}
else
{
fail[next[now][i]] = next[fail[now]][i];
Q.push(next[now][i]);
}
}
}
}
void solve()
{
dp[0][0][0] = 1; // 第一个点必须手动设置
for (int i = 0; i < n; i++)
{
for (int j = 0; j < L; j++)
{
for (int k = 0; k < (1 << m); k++)
{
if (dp[i][j][k])
{
for (int x = 0; x < 26; x++)
{
int newx = i + 1; // 下一个字符
int newy = next[j][x]; // 下一个结点
int newz = k | end[newy]; // 二进制结合
dp[newx][newy][newz] = dp[newx][newy][newz] % MOD + dp[i][j][k] % MOD;
dp[newx][newy][newz] %= MOD;
}
}
}
}
}
int ans = 0;
for (int i = 0; i < (1 << m); i++)
{
// 二进制中没有k个1则略过
bitset<32> bit(i);
if (bit.count() < k)
continue;
for (int j = 0; j < L; j++)
{
ans = ans % MOD + dp[n][j][i] % MOD;
ans %= MOD;
}
}
cout << ans << endl;
}
};
Trie ac;
int main()
{
while (~scanf("%d%d%d", &n, &m, &k), n + m + k)
{
memset(dp, 0, sizeof(dp));
ac.init();
for (int i = 0; i < m; i++)
{
scanf("%s", buf);
ac.insert(buf, i);
}
ac.build();
ac.solve();
}
return 0;
}
1021:Ring(AC自动机+DP)
题意:给 M M M 个小写字母字符串 S [ i ] S[i] S[i] 及其权值 w i w_i wi,现在问构造一个长度为 N N N 个字符串,其权值最大能是多少。若 M M M 个字符串在该字符串中出现重叠,则其权值分开统计。
范围: 0 < N ≤ 50 , 0 < M ≤ 100 , 1 ≤ w i ≤ 100 , ∣ S [ i ] ∣ ≤ 100 0 < N \le 50~,~0 < M \le 100~,~1 \le w_i \le 100~,~|S[i]| \le 100 0<N≤50 , 0<M≤100 , 1≤wi≤100 , ∣S[i]∣≤100
分析:跟上题有些类似,但是难度稍低。因此字符串可以重叠,上 AC自动机,且求最大权值,不可暴力,无贪心策略,考虑 DP。 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示处理到字符串前 i i i 个字符且位于自动机上第 j j j 个结点时的最优解,那么 d p [ i ] [ j ] → d p [ i + 1 ] [ n e x t [ j ] [ x ] ] + w [ j ] dp[i][j] \rightarrow dp[i+1][next[j][x]]+w[j] dp[i][j]→dp[i+1][next[j][x]]+w[j],其中 x ∈ [ 0 , 26 ) x \in [0, 26) x∈[0,26)
本题还需要记录路径 p a t h path path,可以在 dp 的过程中一并处理。
详见代码。
Code:
#include
using namespace std;
const int MAXN = 1200 + 10;
const int MOD = 20090717;
int n, m;
char buf[20];
int dp[60][MAXN];
string path[60][MAXN]; // 记录路径
struct Trie
{
int next[MAXN][26], fail[MAXN], end[MAXN];
int root, L;
int newnode()
{
for (int i = 0; i < 26; i++)
{
next[L][i] = -1;
}
end[L++] = 0;
return L - 1;
}
void init()
{
memset(next, 0, sizeof(next));
memset(fail, 0, sizeof(fail));
memset(end, 0, sizeof(end));
L = 0;
root = newnode();
}
void insert(string buf, int w)
{
int len = buf.length();
int now = root;
for (int i = 0; i < len; i++)
{
if (next[now][buf[i] - 'a'] == -1)
{
next[now][buf[i] - 'a'] = newnode();
}
now = next[now][buf[i] - 'a'];
}
end[now] = w;
}
void build()
{
queue<int> Q;
fail[root] = root;
for (int i = 0; i < 26; i++)
{
if (next[root][i] == -1)
{
next[root][i] = root;
}
else
{
fail[next[root][i]] = root;
Q.push(next[root][i]);
}
}
while (!Q.empty())
{
int now = Q.front();
Q.pop();
end[now] += end[fail[now]]; // 注意累计
for (int i = 0; i < 26; i++)
{
if (next[now][i] == -1)
{
next[now][i] = next[fail[now]][i];
}
else
{
fail[next[now][i]] = next[fail[now]][i];
Q.push(next[now][i]);
}
}
}
}
void solve()
{
memset(dp, -1, sizeof(dp));
dp[0][0] = 0;
path[0][0] = "";
for (int i = 0; i < n; i++)
{
for (int j = 0; j < L; j++)
{
// 如果无法到达该状态,那是白扯,跳过
if (dp[i][j] != -1)
{
for (int x = 0; x < 26; x++)
{
int newx = i + 1;
int newy = next[j][x];
// 更新答案,记录路径
if (dp[i][j] + end[newy] > dp[newx][newy])
{
dp[newx][newy] = dp[i][j] + end[newy];
path[newx][newy] = path[i][j] + (char)(x + 'a');
}
else if (dp[i][j] + end[newy] == dp[newx][newy])
{
string str = path[i][j] + (char)(x + 'a');
if (str < path[newx][newy])
path[newx][newy] = str;
}
}
}
}
}
// 找到最优解中路径字典序最小的
int ans = 0, row;
string str;
for (int i = 0; i <= n; i++)
{
for (int j = 0; j < L; j++)
{
if (ans < dp[i][j])
{
ans = dp[i][j];
row = i;
}
}
}
if (ans == 0)
{
cout << endl;
return;
}
str = "";
for (int i = 0; i < L; i++)
{
if (dp[row][i] == ans && (str > path[row][i] || str == ""))
str = path[row][i];
}
cout << str << endl;
}
};
Trie ac;
string s[MAXN];
int main()
{
int T;
cin >> T;
while (T--)
{
cin >> n >> m;
ac.init();
for (int i = 0; i < m; i++)
{
cin >> s[i];
}
for (int i = 0; i < m; i++)
{
int x;
cin >> x;
ac.insert(s[i], x);
}
ac.build();
ac.solve();
}
return 0;
}
1022:DNA repair(AC自动机+DP)
题意:给出 N N N 个有害的基因序列 S [ i ] S[i] S[i](只包含 A C G T ACGT ACGT),再给出一个当前待修复基因序列 S t r Str Str,问至少需要修改多少位置上的碱基才能消除 S t r Str Str 中的所有还有基因序列。
范围: 1 ≤ N ≤ 50 , 0 ≤ ∣ S [ i ] ∣ ≤ 20 , 0 ≤ ∣ S t r ∣ ≤ 1000 1 \le N \le 50~,~0 \le |S[i]| \le 20~,~0 \le |Str| \le 1000 1≤N≤50 , 0≤∣S[i]∣≤20 , 0≤∣Str∣≤1000
分析:感觉上是可以用 AC自动机 处理后转换成区间重叠问题进行贪
心的,应该是能做的,不过最后还是选择了 DP。 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示处理到字符串前 i i i 个字符且位于自动机上第 j j j 个结点时的至少需要修改的点数量。
转移方程:
① S t r [ i + 1 ] = = n e x t [ j ] [ x ] Str[i+1] == next[j][x] Str[i+1]==next[j][x]结点的字符: d p [ i ] [ j ] → d p [ i + 1 ] [ n e x t [ j ] [ x ] ] dp[i][j] \rightarrow dp[i+1][next[j][x]] dp[i][j]→dp[i+1][next[j][x]], x ∈ [ 0 , 26 ) x \in [0, 26) x∈[0,26)
② S t r [ i + 1 ] ! = n e x t [ j ] [ x ] Str[i+1] != next[j][x] Str[i+1]!=next[j][x]结点的字符: d p [ i ] [ j ] → d p [ i + 1 ] [ n e x t [ j ] [ x ] ] + 1 dp[i][j] \rightarrow dp[i+1][next[j][x]]+1 dp[i][j]→dp[i+1][next[j][x]]+1, x ∈ [ 0 , 26 ) x \in [0, 26) x∈[0,26)
最后我们只需要遍历一遍所有 i = = ∣ S t r ∣ i == |Str| i==∣Str∣ 的 dp 点取最小值即可。
详见代码。
Code:
#include
#include
#include
#include
#include
using namespace std;
const int MAXN = 1500 + 10;
const int INF = 0x3f3f3f3f;
int n, kase = 1;
char buf[MAXN];
int dp[MAXN][MAXN];
map<char, int> mp; // 碱基到数字的映射
struct Trie
{
int next[MAXN][4], fail[MAXN], end[MAXN];
int root, L;
int newnode()
{
for (int i = 0; i < 4; i++)
{
next[L][i] = -1;
}
end[L++] = 0;
return L - 1;
}
void init()
{
memset(next, 0, sizeof(next));
memset(fail, 0, sizeof(fail));
memset(end, 0, sizeof(end));
L = 0;
root = newnode();
}
void insert(string buf)
{
int len = buf.length();
int now = root;
for (int i = 0; i < len; i++)
{
int idx = mp[buf[i]];
if (next[now][idx] == -1)
{
next[now][idx] = newnode();
}
now = next[now][idx];
}
end[now] = 1; // 该字符串需要修改
}
void build()
{
queue<int> Q;
fail[root] = root;
for (int i = 0; i < 4; i++)
{
if (next[root][i] == -1)
{
next[root][i] = root;
}
else
{
fail[next[root][i]] = root;
Q.push(next[root][i]);
}
}
while (!Q.empty())
{
int now = Q.front();
Q.pop();
// 如果后缀中有出现有害基因,则该字符串需要修改
if (end[fail[now]])
end[now] = 1;
for (int i = 0; i < 4; i++)
{
if (next[now][i] == -1)
{
next[now][i] = next[fail[now]][i];
}
else
{
fail[next[now][i]] = next[fail[now]][i];
Q.push(next[now][i]);
}
}
}
}
void solve()
{
string str;
cin >> str;
int len = str.length();
for (int i = 0; i <= len; i++)
{
for (int j = 0; j < L; j++)
{
dp[i][j] = INF;
}
}
dp[0][0] = 0;
for (int i = 0; i < len; i++)
{
for (int j = 0; j < L; j++)
{
// 必须能够到达该状态
if (dp[i][j] < INF)
{
for (int x = 0; x < 4; x++)
{
int newx = i + 1;
int newy = next[j][x];
// 转移状态后有害,则略过
if (end[newy])
continue;
// 相同则无须修改
if (mp[str[newx - 1]] == x)
{
dp[newx][newy] = min(dp[newx][newy], dp[i][j]);
}
// 否则dp值+1
else
{
dp[newx][newy] = min(dp[newx][newy], dp[i][j] + 1);
}
}
}
}
}
// 取最小值
int ans = INF;
for (int i = 0; i < L; i++)
{
ans = min(ans, dp[len][i]);
}
if (ans >= INF)
ans = -1;
cout << "Case " << kase++ << ": " << ans << endl;
}
};
Trie ac;
int main()
{
mp['A'] = 0;
mp['C'] = 1;
mp['G'] = 2;
mp['T'] = 3;
while (cin >> n, n)
{
ac.init();
for (int i = 0; i < n; i++)
{
cin >> buf;
ac.insert(buf);
}
ac.build();
ac.solve();
}
return 0;
}
1023:Lost’s revenge(AC自动机+DP+Hash)
题意:给 N N N 个模式基因序列 S [ i ] S[i] S[i],再给一个基因序列串 S S S,现在可以随意调换 S S S 中碱基的位置,问可以得到的最大匹配数,模式串可以重叠。
范围: 1 ≤ N ≤ 50 , ∣ S [ i ] ∣ ≤ 10 , ∣ S ∣ ≤ 40 1 \le N \le 50~,~|S[i]| \le 10~,~|S| \le 40 1≤N≤50 , ∣S[i]∣≤10 , ∣S∣≤40,字符串只包含 A C G T ACGT ACGT
分析:模式串可以重叠,使用 AC自动机,求最大匹配数,不暴力,无贪心策略,考虑 DP。此外,因为 S S S 的顺序可以随意调换,只需要考虑串中四种碱基的数量,可以状压DP,也可以使用 Hash,这里使用 Hash。
因为字符串 S S S 的长度最多为 40 40 40,因此单种碱基的出现次数最多也为 40 40 40,所以 H a s h Hash Hash 数组的大小为 40 ∗ 40 ∗ 40 ∗ 40 40*40*40*40 40∗40∗40∗40, H a s h [ i ] [ j ] [ k ] [ l ] Hash[i][j][k][l] Hash[i][j][k][l] 表示 A A A 出现 i i i 次、 B B B 出现 j j j 次、 C C C 出现 k k k 次、 D D D 出现 l l l 次时该状态的编号。
d p [ i ] [ j ] dp[i][j] dp[i][j] 表示位于自动机 i i i 结点且当前状态的 h a s h hash hash 值为 j j j 时的最大匹配数。
转移方程: d p [ n e x t [ i ] [ x ] ] [ h a s h 2 ] = m a x ( d p [ n e x t [ i ] [ x ] ] [ h a s h 2 ] , d p [ i ] [ h a s h 1 ] + e n d [ n e x t [ i ] [ x ] ] ) dp[next[i][x]][hash2] = max(dp[next[i][x]][hash2], dp[i][hash1]+end[next[i][x]]) dp[next[i][x]][hash2]=max(dp[next[i][x]][hash2],dp[i][hash1]+end[next[i][x]])
i i i 可以转移到下个字符为 x x x 结点 n e x t [ i ] [ x ] next[i][x] next[i][x], h a s h 1 hash1 hash1 为状态 i i i 的 h a s h hash hash 值, h a s h 2 hash2 hash2 为状态 n e x t [ i ] [ x ] next[i][x] next[i][x] 的状态, e n d end end 为以下个节点为结尾的字符串数量。
详见代码。
Code:
#include
#include
#include
#include
#include
using namespace std;
const int MAXN = 500 + 10;
const int INF = 0x3f3f3f3f;
int n, kase = 1;
char buf[MAXN];
// num保存各个碱基的使用数量 hash表示各个碱基数量情况下的编号
int dp[MAXN][15000], num[4], Hash[45][45][45][45];
map<char, int> mp; // 映射碱基到数字
struct Trie
{
int next[MAXN][4], fail[MAXN], end[MAXN];
int root, L;
int newnode()
{
for (int i = 0; i < 4; i++)
{
next[L][i] = -1;
}
end[L++] = 0;
return L - 1;
}
void init()
{
memset(num, 0, sizeof(num));
memset(next, 0, sizeof(next));
memset(fail, 0, sizeof(fail));
memset(end, 0, sizeof(end));
L = 0;
root = newnode();
}
void insert(string buf)
{
int len = buf.length();
int now = root;
for (int i = 0; i < len; i++)
{
int idx = mp[buf[i]];
if (next[now][idx] == -1)
{
next[now][idx] = newnode();
}
now = next[now][idx];
}
end[now]++;
}
void build()
{
queue<int> Q;
fail[root] = root;
for (int i = 0; i < 4; i++)
{
if (next[root][i] == -1)
{
next[root][i] = root;
}
else
{
fail[next[root][i]] = root;
Q.push(next[root][i]);
}
}
while (!Q.empty())
{
int now = Q.front();
Q.pop();
// 累计后缀中的匹配数量
if (end[fail[now]])
end[now] += end[fail[now]];
for (int i = 0; i < 4; i++)
{
if (next[now][i] == -1)
{
next[now][i] = next[fail[now]][i];
}
else
{
fail[next[now][i]] = next[fail[now]][i];
Q.push(next[now][i]);
}
}
}
}
void solve()
{
string str;
cin >> str;
int len = str.length();
memset(dp, -1, sizeof(dp));
// 计算字符串中各个碱基的数量
for (int i = 0; i < len; i++)
{
num[mp[str[i]]]++;
}
// 为所有碱基数量的情况进行编号
int idx = 0;
for (int i = 0; i <= num[0]; i++)
{
for (int j = 0; j <= num[1]; j++)
{
for (int k = 0; k <= num[2]; k++)
{
for (int l = 0; l <= num[3]; l++)
{
Hash[i][j][k][l] = idx++;
}
}
}
}
dp[0][0] = 0;
int ans = 0;
// 枚举所有碱基数量情况,更新dp
for (int r1 = 0; r1 <= num[0]; r1++)
for (int r2 = 0; r2 <= num[1]; r2++)
for (int r3 = 0; r3 <= num[2]; r3++)
for (int r4 = 0; r4 <= num[3]; r4++)
{
// 当前情况下的hash值
int now = Hash[r1][r2][r3][r4];
// 第一个要跳过
if (now == 0)
continue;
for (int i = 0; i < L; i++)
{
for (int x = 0; x < 4; x++)
{
// 子状态的hash值
int temp;
if (x == 0 && r1 >= 1)
temp = Hash[r1 - 1][r2][r3][r4];
else if (x == 1 && r2 >= 1)
temp = Hash[r1][r2 - 1][r3][r4];
else if (x == 2 && r3 >= 1)
temp = Hash[r1][r2][r3 - 1][r4];
else if (x == 3 && r4 >= 1)
temp = Hash[r1][r2][r3][r4 - 1];
else
continue;
// 子状态无法到达则略过
if (dp[i][temp] == -1)
continue;
// 更新dp值
dp[next[i][x]][now] = max(dp[next[i][x]][now], dp[i][temp] + end[next[i][x]]);
ans = max(ans, dp[next[i][x]][now]);
}
}
}
cout << "Case " << kase++ << ": " << ans << endl;
}
};
Trie ac;
int main()
{
mp['A'] = 0;
mp['C'] = 1;
mp['G'] = 2;
mp['T'] = 3;
while (cin >> n, n)
{
ac.init();
for (int i = 0; i < n; i++)
{
cin >> buf;
ac.insert(buf);
}
ac.build();
ac.solve();
}
return 0;
}
1024:Resource Archiver(AC自动机+SPFA+状压DP)
AC自动机的fail指针跑spfa状压DP求最短串?????
有兴趣的同学移步大佬博客看一看吧,估计我这辈子也做不出来这种题吧(呸!)
【END】感谢观看