算法总结-动态规划(持续更新)

参考:https://mp.weixin.qq.com/s/xQ2cxyGg_vjviU17P1WUfQ

简述

动态规划算法具有时间效率较高,代码量较少的特点,可以考察思维能力、抽象能力以及灵活度,该算法的身影常常出现在面试、笔试或者竞赛中,今天对该算法进行总结并对面试等场合中常出现的题目进行分析。

1. 动态规划定义与理解

动态规划(Dynamic Programming,简称DP),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解。(上述定义来自百度百科)
个人理解,动态规划的核心包含3个重要环节
1)记录求解过程:记忆化存储每个子问题的解,以便下次需要同一个子问题解之时直接查询记录;
2)状态转移方程:整体问题最优解取决于子问题的最优解,因此需要得到子问题与最终问题间的关系;
3)边界问题:状态转移方程是一个递推式,因此需要找到递推终止的条件。

简单点说,动态规划思想就像《红楼梦》第六十二回提到的经典谚语“ ‘大事化为小事,小事化为没事’,方是兴旺之家”的处事方式,对于很多需要求解最优解的场合,动态规划方是解题之道。

2. 动态规划求解步骤:

1)判题题意是否为找出一个问题的最优解;

2)把原问题分解成若干个子问题,分析最优子结构与最终问题之间的关系,从而得到状态转移方程;

3)确定底层边界问题,例如最小的前几个f(n)的值;

4)求解问题,通常使用数组进行迭代求出最优解。

3. 递归、贪心算法、分治策略以及动态规划的比较

1)递归

将原问题归纳为更小的、相似的子问题,递归的过程中存在子问题的重复计算,耗费更多的时间和空间。

2)贪心算法

依赖于当前已经做出的所有选择,采用自顶向下(每一步根据策略得到当前一个最优解,保证每一步都是选择当前最优的)的解决方法,不能保证求出最优解,因此不能用来求最大或最小解问题

3)分治策略

将原问题分解为若干个规模较小、相互独立、类似于原问题的子问题,自顶向下递归求解这些子问题,然后再合并这些子问题的解来建立原问题的解。

4)动态规划

用于解决子问题有重复求解的情况,子问题之间是不独立,既可以用递归实现,也可以用迭代实现,动态规划能求出问题的最优解

4. 面试中常见的动态规划问题

选取字符串、数组以及树三种类别常见的动态规划题目进行分析和编码。

题目1:最长回文子串

题目描述:给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。
示例 1:输入: “babad” 输出: “bab” 注意: "aba"也是一个有效答案。
示例 2:输入: “cbbd” 输出: “bb”

扫描二维码关注公众号,回复: 6219093 查看本文章

解法1:暴力求解

求出该字符串的每一个子串,再判断子串是否是回文串,找到最长的那个。其中求出每个子串的时间复杂度为O(n2),判断是否为回文串的复杂度为O(n),两者是相乘关系,所以整个算法的时间复杂度为O(n3),空间复杂度:O(1)。

解法2:中心扩展法

依次去求得每个字符的最长回文,注意每个字符有奇数长度的回文串和偶数长度的回文串两种情况,记录最长回文的始末位置即可。时间复杂度为 O(n2)。 注:下面代码可左右滑动查看

class Solution {
    int start = 0, end = 0;
    public String longestPalindrome(String s) {
        int len = s.length();
        if (len <= 1) return s;
        char[] chars = s.toCharArray();
        for (int i = 0; i < len; i++) {
            helper(chars, i, i);
            helper(chars, i, i + 1);
        }
        return s.substring(start, end + 1);
    }
    private void helper(char[] chars, int l, int r) {
        while (l >= 0 && r < chars.length && chars[l] == chars[r]) {
            --l;
            ++r;
        }
        if (end - start< r - l - 2) {
            start= l + 1;
            end = r - 1;
        }
    }
}

解法3:动态规划

