动态规划
动态规划:用空间代替重复计算,包含一整套原理和技巧的总和,课程会用非常大的篇幅来全盘介绍
知道怎么算的算法 vs 知道怎么试的算法
有些递归在展开计算时,总是重复调用同一个子问题的解,这种重复调用的递归变成动态规划很有收益
如果每次展开都是不同的解,或者重复调用的现象很少,那么没有改动态规划的必要
下节课会举例,哪些递归没有必要改动态规划的必要
任何动态规划问题都一定对应着一个有重复调用行为的递归
所以任何动态规划的题目都一定可以从递归入手,逐渐实现动态规划的方法
题目1到题目4,都从递归入手,逐渐改出动态规划的实现
尝试策略 就是 转移方程,完全一回事!
推荐从尝试入手,因为代码好写,并且一旦发现尝试错误,重新想别的递归代价轻!
题目一:斐波那契数
问题描述:
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。
方案一:暴力递归
算法思想:
普通暴力递归的思想
代码如下:
public static int fib1(int n) {
return f1(n);
}
public static int f1(int i) {
if (i == 0) {
return 0;
}
if (i == 1) {
return 1;
}
return f1(i - 1) + f1(i - 2);
}
方案一:记忆化搜索(傻缓存法)
算法思想:
我们发现,暴力递归的时候时间复杂度是O(2^n),因为会计算大量重复的工作,就比如说算f(7),
它就去跑f(6)和f(5),但是f(5)可能在别的地方已经计算过了,我们每一次遇见这样就得算一次会大大影响我们的效率。
所以如果我们第一次算出来f(5)就把它缓存起来,下一次需要用的时候直接再缓存表里面查它就行。这样时间复杂度就变成了O(n)
这是一种从顶到底的动态规划
代码如下:
public static int fib2(int n) {
int[] dp = new int[n + 1];
Arrays.fill(dp, -1);
return f2(n, dp);
}
public static int f2(int i, int[] dp) {
if (i == 0) {
return 0;
}
if (i == 1) {
return 1;
}
if (dp[i] != -1) {
return dp[i];
}
int ans = f2(i - 1, dp) + f2(i - 2, dp);
dp[i] = ans;
return ans;
}
方案三:动态规划
算法思想:
如果我们能知道他的规律,一次性把f(1),f(2)....f(n)都算出来,取出f(n)就行。
这样时间复杂度是O(n)
问题的关键是找一个式子算出来
这是一种从底到顶的动态规划
代码如下:
public static int fib3(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
int[] dp = new int[n + 1];
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
使用变量来优化数组:
public static int fib4(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
int lastLast = 0, last = 1;
for (int i = 2, cur; i <= n; i++) {
cur = lastLast + last;
lastLast = last;
last = cur;
}
return last;
}
题目二:最低票价
问题描述:
在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 days 的数组给出。每一项是一个从 1 到 365 的整数。
火车票有 三种不同的销售方式 :
一张 为期一天 的通行证售价为 costs[0] 美元;
一张 为期七天 的通行证售价为 costs[1] 美元;
一张 为期三十天 的通行证售价为 costs[2] 美元。
通行证允许数天无限制的旅行。 例如,如果我们在第 2 天获得一张 为期 7 天 的通行证,那么我们可以连着旅行 7 天:第 2 天、第 3 天、第 4 天、第 5 天、第 6 天、第 7 天和第 8 天。
返回 你想要完成在给定的列表 days 中列出的每一天的旅行所需要的最低消费 。
方案一:暴力递归
算法思想:
1.使用递归,递归函数作用:days[i..... 最少花费是多少 (从dayi往后的最少花费是多少)
2.递归条件:days数组越界(后面已无旅行),返回0
3.递归每一步干的事情:来到当前days[i],算出当前day[i]花费的三种尝试(1,7,30)+后面的day的最小花费,计算出最小花费。
代码如下:
// 无论提交什么方法都带着这个数组 0 1 2
public static int[] durations = {
1, 7, 30 };
// 暴力尝试
public static int mincostTickets1(int[] days, int[] costs) {
return f1(days, costs, 0);
}
// days[i..... 最少花费是多少
public static int f1(int[] days, int[] costs, int i) {
if (i == days.length) {
// 后续已经无旅行了
return 0;
}
// i下标 : 第days[i]天,有一场旅行
// i.... 最少花费是多少
int ans = Integer.MAX_VALUE;
for (int k = 0, j = i; k < 3; k++) {
// k是方案编号 : 0 1 2
//while是计算当前选择1,7,30对应下一次j(去哪一天)该跳到哪
while (j < days.length && days[i] + durations[k] > days[j]) {
// 因为方案2持续的天数最多,30天
// 所以while循环最多执行30次
// 枚举行为可以认为是O(1)
j++;
}
ans = Math.min(ans, costs[k] + f1(days, costs, j));
}
return ans;
}
方案二:记忆化搜索(傻缓存法)
算法思想:
增加缓存dp,每次要求这层的最小花费先查缓存
代码如下:
// 无论提交什么方法都带着这个数组 0 1 2
public static int[] durations = {
1, 7, 30 };
// 暴力尝试改记忆化搜索
// 从顶到底的动态规划
public static int mincostTickets2(int[] days, int[] costs) {
int[] dp = new int[days.length];
for (int i = 0; i < days.length; i++) {
dp[i] = Integer.MAX_VALUE;
}
return f2(days, costs, 0, dp);
}
public static int f2(int[] days, int[] costs, int i, int[] dp) {
if (i