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


    关于上次提出的“最长不完全匹配子串频率计算”的算法练习题,后来我看了下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()">
  • 相关阅读:
    UML 类之间的关系
    [noi.ac省选模拟赛]第11场题解集合
    [CF1270F]Awesome Substrings
    [noi.ac省选模拟赛20200606]赌怪
    FFT,NTT入门
    [AHOI2017/HNOI2017]单旋
    [HNOI2016]最小公倍数
    [HNOI2016]树
    [HNOI2016]大数
    服务外包概论课程资料
  • 原文地址:https://www.cnblogs.com/onlytiancai/p/1562840.html
Copyright © 2020-2023  润新知