[编程题] LeetCode上的Dynamic Programming(动态规划)类型的题目

继上次把backTracking的题目做了一下之后:backTracking ,我把LeetCode的动态规划的题目又做了一下,还有几道比较难的Medium的题和Hard的题没做出来,后面会继续更新和加详细解法解释~
Dynamic Programming链接:https://leetcode.com/tag/dynamic-programming/

以下的答案只是注重能够解题,后续会再注重实践效率。


文章目录


难度-Easy


70. Climbing Stairs 爬楼梯

这是一道经典的DP入门题目。一共有n阶台阶,我们一次可以上一阶或者两阶,问一共有多少种上楼梯的方法。
我们上楼梯的最后一部,也就是到第n阶的那一步,可能是从

  • n-1阶往上走一阶,那么到n-1阶的时候一共有climbStairs(n-1)种走法
  • n-2阶往上走两阶,那么到n-1阶的时候一共有climbStairs(n-2)种走法

于是到第n阶的时候就是上面两种走法之和:climbStairs(n) = climbStairs(n-1) + climbStairs(n-2);

如果我们每次递归climbStairs()的时候都要重新计算一次climbStairs的结果,那么运行时间成指数增长。我们可以开辟一个内存来存放结果,这里我们用HashMap。

这也是动态规划的思想:中间的结果要缓存起来,以备后续使用。

public class Solution {
    int res;
    Map<Integer,Integer> resMap = new HashMap<>();
    public int climbStairs(int n) {
        if(n < 2){
            return 1;
        } else {
            if(resMap.containsKey(n)){
                return(resMap.get(n));
            }
            res = climbStairs(n-1) + climbStairs(n-2);
            resMap.put(n,res);
            return res;
        }
    }
}

上面这个代码实在太慢,我又重新开始做这些题的时候,运行了上面的代码,才 faster than 13% of Java online submissions
于是改成了以下的代码, faster than 46.30% of Java online submissions
用n_1代表到n-1阶的时候的走法的种数,n_2代表n-2

class Solution {
    public int climbStairs(int n) {
        if(n < 2) return 1;
        int n_1 = 1;
        int n_2 = 1;
        int value = 0;
        for(int i=2; i <=n; i++){
            value = n_1 + n_2;
            n_2 = n_1;
            n_1 = value;
        }
        return value;
    }
}

746. Min Cost Climbing Stairs 最小爬楼梯代价

给定一个数组cost代表楼梯,cost[i]代表踏上第i个台阶的代价,爬楼梯乐意一次爬一个或者爬两格,给出爬到楼梯顶部的最小代价。

这个题和上面的70. Climbing Stairs 爬楼梯类似,这里加上了代价。那么,爬到第i个台阶的最小代价是:

  • 在第i-2个楼梯上一次爬
  • 在第i-1个楼梯上一次爬

所以第i个台阶的最小代价是i-1的代价和i-2的代价中最小值加上踩上i台阶的代价。

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        if(cost == null || cost.length < 1) return 0;
        int i_1 = cost[1];
        int i_2 = cost[0];
        int res = 0;
        for(int i = 2; i < cost.length; i++){
            res = Math.min(i_1, i_2) + cost[i];
            i_2 = i_1;
            i_1 = res;
        }
        return Math.min(i_1, i_2);
    }
}

faster than 32.72% of Java online submissions for Min Cost Climbing Stairs.


198. House Robber

你是一个专业的小偷,现在有一排屋子的钱等着你去偷,但是你不能偷相邻屋子的钱。假设以一个数组代表一排屋子,数字的每个元素代表每个屋子里面的钱,问你最多能偷多少钱?

我们说动态规划的思想是:中间的结果要缓存起来,以备后续使用。
因为题目规定相邻的两个屋子不能偷,那么就会有的屋子偷和有的屋子不偷。我们定义两个变量来存储上一次的结果:

  • notRob :表示上一个屋子不偷的时候我们最多能偷多少钱
  • rob:表示上一个屋子的时候我们最多能偷多少钱
public class Solution {
    public int rob(int[] nums) {
        int rob = 0; 
        int notRob = 0;
        for(int num: nums){
            int curRob = notRob + num; //假设现在这个屋子我们偷 curRob 
            notRob = Math.max(rob, notRob); //现在这个屋子我们不偷
            rob = curRob;
        }
        return Math.max(rob, notRob);
    }
}

121. Best Time to Buy and Sell Stock买卖股票问题

给你一个数组表示这支股票在哪一天的价格,数组的下标表示第几天,元素代表这一天的股票价格,问再某一天买入再在某一天卖出最多能挣多少钱?

实际上这道题可以转化为求最大子数组之和的问题:Maximum Subarray。在《算法导论》里面看过用分治法来求,不过比较麻烦,既然它放在了动态规划的tag下面,那就尝试着用动态规划的思想去解。

和Maximum Subarray不一样的是,这道题的买卖股票的值是负的话,那么就干脆不买,即结果为0,而最大子数组的和就算为负数也会输出出来。便参考了Discuss里面的讨论:Kadane’s Algorithm

