人人都会动态规划

人人都会动态规划

动态规划简介

在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果。因此各个阶段决策的选取不能任意确定,它依赖于当前面临的状态,又影响以后的发展。当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线.这种把一个问题看作是一个前后关联具有链状结构的多阶段过程就称为多阶段决策过程,这种问题称为多阶段决策问题。在多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义,称这种解决多阶段决策最优化的过程为动态规划方法

说实话,上面的简介来自百度百科,我自己都有点懵

白话动态规划

第一:动态规划又称为Dynamic programming ,其实这里的programming指的时表格法,而译过来就是规划的意思,所以就称为动态规划,但是一般认为这是一个动态表格的做法。表格就是记录的东西嘛。
第二:动态规划又和分治算法很相近,分治算法会将子问题合并作为原问题的解,而动态规划会根据子问题的最优解合成原问题的最优解,其次,分治所产生的子问题都是新问题,而动态规划所使用的一般都是求解过的子问题,也就是重复求解,这是动态规划的特性二
第三:动态规划的4个基本过程:

  1. 刻画一个最优解的结构特征
  2. 递归定义最优解的值
  3. 计算最优解的值,通常采用自底向上的方法
  4. 利用计算出的信息构造一个最优解

灵魂拷问:你真的会暴力递归吗?你真的会求解递归的时间复杂度吗?

暴力递归补充

暴力递归 就是尝试,把问题转化为规模缩小了的同类问题的子问题,有明确的不需要继续进行 递归 的条件 (base case),有当得到了子问题的结果之后的决策过程,不记录每一个子问题的解。 父问题所作的决定的影响都要在子问题上得到体现。

暴力递归和枚举又不太相同,前者会重复计算相同子问题而耗费大量资源,而枚举会因为计算太多不相关问题而耗费大量资源,但是好在这两个思路都是较为简单,对于可分解子问题的问题还是比较容易实现的,但是往往都是不能ac其他题目的。
对于使用还是后面的例题精讲上面深入思考。

动态规划原理

你必须得明白,这两个基本的原理:

  1. 具有最优子结构性质:通俗的讲就是,原问题的最优解可以通过最问题的最优解得到,这里是不是会想到贪心算法,很好,贪心算法也在一定情况拥有这个性质。但是更官方的讲述就不尽如人意了,一个问题的最优解结构包含其子问题的最优解,这里就不能满足贪心的思路了,这里可以使用复制粘贴来证明,后面贪心再来证明一下吧。
  2. 具有重叠子问题性质:也就是要重复求解同一个子问题。对于这个性质,动态规划为什么要说明为表格法就知道了。
  3. 使用重叠子问题性质的常用方法:备忘方法:在递归求解的时候增加一个备忘录,然后直接查找解,这里要注意问题解的边界。表格方法:一般配合自底向上方法,没解决一个子问题,就将这个子问题记录下来,当需要使用这个子问题就将他的记录值取出使用。这里请注意描述,在备忘方法记录的是一定会使用的子问题解,而表格方法不一定解决一定需要的子问题,这也是两个方法的不同之处和优势所在

还有一个性质,也是需要了解以下的:
无后效性:

将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性

我们可以用这个性质验证是否可以使用动态规求解这个问题。
无权最短简单路径和无权最长简单路径的细微思考:
无权最短简单路径可以描述为,在一个有向图中,从一个点p到一个点g的最短路径选择。为什么可以说明这个问题有最优子结构性质并且可以使用动态规划解决。如果从p->g存在一条路径最短,说明其中会存在一点w从p->w 会最短,而w->g也会最短,证明也是使用复制粘贴方法,加入一个点w1 是p->w1最短,w1->g最短,这样就否定了w是符合点,相同的,也就是否定了一开始的条件,所以这个w1不存在。
现在你可能倾向于无权最长简单路径也是具有最优子结构性质,但是不然,需要证明就不去证明了。
而这两个问题的不同就在有子问题的相关与不相关性了,实际上无权最长简单路径还是一个Np难问题。这里就是因为后效性而否定使用动态规划求解。

