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

来源:互联网 发布:动态gif软件下载 编辑:程序博客网 时间:2024/06/05 15:09
关于上次提出的“最长不完全匹配子串频率计算”的算法练习题,后来我看了下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 == 0continue;
        
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 == 0continue;
            
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()">