将表示股票价格的数组转化为每一天的股票价格差数组arr,然后依次遍历这个数组,当遍历到变量arr[i]的时候
用两个变量来缓存结果:

  • maxIndex 表示目前加上这个元素arr[i]的最大值。若加上这个元素arr[i]之后为负数,则maxIndex 变为0,就是说之前一次买进股票到arr[i]卖出股票,赚的钱为负数,那么干脆抛弃,下次从arr[i+1]开始买进股票
  • maxTotal 表示整个遍历整个数组arr时的最大值,每次都拿maxTotal 和 遍历到目前arr[i]时的maxIndex 比较,并更新为其中的最大值。

最后 maxTotal 就是最大值。

public class Solution {
    public int maxProfit(int[] prices) {
        int maxTotal = 0;
        int maxIndex = 0;
        for(int i = 1; i < prices.length; ++i){
            maxIndex = Math.max(0, maxIndex += prices[i] - prices[i-1]);
            maxTotal = Math.max(maxTotal, maxIndex);
        }
        return maxTotal;
    }
}

303. Range Sum Query - Immutable

给你一个数组和两个下标 i 和 j ,算出 i 到 j 的和,注意sumRange 的方法会调用很多次。

既然题目说sumRange会调用很多次,那么我们就不能在sumRange里面计算 i 到 j 的和。
利用动态规划的思想,用原来的数组存 0 到下标 i 的和。

public class NumArray {
    
    int[] sums;

    public NumArray(int[] nums) {
        sums = new int[nums.length + 1];
        for(int i = 0; i < nums.length; i++){
            sums[i + 1] = sums[i] + nums[i]; 
        }
    }

    public int sumRange(int i, int j) {
        return sums[j+1] - sums[i];
    }
}


// Your NumArray object will be instantiated and called as such:
// NumArray numArray = new NumArray(nums);
// numArray.sumRange(0, 1);
// numArray.sumRange(1, 2);

难度-Medium


53. Maximum Subarray

给你一个数组,求最大的子数组的和是多少。

这道题和买卖股票的思想是一样的,参考上面的过程就好。唯一的不一样的是最大子数组的和有可能是负数。

public class Solution {
    public int maxSubArray(int[] nums) {
        if(nums == null || nums.length == 0) return 0;
        int maxIndex = nums[0];
        int maxTotal = nums[0];
        for(int i = 1; i < nums.length; ++i){
            maxIndex = Math.max(nums[i], maxIndex += nums[i]); //和股票那道题的不一样之处
            maxTotal = Math.max(maxIndex, maxTotal);
        }
        return Math.max(maxIndex, maxTotal);
    }
}

376. Wiggle Subsequence

求最长的扭动序列。注意,这道题可以删除数组里面的某些元素来达到扭动序列。

这道题我们用两个变量来存储中间的计算结果:

  • up:上一次上升趋势的时候的最大扭动序列的长度
  • down: 上一次下降趋势的时候的最大扭动序列的长度

从0开始遍历数组,当遍历到 i 的时候, i-1 到 i 的趋势有:

  • 上升趋势:那么这时候 0 ~ i 序列的最大扭动序列的长度 = 上一次下降趋势的时候的最大扭动序列的长度 + 1
  • 下降趋势:那么这时候 0 ~ i 序列的最大扭动序列的长度 = 上一次上升趋势的时候的最大扭动序列的长度 + 1
  • 趋势不变:那么这时候 0 ~ i 序列的最大扭动序列的长度不变

于是最后的结果就是 up 和 down 之间的最大值。

public class Solution {
    public int wiggleMaxLength(int[] nums) {
        if(nums == null || nums.length == 0) return 0;
        int up = 1;
        int down = 1;
        for(int i = 1; i < nums.length; ++i){
            if((nums[i] - nums[i-1]) > 0) up = down + 1;
            if((nums[i] - nums[i-1]) < 0) down = up + 1;
        }
        return Math.max(up, down);
    }
}

62. Unique Paths

给你一个二维数组,问从左上角走到右下角一共有多少种走法,只能往下走或者往右走。

思路:
我们用一个数组res[][]来存中间的缓存结果,res[i][j] 表示从左上角[0][0]走到[i][j]时一共有多少种走法。
由于只能往下走或者往右走,则

  • res[i][j] = 上面的元素有多少种走法 + 左边的元素有多少种走法。
public class Solution {
    public int uniquePaths(int m, int n) {
        if(n > 100 || m > 100) return 0;
        int[][] res = new int[m][n];
        for(int i = 0; i < m; i++) res[i][0] = 1;
        for(int i = 0; i < n; i++) res[0][i] = 1;
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                res[i][j] = res[i-1][j] + res[i][j-1];
            }
        }
        return res[m-1][n-1];
    }
}

63. Unique Paths II

这道题是上面一道题的扩展,唯一不同的是给出的矩阵里面有障碍物,遇到障碍物则走不通了。只需加一些判断条件就好。

