蛙蛙推荐:统计最长不完全匹配子串频率的非递归解法(动态规划)

关于上次提出的“最长不完全匹配子串频率计算”的算法练习题,后来我看了下google的分析及其它人的解法,知道了这个题要靠“动态规划”来解决,我把其中一份c++代码转换成c#的了,确实很精妙。
算法描述再说一下:
找出一个长字符串里的某个特定的子串出现的频率,匹配的子串的上一个字符和下一个字符不需要紧紧相邻,只要满足下一个字符在当前字符的后面就行。
算法要求:长字符串的宽度最长是500个字符。
输入:一个长字符串,宽度不能超过500个字符,一个短字符串
输出:短字符串在长字符串中出现的次数的后四位,不足四位左边填充零。

举例来说:在“wweell”字符串中找“wel”出现的次数,可以匹配到8次,应输出0008,每个匹配子串的索引序列分别如下
0,2,4
0,2,5
0,3,4
0,3,5
1,2,4
1,2,5
1,3,4
1,3,5

看来算法这东西确实也得需要积累,刚开始脑子里根本没有动态规划这个词,看到这样的题目,能想到的也就是递归来解决,但递归的复杂度很高,对于很大的输入,要计算很长时间才能计算出来,所以该题目递归是最直观的解法,但却是最差的解法,因为要反反复复遍历很多次input字符串,复杂度是指数级的。

后来code jam的资格赛已经过去了,官方给出了算法的分析,而且高手们的解决方案代码也可以下载下来学习,我才知道这道题是一个典型的用动态规划来解决的问题。我去百度了一下,说动态规划是运筹学的一个分支,是求解决策过程最优化的数学方法,地址如下。
http://bk.baidu.com/view/28146.htm
这个定义根本看不懂,后来又找了本书看了看,说一般的递归可能会重复计算,就是第一次递归计算了一些值,而第二次递归的时候又重复计算了这些值,因此浪费了CPU资源,如果把每次计算的值放入一个表里,下一次计算能使用上一次计算出来的结果,或者直接初始化一个表里放入循环计算的一些数据,这样就可以代替复杂的递归,降低算法的复杂度,提高效率,这种做法就叫做动态规划。

具体到这个案例来说,我们是在一个长字符串S中找出小字符串s的出现次数,如果s是个单个字符"x",那就遍历一次S,找出有多少个"x"就行了,如果s是两个字符"xy",那么先找到有多少个y,再看有多少x在y的左边就能算出S中有多少xy了,依次类推,如果s是"xyz",就是先找到所有的z,再看有多少个"xy"在z的左边,然后把每个z的算出来的结果技术加起来就是最终结果了,举例如下。
在"xyyzxyz"中找出"xyz"的出现次数,先找到所有的z,索引下表分别是3和6(从0开始算)。
然后以第一个z左边有2个“xy”,索引序列分别是01,02,第二个z左边有4个"xy",索引序列分别是01,02,05,45,两个结果一加得出最终结果是6。

基于以上的原理,我们声明一个二维数组DP,第一维的长度是input的长度,第二维的长度是匹配字符的长度,如下
int[,] DP = new int[input.Length, math.Length];
然后从左向右遍历input,把第i次循环的结果放到DP[i,XXX]里,XXX的取值范围是从0到math.Length,比如下面几个实例
DP[i,0]表示第i次循环中有多少个x字符,
DP[i,1]表示第i次循环中有多少个xy字符,
DP[i,2]表示第i次循环中有多少个xyz字符
如果循环到第i次,要得出DP[i,1]的值,可以利用DP[i-1,0]的值,因为DP[i-1,0]存着i的左边有多少个x字符。
如果第i个字符是y,那么DP[i,1]就是DP[i-1,0]+DP[i-1,1],加号的前面保存着索引为i的y前面有多少个x,也就是本次发现了多少个xy,加号的右边是之前找到了多少个xy字符,总共加起来就是扫描到i的时候共找到多少次xy字符。
如果第i个字符不是y,那么DP[i,1]就和DP[i-1,1]的值一样,因为本次没有扫描出xy字符,还是上一次的技术。
依次把input扫描完毕,那么最终结果就是DP[input.Length - 1, math.Length - 1]。

原理整明白了,代码就好写了,但难就难在原理不容易整明白,google code jam的算法分析是用英文描述的,我看了好几个晚上没看懂到底是啥意思,最后找了一个选手的源码,对照着分析,才终于开窍了,算是明白了,我把其中一位中国选手littlepig的c代码转成c#的代码了,大家研究研究,代码及其简洁,比我自己写的简单多了,而且效率也高多了,复杂度是O(n)。

private   static   string  math  =   " welcome to code jam " ;
private   static   string  input  =   " weeeeeeeeeeeeeeeeeeeeellllllllllllllllccccoooommmmmee to code qps jam " ;

public   static   void  add( ref   int  i,  int  Delta)
{
    i 
+=  Delta;
    
if  (i  >=   10000 )
        i 
-=   10000 ;
}
private   static   void  Main() {
    
int [,] DP  =   new   int [input.Length, math.Length];
    
for  ( int  i  =   0 ; i  <  input.Length; i ++ ) {
        
if  (input[i]  ==  math[ 0 ])
            add(
ref  DP[i,  0 ],  1 );
        
if  (i  ==   0 continue ;
        
for  ( int  j  =   0 ; j  <  math.Length; j ++ ) {
            
if  (j  >=   1   &&  input[i]  ==  math[j])
                add(
ref  DP[i, j], DP[i  -   1 , j  -   1 ]);
            add(
ref  DP[i, j], DP[i  -   1 , j]);
        }
    }
    
int  ret  =  DP[input.Length  -   1 , math.Length  -   1 ];
    Console.WriteLine(ret);
    Console.ReadKey();
}

其中add方法里大于1000后减去1000,是为了防止溢出,因为结果只要出现次数的后四位,算法还得继续多加练习,入围的题目都折腾一个多礼拜才看明白,真是郁闷呀。
相关链接:
蛙蛙推荐:[算法练习]最长不完全匹配子串频率计算
后记,评论里:司徒正美说想看看js是如何实现该算法的,我刚才写了一个,ie8,ff3下测试通过,代码如下
< script  type ="text/javascript" >
    
var  math  =   " welcome to code jam " .split( '' );
    
var  input  =   " weeeeeeeeeeeeeeeeeeeeellllllllllllllllccccoooommmmmee to code qps jam " .split( '' );

    
function  fun() {
        
var  DP  =   new  Array();
        
for  (i  =   0 ; i  <  input.length; i ++ ) {
            DP[i] 
=   new  Array();
            
for  (j  =   0 ; j  <  math.length; j ++ )
                DP[i][j] 
=   0 ;
        }
        
for  (i  =   0 ; i  <  input.length; i ++ ) {
            
if  (input[i]  ==  math[ 0 ])
                DP[i][
0 =  DP[i][ 0 +   1 ;
            
if  (i  ==   0 continue ;
            
for  (j  =   0 ; j  <  math.length; j ++ ) {
                
if  (j  >=   1   &&  input[i]  ==  math[j])
                    DP[i][j] 
=  (DP[i][j]  +  DP[i  -   1 ][j  -   1 ])  %   10000 ;
                DP[i][j] 
=  (DP[i][j]  +  DP[i  -   1 ][j])  %   10000 ;
            }
        }
        
var  ret  =  DP[input.length  -   1 ][math.length  -   1 ];
        alert(ret);
        
return   false ;
    }
</ script >

< input  type ="button"  value ="click"  onclick ="return fun()" >
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值