上述部分:不是很必要,但是可以让你更好的掌握动态规划算法

从实战开始动态规划

说的再多都是纸上谈兵,要掌握算法在刷题路上需要掌握600~1000道题目才可以一窥算法的奥秘。这里具体用解题思路记录动态规划的实现和使用。
需要的前景知识,我们需要有很好的递归基础和很好的分治策略基础

钢条切割问题

某公司购买长钢条,将其切割为段钢条出售。切割工序本身没有成本支出。公司管理层希望知道最佳的切割方案。
假定我们知道公司出售一段长度为i英寸的钢条的价格为pi(i = 1, 2,…,单位为美元)。钢条的长度均为整英寸。

这个问题描述很耽误事,还是需要外部辅助查看这个问题。

问题分析

对于一根n长度的钢条,假如你只能截取一次,而这一次是你必须使得你所得到的价值是最高的,是不是需要去比较每个切割点获得的利益,选出最大。而切割点就是一个伤脑的问题,这里可以思考一下,是不是切割点只需要到中间就好,也就是长度到0~n/2,很好,这样就可以知道这个问题的最优解结构了。

对于一个n长的钢条,你需要切割n下,所以你可以从k开始切割,然后继续切0~k和k~n的钢条,这样就可以获得这个解
但是你不知道这个k在哪,所以这里直接遍历所有的切割点选取最好就行。
而切割点k产生的值是多少,就是左边可以产生的值+右边可以产生的值。

好的,这就是分治的策略。
Code

// 暴力递归
private static int getMaxPriceWithFZ(Integer[] nums){
        return getMaxPriceWithFZ(nums,0,nums.length-1);
    }

    private static int getMaxPriceWithFZ(Integer[] nums,int low,int high){
        if(low == high){
            return nums[low];
        }
        int mid = low+(high-low)/2;
        int max_price = 0;
        for(int i = low;i<=mid;i++){
            max_price = Math.max(getMaxPriceWithFZ(nums,low,i),getMaxPriceWithFZ(nums,i+1,high));
        }
        return max_price;
    }

很好,你真的会递归的时间复杂度求解吗?

还有一种常见的递归思路:
这种常见的思路就靠你们去找吧,它默认就是左边是不在切的,而右边是需要继续切的,这也是处理序列化问题的常见思路。相对的是,他需要遍历整个n得到问题的解。
Code

// 暴力递归
private static int getMaxPriceWithDG(Integer[] nums,int len){
        if(len == 0){
            return 0;
        }
        int max_price = Integer.MIN_VALUE;

        for(int i = 1;i<=len;i++){
            max_price = Math.max(max_price,nums[i-1]+getMaxPriceWithDG(nums,len-i));
        }
        return max_price;
    }

两种思路都是分治,就看我们选用的是什么思想,前者是需要维护一个区间,而后者不需要,简单的说,后者更好一些,后面这些问题就会一目了然的。

这里就是我们常说的暴力递归,我们必须掌握递归的求解和递归的时间复杂度求解。
接下来,我们就考虑第一种动态规划,也就是备忘录方法,也就是将我们计算了的子问题给记录下来,这里使用第二种code改进。

Code

private static int getMaxPriceWithRememberDG(Integer[] nums){
        // 带备忘的递归
        int[] dp = new int[nums.length+1]; // 这里需要注意一下, 这里零长度也需要记录一下
        Arrays.fill(dp,Integer.MIN_VALUE);

        return doMaxPriceWithRememberDg(nums,nums.length,dp);
    }

    private static int doMaxPriceWithRememberDg(Integer[] nums,int len,int[] dp){
        // 有记录直接就是读取就好
        if(dp[len]>0){
            return dp[len];
        }

        // 长度切为0 则没有价值
        if(len == 0){
            return 0;
        }

        // 初始价值为最小
        int max_price = Integer.MIN_VALUE;

        // 遍历所有的切口 也就是所有长度遍历,计算的是当长度为 i时,可以收获的最大价值
        for(int i = 1;i<=len;i++){

            // 其中的nums[i-1] 表示长度为 i 时,不切割和其他切割位置的值
            max_price = Math.max(max_price,nums[i-1]+doMaxPriceWithRememberDg(nums,len-i,dp));

        }

        dp[len] = max_price; // 记录这个值
        return max_price;
    }

