Python之动态规划算法
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python之动态规划算法相关的知识,希望对你有一定的参考价值。
参考技术A动态规划算法中是将复杂问题递归分解为子问题,通过解决这些子问题来解决复杂问题。与递归算法相比,动态编程减少了堆栈的使用,避免了重复的计算,效率得到显著提升。
先来看一个简单的例子,斐波那契数列.
斐波那契数列的定义如下。
斐波那契数列可以很容易地用递归算法实现:
上述代码,随着n的增加,计算量呈指数级增长,算法的时间复杂度是 。
采用动态规划算法,通过自下而上的计算数列的值,可以使算法复杂度减小到 ,代码如下。
下面我们再看一个复杂一些的例子。
这是小学奥数常见的硬币问题: 已知有1分,2分,5分三种硬币数量不限,用这些硬币凑成为n分钱,那么一共有多少种组合方法。
我们将硬币的种类用列表 coins 定义;
将问题定义为一个二维数组 dp,dp[amt][j] 是使用 coins 中前 j+1 种硬币( coins[0:j+1] )凑成总价amt的组合数。
例如: coins = [1,2,5]
dp[5][1] 就是使用前两种硬币 [1,2] 凑成总和为5的组合数。
对于所有的 dp[0][j] 来说,凑成总价为0的情况只有一种,就是所有的硬币数量都为0。所以对于在有效范围内任意的j,都有 dp[0][j] 为1。
对于 dp[amt][j] 的计算,也就是使用 coins[0:j+1] 硬币总价amt的组合数,包含两种情况计算:
1.当使用第j个硬币时,有 dp[amt-coins[j]][j] 种情况,即amt减去第j个硬币币值,使用前j+1种硬币的组合数;
2.当不使用第j个硬币时,有 dp[amt][j-1] 种情况,即使用前j种硬币凑成amt的组合数;
所以: dp[amt][j] = dp[amt - coins[j]][j]+dp[amt][j-1]
我们最终得到的结果是:dp[amount][-1]
上述分析省略了一些边界情况。
有了上述的分析,代码实现就比较简单了。
动态规划算法代码简洁,执行效率高。但是与递归算法相比,需要仔细考虑如何分解问题,动态规划代码与递归调用相比,较难理解。
我把递归算法实现的代码也附在下面。有兴趣的朋友可以比较一下两种算法的时间复杂度有多大差别。
上述代码在Python 3.7运行通过。
算法专题 之 动态规划
由于动态规划算法具有时间效率较高,代码量较少,可以考察思维能力、抽象能力以及灵活度,该算法的身影常常出现在面试、笔试或者竞赛中,今天对该算法进行总结并对面试等场合中常出现的题目进行分析。
1、动态规划定义与理解
动态规划(Dynamic Programming,简称DP),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解。(上述定义来自百度百科)
个人理解,动态规划的核心包含3个重要环节:
记录求解过程:记忆化存储每个子问题的解,以便下次需要同一个子问题解之时直接查询记录;
状态转移方程:整体问题最优解取决于子问题的最优解,因此需要得到子问题与最终问题间的关系;
边界问题:状态转移方程是一个递推式,因此需要找到递推终止的条件。
简单点说,动态规划思想就像《红楼梦》第六十二回提到的经典谚语“ ‘大事化为小事,小事化为没事’,方是兴旺之家”的处事方式,对于很多需要求解最优解的场合,动态规划方是解题之道。
动态规划求解步骤:
判题题意是否为找出一个问题的最优解;
把原问题分解成若干个子问题,分析最优子结构与最终问题之间的关系,从而得到状态转移方程;
确定底层边界问题,例如最小的前几个f(n)的值;
求解问题,通常使用数组进行迭代求出最优解。
2、递归、贪心算法、分治策略以及动态规划的比较
递归:将原问题归纳为更小的、相似的子问题,递归的过程中存在子问题的重复计算,耗费更多的时间和空间。
贪心算法:依赖于当前已经做出的所有选择,采用自顶向下(每一步根据策略得到当前一个最优解,保证每一步都是选择当前最优的)的解决方法,不能保证求出最优解,因此不能用来求最大或最小解问题。
分治策略:将原问题分解为若干个规模较小、相互独立、类似于原问题的子问题,自顶向下递归求解这些子问题,然后再合并这些子问题的解来建立原问题的解。
动态规划:用于解决子问题有重复求解的情况,子问题之间是不独立,既可以用递归实现,也可以用迭代实现,动态规划能求出问题的最优解。
3、面试中常见的动态规划问题
选取字符串、数组以及树三种类别常见的动态规划题目进行分析和编码。
题目1:最长回文子串
题目描述:给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。
示例 1:输入: "babad" 输出: "bab" 注意: "aba"也是一个有效答案。
示例 2:输入: "cbbd" 输出: "bb"
解法1:暴力求解,求出该字符串的每一个子串,再判断子串是否是回文串,找到最长的那个。其中求出每个子串的时间复杂度为O(n^2),判断是否为回文串的复杂度为O(n),两者是相乘关系,所以整个算法的时间复杂度为O(n^3),空间复杂度:O(1)。
解法2:中心扩展法,依次去求得每个字符的最长回文,注意每个字符有奇数长度的回文串和偶数长度的回文串两种情况,记录最长回文的始末位置即可。时间复杂度为 O(n^2)。 注:下面代码可左右滑动查看
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(n^2),空间复杂度为O(n^2)。注:下面代码可左右滑动查看
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(n^3),求解过程中存在子数组重复计算的问题。
解法2:动态规划
动态规划状态转移方程:
(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];
}
}
以上是关于Python之动态规划算法的主要内容,如果未能解决你的问题,请参考以下文章