LeetCode 总结 - 搞不定 Dynamic Programming 面试题

emmm…动态规划就练几个简单的题吧,其他的搞不定,告辞告辞~

如何想到使用DP

  • 找最大值/最小值(maximum/minimum)
  • 判断是否可行(yes/no)
  • 所有可能结果的数量(count)

解题思路

Matrix DP

[120] Triangle

自底向上:

public int minimumTotal(List<List<Integer>> triangle) {
    if (triangle.size() == 1) return triangle.get(0).get(0);
    int n = triangle.size();
    int[][] dp = new int[n][n];
    // 先算出最底层的值
    for (int i = 0; i < triangle.get(n - 1).size(); i++) {
        dp[n - 1][i] = triangle.get(n - 1).get(i);
    }
    // 自底向上
    for (int i = n - 2; i >= 0; i--) {
        for (int j = 0; j <= i; j++) {
            dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle.get(i).get(j);
        }
    }
    return dp[0][0];
}

自顶向下:

public int minimumTotal(List<List<Integer>> triangle) {
    if (triangle.size() == 1) return triangle.get(0).get(0);
    int n = triangle.size();
    int[][] dp = new int[n][n];
    dp[0][0] = triangle.get(0).get(0);
    // 先要初始化对角线所有点以及左侧边界所有点
    for (int i = 1; i < n; i++) {
        dp[i][0] = dp[i - 1][0] + triangle.get(i).get(0);
        dp[i][i] = dp[i - 1][i - 1] + triangle.get(i).get(i);
    }
    // 自顶向下
    for (int i = 1; i < n; i++) {
        for (int j = 0; j <= i; j++) {
            dp[i][j] = Math.min(dp[i - 1][j], dp[i - 1][j - 1]) + triangle.get(i).get(j);
        }
    }
    // 结果要在最后一行里找最大值
    int res = dp[n - 1][0];
    for (int i = 1; i < n; i++) {
        res = Math.min(res, dp[n - 1][i]);
    }
    return res;
}

参考讲解:

[62] Unique Paths

public int uniquePaths(int m, int n) {
    if (m == 0 || n == 0) return 0;
    int[][] dp = new int[m][n];
    // 初始化:第1行和第1列某个位置的来源都只有一种情况
    // (0,j-1)->(0,j), (j-1,1)->(j,1)
    for (int i = 0; i < m; i++) dp[i][0] = 1;
    for (int j = 0; j < n; j++) dp[0][j] = 1;
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            // 要么从上边来,要么从左边来
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
        }
    }
    return dp[m - 1][n - 1];
}

[63] Unique Paths II

public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    int m = obstacleGrid.length;
    int n = obstacleGrid[0].length;
    if (m == 0 || n == 0) return 0;
    int[][] dp = new int[m][n];
    for (int i = 0; i < m; i++) {
        if (obstacleGrid[i][0] == 0) dp[i][0] = 1;
        else break;
    }
    for (int j = 0; j < n; j++) {
        if (obstacleGrid[0][j] == 0) dp[0][j] = 1;
        else break;
    }
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            if (obstacleGrid[i][j] == 0) dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            else dp[i][j] = 0;
        }
    }
    return dp[m - 1][n - 1];
}

[64] Minimum Path Sum

public int minPathSum(int[][] grid) {
    if (grid == null || grid.length == 0 || grid[0].length == 0) return -1;
    int m = grid.length;
    int n = grid[0].length;
    int[][] dp = new int[m][n];
    // 初始化
    dp[0][0] = grid[0][0];
    for (int i = 1; i < m; i++) dp[i][0] = dp[i - 1][0] + grid[i][0];
    for (int j = 1; j < n; j++) dp[0][j] = dp[0][j - 1] + grid[0][j];
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = grid[i][j] + Math.min(dp[i - 1][j], dp[i][j - 1]);
        }
    }
    return dp[m - 1][n - 1];
}

Sequence DP

[70] Climbing Stairs

问题:爬楼梯要爬n步才能到达楼顶,每次只能爬1步或者2步,一共有多少种方式爬到楼顶?

思路:斐波那契数列的典型应用。
0级台阶:0
1级台阶:1
2级台阶:2
3级台阶:1+2=3
4级台阶:2+3=5
……
n级台阶:f(n-2)+f(n-1)=f(n)
令dp[i]表示爬到第i阶楼梯可行的不同方式数目,接着寻找当前子问题dp[i]与前面子问题的关系。

  • 如果是用两步跳过来的(爬到第i阶楼梯)则dp[i]=dp[i-2],因为这两步已经确定了,那么只有dp[i-2]种可能
  • 如果是用一步跳过来的(爬到第i阶楼梯)则dp[i]=dp[i-1],因为这一步已经确定了,那么只有dp[i-1]种可能
