题意:
现有一个奇葩的国家。总统不允许任何含有非法单词的句子出现。假定这个国家的句子长度固定为m。现给定大小为n 的字符集,和p个长度为min(m,10)的非法单词。任何包含非法单词的句子都是非法的。求所有合法句子的数目。
为了解决这个问题
我们先假定字符集{‘A’,’G’,’C’,’T’}
非法单词有2个,分别是”ACG”, “C”,句子长度为m
我们将非法串建立trie,得到如下的图
这张trie图和普通的trie树不同之处在于,它补全了所有trie树中原本不存在的边。每一个状态节点的某一个子节点若不存在,则将其指向这个状态节点的fail节点所对应的那个相同的子节点。即 若 ch[r][c]==0,ch[r][c]=ch[f[r]][c](大白书的写法) 这样一来原来的trie树变成了一张有向的状态转移图。从trie图中的根节点开始,沿着有向边走m步,即可得到一个长度为m的字串。
我们知道的是,自动机上的字串匹配其实就是各个状态间的转移,后缀与前缀之间的互相匹配的过程。不包含非法单词其实可以理解为,在走m步的过程中没有经过任何trie树上的单词结尾状态节点,也就是上面trie图中所标出的红色节点。因为无论trie图中的状态从何转移而来,其已经匹配过的字串必定包含从根节点到当前状态节点的路径所代表的非法单词。而包含非法单词的句子是不被允许的,因此在沿着有向边转移m次的过程中,不能转向任何非法单词结尾状态节点。同理,也不能从任何的非法节点转出。
现在问题变成了,从根节点开始转移m次至合法节点,共有多少种不同的方案。设计二维状态dp[i][j]表示第i步走到第j个节点的方案总数。
转移方程:dp[i][j] += dp[i-1][k](k到j有一条有向边且k,j均不为非法节点)
dp边界状态为:dp[0][0]=1; dp[0][k] = 0;(0<k<sz,sz为所有状态节点总数目)
需要注意的有两点:
1. 可能某一个非法单词包含了另一个非法单词。如上图中’ACG’和单词’C’,在这种情况下,2状态节点也需要被标记为非法的状态。因为匹配了’ACG’就一定匹配了’C’。在实际过程中只需要判下last[j]==root
2. 题中没有提到取模,因此要使用大数相加。
下面帖代码:
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
#define maxnode 105
#define sigma_size 52
using namespace std;
const int base = 10;
int n, m, p;
int ch[maxnode][sigma_size];
int val[maxnode];
int sz;
int f[maxnode];
int last[maxnode];
char charset[maxnode];
char ban[12];
int get(char a)
{
for(int i = 0; i < n; i++)
if(charset[i] == a)
return i;
return -1;
}
void initial()
{
memset(ch[0], 0, sizeof(ch[0]));
sz = 1;
}
void insert(char *s)
{
int l = strlen(s);
int u = 0;
for(int i = 0; i < l; i++)
{
int c = get(s[i]);
if(!ch[u][c])
{
memset(ch[sz], 0, sizeof(ch[sz]));
val[sz] = 0;
ch[u][c] = sz++;
}
u = ch[u][c];
}
val[u] = 1;
}
void getfail()
{
queue<int >q;
f[0] = 0;
for(int c = 0; c < n; c++)
{
int u = ch[0][c];
if(u)
{
f[u] = 0;
q.push(u);
last[u] = 0;
}
}
while(!q.empty())
{
int r = q.front();
q.pop();
for(int c = 0; c < n; c++)
{
int u = ch[r][c];
if(!u)
{
ch[r][c] = ch[f[r]][c];
continue;
}
q.push(u);
int v = f[r];
while(v && !ch[v][c])
v = f[v];
f[u] = ch[v][c];
last[u] = val[f[u]] ? f[u] : last[f[u]];
// if(last[u])
// val[u] = 1;
}
}
}
struct BigInt
{
int v[maxnode], len;
BigInt(int r = 0)
{
memset(v, 0, sizeof(v));
for(len = 0; r > 0; r /= base)v[len++] = r % base;
}
BigInt operator + (const BigInt &a)
{
BigInt ans;
int i , c = 0;
for(i = 0; i < len || i < a.len || c > 0; i++)
{
if(i < len)c += v[i];
if(i < a.len)c += a.v[i];
ans.v[i] = c % base;
c /= base;
}
ans.len = i;
return ans;
}
void print()
{
printf("%d", len == 0 ? 0 : v[len - 1]);
for(int i = len - 2; i >= 0; i--)
printf("%d", v[i]);
printf("\n");
}
};
BigInt dp[52][maxnode];
int check(int k, int j)
{
int ans = 0;
for(int i = 0; i < n; i++)
if(ch[k][i] == j)
ans++;
return ans;
}
int main()
{
initial();
scanf("%d%d%d", &n, &m, &p);
scanf("%s", charset);
while(p--)
{
scanf("%s", ban);
insert(ban);
}
getfail();
//初始化边界条件
for(int i = 0; i <= m; i++)
for(int j = 0; j < sz; j++)
{
dp[i][j] = BigInt();
}
dp[0][0] = BigInt(1);
//dp过程
for(int i = 1 ; i <= m; i++)
{
for(int j = 0; j < sz; j++)
{
if(val[j] || val[last[j]])
continue;
for(int k = 0; k < sz; k++)
{
if(val[last[k]] || val[k])
continue;
int ti=check(k, j);
for(int c=0;c<ti;c++)
dp[i][j] = dp[i][j] + dp[i - 1][k];
}
}
}
BigInt ans = BigInt();
for(int i = 0; i < sz; i++)
{
ans = ans + dp[m][i];
}
ans.print();
return 0;
}