public class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        if(obstacleGrid[0][0] == 1) return 0;
        int[][] res = new int[obstacleGrid.length][obstacleGrid[0].length];
        for(int i = 0; i < obstacleGrid.length; i++){
            if(obstacleGrid[i][0] == 1) break;
            res[i][0] = 1;
        }
        for(int i = 0; i < obstacleGrid[0].length; i++){
            if(obstacleGrid[0][i] == 1) break;
            res[0][i] = 1;
        }
        for(int i = 1; i < obstacleGrid.length; i++){
            for(int j = 1; j < obstacleGrid[0].length; j++){
                if(obstacleGrid[i][j] == 0){
                    res[i][j] = res[i-1][j] + res[i][j-1];
                }
            }
        }
        return res[obstacleGrid.length-1][obstacleGrid[0].length-1];
    }
}

64. Minimum Path Sum

给你一个二维数组,数组里面有全是正数的值,问从左上走到右下角(只能往下走或者往右走),问路径和最小的值是多少。

思路:动态规划就是存储之前的结果用作下一步计算嘛,我们就用数组的 grid[i][j] 存从 grid[0][0] 到 grid[i][j]的最小路径和。那么 grid[i][j] 怎么求,注意到只能往下走或者往右走,那么 grid[i][j] 就等于它上面的和它左边的两个元素中的最小值,再加上它自己本身:

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

300. Longest Increasing Subsequence

给你一个数组,求它的最长增长子序列(注意不是字串)

用一个dp数组存最长的增长子序列,依次遍历给定的数组,然后把数组的元素 num 和 dp 数组里面的元素比较,有一下的结果:

  • num 比 dp 数组里面所有元素都大,便插入在所有元素的后面
  • num 在 dp 数组所有元素的中间,那么看应该在哪个位置,把那个位置的元素替换为num

这里用到的 Arrays.binarySearch 是用二分法来查找数组里面的元素,若元素存在在数组里面,则返回元素的索引,若不存在,则假设此元素应该存在在索引值为pos的位置,会返回 -pos-1

最后返回 dp 数组里面元素的个数就是最长的子序列的长度。

public class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        int len = 0;
        for(int num: nums){
            int pos = Arrays.binarySearch(dp, 0, len, num); //返回的是-(position)-1
            if(pos < 0) pos = -(pos+1);
            dp[pos] = num;
            if(pos == len) ++len;
        }
        return len;
    }
}

304. Range Sum Query 2D - Immutable

public class NumMatrix {
    int[][] sum;
    int line;
    int col;

    public NumMatrix(int[][] matrix) {
        if(matrix == null || matrix.length == 0 ) return;
        sum = matrix;
        line = matrix.length;
        col = matrix[0].length;
        for(int i = 1; i < line; ++i){
            sum[i][0] += sum[i-1][0];
        }
        for(int j = 1; j < col; ++j){
            sum[0][j] += sum[0][j-1];
        }
        for(int i = 1; i < line; ++i){
            for(int j = 1; j < col; ++j){
                sum[i][j] += sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1];
            }
        }
    }

    public int sumRegion(int row1, int col1, int row2, int col2) {
        if(row1 > row2 || col1 > col2) return 0;
        if(row1 == 0 && col1 == 0) return sum[row2][col2];
        else if(col1 == 0) return sum[row2][col2] - sum[row1-1][col2];
        else if(row1 == 0) return sum[row2][col2] - sum[row2][col1-1];
        else return sum[row2][col2] - sum[row1-1][col2] - sum[row2][col1-1] + sum[row1-1][col1-1];
    }
}


// Your NumMatrix object will be instantiated and called as such:
// NumMatrix numMatrix = new NumMatrix(matrix);
// numMatrix.sumRegion(0, 1, 2, 3);
// numMatrix.sumRegion(1, 2, 3, 4);

91. Decode Ways

字母A-Z对应的数组解码为:

‘A’ -> 1
‘B’ -> 2

‘Z’ -> 26

给你一串数字字符串,问一共可以解码出多少种对应的字母字符串。
如 “12” 可以解码为 “AB” (1 2) 或者 “L” (12) 两种。

思路:
我们依次遍历字符串s,遍历到第 i 个字符的时候,用两个变量记录:

  • last:在遍历到第 i-2 个字符的时候有多少种解码方式
  • now:在遍历到第 i-1 个字符的时候有多少种解码方式
public class Solution {
    public int numDecodings(String s) {
        if(s == null || "".equals(s)) return 0;
        int last = 1;
        int now = 1;
        if(s.charAt(0) == '0') return 0;
        char pre = s.charAt(0);
        for(int i = 1; i < s.length(); i++){
            if(s.charAt(i) == '0') {
                if(pre >= '1' && pre <= '2'){
                    int temp = now;
                    now = last;
                    last = temp;
                } else {
                    return 0;
                }
            } else if(pre == '1'){
                int temp = now;
                now += last;
                last = temp;
            } else if(pre == '2' && s.charAt(i) >= '1' && s.charAt(i) <= '6'){
                int temp = now;
                now += last;
                last = temp;
            } else {
                last = now;
            }
            pre = s.charAt(i);
        }
        return now;
    }
}