这里相对于正常的暴力递归就加了一个查表的思路,当表中已经记录了值的话,那就直接返回记录值就好。

这里请注意,得益于java的地址传递,对象就是引用传递的机制,当dp改变的时候,实际改变的就是同一个表。而如果是c++的话,就需要手动传地址或者引用了

很好,我们已经开始会dp了,虽然维护的递归还是很吃力,这里的思路是自顶向上,但是好的是,这里子问题也就是子求解一次,简单的说,自定向下,自底向上都是同一个渐进时间复杂度,但是他们的常数因子却是不同的。多了一个常数系数,而且需要维护递归的开销。
前文已经介绍,这两个方法各有所长。
使用表的访问模式来进一步降低时空代价,但是如果子问题空间的某些子问题完全不必求解的时候,备忘录就体现出优势了,因为他只会求解有绝对必要的子问题

接下来,我们对备忘录方法改写一下,成为自底向上的实现思路。
Code

    private static int getMaxPriceWithDP(Integer[] nums){
        Integer[] dp = nums;
        dp[1] = Math.max(nums[1],nums[0]+nums[0]);
        for(int i = 2;i<dp.length;i++){
            for(int j = 0;j<=i/2;j++){
                dp[i] = Math.max(dp[i],nums[j]+nums[i-j-1]);
            }
        }
        return dp[nums.length-1];
    }

这里请注意,也就是将dg的位置替换成,之前的记录。
在考虑之前分析的一半原则,所以这里就是i<=j/2;
本来这里的优化应该是后面一个代码应该干的事,但问题不大,这就是dp的步骤。
相当了,还有步,也就是重构最优解,这里应该使用一个容器来存储切割点,但是这里没有做,为什么,因为这里强调,第4步在动态规划中不是很必要的

总结一下:在完成这个问题的时候,我们应该优先考虑分治,为的就是查找他的子问题,考虑是否具有最优子结构性质,然后,在优化这个暴力递归,在这个暴力递归成熟的时候,就可以考虑他是否具有重叠子结构了,然后构造他的备忘版本,一般备忘版本是够用的,不排除一些极难的题目,需要使用对换思想,转换问题求解,然后使用自底向上解法。

** 求解动态规划问题的步骤**

  1. 写出递归方程,编写暴力递归算法
  2. 添加备忘,编写自顶向下的备忘递归算法
  3. 将备忘转换成表格,自底向上构建dp
  4. 时间复杂度计算,对问题进行对换思考。
    有必要的需要重构最优解

求解矩阵连乘问题

输入:n个矩阵A1,A2,…,An,其中Ai的维数为pi-1×pi
Ai 和Ai+1是可乘的

输出:连乘积A1A2A3…An

优化目标:最小计算代价(最优的计算次序)

分析思路,一样的考虑,首先分治想法,将他分为左右,然后寻找切割点,而这次切割就有一定的费用了,也就是左右相乘的代价,所以我们要加上这个代价来查找这个切割点了

在这里插入图片描述

1. 暴力递归
Code

    private static int matrix_chain_mul_dg(Node[] nodes,int low,int high){
        if(low == high){
            return 0;
        }

        int sum = Integer.MAX_VALUE;
        for(int i = low;i<high;i++){
            sum = Math.min(sum,
                    matrix_chain_mul_dg(nodes,low,i)
                            +matrix_chain_mul_dg(nodes,i+1,high)+
                             nodes[low].prev *nodes[i].next*nodes[high].next);
        }
        return sum;
    }

