这是我参与8月更文挑战的第30天,活动详情查看:8月更文挑战
动态规划是数据结构和算法板块中最难的一部分内容,很多人只要一听到题目是动态规划,瞬间心中会产生恐惧心理,同时动态规划也是大厂面试官比较青睐的一个模块,无论是社招还是校招都是大厂面试时的一部分,说实话,最近我也在学习动态规划,我将从一个小白的角度,从0到1整理动态规划相关内容,从暴力破解到记忆化搜索最后到动态规划,循序渐进,一举攻克动态规划。希望能对正在阅读该文章的你提供一些帮助。
第一部分
斐波那契数列
F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)
很容易就可以写出如下的代码:
int fib(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
如果我们计算fib(5)的时候,他的递归树如下图所示:
仅仅计算第5个值的时候,函数就要递归如此多次,而且,有很多次都是重复计算的。
如果我们计算fib(100)的话,哪会有很多很多次重复的计算。
记忆化搜索
为了避免重复的计算,我们可以计算好的结果存到一个容器中,如果之前已经计算了我们需要的值,我们直接返回就可以了,不需要重复的计算。
int fib(int n) {
if (n < 0) {
throw new IllegalArgumentException("n value must be > 0");
}
int[] memo = new int[n + 1];
// 初始化数组中的值都为-1
// 初始化为-1的原因是因为fib计算的结果不可能是-1,都是大于等于0的
for (int i = 0; i < memo.length; i++) {
memo[i] = -1;
}
return fibCore(n, memo);
}
int fibCore(int n, int[] memo) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
if (memo[n] == -1) {
// 之前没有计算过该值,需要计算
memo[n] = fibCore(n - 1, memo) + fibCore(n - 2, memo);
}
return memo[n];
}
上面这种解决问题的方式自上而下的解决,即我们先不算fib(n),而是算fib(n-1)和fib(n-2),一直这样 递推下去,知道出现f(0)或者f(1)才终止。
动态规划
我们可以反过来,自上而下的解决这个问题,即先计算fib(0),fib(1),fib(2)...fib(n),代码如下:
int fib(int n) {
if (n < 0) {
throw new IllegalArgumentException("n value must be > 0");
}
int[] memo = new int[n + 1];
for (int i = 0; i <= n; i++) {
if (i <= 1) {
memo[i] = i;
} else {
memo[i] = memo[i - 1] + memo[i - 2];
}
}
return memo[n];
}
动态规划 将原问题拆解成若干个子问题,同时保存子问题的答案,使得每个子问题的求解只求一次,最终获取原问题的答案。
爬楼梯
题目
假设有一个楼梯,一次可以上一个,或者上两个,问,上n阶台阶,有多少种解法?
我们上第n阶台阶我们可能是在n-1阶台阶或者n-2阶台阶,而我们上n-1阶台阶可能是n-1-1或者n-1-2阶台阶。依次类推,直到我们上第2阶台阶或者第1阶台阶时结束,上第1阶台阶时,只有一种,而上第二阶台阶时可以是1+1或者2,所以有两种。这样我们的代码可以是这样的。
暴力破解
int climb(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
// 直接爬2阶或者一阶一阶爬
return 2;
}
return climb(n - 1) + climb(n - 2);
}
我们设想爬第0阶台阶,我们只有一种,就是没有嘛~,这样我们爬第二阶台阶也可以通过
climb(1)+climb(0)计算出来,于是,我们的代码就变成了这样:
int climb(int n) {
if (n <= 1) {
return 1;
}
return climb(n - 1) + climb(n - 2);
}
细心的小伙伴可以看出,这个就是我们刚刚写的斐波那契数列。
Triangle
给定一个三角形的数字阵列,选择一条自顶向下的路径,使得沿途的所有数字之和最小(每一步只能移动到相邻的格子中) eg:
最小路径为:2+3+5+1=11
MiniMum Path Sum
给出一个m*n矩阵,其中每一个格子包含一个非负的整数,寻找一条从左上角到右下角的路径,使得沿路的路径之和最小。 【注意】每次只能向下或者向右移动。
第二部分
给定一个正整数n,可以将其分割成多个数字的和,若要让这些数字的乘机最大,求分割的方法,(至少分成两个数)。返回这个最大的乘机。 如: n=2,则返回1(2=1+1) n=10,则返回36(10=3+3+4)
暴力解法
回溯遍历将一个数做分割,时间复杂度O(2^n)
- 此处以分割4为例,如下图:
- 对应分割n,如下图:
对应的代码如下:
int breakInteger(int n) {
if (n == 1) {
return 1;
}
int res = -1;
for (int i = 1; i < n - 1; i++) {
// i*(n-i)加入比较的原因
// 因为函数breakInteger一定要将一个数字分割成两部分,
// 所以,如果我们只将n分成两部分不想在往下分割了,那就是 i*(n-i)
res = max(res, i * (n - i), i * breakInteger(n - i));
}
return res;
}
int max(int a, int b, int c) {
return Integer.max(a, Integer.max(b, c));
}
记忆化搜索
上面分割n的图中,我们可以看到该题也会出现”重叠子问题“的情况,所以可以进一步进行”记忆化搜索“,将算好的结果记录下来,避免重复运算。
int breakInteger(int n) {
int[] memo = new int[n + 1];
Arrays.fill(memo, -1);
return breakIntegerCore(n, memo);
}
int breakIntegerCore(int n, int[] memo) {
if (n == 1) {
return 1;
}
if (memo[n] != -1) {
return memo[n];
}
int res = -1;
for (int i = 1; i < n - 1; i++) {
// i*(n-i)加入比较的原因
// 因为函数breakInteger一定要将一个数字分割成两部分,
// 所以,如果我们只将n分成两部分不想在往下分割了,那就是 i*(n-i)
res = max(res, i * (n - i), i * breakIntegerCore(n - i, memo));
}
memo[n] = res;
return res;
}
int max(int a, int b, int c) {
return Integer.max(a, Integer.max(b, c));
}
动态规划
自底向上解决
int integerBreak(int n) {
// memo[i]用于存储将数字i进行分割(至少分割成两部分)后,最大的乘积
int[] memo = new int[n + 1];
Arrays.fill(memo, -1);
memo[1] = 1;
for (int i = 2; i <= n; i++) {
// 计算memo[i]
for (int j = 1; j < i - 1; j++) {
// i*(i-j)
memo[i] = max(memo[i], j * (i - j), j * memo[i - j]);
}
}
return memo[n];
}
Perfect Squares
给定一个正整数,寻找最少的完全平方数,使他们的和为n eg: 12=4+4+4 13=4+9
Decode Ways
一个字符串,包含A-Z的字母,每个字符可以和1-26的数字对应,如:A-1,B-2...Z-26。给出一个数字字符串,问,我们有多少种方法可以解析这个数字字符串? eg: AB 可以解析成(1,2)或者12,最终返回2
Unique path
有一个机器人,从一个m*n的矩阵的左上角出发,要达到这个矩阵的有下角,机器人每次只能向右或者向下移动,问一共有多少种不同的路径。
第三部分
House Robber
假设你是一个专业的小偷,打算洗劫一条街所有的房子,每个房子都有价值不同的宝物,但是如果你连续偷了两栋房子,就会触发报警系统,编程求出你最多可以偷窃价值多少的宝物 eg: [3,4,1,2] -->6 [3,(4),1,(2)] [4,3,1,2] -->6 [(4),3,1,(2)]
暴力解法
检查所有房子的组合,对每一个组合检查是否有相邻的房子,如果没有,记录其价值,找到最大值。 时间复杂度:O((2^n)*n)
递归树如下
状态转移方程
- 我们称对 ”考虑偷取[x...n-1]范围里的房子“ 的递归函数的定义,我们称之为**"状态"**
f(0)=max{ v(0)+f(2), v(1)+f(3), v(2)+f(4), ... v(n-3)+f(n-1), v(n-2), v(n-1) }
上面的函数f(x)代表从第x个房子偷取获取宝物的最大值,v(y)代表去除y号房子代表的价值。本题的是考虑从 "0号房子考虑,获取偷取宝物的最大值",
即,
- 可以是 偷取0号房子的宝物,加上从2号房子考虑偷取宝物的最大值;
- 可以是 偷取1号房子的宝物,加上从3号房子考虑偷取宝物的最大值;
- 可以是 偷取2号房子的宝物,加上从4号房子考虑偷取宝物的最大值; ...
- 可以是 偷取n-3号房子的宝物,加上从n-1号房子考虑偷取宝物的最大值;
- 可以是偷取n-2号房子的宝物,加上从n号房子考虑偷取宝物的最大值,以为n号房子不存在,所以忽略。
- 可以是偷取n-1号房子的宝物...
这些方案中选择最大值即为本题的解。也就是上面的方程。该方程我们称之为**"状态转移方程"**。
暴力破解
/**
* 获取宝物的最大值
*
* @param house 房子
* @return 获取宝物的最大值
*/
public int robs(int[] house) {
return tryRobs(0, house);
}
/**
* 尝试抢劫获取宝物的最大值
*
* @param index 开始考虑抢劫房子的编号
* @param house 房子
* @return 尝试抢劫获取宝物的最大值
*/
private int tryRobs(int index, int[] house) {
if (index >= house.length) {
// 尝试抢劫房子的编号超出房子编号的最大值
return 0;
}
// 用于记录本次抢劫房子的最大值
int res = -1;
for (int i = index; i < house.length; i++) {
res = Integer.max(res, house[i] + tryRobs(i + 2, house));
}
return res;
}
记忆化搜索
/**
* 获取宝物的最大值
*
* @param house 房子
* @return 获取宝物的最大值
*/
public int robs(int[] house) {
int[] memo = new int[house.length];
Arrays.fill(memo, -1);
return tryRobsWithMemo(0, house, memo);
}
/**
* 带记忆化搜索的尝试抢劫获取宝物的最大值
*
* @param index 开始考虑抢劫房子的编号
* @param house 房子
* @param memo memo[x]开始考虑抢劫x号房子获取宝物的最大值
* @return 尝试抢劫获取宝物的最大值
*/
private int tryRobsWithMemo(int index, int[] house, int[] memo) {
if (index >= house.length) {
// 尝试抢劫房子的编号超出房子编号的最大值
return 0;
}
if (memo[index] != -1) {
return memo[index];
}
// 用于记录本次抢劫房子的最大值
int res = -1;
for (int i = index; i < house.length; i++) {
res = Integer.max(res, house[i] + tryRobsWithMemo(i + 2, house,memo));
}
memo[index] = res;
return res;
}
动态规划
int rob(int[] house) {
int length = house.length;
if (length == 0) {
return 0;
}
int[] memo = new int[length];
Arrays.fill(memo, -1);
memo[length - 1] = house[length - 1];
for (int i = length - 2; i >= 0; i--) {
// 计算memo[i]的最大值
for (int j = i; j < length; j++) {
memo[i] = Integer.max(memo[i], house[j] + (j + 2 < length ? memo[j + 2] : 0));
}
}
return memo[0];
}
拓展
- 考虑偷取从[x...n-1]范围里的房子的宝物
- 考虑偷取从[0...x]范围里房子的宝物
House Robber II
和Hourse Robber 一样,不过这次实在环形的街道,也就是给定的数组中,第一个元素和最后一个元素为邻居,在不触碰警报的情况下能够窃取财产的最大值是多少?
Best Time To Buy And Sell Stock with Cooldown
给定一个数组,表示一只股票在每一天的价格。设计一个交易算法,在这些天进行自动交易,要求:每天只能进行一次操作,在买完股票后,在下一天是不能购买的。问如何交易,能让利益最大化。 eg: price=[1,2,3,0,2] 最佳交易方式:[buy, sell, colldown, buy, sell],利润为3,算法返回3