96. Unique Binary Search Trees

给你一个整数 n,问 1~n 这 n 个数可以组成多少种二叉搜索树。
如 n = 3,会有一下几种:

这里写图片描述

思路:
我们可以用自底向上的方法,用数组:

  • num 存从 1~n 的每个数可以组成二叉搜索树的种数。

那么对于第i个数来说,我们可以从 1~i 中随便挑出一个数 j 来当做根节点,那么 1~j-1 就作为 j 的左子树,j+1 ~ i 就作为 j 的右子树,那么

  • 1~j-1 的左子树,一共有 num[j-1] 种组成二叉搜索树的方法
  • j+1 ~ i 的右子树,它组成二叉搜索树的的方法其实和 j+1-j ~ i-j (1~i-j)的方法是一样的。比如:1 2 3 4 和 4 5 6 7是一样的
public class Solution {
    public int numTrees(int n) {
        //1 2 3 4 和 4 5 6 7是一样的
        int[] num = new int[n + 1];
        num[0] = 1;
        num[1] = 1;
        for(int i = 2; i <= n; i++){
            for(int j = 1; j <=i; j++){
                num[i] += num[j-1]*num[i-j];
            }
        }
        return num[n];
    }
}

309. Best Time to Buy and Sell Stock with Cooldown

又是买卖股票的问题,和上面那道买卖股票问题不一样的是这里加了个Cooldown“休息”的状态:

  1. 在你把股票卖了之后,你不能立刻在下一天买进股票。(例如Cooldown“休息一天”)
  2. 同一时间内你不能操作多次交易。(例如你必须卖了股票之后才能再买入)

例如:

股票为:[1, 2, 3, 0, 2]
最大利润:3
交易状态:[buy, sell, cooldown, buy, sell]

思路:
既然这里有3个状态,我们可以用状态机来解决。假设有s0,s1,s2三个状态,如下图所示:
这里写图片描述

  • s0代表cooldown的状态,它可以由上一次是cooldown的状态继续cooldown转化而来,也可以由上一次是卖出股票后cooldown而来。
  • s1代表买进股票之后的状态,他可以由上一次是cooldown然后买进股票而来,也可以是已经买进股票了,继续持有股票而来。
  • s2代表卖出股票的状态,他只能由上一次是s1的状态卖出股票而来。

所以求最大的;利润,就是求每个状态的最大值;

  • s0[i] = max(s0[i - 1], s2[i - 1])
  • s1[i] = max(s1[i - 1], s0[i - 1] - prices[i])
  • s2[i] = s1[i - 1] + prices[i]

最大的利润就是最后状态为刚卖出去股票“s2”和最后状态为cooldown“s0”两者的最大值。

public class Solution {
    public int maxProfit(int[] prices) {
        if(prices == null || prices.length == 0) return 0;
        int[] s0 = new int[prices.length+1];
        int[] s1 = new int[prices.length+1];
        int[] s2 = new int[prices.length+1];
        s0[0] = 0;
        s1[0] = -(prices[0]);
        s2[0] = Integer.MIN_VALUE;
        int index = 1;
        for(int price: prices){
            s0[index] = Math.max(s0[index-1], s2[index-1]);
            s1[index] = Math.max(s1[index-1], s0[index-1] - price);
            s2[index] = s1[index-1] + price;
            index++;
        }
        return Math.max(s2[prices.length], s0[prices.length]);
    }
}

377. Combination Sum IV

给你一个全是正数且不重复的数组和一个数字target,问利用数组里面的数字有多少种方式组合,使得加起来的和等于target。

例如:

nums = [1, 2, 3]
target = 4

可能的组合为:

(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

这道题我一开始是用backTracking的套路来写的,得出来的结果是对的,但是提交的时候超时了,于是就想到要用DP来做,下面是backTracking的代码:

public class Solution {
    public int combinationSum4(int[] nums, int target) {
        if(target == 0 || nums == null || nums.length == 0) return 0;
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);
        backTracking(res, new ArrayList<>(), nums, target);
        return res.size();
    }
    
    private void backTracking(List<List<Integer>> res, List<Integer> list, int[] nums, int target){
        if(target < 0) return;
        if(target == 0) res.add(new ArrayList<>(list));
        else {
            for(int i = 0; i < nums.length; ++i){
                list.add(nums[i]);
                backTracking(res, list, nums, target - nums[i]);
                list.remove(list.size()-1);
            }
        }
    }
}

下面是动态规划DP的代码:

思路:我们用自底向上的方法,用res数组来存中间的缓存结果,res[i] 表示 有多少种组合方式的和等于i,那么对于i+1来说,遍历nums数组,对于每一个元素num,如果num > i+1,那么num是无论如何组合都不会和等于i+1,如果不是:

  • 恰好 num == i+1,即由加一种组合方式,就只是num本身,那么在原来的基础上res[i+1]加1即可
  • num < i+1 的情况,则新的组合方式等于res[i+1-num],那么在原来的基础上res[i+1]加res[i+1-num]即可
