LOADING 2019-09-08
更多内容,欢迎关注微信公众号:全菜工程师小辉。公众号回复关键词,领取免费学习资料。
动态规划算法一直是面试手撕算法中比较有挑战的一种类型。很多的分配问题或者调度问题实际上都可能用动态规划进行解决。(当然,如果问题的规模较大,有时候会抽象模型使用动归来解决,有时候则可以通过不断迭代的概率算法解决查找次优解)
所以,动归很重要,至少算法思想很重要。
通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
最优子结构:当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。重叠子问题:在用递归算法自顶向下解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,在以后尽可能多地利用这些子问题的解。
不理解不用怕,结合后面题目来理解这些概念。这些概念完全是已经会动归的人来总结出来的,所以先理解动归,然后再来看这些文绉绉的概括。
共同点:
二者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小(小到很容易解决)的子问题。然后将子问题的解合并,形成原问题的解。
不同点:
所有的动态规划问题都可以通过多层嵌套循环遍历所有的可能,将符合条件的个数统计起来。只是时间复杂度是指数级的,所以不推荐。
上面提到的三个问题是动态规划里很常见的题目,题目内容可以百度查看一下。篇幅原因,本文后边只讲解前两道题
然后从最小的问题不断往上迭代,即使一直迭代到最大的原问题,也是只依赖于前面的几个最优子结构。这样,空间复杂度就大大简化。也就得到了动归算法算法。
例1: Climbing Stairs(爬楼梯问题)
leetcode原题:你正在爬一个有n个台阶的楼梯,每次只能上1个或者2个台阶,那么到达顶端共有多少种不同的方法?
class Solution {
int climbStairs(int n) {
if (n <= 2) {
return n;
} else {
return climbStairs(n - 1) + climbStairs(n - 2);
}
}
}递归的时间复杂度是由递归层数和最优子结构的个数决定的。这里的阶梯数是 N ,最优子结构个数是2。如果想象成一个二叉树,那么就可以认为是一个高度为N-1,节点个数接近2的N-1次方的树,因此此方法的时间复杂度可以近似的看作是O(2N) 。
这里我们想到了把重复的参数存储起来,下次递归遇到时就直接返回该参数的结果,也就是备忘录算法了,最简单的备忘录就是哈希表。
class Solution {
private Map<Integer, Integer> map = new HashMap<>();
int climbStairs(int n) {
if (n <= 2) {
return n;
} else if (map.containsKey(n)) {
return map.get(n);
} else {
int value = climbStairs(n - 1) + climbStairs(n - 2);
map.put(n, value);
return value;
}
}
}之前都是自顶向下的求解,考虑一下自底向上的求解过程。从F(1)和F(2)边界条件求,可知F(3) = F(1)+F(2)。不断向上,可知F(N)只依赖于前两个状态F(N-1)和F(N-2)。于是我们只需要保留前两个状态,就可以求得F(N)。相比于备忘录算法,我们再一次简化了空间复杂度。
class Solution {
int climbStairs(int n) {
if (n <= 2) {
return n;
}
// 边界条件
int a = 1;
int b = 2;
int result = 0;
// 最优子结构与最终问题之间的关系
for (int i = 3; i <= n; i++) {
result = a + b;
a = b;
b = result;
}
return result;
}
}空间复杂度O(1), 时间复杂度O(N)
例2: Making change using the fewest coins(最少找零钱问题)
Google面试题:假设你是一家自动售货机制造商的程序员。你的公司正设法在每一笔交易 找零时都能提供最少数目的硬币以便工作能更加简单。已知硬币有四种(1美分,5美分,10美分,25美分)。假设一个顾客投了1美元来购买37美分的物品 ,你用来找零的硬币的最小数量是多少?
class Solution {
Set<Integer> coinSet = new HashSet<Integer>() {
{
add(1);
add(5);
add(10);
add(25);
}
};
int getFewestCoins(int n) {
if (n < 1) {
return 0;
}
if (coinSet.contains(n)) {
return 1;
}
int minCoins = n;
int numCoins = Integer.MAX_VALUE;
for (int coin : coinSet) {
if (n >= coin) {
// 如果要计算的n小于单个硬币金额,则不能出现在状态转移方程中
numCoins = 1 + getFewestCoins(n - coin);
}
// 更新最小值
if (numCoins < minCoins) {
minCoins = numCoins;
}
}
return minCoins;
}
}就是将递归里计算的中间变量都保存在一个哈希表,代码略。
自底向上,从找零数等于1开始往上迭代,参考最优子结构,记录下来最少硬币数。一直迭代到实际要求。
class Solution {
Set<Integer> coinSet = new HashSet<Integer>() {
{
add(1);
add(5);
add(10);
add(25);
}
};
int getFewestCoins(int n) {
int[] list = new int[n + 1];
List<Integer> subCal = new ArrayList<>();
for (int i = 0; i <= n; i++) {
// 边界
if (i <= 1) {
list[i] = i;
continue;
}
for (int cent : coinSet) {
if (i >= cent) {
subCal.add(list[i - cent] + 1);
}
}
list[i] = Collections.min(subCal);
subCal.clear();
}
return list[n];
}
}更多内容,欢迎关注微信公众号:全菜工程师小辉。公众号回复关键词,领取免费学习资料。

动态规划有时被称为递归的相反的技术。动态规划方案通常使用一个数组来建立一张表,用于存放被分解成众多子问题的解。当算法执行完毕,最终的解法将会在这个表中找到。今天我们先从我们最熟的斐波那契数列数列开始。
#include <iostream> #include <algorithm> #include <cstring> #include <cstdio&am