定义dp[i][j] 的意思为字符串区间[i, j] 是否为回文串,那么我们分三种情况:
(1)当 i == j 时,那么毫无疑问 dp[i][j] = true;
(2)当 i + 1 == j 时,那么 dp[i][j] 取决于 s[i] == s[j];
(3)当 i + 1 < j 时,那么 dp[i][j] 取决于 dp[i + 1][j - 1] && s[i] == s[j]。
时间复杂度为O(n2),空间复杂度为O(n2)。注:下面代码可左右滑动查看

class Solution {
    public String longestPalindrome(String s) {
        int len = s.length();
        if (len <= 1) return s;
        int st = 0, end = 0;
        char[] chars = s.toCharArray();
        boolean[][] dp = new boolean[len][len];
        for (int i = 0; i < len; i++) {
            dp[i][i] = true;
            for (int j = 0; j < i; j++) {
                if (j + 1 == i) {
                    dp[j][i] = chars[j] ==chars[i];
               } else {
                    dp[j][i] = dp[j + 1][i - 1]&& chars[j] == chars[i];
                }
                if (dp[j][i] && i - j> end - st) {
                    st = j;
                    end = i;
                }
            }
       }
        return s.substring(st, end + 1);
    }
}

题目2:连续子数组的最大和

题目描述:例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和

解法1:暴力求解

首先找出所有子数组,然后求出子数组的和,在所有子数组的和中取最大值。时间复杂度为O(n3),求解过程中存在子数组重复计算的问题。

解法2:动态规划

动态规划状态转移方程:(其实可以缩小空间,只要前面的数>0 就对后面有帮助,记录一下之前求出的和就行了。所以其实只需要O(n)的空间复杂度)
(1)当dp[i - 1]> 0 时,那么dp[i] = dp[i - 1] + nums[i];
(2)当 dp[i - 1]<= 0 时,那么 dp[i] = nums[i];
时间复杂度为O(n),空间复杂度也为O(n)。注:下面代码可左右滑动查看

public int maxSubArray (int[] nums) {
    if (nums.length == 0) return 0;
    if (nums.length == 1) return nums[0];
    int[] dp = new int[nums.length];
    dp[0] = nums[0];
    int max = dp[0];
    for (int i = 1; i < dp.length; i++) {
        if (dp[i - 1] > 0) {
            dp[i] = dp[i - 1] + nums[i];
        } else {
            dp[i] = nums[i];
        }
        max = Math.max(dp[i],max);
    }
    return max;
}

题目3:不同的二叉搜索树

题目描述:给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?
示例 1:输入: 3 输出: 5;
解释:给定 n = 3, 一共有 5 种不同结构的二叉搜索树

   1           3     3      2      1
    \         /     /      / \      \
     3       2     1      1   3      2
    /       /       \                 \
   2       1         2                 3

解法:动态规划

假设DP(n)表示n个节点组成不同二叉搜索树的种类数,分别考虑1到n作为根结点的情况。

(1) 根节点为1,则左子树必定为空,右子树为2…n个节点,那么种类数为1*DP[n-1],也可以表示为DP[0]*DP[n-1]。
(2) 根节点为2,则左子节点为1,右子树为3…n个节点,即DP[1]*DP[n-2]
(3) 根节点为3,则左子节点为1,2,右子树为4…n个节点,即DP[2]*DP[n-3]

每个根有DP[n-1]种情况, 根结点2到n-1时,每个根有DP[左边剩下数字] * DP[右边剩下数字] 种情况。
状态转移方程:DP(n) = DP(0)*DP(n-1)+DP(1)*DP(n-2)+…+DP(n-1)*DP(0) 注:下面代码可左右滑动查看

class Solution {
    public int numTrees(int n) {
        int[] DP = new int[n + 1];
        DP[0] = DP[1] = 1;
        for (int i = 2; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                DP[i] += DP[j - 1] * DP[i - j];
            }
        }
        return DP[n];
    }
}

猜你喜欢

转载自blog.csdn.net/qq_37886086/article/details/90166947