递归和动态规划专题(二)----剑指offer+左程云算法

递归和动态规划专题(二)—-剑指offer+左程云算法


【前言】1.动态规划算法是从暴力搜索算法优化过来的,如果我们不清楚暴力搜索的过程,就难以理解动态规划的实现,当我们了解了动态规划算法的基本原理的文字概述,实现条件之后,这时可能并不是太理解这种思想,去面对实际问题的时候也是无从下手,这个时候我们不能停留在文字层面上,而应该去学习经典动态规划算法的实现,然后倒回来看这些概念,便会恍然大悟。

  2.动态规划算法的难点在于 从实际问题中抽象出动态规划表dp,dp一般是一个数组,可能是一维的也可能是二维的,也可能是其他的数据结构。

 3.总体来说,动态规划算法就是一系列以空间换取时间的算法。
这里写图片描述


(一).矩阵的最小路径和

【题目】给定一个矩阵m,从左上角开始每次只能向右走或者向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和,如果给定的m如下,那么路径1,3,1,0,6,1,0就是最小路径和,返回12.
1 3 5 9
8 1 3 4
5 0 6 1
8 8 4 0

这是经典的动态规划问题:假设m是m行n列的矩阵,那么我们用dp[m][n]来抽象这个问题,dp[i][j]表示的是从原点到i,j位置的最短路径和。我们首先计算第一行和第一列,直接累加即可,那么对于其他位置,要么是从它左边的位置达到,要么是从上边的位置达到,我们取左边和上边的较小值,然后加上当前的路径值,就是达到当前点的最短路径。也就是说问题分为三种情况:第一行dp[0][j] 第一列 dp[i][0] 和非第一行第一列dp[i][j]. dp矩阵称为动态规划表!

import java.util.*;
public int minPathSum1(int[][] m){
    if(m==null||m.length==0||m[0]==null||m[0].length==null){
        return 0;
    }
    rows=m.length;
    cols=m[0].length;
    int[][] dp=new int[rows][cols];
    dp[0][0]=m[0][0];
    //第一行都是前面的累加
    for(int j=1;j<cols;j++){
        dp[0][j] = dp[0][j-1]+m[0][j];
    }
    //第一列都是上面的累加
    for(int i=1;i<cols;i++){
        dp[i][0] = dp[i-1][0]+m[i][0];
    }
    //构建动态规划表dp
    for(int i=2;i<cols;i++){
        for(int j=2;j<cols;j++){

            dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+m[i][j];
        }
    }
    return dp[rows-1][cols-1];
}

该方法时间复杂度为O(N*M),空间复杂度由于新建了动态规划表dp,所以为O(N*M)!但是动态规划可以通过空间压缩的方法将额外空间复杂度变成O(min{M,N}),也就是不使用M*N的dp矩阵,而是使用arr[]一维数组。

import java.util.*;
public int minPathSum1(int[][] m){
    if(m==null||m.length==0||m[0]==null||m[0].length==null){
        return 0;
    }
    int more = Math.max(m.length,m[0].length);//行数和列数较多的那个
    int less = Math.min(m.length,m[0].length);//行数和列数较少的那个

    int[] arr= new int[less];
    boolean rowMore = m.length==more;//判断是行数多还是列数多
    arr[0]==m[0][0];
    //让第一行或第一列赋值给arr数组
    for(int i=1;i<less;i++){
        arr[i]=arr[i-1]+(rowMore ? m[0][i]:m[i][0]);
    }

    //arr数组往下滚 从arr[0]开始逐渐得到该行的最短路径;
    for(int i=1;i<more;i++){
        arr[0]=arr[0]+(rowMore ? m[i][0]:m[0][i]);
        for(int j=1;j<less;j++){
            arr[j] = Math.min(arr[j-1],arr[j])+(rowMore?m[i][j]:m[j][i]);
        }
    }

    return arr[less-1];

}

(二).换钱的方法

【题目】给定数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求换钱有多少种方法。

【举例】

arr=[5,10,25,1],aim=0。组成0元的方法有1种,就是所有面值的货币都不用。所以返回1。

arr=[5,10,25,1],aim=15。组成15元的方法有6种,分别为3张5元,1张10元+1张5元,1张10元+5张1元,10张1元+1张5元,2张5元+5张1元,15张1元。所以返回6。

arr=[3,5],aim=2。任何方法都无法组成2元。所以返回0。

这个题目讲的是从暴力递归到动态规划:
递归 –> 记忆化搜索(备忘录方法)– dp –> dp状态合并!在这里直接给出动态规划基础方法及动态规划的优化方法!

【分析】生成行数为N,列数为aim+1的动态规划表矩阵dp,dp[i][j]的含义表示使用arr[0…i]种货币组成要找的数j有多少种?

  1. 对于矩阵dp第一列 ,dp[0….i][0]表示组成要找的数为0,很明显是1种即为不使用任何货币!
  2. 对与矩阵dp第一行,dp[0][0……aim]表示组成的数为0…aim,很明显dp[0][k*arr[0]]==1(0<=arr[0]*k<=aim)!
  3. 对于矩阵dp上非第一行第一列的数,即为位置(i ,j)。dp[i][j]有以下多种情况:

1.完全不用arr[i],用arr[0….i-1]货币组成j;
2.用一张arr[i],用arr[0….i-1]货币组成j-arr[i];
………
k+1.用k张arr[i],用arr[0….i-1]货币组成j-k*arr[i] (j-k*arr[i]>=0);