public int climbStairs(int n) {
    if (n <= 2) return n;
    int[] dp = new int[n + 1];
    // 初始条件:一级楼梯是一种解法,两级楼梯是两种解法
    dp[1] = 1;
    dp[2] = 2;
    for (int i = 3; i <= n; i++) {
        // 可以从第i-1层阶梯过来,也可以从第i-2层阶梯过来,那么到达第i层阶梯的方法数是两个的和
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

由于动态规划的时候只用到了前面2步,因此可以用两个变量记录一下,这样就将空间复杂度降到O(1)了。

public int climbStairs(int n) {
    int result = 0;
    int num1 = 0, num2 = 1;
    for (int i = 1; i <= n; i++) {
        result = num1 + num2;
        num1 = num2;
        num2 = result;
    }
    return result;
}

参考讲解:

[55] Jump Game

public boolean canJump(int[] nums) {
    boolean[] dp = new boolean[nums.length];
    dp[0] = true;
    for (int i = 1; i < nums.length; i++) {
        // 枚举上一步跳到哪儿了,也就是i可以是从哪个j跳过来的
        for (int j = 0; j < i; j++) {
            if (dp[j] && j + nums[j] >= i) {
                dp[i] = true;
                break;
            }
        }
    }
    return dp[nums.length - 1];
}

public boolean canJump2(int nums[]) {
    int maxLocation = 0;
    for (int i = 0; i < nums.length; i++) {
        // 如果先前的maxLocation小于i,意味着不能到达i位置,因此返回false
        if (maxLocation < i) return false;
        // greedy
        maxLocation = (i + nums[i]) > maxLocation ? i + nums[i] : maxLocation;
    }
    return true;
}

[300] Longest Increasing Subsequence

public int lengthOfLIS(int[] nums) {
    if (nums == null || nums.length == 0) return 0;
    int n = nums.length;
    // dp[i]表示前i个数并且第i个数是LIS中最后一个数的最长LIS的长度
    int[] dp = new int[n];
    int max = 0;
    for (int i = 0; i < n; i++) {
        dp[i] = 1;
        // 枚举倒数第二个数是什么
        for (int j = 0; j < i; j++) {
            // 前面的数比第i个数小,满足递增条件
            if (nums[j] < nums[i]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
        max = Math.max(max, dp[i]);
    }
    return max;
}

Two Sequences DP

[72] Edit Distance

问题:给出两个单词word1和word2,计算出将word1 转换为word2的最少操作次数。你总共三种操作方法:插入一个字符、删除一个字符、替换一个字符。

思路:

public int minDistance(String word1, String word2) {
    int m = word1.length();
    int n = word2.length();
    // dp[i][j]表示word1的第i个字符匹配上word2的前j个字符
    int[][] dp = new int[m + 1][n + 1];
    // 删掉i个字符
    for (int i = 0; i < m + 1; i++) dp[i][0] = i;
    // 删掉j个字符
    for (int j = 0; j < n + 1; j++) dp[0][j] = j;
    // 枚举word1的所有位置
    for (int i = 1; i < m + 1; i++) {
        // 枚举word2的所有位置
        for (int j = 1; j < n + 1; j++) {
            if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1]));
            }
        }
    }
    return dp[m][n];
}

[LintCode 77] Longest Common Subsequence

问题:给出两个字符串,找到最长公共子序列(LCS),返回LCS的长度。

思路:

public int longestCommonSubsequence(String A, String B) {
    int m = A.length();
    int n = B.length();
    int[][] dp = new int[m + 1][n + 1];
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            if (A.charAt(i - 1) == B.charAt(j - 1))
                dp[i][j] = dp[i - 1][j - 1] + 1;
        }
    }
    return dp[m][n];
}

[322] Coin Change

问题:给定一组硬币和一个目标金额,返回最少用几个硬币能凑出目标金额,如果不能返回-1。

思路:数组dp用来记录能凑出从1到amount最少的硬币数量。

public int coinChange(int[] coins, int amount) {
    int[] dp = new int[amount + 1];
    for (int i = 1; i <= amount; i++) {
        // dp[i]表示凑齐钱数i需要的最少硬币数
        dp[i] = Integer.MAX_VALUE;
        for (int j = 0; j < coins.length; j++)
            if (i >= coins[j] && dp[i - coins[j]] != Integer.MAX_VALUE)
                // 用当前的硬币能组合成啥,取最小
                // dp[i + coins[j] ] = min(dp[i + coins[j] ] , dp[i] + 1)
                dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
    }
    return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}

参考讲解:

[746] Min Cost Climbing Stairs 爬楼梯的最小代价

问题:我们爬楼梯,每一步都会有代价,例如第i步代价为cost[i],你可以从第0个台阶爬起,也可以从第一个台阶爬起,同时,每次你可以跳一步,也可以跳两步。求爬完楼梯的最小代价是多少。

思路:

  • Recursion + Memorization 记忆化递归
    dp[n]表示爬到第n阶楼梯的最小代价,我们来思考一下如何才能到第n层呢?是不是只有两种可能性,一个是从第n-2层上直接跳上来,一个是从第n-1层上跳上来。那么dp[n] = min(dp(n-1)+cost[n-1], dp(n-2)+cost[n-2]),也就是说我可以从第n-1阶爬1步到达第n阶楼梯,但需要cost[n-1]的代价;我也可以从第n-2阶爬2步到达第n阶楼梯,但需要cost[n-2]的代价。最后我们返回最后一个数字dp[n]即可。
