小白学DP(动态规划)

Posted ZSYL

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小白学DP(动态规划)相关的知识,希望对你有一定的参考价值。

动态规划

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。

动态规划算法分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次

如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。

概念

动态规划(英语: Dynamic programming,简称 DP) 是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划算法的核心就是记住已经解决过的子问题的解;而记住求解的方式有两种:

自顶向下的备忘录法

比如:斐波拉契数列 Fibonacci。

public static int fibonacci(int n) {
	if (n <= 1)
		return 1;
	if (n == 2)
		return 2;
	return fibonacci(n-1) + fibonacci(n-2);
}

我们分析以前写过的递归就会发现有很多节点被重复执行,如果在执行的时候把执行过的子节点保存起来,后面要用到的时候直接查表调用的话可以节约大量的时间。

public class Fibonacci {
	public static void main(String[] args) {
		//创建备忘录
		int[] memo = new int[n+1];
		System.out.println(fibonacci(7));
	}
	/**
	 * 自顶向下备忘录法
	 * @param n
	 * @param memo	备忘录
	 * @return
	 */
	public static int fibonacci(int n, int[] memo) {
		// 如果已经求出了fibonacci(n)的值直接返回
		if(memo[n] != 0) return memo[n];
		// 否则将求出的值保存在 memo 备忘录中。
		if(n<=2)
			memo[n]=1;
		else {
			memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo);
		}
		return memo[n];
	}
}

这个方法是由上至下,比如求f(5),我们要求f(4)和f(3),求出来后放入备忘录,当求f(4)时需要f(3)和f(2),我们可以直接从备忘录取f(3)而不是再去求一遍。

自底向上的动态规划

备忘录法是利用了递归,上面算法不管怎样,计算 fib(6)的时候最后还是要计算出 fib(1), fib(2), fib(3) ……,那么何不先计算出 fib(1), fib(2), fib(3) ……,呢?这也就是动态规划的核心,先计算子问题,再由子问题计算父问题。

public class FibonacciPlus {
	/**
	 * 自底向上的动态规划
	 * @param n
	 * @return
	 */
	public static int fib(int n) {
		if(n<=0)return -1;
		//创建备忘录
		int[] memo = new int[n+1];
		memo[0]=0;
		memo[1]=1;
		for(int i=2;i<=n;i++) {
			memo[i]=memo[i-1]+memo[i-2];
		}
		return memo[n];
	}
	/**
	 * 参与循环的只有 i, i-1 , i-2 三项,可以优化空间
	 * @param n
	 * @return
	 */
	public static int fibPlus(int n) {
		if(n<=0)return -1;
		int memo_i_2=0;
		int memo_i_1=1;
		int memo_i=1;
		for(int i=2;i<=n;i++) {
			memo_i = memo_i_1+memo_i_2;
			memo_i_2 = memo_i_1;
			memo_i_1 = memo_i;
		}
		return memo_i;
	}
}

例题

区域和检索-数组不可变

问题描述

给定一个整数数组 nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。

解法:
暴力循环从下标i到j求和,如果检索次数较多,则会超出时间限制。
- 降低时间复杂度,最理想情况O(1),求前缀和,sumRange(i, j) = (0-j+1)-(0-i-1的和)

代码
Java版

class NumArray {
    int[] sums;

    public NumArray(int[] nums) {
        int n = nums.length;
        sums = new int[n + 1];
        for (int i = 0; i < n; i++) {
            sums[i + 1] = sums[i] + nums[i];
        }
    }
    
    public int sumRange(int i, int j) {
        return sums[j + 1] - sums[i];
    }
}

Python版

class NumArray:

    def __init__(self, nums: List[int]):
        self.sums = [0]
        _sums = self.sums

        for num in nums:
            _sums.append(_sums[-1] + num)

    def sumRange(self, i: int, j: int) -> int:
        _sums = self.sums
        return _sums[j+1] - _sums[i]

复杂度分析

