[Pku 2778 3208] 字符串(五) {有限状态自动机}

{

本文接着上一篇文章

{请先阅读上一篇文章AC自动机上DP由AC自动机构造DFA的段落}

这篇文章着重讨论确定性有限状态自动机

在字符串处理方面的应用

}

 

确定性有限状态自动机(DFA)是一个有向图

相比AC自动机而言 DFA在字符串处理方面更为自由 更为强大

同时 对对自动机的理解也有更高的要求

本文避开一些理论知识 从几个具体问题开始探讨DFA的应用

 

预备问题 Pku1625

题意是统计不包含某些串的串的个数

思路是建立AC自动机然后递推 (也由AC自动机可以构造出DFA)

记录Opt[i][j]为主串第i位 在自动机上到达j这个节点的方案个数

Opt[i][j]=Sum{Opt[i-1][Prev[j]]} {Prev[j]和j都是安全的}

由于自动机不记录前驱而是记录后继 这个方程可以顺推实现

求出Opt[n][i]累加就是答案了

最后由于数据的范围 要用高精度运算来实现求和

写的不是很详细 参考上一篇文章的思路即可

 

问题1. Pku2778

题意和Pku1625基本类似

只不过不是求具体答案而是求答案Mod 100000的值了

由于舍去了高精度运算 我们可以很容易的写出代码

先由AC自动机构造出DFA 再在DFA上进行一遍DP

方程同Pku 1625 注意求模

DNA Sequence DP
const maxn=100;
maxm
=50000;
base
=100000;
var tr:array['A'..'Z']of longint;
n,m,i,j,k,tt,h,t,p,root,ans:longint;
s:
array[1..maxn,1..4]of longint;
opt:
array[0..maxm,1..maxn]of longint;
f,q:
array[1..maxn]of longint;
d:
array[1..maxn]of boolean;
ch:char;
procedure allot(var x:longint);
var i:longint;
begin
inc(tt); x:
=tt;
d[x]:
=false; f[x]:=0;
for i:=1 to 4 do
s[x][i]:
=0;
end;
begin
assign(input,
'DNAS.in'); reset(input);
assign(output,
'DNAS2.out'); rewrite(output);
tr[
'A']:=1; tr['C']:=2;
tr[
'G']:=3; tr['T']:=4;
readln(n,m);
tt:
=0; allot(root);
for i:=1 to n do
begin
p:
=root;
while not eoln do
begin
read(ch);
k:
=tr[ch];
if s[p][k]=0
then allot(s[p][k]);
p:
=s[p][k];
end;
d[p]:
=true;
readln;
end;
h:
=1; t:=0;
f[root]:
=root;
for i:=1 to 4 do
if s[root][i]=0
then s[root][i]:=root
else begin
inc(t);
q[t]:
=s[root][i];
f[q[t]]:
=root;
end;
while h<=t do
begin
for i:=1 to 4 do
begin
p:
=f[q[h]];
while (p<>root)and(s[p][i]=0) do
p:
=f[p];
if s[p][i]=0
then p:=root
else p:=s[p][i];
if s[q[h]][i]=0
then s[q[h]][i]:=p
else begin
inc(t);
q[t]:
=s[q[h]][i];
if d[p] then d[q[t]]:=true;
f[q[t]]:
=p;
end;
end;
inc(h);
end;
fillchar(opt,sizeof(opt),
0);
opt[
0][root]:=1;
for i:=0 to m-1 do
for j:=1 to tt do
for k:=1 to 4 do
begin
n:
=s[j][k];
if not(d[j]) and not(d[n])
then begin
t:
=i+1;
opt[t][n]:
=(opt[t][n]+opt[i][j])mod base;
end;
end;
ans:
=0;
for i:=1 to tt do
ans:
=(ans+opt[m][i])mod base;
writeln(ans);
close(input); close(output);
end.

这个算法的复杂度达到了O(NM) 其中N为主串长 M为模式串长度总和

再看数据范围N<=2,000,000,000 很大 不可能AC

我们考虑优化 这个递推本质上求的是在DFA上从Root开始长为N的路径的条数

当然 路径不经过模式串串尾 也就是所谓的危险节点 忽略这些点即可

这是一个经典的问题 有向图上从A到B长为N的路径的条数

运用矩阵乘法即可 证明有各种方法 可以数学归纳法

而本质上是运用矩阵乘法概括了上面的DP方程

 由于矩阵乘法的结合律 运用快速幂可以使复杂度达到O(M^3*Log2N)

可以AC本题

给出具体的代码