2. 搭建备忘dp
Code

    private static int matrix_chain_mul_with_remember_dg(Node[] nodes){
        int[][] dp = new int[nodes.length+1][nodes.length+1];
        for(int i =1;i<dp.length;i++){
            Arrays.fill(dp[i],Integer.MIN_VALUE);
        }
        return matrix_chain_mul_with_remember_dg(nodes,0,nodes.length-1,dp);
    }

   private static int matrix_chain_mul_with_remember_dg(Node[] nodes,int low,int high,int[][] dp){
        if(dp[low][high]>0){
            return dp[low][high];
        }

        if(low == high){
            return 0;
        }

        int sum = Integer.MAX_VALUE;
        for(int i = low;i<high;i++){
            sum = Math.min(sum,
                    matrix_chain_mul_with_remember_dg(nodes,low,i,dp)
                            +matrix_chain_mul_with_remember_dg(nodes,i+1,high,dp)+
                            nodes[low].prev *nodes[i].next*nodes[high].next);
        }
        dp[low][high] = sum;
        return sum;
    }

这里预留一个小问题,也就是重构自底向上的解法。可以尝试一下

多加一步,重构最优解
Code

	private static int[][] matrix_chain_mul_(Node[] nodes,int low,int high){
        int[][] s = new int[nodes.length+1][nodes.length+1];
        System.out.println(matrix_chain_mul_dg(nodes,low,high,s));
        return s;
    }

    private static int matrix_chain_mul_dg(Node[] nodes,int low,int high,int[][] s){
        if(low == high){
            s[low][high] = low;
            return 0;
        }

        int sum = Integer.MAX_VALUE;
        for(int i = low;i<high;i++){
            int temp = matrix_chain_mul_dg(nodes,low,i,s)
                    +matrix_chain_mul_dg(nodes,i+1,high,s)+
                    nodes[low].prev *nodes[i].next*nodes[high].next;
            if(sum>temp){
                sum = temp;
                s[low][high] = i;
            }
        }
        return sum;
    }
 	private static void print(int[][] s,int low,int high){
        if(low == high){
            System.out.print("A"+low);

            return;
        }

        System.out.print("(");
        print(s,low,s[low][high]);
        print(s,s[low][high]+1,high);
        System.out.print(")");

    }

将这个记忆好的s递归输出打印一下就ok了。
在这里插入图片描述
这里应该知道,这个Node就是构造的矩阵,现在的表示都是n+1,然后第k个矩阵为
{A[k],A[k+1]},只是一些处理而已,问题不大的。

按照这个傻瓜式方案,相信没人不会动态规划。

最长公共子序列

这是一个经典的问题,也是很多问题的变现,该问题也就是常说的LCS问题:
Longest Common Subsequence Problem
在DNA序列相似度检测算法上,就是用了这个算法,相对一些工程问题,进化算法上面也是一下相似度检测算法。
子串:按原顺序依次出现,禁止跳过某元素
子序列:在保持元素前后关系的前提下,可以跳过某些元素的序列

先是递归子问题;
还是分治思路,但是这里的分治直接到极致,也就是右边就一个,而左边继续递归,当右边两个相同,那么问题的解就是两个位置左边的解加一,而不等,那就是要不是左边推移位继续比,要不就是右边退一位继续比,然后选最大的。
而base element 就是当左边没有了,就是0

在这里插入图片描述
1. 暴力递归
**Code **

    // 暴力递归
    private static int lcs(char[] str_one,char[] str_two,int str1, int str2){
        if(str1 == -1 || str2 == -1){
            return 0;
        }
        if(str_one[str1] == str_two[str2]){
            return lcs(str_one,str_two,str1-1,str2-1)+1;
        }else {
            return Math.max(lcs(str_one,str_two,str1-1,str2),lcs(str_one,str_two,str1,str2-1));
        }
    }

备忘递归

	private static int lcs(char[] str_one,char[] str_two){
        int[][] dp = new int[str_one.length][str_two.length];
        int result =  lcs(str_one,str_two,dp,str_one.length-1,str_two.length-1);

        return result;
    }

    // 备忘递归
    private static int lcs(char[] str_one,char[] str_two,int[][] dp,int str1, int str2){
        if(str1 == -1 || str2 == -1){
            return 0;
        }

        if(dp[str1][str2]>0){
            return dp[str1][str2];
        }

        if(str_one== null || str_one.length == 0||str_two==null||str_two.length == 0){
            return 0;
        }


        if(str_one[str1] == str_two[str2]){
            dp[str1][str2] = Math.max(dp[str1][str2],lcs(str_one,str_two,dp,str1-1,str2-1)+1);
        }else {
            dp[str1][str2] = Math.max(lcs(str_one,str_two,dp,str1-1,str2),lcs(str_one,str_two,dp,str1,str2-1));
        }
        return dp[str1][str2];
    }