时间复杂度
初始化需要O(n),每次检索、O(1),其中n是nums的长度。
初始化需要检索遍历数组nums的前缀和,时间复杂度O(n)。

空间复杂度:
空间复杂度:O(n)O(n),其中 nn 是数组 \\textit{nums}nums 的长度。需要创建一个长度为 n+1n+1 的前缀和数组

使用最小花费爬楼梯

问题描述

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

解法

假设数组 cost 的长度为 n,则 n 个阶梯分别对应下标 0 到 n-1,楼层顶部对应下标 n,问题等价于计算达到下标 nn 的最小花费。可以通过动态规划求解。
创建长度为n+1的数组dp,dp[i]代表达到下标i的最小花费。
由于可以选择下标 00 或 11 作为初始阶梯,因此dp[0]=dp[1]=0。
当 2≤i≤n 时,可以从下标 i-1 使用 cost[i-1]的花费达到下标 i,或者从下标 i−2 使用cost[i-2] 的花费达到下标 i。为了使总花费最小,dp[i] 应取上述两项的最小值,因此状态转移方程如下:dp[i]=min(dp[i−1]+cost[i−1],dp[i−2]+cost[i−2])

Java版

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

Python版

class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        dp = [0, 0]
        n = len(cost)
        for i in range(2, n+1):
            dp.append(min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]))
        return dp[n]

优化版
上述代码的时间复杂度和空间复杂度都是 O(n)。注意到当 i≥2 时,dp[i] 只和 dp[i−1] 与 dp[i−2] 有关,因此可以使用滚动数组的思想,将空间复杂度优化到 O(1)。

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        int pre = 0, cur = 0, next = 0;
        for (int i = 2; i <= n; i++) {
            next = Math.min(cur+cost[i-1], pre + cost[i-2]);
            pre = cur;
            cur = next;
        }
        return cur;
    }
}

复杂度分析

时间复杂度:O(n),其中 n 是数组 cost 的长度。需要依次计算每个 dp 值,每个值的计算需要常数时间,因此总时间复杂度是 O(n)。

空间复杂度:O(1)。使用滚动数组的思想,只需要使用有限的额外空间。

小白上楼梯(三步问题)

爬楼梯一次可以爬一步,两步,三步,问爬到n阶台阶的方法

Java版 递归

static int f(int n) {
	if (n == 0) return 1;
	if (n == 1) return 1;
	if (n == 2) return 2;
	return f(n-1)+f(n-2)+f(n-3);
}

Python版 递推

class Solution:
    def waysToStep(self, n: int) -> int:
        if n == 1:
            return 1
        if n == 2:
            return 2
        if n == 3:
            return 4
        a = 1; b = 2; c = 4; d = 0
        for i in range(4, n+1):
            d = ((a+b) % 1000000007 + c) % 1000000007
            a = b
            b = c
            c = d
        return d

小白上楼梯2

爬楼梯每次至少爬d阶,爬到n阶台阶的方案数
动态规划,每次向前推d,第i-d的阶数方案会对下一次造成影响。

int sum = 1;
int mod = 1000000007;
System.out.println(mod);
for (int i = d; i <= n; i++) {
    sum += x[i-d];
    x[i] += sum;
    x[i] %= mod;
    sum %= mod;
}
System.out.println(x[n]);

最大字段和

给定序列,找出最大的子段和
累加每个元素,当sum<0,将其初始化为0(前缀和的思想)

每次都有前面最长连续和+上当前与不加当前值的两种选择

long sum = 0, max = 0;
for (int i = 0; i < n; i++) {
     sum += x[i];
     max = Math.max(max, sum);
     if (sum < 0)
         sum = 0;
 }
 System.out.println(max);
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        for i in range(1, len(nums)):
           nums[i] += max(nums[i-1], 0)
        return max(nums)

最长公共子串

在字符串中任意个连续的字符组成的子序列成为该串的子串,给定两个字符串,求出最长的公共子串的长度
思想:从头遍历一个字符串,当字符串不包含则将指针++

