动态规划

1,545 阅读6分钟

前言

最近晚上没事的时候看了一下leetcode上面的算法题,这几天看了一下动态规划的题目,发现这些题目都很有趣,比如爬楼梯最小花费爬楼梯打家劫舍等,用的思想都很巧妙,所以记录一下。由于好长时间没有用kotlin了,所以我这里给出java和kotlin两种写法,复习复习kotlin的用法(这里再加一种dart写法)。

定义

动态规划:通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划的基本思想:若要解一个给定问题,我们需要解其不同部分(即子问题),在根据子问题的解得出原问题的解。

例子

  • 爬楼梯
  • 最小花费爬楼梯
  • 打家劫舍

爬楼梯

问题:有n阶的楼梯,每次你可以爬1或2个台阶。你有多少种方法可以爬到楼顶?

分析:这个应该属于最简单的动态规划问题了,基本上有一定数学基础都能写出来。首先我们来分析一下:
第i阶楼梯可以由以下两种方法得到:

  1. 在第(i - 1)阶后向上爬一阶。
  2. 在第(i - 2)阶后向上爬2阶。

所以第i阶总数就是第(i -1)阶和第(i - 2)阶的方法数之和。
数学表达式为:f(i) = f(i - 1) + f(i - 2);
由此我们可以得出代码(java)。

public class Solution {
    public int climbStairs(int n){
        if(n==0 || n == 1){
            return n;
        }
        //这里设置数组长度为n+1是为了让数组从1开始计数。
        //如果从0开始计数则设置数组长度为n;
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3; i<= n; i++){
            dp[i] = dp[i -1]+ dp[i-2];
        }
        return dp[n];
    }
}

下面我们介绍一下kotlin的写法(kotlin)

fun clibStairs(n:Int) : Int{
    if(n == 0 || n == 1){
        return n
    }
    val dp = IntArray(n +1)
    dp[1] = 1
    dp[2] = 2
    for(i in 3..n){
        dp[i] = dp[i -1]+ dp[i -2]
    }
    return dp[n]
}

下面介绍一下dart的写法(dart)

int climbStairs(int n){
    if(n == 0 || n == 1){
        return n;
    }
    var dp = List<Int>(n + 1);
    dp[1] = 1;
    dp[2] = 2;
    for(int i = 3; i <= n; i++){
        dp[i] = dp[i - 1] + dp[i -2];
    }
    return dp[n];
}

最小花费爬楼梯

问题:数组的每个索引作为一个阶梯,第i个阶梯对应着一个非负数的体力花费值cost[i](索引从0开始)。每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。求到达楼顶的最低花费。(开始时,你可以选择从索引为0或1的元素作为初始阶梯)

分析:这个问题在爬楼梯的基础上面加了一个体力值的消耗,所以它不再是简单的找到所有路径,而是找出这些路径中花费体力最小的路径,如果我们从整体来说可能无从下手,总不能把所有路径的花费值都算出来比较大小吧。这个时候我们需要将问题划分为子问题,从子问题中归纳出整体的解。分析步骤如下:
我们假设有i个阶梯,数组用nums表示(数组下标从0开始,所以这里我们让i也从0开始);

  1. 当i=0时,花费最小体力值nums[0];
  2. 当i=1时,花费最小体力值nums[1];
  3. 当i=2时,有两种情况:
    1. 当nums[0] > nums[1]时,最小花费体力为nums[0] + nums[2];
    2. 当nums[0] < nums[1]时,最小花费体力为nums[1] + nums[2];

根据上面的推论,我们可以得出一个数学表达式:
f(i) = min(f(i-1), f(i -2))+nums[i]

有了数学表达式,我们可以写出代码如下(java):

public class Solution{
    public int minCostClimbingStairs(int[] cost){
        if(cost.length == 0){
            return 0;
        }else if(cost.length == 1){
            return cost[0];
        }
        int[] dp = new int[cost.length];
        dp[0] = cost[0];
        dp[1] = cost[1];
        for(int i = 2; i < cost.length; i++){
            dp[i] = Math.min(dp[i -1], dp[i - 2]) + cost[i];
        }
        return Math.min(dp[cost.length - 1], dp[cost.length - 2]);
    }
}

同样给出kotlin下的写法(kotlin)

fun minCostClimbingStairs(cost:Array<Int>) : Int{
    if(cost.isEmpty()){
        return 0
    }else if(cost.size == 1){
        return cost[0]
    }
    val dp = IntArray(cost.size)
    dp[0] = cost[0]
    dp[1] = cost[1]
    for(i in 2..(cost.size - 1)){
        dp[i] = Math.max(dp[i - 1], dp[i - 2]) + cost[i]
    }
    return Math.min(dp[cost.size - 1], dp[cost.size - 2])
}

dart语言下的写法(dart)

int minCostClimbingStairs(List<Int> cost){
    if(cost.isEmpty){
        return 0;
    }else if(cost.length == 1){
        return cost[0];
    }
    var dp = List<int>(cost.length);
    dp[0] = cost[0];
    dp[1] = cost[1];
    for(int i = 2; i < cost.length; i++){
        //这里要导入 'dart:math'类
        dp[i] = max(dp[i - 1],dp[i - 2]) + cost[i];
    }
    return min(dp[cost.length - 1], dp[cost.length - 2]);
}

打家劫舍

问题:你是专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动报警装置的情况下,能够偷窃到的最高金额。

分析:这个问题在爬楼梯问题的基础上面加深了一下,但是原理还是差不多的,我们假设有i个阶梯,数组为nums;

  1. 当i=1时,最大金额为nums[0];
  2. 当i=2时,最大金额为max(nums[0],nums[1]);
  3. 当i=3时,有两种情况:
    1. 抢第三个房子,将数额与第一个房子相加
    2. 不抢第三个房子,保持现有最大金额。

根据上面的推论,我们可以得出一个数学表达式:
f(i) = max(f(i -1),f(i - 2) + nums[i])
动态规划最重要的一点就是你能够根据简单的子问题归纳出整个问题的解,用数学表达式表示出来。有了数学表达式我们就很好写出代码。
具体代码如下(java)

public class Solution{
    publc int rob(int[] nums){
        if(nums.length == 0){
            return 0;
        }
        //这里我们让dp数组从下标1开始计数,所以数组长度加了1,当然也可以直接从0开始计数。
        int[] dp = new int[nums.length + 1];
        dp[0] = 0;
        dp[1] = nums[0];
        for(int i = 2; i<= nums.length; i++){
            dp[i] = Math.max(dp[i -1],dp[i -2] + nums[i - 1]);
        }
        return dp[nums.length];
    }
}

同样给出kotlin下的写法(kotlin)

fun rob(nums: Array<Int>) : Int{
    if(nums.isEmpty()){
        return 0
    }
    val dp = IntArray(nums.size + 1)
    dp[0] = 0
    dp[1] = nums[0]
    for (i in 2..nums.size){
        dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1])
    }
    return dp[nums.size]
}

dart语言的写法(dart)

int rob(List<int> nums){
    if(nums.isEmpty){
        return 0;
    }
    var dp = List<int>(nums.length + 1);
    dp[0] = 0;
    dp[1] = nums[0];
    for(int i = 2; i <= nums.length; i++){
        //这里要导入 'dart:math'类
        dp[i] = max(dp[i - 1],dp[i - 2] + nums[i - 1]);
    }
    return dp[nums.length];
}

总结

这三个问题算是动态规划中非常简单而又经典的题目了,而且将动态规划中拆分成相对简单的子问题来解决复杂问题用到了极致。可以作为我们理解动态规划入门的算法题。

参考文献