算法设计-帮帮忙
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法设计-帮帮忙相关的知识,希望对你有一定的参考价值。
这样的,有如下问题:
[数字搜索]
有正则表达式
1. 0, 1, …, 9,0*, 1*, …, 9*是正则表达式;
2. 如果A和B是正则表达式,则(A),A+B,AB,(A)*都是正则表达式;
3. 只有按以上方法构造出来的表达式才是正则表达式。
其中,A+B表示“或者”关系,AB表示“连接”关系,(A)*表示A的内容“重复”零次或者多次。比如正则表达式(12+3)(4+5)6*,就可以匹配以124,125,34,35之一开头,之后接零0个或任意多个6的字符串(例如字符串12566)。正则表达式(1+0)*可以匹配所有由0和1构成的字符串,或者是空串。如果一个正则表达式不能匹配空串,则称它是非空的。只考虑非空。
如果在给定文本的某一个位置,存在一个以该位置结束的子串,能够被给定的非空正则表达式匹配,则称该位置是可匹配的。
任务就是找出所有可匹配的位置
表达式长500以内
匹配的字符串长10,000,000以内!
要求1s以内出解,内存64M
求算法,给Pascal、C、C++、Basic代码或伪代码,附上思路就行,谢谢
经研究,放松时限为10s,呵呵
否则太累了
--我们必须尽量优化程序才能达到题目的要求。显然n实在是太大了,所以我们不但不可能忍受O(n2)的复杂度,甚至连n前面的系数过大都会造成超时。为了尽量减小时间复杂度,我们只能依靠自动机的理论。
自动机理论
一个DFA(确定性有限自动机)是一个五元组<∑, U, s, T, φ>,其中∑是字符集U 是一个有限的状态集合, s 是 U 的一个元素表示初始状态, T 是 U的一个子集,表示终止状态and φ : U × ∑ → U 是转移函数。
开始时自动机在初始状态s,每步它读入一个字符c,然后根据state=φ(state,c)进行状态的转移。如果当字符串结束时它达到了终止状态,那么就说它接受了字符串。
NFA(非确定性自动机)与DFA的主要区别就是,NFA从一个状态按照一个字符转移到的状态可能有多个。因此NFA需要使用一个状态集合来表示当前可能处在的状态。
把自动机用图来表示的话,一般用结点表示状态,而结点之间有标有某个字符的有向边,表示可以从边的起点通过这个字符转移到边的终点。
此外NFA还可以包含标有ε的边,这种边表示不需要任何字符就可以直接进行状态的转移。
容易想到,如果我们建立了一个只接受符合正则表达式的自动机,就可以通过它来较快的判断哪些位置是可匹配的。
具体应用
比较常见的字符串匹配自动机大多都是DFA。但是由于有+,*,把正则表达式直接转变成DFA是非常困难的。因此我们只能先把它转化成NFA。
先看一下一般的转换规则:
如果正则表达式是一个字符,那么对应的NFA是:
p0-a->p1
如果正则表达式是A+B的形式,那么只要把它们的起始状态和终止状态合并就可以:
p0-a->p1
p0-b->p1
如果正则表达式是AB的形式,那么只要把A和B对应的自动机首尾相接:
p0-a->p1-b->p2
如果正则表达式是A*的形式,那么就需要按下面的图来进行处理:
p0-a->p0
p0->p1
通过上面的步骤可以得出:NFA的状态数不会超过正则表达式长度。
NFA中的状态转移
在NFA中进行状态转移比DFA要困难一些。我们可以定义两个函数,一个叫做epsilon-closure,用来计算一组NFA状态通过epsilon边能够到达的NFA状态集合。另外一个move用来在NFA上进行状态的转移。
设当前的状态集合是now,那么状态转移的过程就是:
new =
for every state s in now do
new = new ∪ t | (s,t) = c
new = epsilon_closure(new)
其中的(s,t)=c表示从s状态经过一个字符c可以转化到t状态。因为这个语句需要的集合总数也不会太多,所以可以通过预先计算出它们来加快速度。预处理后这个过程的复杂度是O(m2),而系数是比较小的。
但是由于在NFA中,我们必须把状态的集合作为一个真正的状态,每次状态转移都是对这个集合的处理。这样转移一次的复杂度是O(m2),这是不可忍受的(最大的数据需要大约2分钟才能出解)。
既然NFA速度过慢,我们自然想要把它转化为DFA。因为DFA中的状态转移是O(1)的。可是并没有一个多项式复杂度的方法可以把NFA转换成DFA。一般介绍的NFA转换到DFA的方法都是通过类似BFS的方法来把NFA中所有可能出现的状态集合对应成DFA的一个状态。
这种转换在本题中显然是不可行的,因为NFA的结点数最多是500,而转化成的DFA则可能有多达2500个状态,即使实际有许多状态不能达到,也是无论如何不可以忍受的。
看来把NFA转换成DFA是行不通的。但是我们还是从NFA转DFA的过程中受到了一些启发:NFA之所以速度慢,是因为我们在进行状态转移的时候可能进行许多重复操作:比如从1,2沿一个字符1转移后是2,3,以后我们还可能遇到相同的情况,而这时我们还会用O(m2)的时间去进行状态转移。这是一种很严重的浪费。因此我们每进行一次状态转移,就把这个状态转移的情况放到hash表中,以后使用的时候就可以查找了。这相当于只把我们需要的一部分子集转移成DFA,虽然一般情况下并不能降低时间复杂度,但是在实际应用中确实很有效。(特别是对于没有*和括号的自动机,这样做可以保证线形的时间复杂度)
算法回顾和细节
我们重新来看一下算法的框架:
根据正则表达式建立NFA
now = start
while not eof do begin
read(c);
if 曾经进行过now,c的转移 then 利用以前的结果
else 进行状态转移并记录结果
if 终止状态 in now then 输出当前位置
end
建立NFA有各种方法,由于NFA并不会很大,所以我们可以使用递归的方法来实现。为了尽量减小系数,我们使用位压缩的方式来实现集合,并采用较为简化的hash函数(比如把一个集合位压缩后的一系列整数加起来取对220的余数),并在输入和输出的时候通过缓冲区来加快速度。这样就基本可以达到题目的时间要求。
但是注意到字符串可能有10M,而产生的NFA的状态集合最多也可能有10M个,而每个集合都需要100到200字节的存储空间,大大超出了允许的空间范围。因此我们需要对hash表的规模加以严格限制。如果规模过大就不得不放弃存储状态的想法,牺牲一定的时间来换取空间。 参考技术A
第5章:常用算法设计(软件设计篇)免费下载
链接:https://pan.baidu.com/s/1EsbJAHii_AXsOL6JuSnRHw
《算法设计》是2007年清华大学出版社出版发行的图书,作者是Jon Kleinberg / Éva Tardos。
求大神帮忙算法分析(请写出下列各算法的功能)
算法分析(请写出下列各算法的功能)
Void a2()
Queue Q;
InitQueue(Q);
Char x=’e’,y=’c’;
EnQueue(Q,’h’);
EnQueue(Q,’r’);
EnQueue(Q,’y’);
x=DeQueue(Q); EnQueue(Q,x);
x=DeQueue(Q); EnQueue(Q,’a’);
While(!QueueEmpty(Q))
y=DeQueue(Q);
Printf(“%c”,y)
Printf(“%c”,x);
Void a2()
Queue Q; /* 定义队列 */
InitQueue(Q); /* 初始化队列 */
Char x=’e’,y=’c’; /* 定义两个字符变量并初始化 */
EnQueue(Q,’h’); /* 把h入队 */
EnQueue(Q,’r’); /* 把r入队 */
EnQueue(Q,’y’); /* 把y入队 */
x=DeQueue(Q); EnQueue(Q,x); /* 把队首的h出队到x后,移到队尾 */
x=DeQueue(Q); EnQueue(Q,’a’); /* 把队首的r出队到x,再将a入队 */
While(!QueueEmpty(Q)) /* 当队不空时做 */
y=DeQueue(Q); /* 出队元素放到y */
Printf(“%c”,y) /* 打印 y */
Printf(“%c”,x); /* 打印 x */
实际打印输出为:
yhar 参考技术A InitQueue是初始化队列,EnQueue是将某个元素入队,QueueEmpty是判断队列是否为空,DeQueue是出队。
以上是关于算法设计-帮帮忙的主要内容,如果未能解决你的问题,请参考以下文章
设计一个 O(n) 算法来找到一个不在 [0,n-1] 范围内的数字 [重复]