public int minCostClimbingStairs(int[] cost) {
    int m[] = new int[cost.length + 1];
    return dp(cost, m, cost.length);
}

private int dp(int[] cost, int[] m, int i) {
    // 在第0阶或第1阶开始爬是不需要代价的
    if (i <= 1) return 0;
    // 已求解过子问题,直接返回
    if (m[i] > 0) return m[i];
    return m[i] = Math.min(dp(cost, m, i - 1) + cost[i - 1],
            dp(cost, m, i - 2) + cost[i - 2]);
}
  • DP
// DP,空间复杂度O(n)
public int minCostClimbingStairs(int[] cost) {
    int n = cost.length;
    // dp[i]表示爬到第i阶楼梯的最小代价
    int[] dp = new int[n + 1];
    for (int i = 2; i <= n; i++) {
        // 要到达第i阶楼梯,可以从第i-1层上花cost[i-1]爬一步上来,也可以从第i-2层上花cost[i-2]爬两步上来
        dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
    }
    return dp[n];
}

// 滚动数组,空间复杂度O(1)
public int minCostClimbingStairs(int[] cost) {
    int n = cost.length;
    int dp1 = 0;
    int dp2 = 0;
    for (int i = 2; i <= n; i++) {
        int dp = Math.min(dp1 + cost[i - 1], dp2 + cost[i - 2]);
        dp2 = dp1;
        dp1 = dp;
    }
    // 因为dp1是最后的dp
    return dp1;
}

参考讲解:

[LintCode 20] Dice Sum

http://lintcode.com/en/problem/dices-sum

问题:扔n个骰子,所有骰子朝上一面的点数之和为s。输入骰子个数n,求点数之和为s出现的概率。

思路:假设dp(n,x)表示投第n个骰子的时候,点数之和为x出现的次数,投第n个骰子的点数之和只与投第n-1个骰子有关。我们得到递归方程:
f(n,x)=f(n-1,x-1)+f(n-1,x-2)+f(n-1,x-3)+f(n-1,x-4)+f(n-1,x-5)+f(n-1,x-6),表示本轮点数之和为n出现次数等于上一轮点数之和为n-1,n-2,n-3,n-4,n-5,n-6出现的次数之和。

public double probability(int n, int x) {
    // dp(n,x)表示投第n个骰子的时候,点数之和为x出现的次数,投第n个骰子的点数之和只与投第n-1个骰子有关
    int[][] dp = new int[n + 1][6 * n + 1];
    // 初始化n=1的情况
    for (int j = 1; j <= 6; j++) {
        dp[1][j] = 1;
        System.out.println("dp[" + 1 + "][" + j + "] = " + dp[1][j]);
    }
    for (int i = 2; i <= n; i++) {
        // i个筛子至少得到i点,至多得到6*i点
        for (int j = i; j <= 6 * i; j++) {
            // k表示最后一个筛子能取的点数
            for (int k = 1; k <= 6; k++) {
                // 表示本轮点数之和为n出现次数等于上一轮点数之和为n-1,n-2,n-3,n-4,n-5,n-6出现的次数之和
                if (j - k >= 0) dp[i][j] += dp[i - 1][j - k];
            }
            System.out.println("dp[" + i + "][" + j + "] = " + dp[i][j]);
        }
    }
    // 事件总数为6^n,dp[n][x]除以它就是发生的概率
    return dp[n][x] / (Math.pow(6, n));
}

而LintCode上是要我们求所有可能的x出现的概率:

public List<Map.Entry<Integer, Double>> dicesSum(int n) {
    // dp(n,s)表示投第n个骰子的时候,点数之和为s出现的次数,投第n个骰子的点数之和只与投第n-1个骰子有关
    List<Map.Entry<Integer, Double>> results = new ArrayList<>();
    for (int s = n; s <= 6*n; s++) {
        // 要用long存储,否则过不了后面大的Test Case
        long[][] dp = new long[n + 1][6 * n + 1];
        for (int j = 1; j <= 6; j++) {
            dp[1][j] = 1;
        }
        for (int i = 2; i <= n; i++) {
            for (int j = 1; j <= 6 * i; j++) {
                for (int k = 1; k <= 6; k++) {
                    // 表示本轮点数之和为n出现次数等于上一轮点数之和为n-1,n-2,n-3,n-4,n-5,n-6出现的次数之和
                    if (j - k >= 0) dp[i][j] += dp[i - 1][j - k];
                }
            }
        }
        // 事件总数为6^n,dp[n][s]除以它就是发生的概率
        double probability = dp[n][s] / (Math.pow(6, n));
        results.add(new AbstractMap.SimpleEntry<Integer, Double>(s, probability));
    }
    return results;
}

参考讲解:

    原文作者:野狗子嗷嗷嗷
    原文地址: https://www.jianshu.com/p/4f4e8428f113
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