public class Solution {
    public int combinationSum4(int[] nums, int target) {
        if(target == 0 || nums == null || nums.length == 0) return 0;
        int[] res = new int[target+1];
        Arrays.sort(nums);
        for(int i = 1; i < res.length; ++i){
            for(int num: nums){
                if(num > i) break;
                else if(i == num) res[i] += 1;
                else {
                    res[i] += res[i-num];
                }
            }
        }
        return res[target];
    }
}

279. Perfect Squares

给定一个正整数n,让我们找出最少的平方数使得他们加起来等于n。(平方数就是由某个数的平方得来的)

例如:

n = 12

返回 3,因为

12 = 4 + 4 + 4

思路:
动态规划的思想就是缓存之间的结果。我们用自底向上的方法,用res数组存1到n的结果。res[i]表示最少的平方数使得他们加起来等于i。

public class Solution {
    public int numSquares(int n) {
        int[] res = new int[n+1];
        Arrays.fill(res, Integer.MAX_VALUE);
        res[0] = 0;
        for(int i = 1; i < res.length; ++i){
            int min = Integer.MAX_VALUE;
            int j = 1;
            while(i >= j*j){
                min = Math.min(min, res[i-j*j] + 1);
                ++j;
            }
            res[i] = min;
        }
        return res[n];
    }
}

357. Count Numbers with Unique Digits

public class Solution {
    public int countNumbersWithUniqueDigits(int n) {
        if(n == 0) return 1;
        int res = 10;
        int base = 9;
        for(int i = 2; i <= n && i <=10; ++i){
            base = base * (9 + 2 - i);
            res += base;
        }
        return res;
    }
}

322. Coin Change换零钱

给定一个数组coins代表有零钱的面额,问找开amount这个数的钱用的最少零钱的张数是多少?找不开则返回-1

例如:

coins = [1, 2, 5], amount = 11

返回:

3 因为(11 = 5 + 5 + 1)

思路:
用一个res 数组缓存中间结果,用自底向上的方法,res[i]表示找开i的最少零钱数。res[i]先初始化为Integer.MAX_VALUE,表示找不开i。

public class Solution {
    public int coinChange(int[] coins, int amount) {
        if(coins == null || coins.length == 0) return -1;
        if(amount == 0) return 0;
        int[] res = new int[amount + 1];
        for(int i = 1; i < res.length; ++i){
            res[i] = Integer.MAX_VALUE;
        }
        Arrays.sort(coins);
        for(int i = 1; i < res.length; ++i){
            for(int j = 0; j < coins.length; ++j){
                if(i < coins[j]) continue;
                if(i >= coins[j]){
                    int tag = res[i - coins[j]];
                    if(tag != Integer.MAX_VALUE){ //如果找的开
                        res[i] = Math.min(tag + 1, res[i]);
                    }
                }
            }
        }
        return res[amount] == Integer.MAX_VALUE? -1: res[amount];
    }
}

132. Palindrome Partitioning II

给定一个字符串s,返回最少的分割次数使得字串全是回文。

思想:

  • 矩阵dp[i][j] 存的是给定的字符串 s 的 s[i~j] 字串是否是回文
  • 数组res[i] 存的是 s[i~n-1] 的最小分割次数

然后如果 dp[i][j] == true 的话

  • 如果 j == n-1 说明 s[i~n-1] 是回文,则不用分割,即分割次数为0,res[i] = 0;
  • 如果 j!= n-1 说明 对s[i~n-1],在 j 这里切一刀,s[i~j] 是回文,看 s[j+1…n-1]的最小切割数(res[j+1])是多少,res[j+1] + 1 和 原来的 res[i] 比较,取最小值。
  • 即 res[i] = Math.min(res[i], res[j+1]+1)

res[0] 就是答案。

public class Solution {
    public int minCut(String s) {
        if(s == null || s.length() < 2) return 0;
        int length = s.length();
        boolean[][] dp = new boolean[length][length];
        int[] res = new int[length];
        for(int i = 0; i < length; i++){
            Arrays.fill(dp[i], false);
        }
        for(int i = length-1; i >= 0; i--){
            res[i] = length - i - 1; //worse
            for(int j = i; j < length; j++){
                /*如果字符串的两边相等的情况下
                  1、这个字符串只有两个字符
                  2、这个字符串不止两个字符,但是除了这两个字符的中间字符串是回文
                  那么这个字符串就是回文
                */
                if(s.charAt(i) == s.charAt(j) && (j-i < 2 || dp[i+1][j-1])){
                    dp[i][j] = true;
                    if(j == length-1){
                        res[i] = 0;
                    } else {
                        res[i] = Math.min(res[i], res[j+1]+1);
                    }
                }
            }
        }
        return res[0];
    }
}

338. Counting Bits

给定一个正整数num,对于 0 ≤ i ≤ num 的每一个 i ,计算它的二进制表示中有多少个1,结果作为一个数组返回。