String s = sc.nextLine();
String x = sc.nextLine();
 int max = 0, i = 0;
 StringBuilder sb = new StringBuilder();
 for (char c : s.toCharArray()) {
     sb.append(c);
     String ss = sb.substring(i);
     if (x.contains(ss)) {
         max = Math.max(max, ss.length());
     } else {
         i++;
     }
 }
 System.out.println(max);

最长公共子序列

  • 最长公共子序列(longest common sequence)和最长公共子串(longest common substring)不是一回事儿。什么是子序列呢? 即一个给定的序列的子序列,就是将给定序列中零个或多个元素去掉之后得到的结果。什么是子串呢 给定串中任意个连续的字符组成的子序列称为该串的子串。

  • 举个例子(S1={1,3,4,5,6,7,7,8}和S2={3,5,7,4,8,6,7,8,2}),

    • 假如S1的最后一个元素与S2的最后一个元素相等,那么S1和S2的LCS就等于 {S1减去最后一个元素} 与 {S2减去最后一个元素} 的 LCS 再加上 S1和S2相等的最后一个元素。
    • 假如S1的最后一个元素与S2的最后一个元素不等(本例子就是属于这种情况),那么S1和S2的LCS就等于: {S1减去最后一个元素} 与 S2 的LCS, {S2减去最后一个元素} 与 S1 的LCS 中的最大的那个序列。
int[] x = {0, 2, 5, 7, 3, 6, 8, 4};
int[] y = {0, 3, 4, 7, 3, 6, 4};
int[][] xx = new int[x.length+1][y.length+1];
for (int i = 1; i < x.length; i++) {
    for (int j = 1; j < y.length; j++) {
        if (x[i] == y[j])
            xx[i][j] = xx[i-1][j-1]+1;
        else
            xx[i][j] = Math.max(xx[i-1][j], xx[i][j-1]);
    }
}
System.out.println(xx[x.length-1][y.length-1]);
  • 输出最长公共子序列
    类似的倒推回去:当x[n] i= y[n]时,比较xx[i-1][j]与x[i][j-1]如果若大于就选i-1与j反之选择i与j-1,当等于时选择不同的方向有不同的结果。
int i = x.length-1, j = y.length-1;
StringBuilder sb = new StringBuilder();
while (xx[i][j] > 0) {
    if (x[i] == y[j]) {
        sb.append(x[i]);
        i--;
        j--;
    } else if (x[i] != y[j]) {
        if (xx[i - 1][j] > xx[i][j - 1]) {
            i--;
        } else {
            j--;
        }
    }
}
System.out.println(sb.reverse().toString());

最长递增子序列

  • DP-动态规划
    状态设计:用一个维护数组dp[i]表示以a[i]结尾的最长递增子序列的长度
    状态转移:之后向前找到一个小于a[i]的进行状态转移dp[i] = Math.max(dp[i], dp[j]+1);
    边界处理:dp[i]=1(0<=j<n)
    时间复杂度:O(n2)
int[] x = {3, 1, 2, 1, 8, 5};
// dp[i]表示以a[i]结尾的最长递增子序列的长度
dp = new int[x.length];
int ans = 0;
for (int i = 0; i < x.length; i++) {
    // 初始化每一个dp[i]=1
    dp[i] = 1;
    for (int j = 0; j < i; j++) {
        if (x[i] > x[j])
            dp[i] = Math.max(dp[i], dp[j]+1);  // 状态转移
    }
    ans = Math.max(ans, dp[i]);  信息学奥赛一本通 5.2 树形动态规划

ACM - 动态规划小白入门:背包 / 线性 / 区间 / 计数 / 数位统计 / 状压 / 树形 / 记忆化 DP

学习之DP问题动态规划(DP)之数组问题

告别动态规划,清华学霸提灯给你讲解DP,听不懂你打我

信息学奥赛一本通 5.1 区间类动态规划

动态规划优化篇