打家劫舍系列题
198.打家劫舍
不能闯入相邻的房子,很自然想到要么是选所有第奇数个房子,要么是选择所有第偶数个房子,不过当然不可能这样解题,不是说当前闯入了第 i 个,下一个就一定要闯入第 i + 2 个,有可能第 i + 3 个房子中的现金很多很多,如果闯入第 i + 2 个,就肯定选不了第 i + 3 个了,这样的话到最后得到的总现金可能就不是最多的
用动规解题,dp[i] 表示闯入到 nums[i] 时,所能获得的最多现金,到达 nums[i] 的前一步,可能是在 nums[i - 2],也可能是在 nums[i - 3],要看 dp[i - 2] 跟 dp[i - 3] 哪个值更大 (不可能是 nums[i - 4] 了,如果是从 nums[i - 4] 直接到 nums[i],那显然应该在中间再选一个 nums[i - 2] 才是最优选择)。所以 dp[i] = max(dp[i - 2],dp[i - 3]) + nums[i]
public int rob(int[] nums) {
int len = nums.length;
//状态方程至少需要从dp[3]开始,所以对数组元素只有1,2,3个的情况额外处理
if(len == 1) return nums[0];
else if(len == 2) return Math.max(nums[0],nums[1]);
else if(len == 3) return Math.max(nums[0] + nums[2],nums[1]);
int[] dp = new int[len];
dp[0] = nums[0];
dp[1] = nums[1];
dp[2] = nums[0] + nums[2];
for(int i = 3;i < len;i++){
dp[i] = Math.max(dp[i - 2],dp[i - 3]) + nums[i];
}
return Math.max(dp[len - 1],dp[len - 2]);
}
213.打家劫舍Ⅱ
与 198 题的差别在于,房子是围成一圈的,所以第一间房子跟最后一间房子是相邻的,也就是说第一间房子跟最后一间房子不可能一起被选,那么最终的最大窃取金额的房间选取方式中,要么是第一间被选了而最后一间没被选,要么是最后一间被选了而第一间没被选,要么是两间都没被选,只会是这三种可能
如果是第一间被选了而最后一间没有被选,那就相当于对移除了最后一间房子后,对剩下的所有房子进行 198 题的解法;如果是最后一间被选了而第一间没被选,就相当于对移除了第一间房子后,对剩下的房子进行 198 题的解法;如果是两间都没被选,其实也看做是移除了最后一间房子后,对剩下的所有房子进行 198 题的解法时可能得到的答案
public int rob(int[] nums) {
int len = nums.length;
//rob1方法需要至少有四个房子,而且调用rob1之前还需要先移除掉
//第一个或者最后一个房子,所以需要对数组元素为1,2,3,4个的情况先行处理
if(len == 1){
return nums[0];
}else if(len == 2){
return Math.max(nums[0],nums[1]);
}else if(len == 3){
return Math.max(Math.max(nums[0],nums[1]) , nums[2]);
}else if(len == 4){
return Math.max(nums[0] + nums[2],nums[1] + nums[3]);
}else{
int[] num = new int[len - 1];
//去除最后一个房子
System.arraycopy(nums,0,num,0,len - 1);
int res = rob1(num);
//去除第一个房子
System.arraycopy(nums,1,num,0,len - 1);
//选择最优解
return Math.max(rob1(num),res);
}
}
//rob1方法就是上面198题的方法
public int rob1(int[] nums) {
int len = nums.length;
int[] dp = new int[len];
dp[0] = nums[0];
dp[1] = nums[1];
dp[2] = nums[0] + nums[2];
for(int i = 3;i < len;i++){
dp[i] = Math.max(dp[i - 2],dp[i - 3]) + nums[i];
}
return Math.max(dp[len - 1],dp[len - 2]);
}
337.打家劫舍Ⅲ-树形dp
一看到二叉树的题就要想到 递归,想到遍历
一开始的想法是基于后序遍历,对于当前节点,递归左右子树返回左右子树上能得到的最大金钱数目以及在这个最大金钱数目的情况下,左右儿子有没有被选取,即返回一个这样的结构:
class retVal{
int money;
boolean used;
public retVal(int money,boolean used){
this.money = money;
this.used = used;
}
}
然后再根据左右儿子是否被选取的各种情况算出当前节点能得到的最优解
不过很快就发现了问题:假设左儿子得到的最优解是在左儿子被选了的情况下得到的,那么如果要保留左子树的最优解,即左子树要被选,当前节点就不能被选;如果考虑一下要选取当前节点的解,那左儿子就不能被选,由于方法返回的最优解是在左儿子被选了的前提下提出的,对于左儿子不要选的情况,这个返回值就没有什么作用了
所以,由于当前节点的选与不选需要考虑左右儿子选与不选,所以递归左右子树的返回值不应该只有在左右子树上选取时的最优解,应该返回左右儿子选或不选两种情况下各自得到的解
最后还是看了 大佬题解 的思路。看完自己总结:
public int rob(TreeNode root) {
int[] res = rec(root);
//最后对于根节点选与不选还要进行一次决策
return Math.max(res[0],res[1]);
}
//递归函数对一棵子树进行递归,返回一个二元的一维数组
//其中0号下标元素表示该子树根节点不选时,能得到的最优解
//1号下标元素则表示根节点要选上时能得到的最优解
public int[] rec(TreeNode t){
//递归边界
if(t == null) return new int[]{0,0};
int[] l = rec(t.left);
int[] r = rec(t.right);
//0下标元素表示当前节点不选得到的解,既然当前节点不选,那么左右儿子就可以选也可以不选,选出最优的一组解即可
//1下标元素表示当前节点要选得到的解,所以左右儿子都不能选
/*那么对于当前节点,共有四种情况
1. 左右儿子都不选,那么当前节点就能选,得到的解为r[0] + l[0] + t.val;当然,当前节点也可以不选,得到r[0] + l[0]
2. 左儿子选右儿子不选,那么当前节点不能选,得到的解为r[0] + l[1]
3. 左儿子不选右儿子选,r[1] + l[0]
4. 左右儿子都选,r[1] + l[1]*/
//那么当前节点不选的话,最优解就是r[0] + l[0],r[0] + l[1],r[1] + l[0],r[1] + l[1]中的最大值,对应于Math.max(l[0],l[1]) + Math.max(r[0],r[1])
//而当前节点选上的话,得到的解就是 l[0] + r[0] + t.val,那么就可以返回当前子树的二元组了
return new int[]{ Math.max(l[0],l[1]) + Math.max(r[0],r[1]) , l[0] + r[0] + t.val};
}
在树上做记忆化,做决策,所以称为 树形dp
买卖股票系列题
121.买卖股票的最佳时机
贪心做法:遍历枚举每个数组元素,找它左边的数组元素中最小的价格,然后当前价格减掉这个最小价格得到的就是以当前价格卖出所能得到的最大利润,每个数组元素所能得到的最大利润中,最大值就是最终的最大利润 minNum 维护遍历过程中寻找到的最小价格;res 维护遍历过程中得到的最大收益
public int maxProfit(int[] prices) {
int len = prices.length;
if(len == 1) return 0;
int minNum = prices[0];
int res = -1;
for(int i = 1;i < len;i++){
minNum = Math.min(minNum,prices[i]);
//当前价格减掉前面记录的最小价格得到的解再跟前面记录的最大收益相比,较大的就是新的最大收益
res = Math.max(res,prices[i] - minNum);
}
return res;
}
122. 买卖股票的最佳时机 II
来自 评论区。贪心策略就是,所有价格上涨的交易日都进行买卖,所有价格下跌的交易日都不买卖,相当于不错过任何赚钱机会,也不经过任何亏钱的时候
public int maxProfit(int[] prices) {
int profit = 0;
for (int i = 1; i < prices.length; i++) {
int tmp = prices[i] - prices[i - 1];
if (tmp > 0) profit += tmp;
}
return profit;
}
123. 买卖股票的最佳时机 III
每一天所处的 状态 可能会是以下五种之一:
未进行过任何买卖操作;
已进行过一次买操作,此状态下累计收益记为 buy1;
已进行过一次买和卖操作,此状态下累计收益记为 sell1;
已进行了第二次买操作,此状态下累计收益记为 buy2;
已进行了第二次卖操作,此状态下累计收益记为 sell2
接下来就是 状态转移,根据前一天的状态转移到当前第 i 天的状态:
要使当前状态为 buy1,可以是前一天未进行过任何操作的情况下购买当前的股票,也可以是前一天状态就为 buy1 然后当前不做任何操作,于是
buy1 = max(buy1,-price[[i])
要使当前状态为 sell1,可以是前一天已处于 buy1 的情况下,以当前价格卖出股票,也可以是前一天已处于 sell1 的状态然后当前不做任何操作,于是
sell1 = max(sell1,buy1 + price[i])
要使当前状态为 buy2,可以是前一天已处于 sell1 的状态下,以当前价格买入股票,也可以是前一天已处于 buy2 的状态然后当前不做任何操作,于是
buy2 = max(buy2,sell1 - price[i])
要使当前状态为 sell2,可以是前一天已处于 buy2 的状态下,以当前价格卖出股票,也可以是前一天已处于 sell2 的状态然后当前不做任何操作,于是
sell2 = max(sell2,buy2 + price[i])
以上即为状态转移方程,接下来是 边界,就是第 0 天的状态,buy1 = -priice[0];由于可以当天买入又当天卖出股票,所以可以有 sell1 = 0,同理可以有 buy2 = -price[1],sell2 = 0
最后返回的结果本来是四种状态中的最大值,当然,有卖操作最后的收益肯定比没有卖操作的收益大,所以应该是 0,sell1,sell2 中的最大值,由于 sell1 跟 sell2 初值为 0,在运算过程中我们都是维护他们的最大值,所以最后他们的值一定大于等于 0,而且由于我们认为可以当天买入又卖出股票,所以就算最后一天是 sell1 可以当前买入又卖出股票来转变为 sell2,所以最终答案应返回 sell2 的值
public int maxProfit(int[] prices) {
intn = prices.length;
int buy1 = -prices[0], sell1 = 0,buy2 = -prices[0],sell2 = 0;
for (int i = 1; i < n; i++) {
buy1 = Math.max(buy1, -prices[i]);
sell1 = Math.max(sell1, buy1 + prices[i]);
buy2 = Math.max(buy2, sell1 - prices[i]);
sell2 = Math.max(sell2, buy2 + prices[i]);
}
return sell2;
}
309. 最佳买卖股票时机含冷冻期
每一天结束时可能处于以下三种 状态:
当天持有股票,记此状态下累积收益为 buy;
当天卖出股票,记此状态下累积收益为 sell;
当天为冷冻期,或者没有进行任何操作,记此状态下累积收益为 frozen
要使当天状态为 buy,可以是前一天状态就为 buy 然后当天不进行任何操作,也可以是前一天处于冷冻期或者没有进行任何操作然后当天进行买入操作,于是 buy = max(buy,frozen - price[i])
要使当天状态为 frozen,可以是前一天刚刚卖出股票为 sell 状态,也可以是前一天为冷冻期或者没有进行任何操作,然后当天也不进行任何操作,于是 frozen = max(sell,frozen)
要使当天状态为 sell,那么前一天必须是持有股票,即 buy 状态,然后当天卖出股票,即 sell = buy + price[i]
边界即为第一天的状态,如果第一天买入股票,buy = -price[0];如果当天没有进行任何操作, frozen = 0;如果当天买入又卖出股票,sell = 0
public int maxProfit(int[] prices) {
int len = prices.length;
int buy = -prices[0],frozen = 0,sell = 0;
for(int i = 1;i < len;i++){
buy = Math.max(buy,frozen - prices[i]);
frozen = Math.max(sell,frozen);
sell = buy + prices[i];
}
return Math.max(sell,frozen);
}
188. 买卖股票的最佳时机 IV
本题其实就是 123. 买卖股票的最佳时机 III 的升级版,从最多可以进行两次买卖升级为最多可以进行 k 次,所以思路其实一样,只不过状态变多变为了 2k 个而已
state 数组用于记录每一天处于每种状态的累积收益,其中 state[0] 记录当前处于已进行了第一次买操作的状态时的累积收益,state[1] 记录当前处于已进行了第一次卖操作的状态时的累积收益,state[2] 记录当前处于已进行了第二次买操作的状态时的累积收益,…,state[2k - 1] 记录当前处于已进行了第 k 次卖操作的状态时的累积收益
要使当前状态为 state[0],可以是之前已处于 state[0] 然后当前不做任何操作,也可以是当前进行第一次买操作;
要使当前状态为 state[1],可以是之前已处于 state[1] 然后当前不做任何操作,也可以是之前处于已进行第一次买操作的 state[0] 状态然后当前卖出股票。以此类推可以得到状态转移方程
边界同样为第一天的状态,要记得可以当天买入又卖出股票,所以如果第一天要处于已买入 k 次股票的状态,state[k] 都等于 -prices[0],如果要处于已卖出 k 次股票的状态,state[k] 都等于 0,相当于第一天多次买入又多次卖出股票,最终收益还是 0
状态转移完毕后,state 数组中的最大值就是本题答案
public int maxProfit(int k, int[] prices) {
int len = prices.length;
if(len == 0 || k == 0) return 0;
int stateCount = 2 * k;
int[] state = new int[stateCount];
//初始化边界
for(int i = 0;i < stateCount;i += 2){
state[i] = -prices[0];
state[i + 1] = 0;
}
//状态转移
for(int i = 1;i < len;i++){
state[0] = Math.max(state[0],-prices[i]);
state[1] = Math.max(state[1],state[0] + prices[i]);
for(int j = 2;j < stateCount;j += 2){
state[j] = Math.max(state[j],state[j - 1] - prices[i]);
state[j + 1] = Math.max(state[j + 1],state[j] + prices[i]);
}
}
//得到答案
int res = -1;
for(int i = 0;i < stateCount;i++){
res = Math.max(res,state[i]);
}
return res;
}
714. 买卖股票的最佳时机含手续费
每天结束时处于以下两种状态之一:上一个动作为买入股票,记此时累积收益为 buy;上一个动作为卖出股票,记此时累积收益为 sell
要使当天状态为 buy,可以是之前已经处于 buy 然后当前不做任何操作,也可以是之前处于 sell 的状态然后当前买入股票
要使当天状态为 sell,可以是之前已经处于 sell 然后当前不做任何操作,也可以是之前处于 buy 的状态然后当前卖出股票同时支付手续费
边界为第一天,如果第一天结束处于 buy 状态,即第一天就买入股票,buy = -prices[0];如果处于 sell 状态,就是第一天当天买入又卖出股票,那么 sell = 0
public int maxProfit(int[] prices, int fee) {
int len = prices.length;
int buy = -prices[0],sell = 0;
for(int i = 1;i < len;i++){
buy = Math.max(buy,sell - prices[i]);
sell = Math.max(sell,buy + prices[i] - fee);
}
return sell;
}