思路:
总不能每个都转化为二进制,再来数里面有多少个1吧,这是最笨的方法。那么我们再想远一点。
我们可以在纸上画一下各个数字的二进制表示:

0 0
1 01
2 10
3 11
4 100
5 101
6 110
7 111
8 1000
9 1001

可以看出:

  • 数字1和2是在数字0的二进制后面分别拼接上1和0而形成的
  • 数字2和3是在数字1的二进制后面分别拼接上1和0而形成的
  • ···
  • 数字n-1和n是在数字n/2的二进制后面分别拼接上1和0而形成的

所以数字n的二进制有多少个1等于数字n/2的二进制有多少个1加上n的末尾是不是1:

res[i] = res[i / 2] + i % 2.

public class Solution {
    public int[] countBits(int num) {
        int[] res = new int[num+1];
        if(num < 0) return res;
        for(int i = 0; i < res.length; i++){
            res[i] = res[i >> 1] + (i & 1);
        }
        return res;
    }
}

486. Predict the Winner 纸牌博弈的问题

一个全是正数的数组nums代表纸牌,A和B依次拿走里面的纸牌,规定A先拿,B后拿,并且每次只能拿走最左边或者最右边的纸牌。A和B都绝顶聪明,谁最后手里的纸牌的值的和最多谁就获胜,请问A最后是否获胜。

我们用两个矩阵缓存中间的结果:

  • f[i][j]:代表num[i~j]的牌中绝顶聪明的人拿,这个人最终能获得多少分
  • s[i][j]:代表num[i~j]的牌中绝顶聪明的人拿,这个人最终能获得多少分

会出现的情况是:

  1. i==j,只有一张牌,这时先拿的人肯定把这一张牌拿走了,所以f[i][j]=nums[i]
  2. i!=j,先拿的人不是拿走nums[i]就是拿走nums[j],对于先拿的人来说有两种结果,并且因为他绝顶聪明,所以会拿这两种情况的最大者:
    (1). 拿走nums[i],在num[i+1~j]中后拿
    (2). 拿走nums[j],在num[i~j-1]中后拿
    对于后拿的人来说有两种结果,因为先拿那个人绝顶聪明,只会在下面两种情况中留下最小值给后拿的人:
    (1). 在num[i+1~j]中先拿
    (2). 在num[i~j-1]中先拿

下面第一个循环从j开始,是因为:

  1. 最后要求的是f[0][nums.length-1]和s[0][nums.length-1],而不是f[nums.length][0]
  2. 从i开始数组会越界
class Solution {
    public boolean PredictTheWinner(int[] nums) {
        //if(nums == null || nums.length == 0) return false;
        int[][] f = new int[nums.length][nums.length];
        int[][] s = new int[nums.length][nums.length];
        for(int j = 0; j < nums.length; j++){
            f[j][j] = nums[j];
            s[j][j] = 0;
            for(int i = j-1; i >=  0; i--){
                f[i][j] = Math.max(nums[i]+s[i+1][j], nums[j]+s[i][j-1]);
                s[i][j] = Math.min(f[i+1][j], f[i][j-1]);
            }
        }
        return f[0][nums.length-1] >= s[0][nums.length-1];
    }
}

faster than 25.82% of Java online submissions for Predict the Winner.


877. Stone Game 石头游戏

其实和上面的《486. predict-the-winner 纸牌博弈的问题》一毛一样,这里就不重复了。

class Solution {
    public boolean stoneGame(int[] p) {
        int l = p.length;
        int[][] f = new int[l][l];
        int[][] s = new int[l][l];
        f[0][0] = p[0];
        for(int j = 0; j < l; j++){
            f[j][j] = p[j];
            for(int i = j-1; i >= 0; i--){
                f[i][j] = Math.max(p[i]+s[i+1][j], p[j]+s[i][j-1]);
                s[i][j] = Math.min(f[i+1][j], f[i][j-1]);
            }
        }
        return f[0][l-1] >= s[0][l-1];
    }
}

faster than 23.31% of Java online submissions for Stone Game.


55. Jump Game 跳跃游戏

有一个数组n,n[i]=k表示从位置i可以向右跳1,2,…,k个距离中的一个,问最后是否会能跳到最后一个位置。

设置一个变量next,表示:遍历到i的时候,0~i的位置上最远能够跳到的地方,所以如果i>next,表示跳不到i

class Solution {
    public boolean canJump(int[] n) {
        if(n == null || n.length == 0) return false;
        int next = 0;
        for(int i=0; i < n.length; i++){
            if(next < i) return false;
            next = Math.max(next, i+n[i]);
        }
        return true;
    }
}

改一下题目的话,假如能够跳到最后一个位置,最小跳多少步?

用以下两个变量:

  • jump:表示现在最少跳了多少步
  • curFarthest:表示如果只能跳jump步,最远跳到哪里
  • nextFarthest:表示如果跳jump+1步,最远跳到哪里

这时jump就是最小跳的步数

