有些时候,我们会碰到这样的一类问题:给定n个字符串,求长度为m的(不)包含这n个字符串的字符串个数。即字符串匹配的计数问题。
我们先来看这样一道题:
例题一
【poj 2778】给出m个疾病基因片段(m<=10),每个片段不超过10个字符。求长度为n的不包含任何一个疾病基因片段的DNA序列共有多少种?(n<=2000000000)
【分析】
part1:
我们先考虑一下朴素的dp:设dp(i,j)dp(i,j)dp(i,j)表示用了i个字符,拼出了符合题意的字符串j的方案数。则dp(i,j)=∑j′⊆jdp(i−1,j′)dp(i, j)=\sum_{j' \subseteq j}{dp(i-1,j')}dp(i,j)=j′⊆j∑dp(i−1,j′)
这个想法不得不说着实不好
part2:
考虑使用AC自动机。则原问题转化为:求使这m个字符串都匹配不上的字符串的个数。所以,匹配过程中经过的点,其fail链上的节点(包括本身)都不能有结束节点。
按照这个想法,我们修改一下状态:设dp(i,j)dp(i,j)dp(i,j)表示从根节点开始走i步到j号节点的方案数(路径合法),则:
dp(i,j)=∑!t[k].flagdp(i−1,k)(t[k].flag表示k号节点的fail链上是否有结束节点)dp(i,j)=\sum_{!t[k].flag}{dp(i-1,k)}(t[k].flag表示k号节点的fail链上是否有结束节点)dp(i,j)=!t[k].flag∑dp(i−1,k)(t[k].flag表示k号节点的fail链上是否有结束节点)
当n的范围较小时,这个算法足以解决问题(比方说下面的那道题)。但发现n的范围……GG
part3:
再仔细分析一下。如果我们刚拿到AC自动机,可以得出的是到了节点i,若打算匹配字符x将会跳转到节点j。于是,我们如果把这个关系看成一个图,则对于每一个这样的关系,给i和j连边,得到了一个邻接矩阵G。
不难发现,G[i][j]G[i][j]G[i][j]表示从i走一步到j的方案数。
联想到矩阵乘法:
c[i][j]=∑k=1na[i][k]∗b[k][j]c[i][j]=\sum_{k=1}^na[i][k]*b[k][j]c[i][j]=k=1∑na[i][k]∗b[k][j]
根据乘法原理+加法原理,若将G[i][j]G[i][j]G[i][j]平方,得到的新矩阵则表示从i走两步到j的方案数。
推广一下:Gn[i][j]G^n[i][j]Gn[i][j]表示从i走n步到j的方案数。
于是我们用矩阵快速幂求得G的n次方G’,则有:
ans=∑i=0num∣矩阵大小G′[0][i]ans = \sum_{i=0}^{num|矩阵大小}G'[0][i]ans=i=0∑num∣矩阵大小G′[0][i]
至此,本题完美解决
【代码】
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define ll long long
using namespace std;
const int mm = 15, mod = 100000;
int cnt;
struct trie
{
int son[4], fail;
bool flag;
} t[105];
struct matrix
{
ll a[105][105];
matrix(){memset(a, 0, sizeof a);}
} ret, h;
matrix operator *(const matrix &a, const matrix &b)
{
matrix ret;
for(int i = 0; i <= cnt; i++)
for(int j = 0; j <= cnt; j++)
for(int k = 0; k <= cnt; k++)
ret.a[i][j] += (a.a[i][k] * b.a[k][j]) % mod, ret.a[i][j] %= mod;
return ret;
}
queue<int > q;
bool vis[10005];
char s[mm];
void ksm(int b)
{
while(b)
{
if(b & 1)
ret = ret * h;
h = h * h, b >>= 1;
}
}
inline int getnum(char c)
{
if(c == 'A')
return 0;
else if(c == 'C')
return 1;
else if(c == 'G')
return 2;
else
return 3;
}
void push()
{
int n = strlen(s), r = 0;
for(int i = 0; i < n; i++)
{
int v = getnum(s[i]);
if(!t[r].son[v])
t[r].son[v] = ++cnt;
r = t[r].son[v];
}
t[r].flag = 1;
}
void get_fail()
{
q.push(0);
while(!q.empty())
{
int s = q.front();
q.pop();
for(int i = 0; i < 4; i++)
{
int to = t[s].son[i];
if(to)
q.push(to);
if(!s)
{
if(to)
t[to].fail = 0;
else t[s].son[i] = 0;
}
else
{
if(to)
t[to].fail = t[t[s].fail].son[i], t[to].flag|=t[t[to].fail].flag;
else
t[s].son[i] = t[t[s].fail].son[i];
}
to = t[s].son[i];
if(!t[to].flag)
h.a[s][to]++;
//对于修改儿子节点的分析见例题二
}
}
}
int main()
{
int n, m, i;
scanf("%d%d", &m, &n);
while(m--)
scanf("%s", s), push();
get_fail();
for(i = 0; i <= cnt; i++)
ret.a[i][i] = 1;
ksm(n);
ll ans = 0;
for(i = 0; i <= cnt; i++)
ans += ret.a[0][i], ans %= mod;
printf("%lld\n", ans);
}
例题2
【bzoj1030】题意:JSOI交给队员ZYX一个任务,编制一个称之为“文本生成器”的电脑软件:该软件的使用者是一些低幼人群,
他们现在使用的是GW文本生成器v6版。该软件可以随机生成一些文章―――总是生成一篇长度固定且完全随机的文
章—— 也就是说,生成的文章中每个字节都是完全随机的。如果一篇文章中至少包含使用者们了解的一个单词,
那么我们说这篇文章是可读的(我们称文章a包含单词b,当且仅当单词b是文章a的子串)。但是,即使按照这样的
标准,使用者现在使用的GW文本生成器v6版所生成的文章也是几乎完全不可读的?。ZYX需要指出GW文本生成器 v6
生成的所有文本中可读文本的数量,以便能够成功获得v7更新版。你能帮助他吗?
Input
输入文件的第一行包含两个正整数,分别是使用者了解的单词总数N (<= 60),GW文本生成器 v6生成的文本固
定长度M;以下N行,每一行包含一个使用者了解的单词。这里所有单词及文本的长度不会超过100,并且只可能包
含英文大写字母A…Z
Output
一个整数,表示可能的文章总数。只需要知道结果模10007的值。
【分析】
正面处理有些困难,所以我们转而求不合法的方案数。
那么好像就跟上面那个题一样了
如果按照part3的方法做的话,发现连一次矩阵乘法都做不完,还会炸空间
此时我们就可以按照part2的方法来做:
设dp(i,j)dp(i,j)dp(i,j)表示从根节点开始走i步到j号节点的方案数(路径合法),则:
dp(i,j)=∑!t[k].flagdp(i−1,k)(t[k].flag表示k号节点的fail链上是否有结束节点)dp(i,j)=\sum_{!t[k].flag}{dp(i-1,k)}(t[k].flag表示k号节点的fail链上是否有结束节点)dp(i,j)=!t[k].flag∑dp(i−1,k)(t[k].flag表示k号节点的fail链上是否有结束节点)
【代码】
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int mn = 6005, mod = 10007;
struct trie
{
int son[26], fail;
bool flag;
}t[mn];
queue<int >q;
int f[mn][mn], cnt;
char s[mn];
void push()
{
int n = strlen(s), from = 0;
for(int i = 0; i < n; i++)
{
int to = s[i] - 'A';
if(!t[from].son[to])
t[from].son[to] = ++cnt;
from = t[from].son[to];
}
t[from].flag = 1;
}
void init()
{
for(int i = 0; i < 26; i++)
if(t[0].son[i])
q.push(t[0].son[i]);
while(!q.empty())
{
int from = q.front(); q.pop();
for(int i = 0; i < 26; i++)
{
int to = t[from].son[i];
if(to)
t[to].fail = t[t[from].fail].son[i], q.push(to), t[to].flag |= t[t[to].fail].flag;
else
t[from].son[i] = t[t[from].fail].son[i];
//由于对儿子节点的修改,son[i]的含义发生了改变。它不再表示儿子节点的位置,而是表示从该节点开始走一步到达的节点位置
}
}
}
int ksm(int a, int b)
{
int ret = 1, h = a;
while(b)
{
if(b & 1)
ret *= h, ret %= mod;
h *= h, h %= mod, b >>= 1;
}
return ret;
}
int main()
{
int n, m, i, j, k;
scanf("%d%d", &n, &m);
for(i = 1; i <= n; i++)
scanf("%s", s), push();
init(), f[0][0] = 1;
for(i = 1; i <= m; i++)
for(j = 0; j <= cnt; j++)
if(!t[j].flag)
for(k = 0; k < 26; k++)
f[i][t[j].son[k]] += f[i - 1][j], f[i][t[j].son[k]] %= mod;
int ans = 0;
for(i = 0; i <= cnt; i++)
if(!t[i].flag)
ans += f[m][i], ans %= mod;
printf("%d\n", ((ksm(26, m) - ans) % mod + mod) % mod);
}