自底向上
Code

// 自底向上
    private static int lcsWithDp(char[] str_one,char[] str_two){
        int[][] dp = new int[str_one.length+1][str_two.length+1];
        for(int i = 0;i<dp.length;i++){
            for(int j = 0;j<dp[0].length;j++){
                if(i == 0 || j == 0){
                    dp[i][j] = 0;
                    continue;
                }
                if(str_one[i-1] == str_two[j-1]){
                    dp[i][j] = dp[i-1][j-1]+1;
                }else{
                    dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }

        return dp[str_one.length][str_two.length];
    }

相信这两个案例,我们已经有了一定基础的动态规划思路
那么好,我们继续

最长递增子序列

相信这个问题都不会陌生,但是他的递归方程是有点棘手的,我们这里对换一下函数的作用,我们加入一个函数的作用的以一个下标为结尾的字符是一个序列,而返回的就是这个最长的递增子序列的长度,那么,我们只需要比较每个字符为结尾的长度就好。
问题又来了,那怎么确定以这个字符为结尾的最长序列呢,很简单,如果遇见比这个序列小的,那么就在他的基础上加一与原本的相比,大的保留就好,最后返回这个值。
暴力递归
Code

 // 暴力递归
 private static int findWithDg_(Integer[] nums,int index){
        int result = 1;
        for(int i = 0;i<nums.length;i++){
            result = Math.max(result,findWithDg(nums,i));
        }
        return result;
    }
    private static int findWithDg(Integer[] nums,int index){
        if(index == 0){
            return 1;
        }

        int result = Integer.MIN_VALUE;
        for(int i = 1;i<=index;i++){
            if(nums[index]>nums[index-i]){
                result = Math.max(findWithDg(nums,index-i)+1,result);
            }else{
                result = Math.max(1,result);
            }
        }
        return result;
    }

备忘递归

private static int find(Integer[] nums){
        Integer[] dp = new Integer[nums.length+1];
        Arrays.fill(dp,0);
        find(nums,nums.length-1,dp);

//        NumberArrayUtil.print(dp);
        return dp[dp.length-1];
    }

    private static int find(Integer[] nums,int index,Integer[] dp){
        if(index == 0){
            dp[index] = 1;
            return 1;
        }

        if(dp[index]>0){
            return dp[index];
        }

        for(int i = 1;i<=index;i++){
            int temp = find(nums,index-i,dp);
            if(nums[index]>nums[index-i]){
                dp[index] = Math.max(temp+1,dp[index]);
                dp[dp.length-1] = Math.max(dp[dp.length-1],dp[index]);
            }else{
                dp[index] = Math.max(1,dp[index]);
                dp[dp.length-1] = Math.max(dp[dp.length-1],dp[index]);
            }
        }

        return dp[index];
    }

自底向上
Code

private static int lengthOfLIS(Integer[] nums) {
        if(nums.length == 0){
            return 0;
        }
        // 最长递增子序列
        int result = 1;
        int[] dp = new int[nums.length];

        Arrays.fill(dp,1);
        for(int i = 1;i<dp.length;i++){
            // 当前的与之前的所有值比较,大于就状态转移更新,小于等于就不变
            for(int j = 0;j<i;j++){
                if(nums[i]>nums[j]){
                    dp[i] = Math.max(dp[i],dp[j]+1);
                }
            }
            result = Math.max(result,dp[i]);
        }

        return result;
    }

如果你是一步一步思考过来的,那么请思考,有没有比这个还快的算法。答案是肯定的。
这里的时间复杂度是O(n^2)但是有一个使用线段树搜索的算法却可以压缩至O(nlgn),这里不给出解法,给出思路思考。
一个长度为i的候选子序列的尾元素至少不比一个长度为i-1候选子序列的尾元素小,因此,可以在输入子序列中将候选子序列链接起来

看一下这道题目的变种问题。

最长理想子序列

给你一个由小写字母组成的字符串 s ,和一个整数 k 。如果满足下述条件,则可以将字符串 t 视作是 理想字符串 :

  • t 是字符串 s 的一个子序列。
  • t 中每两个 相邻 字母在字母表中位次的绝对差值小于或等于 k 。
  • 返回 最长 理想字符串的长度。

很简单的思路:继续上面的做法
备忘递归
Code

private static int longestIdealString(String s, int k) {
        if(s.length() == 1){
            return 0;
        }
        char[] chars = s.toCharArray();
        int[] dp = new int[chars.length+1];
        longestIdealString(chars,k,dp,chars.length-1);
        return dp[dp.length-1];
    }

    private static int longestIdealString(char[] chars,int k,int[] dp,int high){
        if(high == 0){
            dp[high] = 1;
            return 1;
        }
        if(dp[high]>0){
            return dp[high];
        }

        for(int i = 1;i<=high;i++){
            int temp = longestIdealString(chars, k, dp, high - i);
            if(Math.abs(chars[high]-chars[high-i])<= k){
                dp[high] = Math.max(temp+1,dp[high]);
                dp[dp.length-1] = Math.max(dp[dp.length-1],dp[high]);
            }else{
                dp[high] = Math.max(dp[high],1);
                dp[dp.length-1] = Math.max(dp[dp.length-1],dp[high]);
            }
        }
        return dp[high];
    }

因为这里的主角不是自底向上,所以我们不加入暴力递归

自底向上解法
Code

// 暴力dp
    private static int longestIdealString_(String s,int k){
        if(s.length() == 1){
            return 1;
        }

        char[] chars = s.toCharArray();
        int[] dp = new int[chars.length];
        Arrays.fill(dp,1);
        int result = 1;
        for(int i = 1;i<chars.length;i++){
            for(int j = 0;j<i;j++){
                if(Math.abs(chars[i]-chars[j])<=k){
                    dp[i] = Math.max(dp[i],dp[j]+1);
                }
            }
            result = Math.max(result,dp[i]);
        }
        return result;
    }

当我们写到这里就结束了,那么就超时了,很显然,这是O(n^2)的。
我在纠结这道题目的时候,还打算使用线段树去搜索的,后面看了大家的一个解法,瞬间就拍案叫绝,这里请看,我们的dp是以index也就是下标为记录的,但是这样就会有n个记录值,也就是要重复遍历n-1遍。
看一下题目的范围,是a~c,那么这个dp就可以记录以某一个字母结尾的最长,然后遍历的就是26的长度,那么这个O(n^2)就会成为O(26n)也就是O(n)的渐进。
对换问题Dp
Code

// 兑换dp
    private static int longestIdealString_2(String s,int k){
        if(s.length() == 1){
            return 1;
        }

        char[] chars = s.toCharArray();
        int[] dp = new int[26];

        int result = 1;
        for (char aChar : chars) {
            int temp = dp[aChar - 97];
            for (int j = 0; j < dp.length; j++) {
                if (Math.abs((int) aChar - 97 - j) <= k) {
                    temp = Math.max(temp, dp[j] + 1);
                }
            }
            dp[aChar - 97] = Math.max(dp[aChar - 97], temp);
            result = Math.max(result, dp[aChar - 97]);
        }
        return result;
    }

实际就是原来的思路,但是改变一下方位,就成了从字母的角度了。

编辑距离

实际这个问题就是最长公共子序列的变换问题,也就是如何将一个序列转成另一个序列使用最少的步骤,加入这些步骤的开销都是一致的。
步骤分为,添加一个字符,删除一个字符,修改一个字符,终止等

一般常用就是前三个操作,还是一样,我们使用减而治之的方法去思考这个问题,当两个位置上的值是一致的,那么就不需要开销,也就是直接比较两个位置的前一个,如果不相等,那么分别算出三种的开销,选最小的就好,而添加就相对另一个减少,而删除就是自己减少,修改就是两个同时减少,最后,就可以得到递归方程:
在这里插入图片描述
直接写出暴力递归算法
暴力递归
Code

 // 暴力递归
    	private static int editerDistance(char[] s1,char[] s2,int h1, int h2){
        if(h2 == 0 || h1 == 0){
            return Math.max(h2,h1);
        }

        // 最后一位相等时,编辑距离就是前一位的距离
        if(s1[h1] == s2[h2]){
            return editerDistance(s1,s2,h1-1,h2-1);
        }else{

            // 不相等的时候就是有三种情况
            // 1. 修改
            int update = editerDistance(s1,s2,h1-1,h2-1)+1;
            // 2. 删除
            int delete = editerDistance(s1,s2,h1-1,h2)+1;
            // 3. 添加
            int insert = editerDistance(s1,s2,h1,h2-1)+1;

            if(update<delete && update<insert){
                return update;
            }else if(delete<update && delete<insert){
                return delete;
            }else{
                return insert;
            }
        }
    }

备忘递归
Code

// 备忘递归
    private static int editerDistanceRememberWithDg(String s1,String s2){
        int[][] dp = new int[s1.length()][s2.length()];
        for (int[] ints : dp) {
            Arrays.fill(ints, 0);
        }
        return editerDistance(s1.toCharArray(),s2.toCharArray(),s1.length()-1,s2.length()-1,dp);
    }

    // 备忘递归
    private static int editerDistance(char[] s1,char[] s2,int h1, int h2,int[][] dp){
        if(dp[h1][h2]>0){
            return dp[h1][h2];
        }

        if(h2 == 0 || h1 == 0){
            return Math.max(h2,h1);
        }

        // 最后一位相等时,编辑距离就是前一位的距离
        if(s1[h1] == s2[h2]){
            dp[h1][h2] = editerDistance(s1,s2,h1-1,h2-1);
        }else{
            // 不相等的时候就是有三种情况
            // 1. 修改
            int update = editerDistance(s1,s2,h1-1,h2-1)+1;
            // 2. 删除
            int delete = editerDistance(s1,s2,h1-1,h2)+1;
            // 3. 添加
            int insert = editerDistance(s1,s2,h1,h2-1)+1;

            if(update<delete && update<insert){
                dp[h1][h2] = update;
            }else if(delete<update && delete<insert){
                dp[h1][h2] = delete;
            }else{
                dp[h1][h2] = insert;
            }
        }
        return dp[h1][h2];
    }

自底向上
Code

// 自底向上
    	private static int editerDistanceWithDp(char[] s1,char[] s2){
        int[][] dp = new int[s1.length+1][s2.length+1];
        for(int i = 0;i<dp.length;i++){
            for(int j = 0;j<dp[0].length;j++){
                if(i == 0|| j == 0){
                    dp[i][j] = Math.max(i,j);
                    continue;
                }
                if(s1[i-1] == s2[j-1]){
                    dp[i][j] = dp[i-1][j-1];
                }else{
                    if(dp[i-1][j-1]<dp[i-1][j] && dp[i-1][j-1]<dp[i][j-1]){
                        dp[i][j] = dp[i-1][j-1]+1;
                    }else if(dp[i][j-1]<dp[i-1][j-1] && dp[i][j-1]<dp[i-1][j]){
                        dp[i][j] = dp[i][j-1]+1;
                    }else{
                        dp[i][j] = dp[i-1][j]+1;
                    }
                }
            }
        }
        return dp[s1.length][s2.length];
    }

在看完这些解法的时候,就是一种感觉,动态规划就是太简单,相当的,找出他们的子问题和对换问题就是太难,想要掌握好这些思路或者是动态规划,还是要回到上文的原理和到刷题,600~1000道题,我们值得拥有。

对于其他动态规划算法,还需要到后面使用时解析。区间dp和序列问题dp这里都初步的掌握,图算法中的dp其实也是有捷径的,后话后话。
现在思考一下,下一节准备树的专题还是贪心算法的专题呢。得看。
记录太多不是好事,刷题才是必胜客。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BoyC啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值