class Solution {
    public boolean canJump(int[] n) {
        if(n == null || n.length == 0) return false;
        int jump = 0;
        int curFarthest = 0;
        int nextFarthest = 0;
        for(int i=0; i < n.length; i++){
            if(curFarthest < i){
                jump++;
                curFarthest = nextFarthest ;
            }
            if(nextFarthest < i) return false;
            nextFarthest = Math.max(nextFarthest , i+n[i]);
        }
        return true;
    }
}

647. Palindromic Substrings 回文子串个数

给定一个字符串s,返回这个字符串的子串是回文的子串个数
例如字符串是"aaa",那么返回6,因为有6个回文子串"a", “a”, “a”, “aa”, “aa”, “aaa”.

新建一个二维数组dp[][]作为缓存,dp[i][j]表示字符串s[i~j]是否是回文,如果是,那么久为true,不是为false。那么dp[i][j]怎么求呢?

判断s[i~j]是否是回文,先判断s[i]和s[j]是否相等,如果不相等的话就肯定不是了,如果相等的话, s[i~j]只有1个元素或者两个元素,那么他就是回文,如果超过2个元素,判断 s[i+1~j-1]是否是回文,是的话,那么 s[i~j]就是回文。

于是从s的每一个元素作为中心点往外进行扩散,寻找所有的可能的子串,再判断是否是回文,是的话记录就加1,最后便求得结果。

class Solution {
    public int countSubstrings(String s) {
        if(s == null || s == "") return 0;
        int l = s.length();
        boolean[][] dp = new boolean[l][l];
        int res = 0;
        for(int i=0; i < l; i++){
            for(int j=i; j >= 0; j--){
                if(s.charAt(i) == s.charAt(j) && (i-j<3 || dp[i-1][j+1])){
                    dp[i][j] = true;
                    res++;
                }
            }
        }
        return res;
    }
}

faster than 22.11% of Java online submissions for Palindromic Substrings.


5. Longest Palindromic Substring 最长回文子串

求字符串s的最长回文子串。

和上题《647. Palindromic Substrings 回文子串个数》的思路是一样的。用dp[i][j]表示字符串s[i~j]是否是回文,同时记录最长子串的长度和开始位置。

class Solution {
    public String longestPalindrome(String s) {
        if(s == null || s == "") return "";
        int l = s.length();
        int start = 0;
        int maxLen = 0;
        boolean[][] dp = new boolean[l][l];
        for(int i=0; i < l; i++){
            for(int j=i; j >= 0; j--){
                if(s.charAt(i) == s.charAt(j) && (i-j<3 || dp[j+1][i-1])){
                    dp[j][i] = true;
                    if(i-j+1 > maxLen){
                        maxLen = i-j+1;
                        start = j;
                    }
                }
            }
        }
        return s.substring(start, start+maxLen);
    }
}

712. Minimum ASCII Delete Sum for Two Strings

给定两个字符串s1和s2,通过删除s1和s2的字符使得两个字符串相等,每删除一个字符的代价是这个字符的ASCII 码,求使得两个字符串相等的最小代价。

新建dp[s1.length()+1][s2.length()+1]的数组,dp[i][j]代表s1[0~i-1]和 s2[0~j-1]删除成相等字符串的代价。

所以dp[0][0代表空串编辑成空串的代价,自然是0;
dp[i][j]的值为以下两种情况:

  1. 如果s1[i-1] == s2[j-1],那么最小代价就只为dp[i-1][j-1]
  2. 如果s1[i-1] != s2[j-1],那么最小代价为以下的最小值,删除s1[i-1],把s1[0~i-2] 和 s2[0~j-1] 编辑成相同的字符串;删除 s2[j-1],把 s1[0~i-1] 和 s2[0~j-2] 编辑成相同的字符串;
class Solution {
    public int minimumDeleteSum(String s1, String s2) {
        int l1 = s1.length();
        int l2 = s2.length();
        int[][] dp = new int[l1+1][l2+1];
        for(int i = 1; i < l1+1; i++){
            dp[i][0] = dp[i-1][0] + s1.charAt(i-1);  
        }
        for(int j = 1; j < l2+1; j++){
            dp[0][j] = dp[0][j-1] + s2.charAt(j-1);  
        }
        for(int i = 1; i < l1+1; i++){
            for(int j = 1; j < l2+1; j++){
                if(s1.charAt(i-1) == s2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1];
                }else{
                    dp[i][j] = Math.min(s1.charAt(i-1)+dp[i-1][j], s2.charAt(j-1)+dp[i][j-1]);
                }
            }
        }
        return dp[l1][l2];
    }
}

faster than 29.57% of Java online submissions for Minimum ASCII Delete Sum for Two Strings.


难度-Hard


72. edit-distance 最小编辑代价

给定两个字符串s1和s2,每删除,增加和编辑一个字符的代价是1,返回最小将s1编辑成s2的最小代价。

首先生成 [ s 1. l e n g t h ( ) + 1 ] [ s 2. l e n g t h ( ) + 1 ] [s1.length()+1] *[ s2.length()+1] 大小的矩阵dp,dp[i+1][j+1]的值代表s1[0~i]编辑成
s2[0~j]的最小代价。