DNA Sequence Matrix
const maxn=100;
base
=100000;
type mat=array[1..maxn,1..maxn]of int64;
var tr:array['A'..'Z']of longint;
n,m,i,j,k,tt,h,t,p,root,ans:longint;
s:
array[1..maxn,1..4]of longint;
f,q:
array[1..maxn]of longint;
d:
array[1..maxn]of boolean;
g,opt,temp:mat;
ch:char;
procedure allot(var x:longint);
var i:longint;
begin
inc(tt); x:
=tt;
d[x]:
=false; f[x]:=0;
for i:=1 to 4 do
s[x][i]:
=0;
end;
procedure matrix(var a,b:mat);
var i,j,k:longint;
begin
for i:=1 to tt do
for j:=1 to tt do
begin
temp[i][j]:
=0;
for k:=1 to tt do
temp[i][j]:
=temp[i][j]+a[i][k]*b[k][j];
temp[i][j]:
=temp[i][j] mod base;
end;
a:
=temp;
end;
procedure power(x:longint);
begin
if x=1 then exit;
power(x
div 2);
matrix(opt,opt);
if x and 1=1 then matrix(opt,g);
end;
begin
assign(input,
'DNAS.in'); reset(input);
assign(output,
'DNAS3.out'); rewrite(output);
tr[
'A']:=1; tr['C']:=2;
tr[
'G']:=3; tr['T']:=4;
readln(n,m);
tt:
=0; allot(root);
for i:=1 to n do
begin
p:
=root;
while not eoln do
begin
read(ch);
k:
=tr[ch];
if s[p][k]=0
then allot(s[p][k]);
p:
=s[p][k];
end;
d[p]:
=true;
readln;
end;
h:
=1; t:=0;
f[root]:
=root;
for i:=1 to 4 do
if s[root][i]=0
then s[root][i]:=root
else begin
inc(t);
q[t]:
=s[root][i];
f[q[t]]:
=root;
end;
while h<=t do
begin
for i:=1 to 4 do
begin
p:
=f[q[h]];
while (p<>root)and(s[p][i]=0) do
p:
=f[p];
if s[p][i]=0
then p:=root
else p:=s[p][i];
if s[q[h]][i]=0
then s[q[h]][i]:=p
else begin
inc(t);
q[t]:
=s[q[h]][i];
if d[p] then d[q[t]]:=true;
f[q[t]]:
=p;
end;
end;
inc(h);
end;
for i:=1 to tt do
for j:=1 to 4 do
if not(d[i]) and not(d[s[i][j]])
then inc(g[i,s[i][j]]);
move(g,opt,sizeof(g));
power(m);
ans:
=0;
for i:=1 to tt do
ans:
=(ans+opt[1][i])mod base;
writeln(ans);
close(input); close(output);
end.

 

概括起来说 这个问题的思路就是 AC自动机+DP -> DFA+DP -> DFA+图论

实际上由于DFA的确定性和有限性 我们可以很轻松的把它和其他算法结合

从而得到更为简明高效的问题解决方案

实际上 如果不考虑DFA 单用AC自动机是不能解决这个问题的

通过上面的一个例子

我们可以发现DFA由AC自动机的优化产生 却拥有更强大的性质

而且 DFA的灵活性也更高 我们可以根据自己的需要 构造出合适的DFA

譬如我们可以使得DFA成为状态转移的利器 更方便我们DP

下面这个例子将说明这一点

 

问题2. Pku 3208

题意

  求所有合法解中排第K的解

  合法解是一个自然数 包含子串"666"

首先我们得知道求字典序排第K的问题的通用解法

这类问题可以转化为求方案数然后O(kN)递推来解决

先考虑最简单的方法 由小到大枚举出所有方案 然后得到第K个

虽然时间复杂度很高 但是却是我们O(kN)递推的基础

首先可以用DP等方法求出指定前缀时的方案数

对于每一个前缀 搜索的策略是遍历这个前缀下所有的方案

枚举出一个解就让计数器+1 不断的逼近排名K的方案

而有了求出的指定前缀的方案数

我们就可以大步地累加计数器 来逼近排名位为K的方案

假设我们已经求出了前i-1位正在求第i位

由字典序从小到大枚举第i位

每枚举出一位 我们就得到了一个长为i的前缀

判断计数器加上前缀状况下的方案数是不是大于K

大于就表明这一位就是当前枚举出的值

否则让计数器加上方案数 继续枚举

这样复杂度就是O(kN)了 k就是每一位可以取的值总数

那么这个问题就转化为求解长度为指定前缀的串的总数

通过前面几篇文章的几个问题

我们知道 具体走到DFA上的哪一个节点可以概括一个前缀

所以建立一个DFA 包含串"666" 边为'0'-'9'的字符

我们还是这样表示状态 第一维关于步数 第二维是节点

Opt[i][j]表示走到j这个节点再走i步能包含666的方案数

但是考虑到 包含666这个条件比较模糊

我们修改DFA 把666的词尾节点所有的边指向自己

那么只要再走i步停留在词尾节点就可以了

DP方程: Opt[i][j]=Sum{Opt[i-1][Next[j][k]]}

初始条件: Opt[0][666词尾节点]=1

这样这个问题就解决了

给出具体的代码

 

SomeDay
var s:array[1..4,0..9]of longint;
opt:
array[0..11,1..4]of int64;
ans:
array[1..11]of longint;
n,i,j,k,x,y,m,p,ca:longint;
begin
assign(input,
'SomeDay.in'); reset(input);
assign(output,
'SomeDay.out'); rewrite(output);
for i:=1 to 3 do
begin
for j:=0 to 9 do
s[i][j]:
=1;
s[i][
6]:=i+1;
end;
for j:=0 to 9 do
s[
4][j]:=4;
opt[
0][4]:=1;
for i:=1 to 11 do
for j:=1 to 4 do
for k:=0 to 9 do
opt[i][j]:
=opt[i][j]+opt[i-1][s[j][k]];
readln(ca);
while ca>0 do
begin
dec(ca);
readln(n);
for i:=1 to 11 do
if opt[i][1]>=n then break;
m:
=i; j:=0;
n:
=n-opt[i-1][1];
p:
=1;
while i>0 do
begin
if i=m
then k:=1
else k:=0;
for j:=k to 9 do
if n>opt[i-1][s[p][j]]
then n:=n-opt[i-1][s[p][j]]
else break;
ans[i]:
=j; dec(i);
p:
=s[p][j];
end;
for i:=m downto 1 do
write(ans[i]);
writeln;
end;
close(input); close(output);
end.

 

下一篇文章开始介绍后缀数组 功能强大 易于实现

 

Bob Han原创 转载请注明出处 http://www.cnblogs.com/Booble/

posted on 2010-12-09 21:35  Master_Chivu  阅读(1682)  评论(0编辑  收藏  举报

导航