LeetCode动态规划上之递归+记忆化搜索+DP逐步进阶(上)

  • 2019 年 10 月 6 日
  • 筆記

LeetCode动态规划上之递归+记忆化搜索+DP逐步进阶(上)

类似题目:

  • 53.最大子序和
  • 70. 爬楼梯
  • 120. 三角形最小路径和

1.53.最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4],  输出: 6  解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。  

进阶:

如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

实现思路:

递推式:动态规划:s(n)=Math.max(s(n-1)+nums[n],nums[n])

实现:

class Solution {      public int maxSubArray(int[] nums) {          int sum=0;          int maxsum=nums[0];          for(int num:nums){              sum=Math.max(num,sum+num);              maxsum = Math.max(sum,maxsum);          }          return maxsum;      }  }  

实现思路:

二分法:不断将区间划分为左、中、右!中间直接计算,左与右递归处理。

实现:

class Solution {     public int maxSubArray(int[] nums) {          int res;          if(nums.length==0) return 0;          res = maxSub(nums,0,nums.length-1);          return res;      }        public int maxSub(int[] nums, int left, int right){          if(left>right) return Integer.MIN_VALUE;          if(left==right) return nums[left];          int mid=(left+right)/2;          int l=maxSub(nums,left,mid-1);//求左半边最大子序和          int r=maxSub(nums,mid+1,right);//求右半边最大子序和          int t=nums[mid];          int max_num=nums[mid];          for(int i=mid-1;i>=left;i--)//整合左半部分          {              t+=nums[i];              max_num=Math.max(max_num,t);          }          t=max_num;          for(int i=mid+1;i<=right;i++)//整合右半部分          {              t+=nums[i];              max_num=Math.max(max_num,t);          }          return Math.max(Math.max(r,l),max_num);      }  }  

2.70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

输入: 2  输出: 2  解释: 有两种方法可以爬到楼顶。  1.  1 阶 + 1 阶  2.  2 阶  

示例 2:

输入: 3  输出: 3  解释: 有三种方法可以爬到楼顶。  1.  1 阶 + 1 阶 + 1 阶  2.  1 阶 + 2 阶  3.  2 阶 + 1 阶  

实现思路:

直接递归:因为有众多重复计算,所以通不过!

实现:

class Solution {      public int climbStairs(int n) {          if(n==1||n==0){              return 1;          }          return climbStairs(n-1)+climbStairs(n-2);      }  }  

实现思路:

递归加缓存memo,由于每次计算都有许多重复计算,比如f4=f3+f2,f5=f4+f3,f4与f3被多次计算,如果能够在第一次的时候保存下来,后面直接访问,岂不是效率会高。

实现:

class Solution {        public int climbStairs(int n) {          int[] memo = new int[n+1];          return fibStair(memo,n);      }        public int fibStair(int[] memo,int n){          if(n==1||n==0){              memo[n]=1;          }else if(memo[n]==0){              memo[n]=fibStair(memo,n-1)+fibStair(memo,n-2);          }          return memo[n];      }  }  

实现思路:

动态规划:自底向上。

实现:

class Solution {        public int climbStairs(int n) {          int[] memo=new int[n+1];          memo[0]=memo[1]=1;          for (int i=2;i<=n;i++){              memo[i]=memo[i-1]+memo[i-2];          }          return memo[n];      }    };  

递归:自顶向下。

实现:

class Solution {      public int minPathSum(int[][] grid) {            int m = grid.length;          int n = grid[0].length;          int res = fibPath(grid, m - 1, n - 1);            return res;      }        public int fibPath(int[][] grid, int i, int j) {          //左上角          if (i == 0 && j == 0) {              return grid[i][j];          }            //第一行          if (i==0&&j == 1) {              return grid[i][j] + grid[0][0];          } else if (j==0&&i == 1) { //第一列              return grid[0][0] + grid[i][j];          }          //递归第一列          if (i > 0 && j == 0) {             return fibPath(grid, i - 1, j) + grid[i][j];          } else if (j > 0 && i == 0) { //递归第一行              return fibPath(grid, i, j - 1) + grid[i][j];          }          //递归中间部分          return Math.min(fibPath(grid, i, j - 1), fibPath(grid, i - 1, j)) + grid[i][j];      }  }  

实现思路:

递归+记忆化搜索法:自顶向下。新开一个存储空间即可。

实现:

class Solution {      public int minPathSum(int[][] grid) {            int m = grid.length;          int n = grid[0].length;          int[][] memo = new int[m][n];            for (int i = 0; i < m; i++) {              for (int j = 0; j < n; j++) {                  memo[i][j] = -1;              }          }          memo[0][0] = grid[0][0];          return fibPath(memo, grid, m - 1, n - 1);      }        public int fibPath(int[][] memo, int[][] grid, int i, int j) {          if (i == 0 && j == 0) {              return grid[i][j];          }          if (memo[i][j] != -1) {  //记忆搜索存储条件              return memo[i][j];          }          if (i==0&&j == 1) {              memo[i][j] = grid[i][j] + grid[0][0];          } else if (j==0&&i == 1) {              memo[i][j] = grid[0][0] + grid[i][j];          }            if (i > 0 && j > 0) {              memo[i][j] = Math.min(fibPath(memo, grid, i, j - 1), fibPath(memo, grid, i - 1, j)) + grid[i][j];          } else if (i > 0 && j == 0) {              memo[i][j] = fibPath(memo, grid, i - 1, j) + grid[i][j];          } else if (j > 0 && i == 0) {              memo[i][j] = fibPath(memo, grid, i, j - 1) + grid[i][j];          }            return memo[i][j];      }  }  

实现思路:

动态规划:自底向上。

实现:

开辟二维数组存储:

class Solution {      public int minPathSum(int[][] grid) {          int m = grid.length;          int n = grid[0].length;          int[][] memo = new int[m][n];          memo[0][0] = grid[0][0];          int i,j;          for(i=1;i<n;i++){              memo[0][i]=memo[0][i-1]+grid[0][i];          }          for(i=1;i<m;i++){              memo[i][0]=memo[i-1][0]+grid[i][0];          }            for(i=1;i<m;i++){              for(j=1;j<n;j++){                  memo[i][j]=Math.min(memo[i][j-1],memo[i-1][j])+grid[i][j];              }          }          return memo[m-1][n-1];      }  }  

开辟一维数组存储:

1 3 1  1 5 1  4 2 1  -----------  1 4 5  2 7 6  6 8 7  

实际上用一维数组把巧妙的保存了当前(i,j)位置对应的上一次(i-1,j)与更新后的(i,j-1)的值。

class Solution {      public int minPathSum(int[][] grid) {          int m = grid.length;          int n = grid[0].length;          int[] memo = new int[n];          memo[0] = grid[0][0];          int i,j;          for(i=1;i<n;i++){              memo[i]=memo[i-1]+grid[0][i];          }            for(i=1;i<m;i++){              for(j=0;j<n;j++){                  if(j==0){                      memo[j]+=grid[i][j]; //巧妙的对边界进行相加!                  }else{                      memo[j]=Math.min(memo[j-1],memo[j])+grid[i][j];                  }              }          }          return memo[n-1];      }  }  

3.120. 三角形最小路径和

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

例如,给定三角形:

[       [2],      [3,4],     [6,5,7],    [4,1,8,3]  ]  

自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

说明:

如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。

实现思路:

递归

实现:

class Solution {      public int minimumTotal(List<List<Integer>> triangle) {          return fibTri(triangle,0,0,0);      }      public int fibTri(List<List<Integer>> triangle,int row,int col,int min_sum){          if(row>=triangle.size()){              return min_sum;          }          min_sum+=triangle.get(row).get(col);          return Math.min(fibTri(triangle,row+1,col,min_sum),fibTri(triangle,row+1,col+1,min_sum));      }  }  

记忆化搜索+递归

class Solution {      public int minimumTotal(List<List<Integer>> triangle) {          return helper(triangle, 0, 0, new HashSet<>());      }      public int helper(List<List<Integer>> triangle, int x, int y, Set<String> visited) {          //递归终止条件          if (x == triangle.size()) {              return 0;          }          //使用记忆化搜索的条件          if (visited.contains(x + " " + y)) {              return triangle.get(x).get(y);          }          int left = helper(triangle, x + 1, y, visited);          int right = helper(triangle, x + 1, y + 1, visited);          int sum = Math.min(left, right) + triangle.get(x).get(y);          triangle.get(x).set(y, sum);          visited.add(x + " " + y);          return sum;      }  }  

使用了二维数组空间的动态规划!

直接为tmp多分配一行一列数据,那么就可以不用事先对tmp赋值。

class Solution {      public int minimumTotal(List<List<Integer>> triangle) {         int n = triangle.size();          int[][] tmp = new int[n+1][n+1];          for(int i=n-1;i>=0;i--){              for(int j=0;j<=i;j++){                  tmp[i][j]=Math.min(tmp[i+1][j],tmp[i+1][j+1])+triangle.get(i).get(j);              }          }          return tmp[0][0];      }    }  

使用了一维数组空间的动态规划!

每次使用一个数组来存储上一次的运算结果。自底向上动态规划。

class Solution {      public int minimumTotal(List<List<Integer>> triangle) {          int rows = triangle.size();          //定义一维数组存放最近一行数据。          int[] arr = new int[rows];          //初始化最底层          for(int i=0;i<rows;i++){              arr[i]=triangle.get(rows-1).get(i); //将最后一行数据转存到arr数组。          }          for(int i=rows-2;i>=0;i--){              for(int j=0;j<=i;j++){ //刚好是第几行就有几列数据                  arr[j]=Math.min(arr[j],arr[j+1])+triangle.get(i).get(j);              }          }          return arr[0];      }  }  

上述第一个循环其实可以省去,怎么省去?

直接为arr多分配一列数据空间,保证不溢出。

class Solution {      public int minimumTotal(List<List<Integer>> triangle) {          int rows = triangle.size();          //定义一维数组存放最近一行数据。          int[] arr = new int[rows+1];          for(int i=rows-1;i>=0;i--){              for(int j=0;j<=i;j++){ //刚好是第几行就有几列数据                  arr[j]=Math.min(arr[j],arr[j+1])+triangle.get(i).get(j);              }          }          return arr[0];      }  }  

不用自定义空间的两种动态规划!

实现思路:

自底向上动态规划。每次挑选当前对应的下一行的左边与右边元素中最小元素,然后依次原地累加,直到最后累加到第一行就是最后结果。

实现:

class Solution {      public int minimumTotal(List<List<Integer>> triangle) {            for(int i=triangle.size()-2;i>=0;i--){              for(int j=0;j<triangle.get(i).size();j++){                  triangle.get(i).set(j,triangle.get(i).get(j)+Math.min(triangle.get(i+1).get(j),triangle.get(i+1).get(j+1)));              }          }          return triangle.get(0).get(0);      }  }  

实现思路:

自顶向下动态规划。分为三种情况,左边界、中间、右边界。

class Solution {      public int minimumTotal(List<List<Integer>> triangle) {          for (int i = 1; i < triangle.size(); i++) {              for (int j = 0; j < triangle.get(i).size(); j++) {                  if (j == 0)                      //当前行(i,j)与上一行(i-1,j)相关                      //将当前行(i,j)对应的值设置为当前行(i,j)的值加上上一行(i-1,j)对应的值。                      triangle.get(i).set(j, triangle.get(i-1).get(j) + triangle.get(i).get(j));                  else if (j == triangle.get(i).size()-1)                      //当前行(i,j)与上一行(i-1,j-1)相关                      //将当前行(i,j)对应的值设置为当前行(i,j)的值加上上一行(i-1,j-1)对应的值。                      triangle.get(i).set(j, triangle.get(i-1).get(j-1) + triangle.get(i).get(j));                  else                      //中间部分与上一行的(i-1,j)、(i-1,j-1)相关。                      triangle.get(i).set(j, Math.min(triangle.get(i-1).get(j-1), triangle.get(i-1).get(j))                      + triangle.get(i).get(j));              }          }          return Collections.min(triangle.get(triangle.size()-1)); //取出最后一行的最小值即可。      }    }