【注意】通过分析我们可以发现上述的第三种情况可以简化为dp[i][j] = dp[i-1][j]+dp[i][j-arr[i]];最终代码如下,时间复杂度为O(N*aim),空间复杂度为O(N*aim);
这里写图片描述




import java.util.*;

public class Exchange {
    public int countWays(int[] penny, int n, int aim) {


    if(penny==null||penny.length==0||aim==0){
        return 0;
    }
    int[][] dp=new int[penny.length][aim+1];
    //第一列,均为0;
    for(int i=0;i<penny.length;i++){
        dp[i][0]=1;
    }
    //第一行,k*arr[0]为0;
    for(int j=0;j*penny[0]<aim+1;j++){
        dp[0][j*penny[0]]=1;
    }
    for(int i=1;i<penny.length;i++){
        for(int j=1;j<aim+1;j++){

            dp[i][j]=dp[i-1][j];
            dp[i][j]+=j-penny[i]>=0?dp[i][j-penny[i]]:0;


        }
    }
    return dp[penny.length-1][aim];

    }
}

【注意】通过动态规划的空间压缩,把dp矩阵用dp一维矩阵表示,滚下一行!使额外空间复杂度变为O(aim)方法。

import java.util.*;

public class Exchange {
    public int countWays(int[] penny, int n, int aim) {


    if(penny==null||penny.length==0||aim==0){
        return 0;
    }

    int[] dp=new int[aim+1];

    for(int i=0;i*penny[0]<=aim;i++){
        dp[i*dp[0]]=1;
    }

    for(int i=1;i<n;i++){
        for(int j=1;j<=aim;j++){

            dp[j]+=j-penny[i]>=0?dp[j-penny[i]]:0;
        }
    }

    return dp[aim];
    }
}

(三).记录几道《剑指offer》中与连续子序列(连续子数组)有关的题目!

【题目】HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。你会不会被他忽悠住?(子向量的长度至少是1)

public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        //记住:整数最大值为:2^31-1 0x7FFFFFFF   最小值:-2^31  0x80000000

        if(array==null||array.length==0){
            return 0;
        }
        int sum=0;
        int greatSum=0x80000000;
        for(int i=0;i<array.length;i++){
            //如果sum小于0,把之前累加的都去除
            if(sum<=0){
                sum=array[i];
            }else{
                sum+=array[i];

            }

            if(sum>greatSum){
                    greatSum = sum;
            }
        }

        return greatSum;             

    }
}

【题目】小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列?
Good Luck!

import java.util.ArrayList;
public class Solution {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
       //和为S的连续正数序列;

        ArrayList<ArrayList<Integer> > arrayList = new ArrayList<ArrayList<Integer> > ();
        if(sum<3){
            return arrayList;
        }
        int small=1;
        int big=2;
        int middle = (1+sum)/2;
        int s=small+big;
        //至少包括俩个数,所以由此可以把small<middle作为循环判断条件
        while(small<middle){
            if(s==sum){
                arrayList.add(Sequence(small,big));
            }
            //当系列和大于sum以及small仍小于middle时;
            while(s>sum&&small<middle){
                s-=small;
                small++;
                if(s==sum){
                    arrayList.add(Sequence(small,big));
                }
            }

            big++;
            s+=big;

        }

        return arrayList;

    }

    public ArrayList<Integer> Sequence(int small,int big){

        ArrayList<Integer> list = new ArrayList<Integer>();
        if(small<big){
            for(int i=small;i<=big;i++){
                list.add(i);
            }
        }
        return list;

    }

}

【题目】输入一个递增排序的数组和一个数字S,在数组中查找两个数,是的他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
        //因为递增排序,所以考虑从左右俩边开始往中间遍历;
        ArrayList<Integer> list= new ArrayList<Integer>();
        if(array==null||array.length<2){
            return list;
        }

        int i=0;
        int j=array.length-1;
        while(i<j){
            int s=array[i]+array[j];
            if(s>sum){
                j--;
            }else if(s<sum){
                i++;
            }else if(s==sum){
                list.add(array[i]);
                list.add(array[j]);
                return list;
            }
        }
        return list;
    }
}

(四).最长上升子序列问题(LIS)

【注意】为什么上面第三点要记录《剑指offer》里的子序列题目呢?因为我刷到这道题时发现思路和上面的几道题相似!然而非也,我错以为最长上升子序列为最长连续上升子序列,所以最优解仍然需要使用动态规划!

如果为最长连续上升子序列:

import java.util.*;

public class LongestIncreasingSubsequence {

    public int getLIS(int[] A, int n) {
        //记录上升子序列的长度
        int count=1;
        if(A==null||n==0){
            return 0;
        }

        int i=0;
        int greatNum=1;
        for(int j=0;j<n-1;j++){
            if(A[j]>=A[j+1]){
                count=1;
            }else{
                count++;
            }

            if(count>greatNum){
                greatNum=count;
            }

        }
        return greatNum;
    }
}

按照题意,使用动态规划的解法:

import java.util.*;

public class LongestIncreasingSubsequence {
   public int getLIS(int[] A, int n) {
        // write code here
        if (n <= 0)
            return -1;
        int[] dp = new int[n];
        dp[0] = 1;
        for (int i = 1; i < n; i++) {
            int j = i - 1;
            int max = 1;
            while (j >= 0) {
                if (A[i] > A[j]) {
                    if (dp[j] + 1 > max)
                        max = dp[j] + 1;
                }
                j--;
            }
            dp[i] = max;
        }
        int max = 1;
        for (int i = 0; i < dp.length; i++) {
            if (dp[i] > max)
                max = dp[i];
        }
        return max;

    }
}

****要回家啦,未完待续**

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页