记忆化搜索一个模板秒杀6道股票题
首先可以从最暴力的方法出发,再加上缓存就是记忆化搜索啦
方法一:回溯
- 从第0天开始到第n-1天,枚举买入或者不买入两种操作,记录能获得的最大值。
- 定义了三种状态state
- state == 0,初始状态,没有买入卖出股票
- 当前可以不买入股票,此时能获得的价值为:dfs(index + 1, state)
- 或者买入股票,此时能获得的价值为:dfs(index + 1, state + 1) - prices[index]
- state == 1,已经买入了一支股票
- 当前可以不卖出股票,此时能获得的价值为:dfs(index + 1, state)
- 或者卖出股票,此时能获得的价值为:dfs(index + 1, state + 1) + prices[index]
- state == 2,已经买入卖出一支股票,操作结束
- 在state == 0 和 state == 1的时候返回最大的价值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Solution { public int maxProfit(int[] prices) { this.prices = prices; return dfs(0, 0); }
int[] prices;
private int dfs(int index, int state) { if (index == prices.length || state == 2) return 0; if (state == 0) return Math.max(dfs(index + 1, state), dfs(index + 1, state + 1) - prices[index]); return Math.max(dfs(index + 1, state), dfs(index + 1, state + 1) + prices[index]); } }
|
方法二:记忆化搜索
定义dp数组,用于遍历过每种状态能获得的值,dp = new int[2][prices.length];
在return前,把当前的值记录在dp数组中
- return dp[state][index] = Math.max(dfs(index + 1, state), dfs(index + 1, state + 1) - prices[index]);
- return dp[state][index] = Math.max(dfs(index + 1, state), dfs(index + 1, state + 1) + prices[index]);
很多子问题可能会被多次调用,导致重复计算,这会极大地增加算法的时间复杂度。记忆化数组的作用就是将已经计算过的子问题的结果保存起来,下次遇到相同的子问题时,直接从记忆数组中取得结果,避免重复计算。
1 2
| if (dp[state][index] != -1) return dp[state][index];
|
base case:index超过了数组长度或者卖出了股票
1 2
| if (index == prices.length || state == 2) return 0;
|
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Solution { public int maxProfit(int[] prices) { this.prices = prices; dp = new int[2][prices.length]; for (int[] arr : dp) Arrays.fill(arr, -1); return dfs(0, 0); }
int[] prices; int[][] dp;
private int dfs(int index, int state) { if (index == prices.length || state == 2) return 0; if (dp[state][index] != -1) return dp[state][index]; if (state == 0) return dp[state][index] = Math.max(dfs(index + 1, 0), dfs(index + 1, 1) - prices[index]); return dp[state][index] = Math.max(dfs(index + 1, 1), dfs(index + 1, 2) + prices[index]); } }
|
接下来使用记忆化搜索解决其他5道股票问题
有两处代码需要修改
买入一只股票后,可以不操作,或者卖出,由于可以买卖任意支股票,选择卖出股票后,状态回到0
1
| return dp[state][index] = Math.max(dfs(index + 1, 1), dfs(index + 1, 0) + prices[index]);
|
base case:删除state == 2,这段条件,因为现在可以买卖任意支股票
1 2
| if (index == prices.length) return 0;
|
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Solution { public int maxProfit(int[] prices) { this.prices = prices; dp = new int[2][prices.length]; for (int[] arr : dp) Arrays.fill(arr, -1); return dfs(0, 0); }
int[] prices; int[][] dp;
private int dfs(int index, int state) { if (index == prices.length) return 0; if (dp[state][index] != -1) return dp[state][index]; if (state == 0) return dp[state][index] = Math.max(dfs(index + 1, 0), dfs(index + 1, 1) - prices[index]); return dp[state][index] = Math.max(dfs(index + 1, 1), dfs(index + 1, 0) + prices[index]); } }
|
多定义两种state即可 ,state为偶数的时候可以选择不操作或者不买入,state为奇数的时候可以选择不操作或不卖出
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Solution { public int maxProfit(int[] prices) { this.prices = prices; dp = new int[4][prices.length]; for (int[] arr : dp) Arrays.fill(arr, -1); return dfs(0, 0); }
int[] prices; int[][] dp;
private int dfs(int index, int state) { if (index == prices.length || state == 4) return 0; if (dp[state][index] != -1) return dp[state][index]; if (state == 0 || state == 2) return dp[state][index] = Math.max(dfs(index + 1, state), dfs(index + 1, state + 1) - prices[index]); else return dp[state][index] = Math.max(dfs(index + 1, state), dfs(index + 1, state + 1) + prices[index]); } }
|
和123. 买卖股票的最佳时机 III:最多买卖两支股票类似,买卖一支股票我们需要开2个dp数组,买卖两支股票我们需要开4个dp数组,买卖k支股票需要开2*k个dp数组
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Solution { public int maxProfit(int k, int[] prices) { this.prices = prices; this.k = k; dp = new int[2 * k][prices.length]; for (int[] arr : dp) Arrays.fill(arr, -1); return dfs(0, 0); }
int[] prices; int[][] dp; int k;
private int dfs(int index, int state) { if (index == prices.length || state == 2 * k) return 0; if (dp[state][index] != -1) return dp[state][index]; if (state % 2 == 0) return dp[state][index] = Math.max(dfs(index + 1, state), dfs(index + 1, state + 1) - prices[index]); else return dp[state][index] = Math.max(dfs(index + 1, state), dfs(index + 1, state + 1) + prices[index]); } }
|
与122. 买卖股票的最佳时机 II:可以买卖任意支股票类似,在卖出的时候多 - fee就ok
1
| return dp[state][index] = Math.max(dfs(index + 1, 1), dfs(index + 1, 0) + prices[index] - fee);
|
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Solution { public int maxProfit(int[] prices, int fee) { this.prices = prices; this.fee = fee; dp = new int[2][prices.length]; for (int[] arr : dp) Arrays.fill(arr, -1); return dfs(0, 0); }
int[] prices; int[][] dp; int fee;
private int dfs(int index, int state) { if (index == prices.length) return 0; if (dp[state][index] != -1) return dp[state][index]; if (state == 0) return dp[state][index] = Math.max(dfs(index + 1, 0), dfs(index + 1, 1) - prices[index]); else return dp[state][index] = Math.max(dfs(index + 1, 1), dfs(index + 1, 0) + prices[index] - fee); } }
|
与122. 买卖股票的最佳时机 II:可以买卖任意支股票类似,在卖出的时候往后走两天即可dfs(index + 2, 0),同时base case小修
base case
1 2
| if (index >= prices.length) return 0;
|
state == 1
1
| return dp[state][index] = Math.max(dfs(index + 1, 1), dfs(index + 2, 0) + prices[index]);
|
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Solution { public int maxProfit(int[] prices) { this.prices = prices; dp = new int[2][prices.length]; for (int[] arr : dp) Arrays.fill(arr, -1); return dfs(0, 0); }
int[] prices; int[][] dp;
private int dfs(int index, int state) { if (index >= prices.length) return 0; if (dp[state][index] != -1) return dp[state][index]; if (state == 0) return dp[state][index] = Math.max(dfs(index + 1, 0), dfs(index + 1, 1) - prices[index]); else return dp[state][index] = Math.max(dfs(index + 1, 1), dfs(index + 2, 0) + prices[index]); } }
|
最后
打个比方
- dp(手动挡):需要初始化,考虑遍历顺序
- 记忆化搜索(自动挡):不需要初始化,考虑遍历顺序
记忆化搜索和动态规划都是解决问题的常用方法,它们各自有一些优点和缺点。
记忆化搜索的优点:
减少重复计算:记忆化搜索通过缓存已经计算过的结果,避免了重复计算,提高了算法的效率。 简单易实现:相对于动态规划,记忆化搜索的实现通常更加简单直观,只需要在递归的基础上加上缓存即可。 记忆化搜索的缺点:
可能会占用较大的内存:记忆化搜索需要缓存所有的中间结果,如果问题规模较大,可能会占用较多的内存空间。 递归调用可能导致栈溢出:对于问题规模较大的情况,递归调用深度可能较深,可能导致栈溢出。 动态规划的优点:
自底向上的思路:动态规划通常采用自底向上的计算方式,从小规模问题开始逐步推导得到最终结果,确保所有子问题的解都被计算到。 适用于一些具有最优子结构的问题:动态规划适用于具有最优子结构的问题,即整个问题的最优解可以由子问题的最优解推导得到。 动态规划的缺点:
实现较为复杂:相对于记忆化搜索,动态规划的实现通常较为复杂,需要设计合适的状态转移方程和数组结构。 可能会占用较大的内存:类似于记忆化搜索,动态规划也需要维护一个数组来保存中间结果,可能会占用较多的内存空间。 综上所述,记忆化搜索适用于递归问题,能够减少重复计算,但可能会占用较大内存和导致栈溢出。动态规划适用于具有最优子结构的问题,通过自底向上的方式计算,但实现较为复杂且可能会占用较多内存。在实际应用中,根据问题的性质和规模选择合适的方法来解决问题。