为什么dp的大小不是 [ s 1. l e n g t h ( ) ] [ s 2. l e n g t h ( ) ] [s1.length()] *[ s2.length()] 而是 [ s 1. l e n g t h ( ) + 1 ] [ s 2. l e n g t h ( ) + 1 ] [s1.length()+1] *[ s2.length()+1] 呢?因为我们考虑到了空串"",dp[0][j+1] 的意思就是将空串""编辑成s2[0~j]的最小代价,自然就是增加j个字符就可以了。如果不考虑空串的话,dp矩阵就难算很多。

dp[i][j]的值可能来自于下面的4种情况:

  1. 如果 s1[i-1] == s2[j-1],那么只需要将s1[0~i-2]编辑成 s2[0~j-2]就好,那么自然最小代价就为dp[i-1][j-1]
  2. 如果 s1[i-1] != s2[j-1], 那么就编辑一个字符,将s1[i-1] 编辑成 s2[j-1],那么自然最小代价就为dp[i-1][j-1]+1
  3. 先删除字符s1[i-1],于是s1[0~i-1] 变成了s1[0~i-2], 再将s1[0~i-2] 编辑成 s2[0~j-1], 将s1[0~i-2] 编辑成 s2[0~j-1]的最小代价为dp[i-1][j],那么自然最小代价就为dp[i-1][j]+1
  4. 先删除字符s2[j-1],于是s2[0~j-1] 变成了s2[0~j-2], 再将s1[0~i-1] 编辑成 s2[0~j-2], 将s1[0~i-1] 编辑成 s2[0j-2]的最小代价为dp[i][j-1],再增加一个字符就变成了s2[0j-1],那么自然最小代价就为dp[i][j-1]+1
class Solution {
    public int minDistance(String s1, String s2) {
        if(s1 == null || s2 == null) return 0;
        int row = s1.length()+1;
        int col = s2.length()+1;
        int[][] dp = new int[row][col];
        for(int i=1; i < row; i++){
            dp[i][0] = i;
        }
        for(int j=1; j < col; j++){
            dp[0][j] = j;
        }
        for(int i=1; i < row; i++){
            for(int j=1; j < col; j++){
                if(s1.charAt(i-1) == s2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1];
                } else {
                    dp[i][j] = dp[i-1][j-1] + 1;
                }
                dp[i][j] = Math.min(dp[i-1][j]+1, dp[i][j]);
                dp[i][j] = Math.min(dp[i][j-1]+1, dp[i][j]);
            }
        }
        return dp[row-1][col-1];
    }
}

faster than 20.52% of Java online submissions for Edit Distance. 额…有点慢…


174. dungeon-game 地下城游戏

一张二维数组表示一张地图,骑士从左上角出发,到达右下角。每一个位置上都会有一个值,正数代表回血,负数代表失血,并且骑士在任何一个位置的时候血量都要大于0。请问骑士为了到达右下角,初始的血量最少是多少。

我们一般遇到动态规划的题,一开始都是想着中间的缓存结果矩阵dp从上到下,左到右进行计算,比如这道题的dp[i][j]就代表着从左上角到第i,j这个点骑士需要的最小血量是多少,做的时候会发现很难计算dp矩阵,这时候就可以换一个思路,从下到上,右到左进行计算

dp[i][j]代表从(i,j)这个点作为初始点,到达右下角的最少初始血量。下到上,右到左进行计算,dp[0][0]就是结果。

dp[i][j]的计算:

  1. 向右走,于是骑士在当前位置加完血和减完血之后的血量要求是只要等于dp[i][j+1]就好,就是dp[i][j+1]-map[i][j],同时骑士的血量要大于1,所以是max(dp[i][j+1]-map[i][j], 1)
  2. 向下走,同理

dp[i][j]的值为上面两种情况的最小值。

class Solution {
    public int calculateMinimumHP(int[][] d) {
        if(d == null || d.length == 0 || d[0] == null || d[0].length == 0) return 1;
        int row = d.length;
        int col = d[0].length;
        int[][] dp = new int[row][col];
        dp[row-1][col-1] = d[row-1][col-1] > 0 ? 1 : 1-d[row-1][col-1];
        for(int i = row-2; i >= 0; i--){
            dp[i][col-1] = Math.max(1, dp[i+1][col-1] - d[i][col-1]);
        }
        for(int j = col-2; j >= 0; j--){
            dp[row-1][j] = Math.max(1, dp[row-1][j+1] - d[row-1][j]);
        }
        for(int i = row -2; i >= 0; i--){
            for(int j = col-2; j >= 0; j--){
                int r = Math.max(1, dp[i+1][j] - d[i][j]);
                int l = Math.max(1, dp[i][j+1] - d[i][j]);
                dp[i][j] = Math.min(r, l);
            }
        }
        return dp[0][0];
    }
}

faster than 96.05% of Java online submissions for Dungeon Game.

猜你喜欢

转载自blog.csdn.net/notHeadache/article/details/52372242