LeetCode 714 买卖股票的最佳时机含手续费
(中等)
题目
描述
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
示例1
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
示例 2:
输入:prices = [1,3,7,5,10,3], fee = 3
输出:6
思路
此题与 LeetCode 122 买卖股票的最佳时机 II 题目相似,但是本题多了手续费这个额外条件,因此又提升了一个难度层次(我们无法再只是简单得求数组的两位之差并返回正数和来算出正确结果,具体可参考之前文章:LeetCode 122:买卖股票的最佳时机 II)。
本题的思路如果笼统地讲的话其实十分清晰,就是在股票价格极小值的时候买入,价格极大值的时候卖出,但是,如何判断什么时候是极小值与极大值呢?因为价格极小值一定在价格数组中,并且可能有多个,因此我们先令极小值 minPrice 的初值为 prices[0],每次遍历时,如果当前价格小于 minPrice,那么说明此价格更小,更可能是极小值,应让 minPrice 等于此值;如果当前价格高于 minPrice 和 手续费之和,那么说明此时是处于收益状态的,但可能并未到达价格的最高值,因此不能确定是否卖出,但是完全可以将此次产生的当天利润给加到结果变量 profit 中,即执行 profit += prices[i] - minPrice - fee,由于明天可能还要盈利,因此不一定是真正的卖出,而为了避免计算利润每次都要减去手续费,并且要更新极小值 minPrice ,所以让 minPrice = prices[i] - fee,这样在明天收获利润的时候,才不会多减一次手续费。
实现
public class TX17买卖股票的最佳时机含手续费 {
public int maxProfit(int[] prices, int fee) {
if (prices == null || prices.length == 0 || fee < 0) {
return 0;
}
int profit = 0;
int minPrice = prices[0];
for (int i = 1; i < prices.length; i++) {
//价格低-->买入
if (prices[i] < minPrice) {
minPrice = prices[i];
}
//价格高-->持续收益,最后卖出
if (prices[i] > minPrice + fee) {
profit += prices[i] - minPrice - fee;
minPrice = prices[i] - fee;
}
}
return profit;
}
public static void main(String[] args) {
TX17买卖股票的最佳时机含手续费 s = new TX17买卖股票的最佳时机含手续费();
System.out.println("s.maxProfit(new int[]{1, 3, 2, 8, 4, 9}, 2) = " + s.maxProfit(new int[]{1, 3, 2, 8, 4, 9}, 2));
}
}
LeetCode 968 监控二叉树
(困难)
题目
描述
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例
思路
本题可以用贪心算法解决,其中的贪心思想是从最下层叶子结点的父结点开始安置摄像头,这样就能尽可能多得覆盖结点,从而使得最终使用摄像头的数量最少。
明白了本题的贪心思想后,还有一个问题需要解决,那就是如何在遍历树的时候,能够知道此结点,或是此结点的左右子结点有摄像头或是没有摄像头从而进行合适的操作与判断呢?要解决这个问题,我们可以假设树中结点有三种状态,分别对应数字 0,1,2,对应含义为 0—没被监视覆盖,1—有摄像头,2—被监视覆盖(可能您会有疑问:有1—摄像头和2—被监视覆盖不是一种情况吗,确实,从状态上说,他们都是被监视状态,但是,有摄像头是一种特殊情况,它直接影响一个结点的父结点、子结点的状态情况,因此需要单独拿出来,而与之对应的,还有一种无摄像头的情况,这种情况由于是属于0—没被监视覆盖,而且我们不需要单独处理这种情况,因此,便不再单独考虑这种无摄像头的,而是直接用没被监视覆盖来概括,当然,如果您是用逆向思维的话,那么也可以反过来想啦)。
还有一个问题需要解决,因为核心算法中避免不了深度遍历与其中的递归结果记录,那就是由于在遍历中如果遇到空结点怎么办,要返回什么,其实可以将空结点单独归为一种状态,但是那样会多出很多额外的判断语句与特殊情况,其实我们可以这样想,空结点可以假设为2—被监视覆盖,这样不会影响到叶子结点的状态,也不会对最终产生任何影响,因为一个结点是2—被监视覆盖的话,那么其实可以不用额外考虑为了它而加摄像头的,因为它已经被覆盖了。
接下来,就可以正式考虑摄像头加不加的各种情况了,总共有 3 种基本情况,2 种特殊情况(再次声明,是从下向上来进行加摄像头的判断):
-
情况1:若一个结点的左右子结点都有覆盖,那么便可以在此结点的上层父结点来安置摄像头,而此层不用安装,返回 0;
-
情况2:只要左右子结点有一个没被覆盖,由于是从下向上覆盖,那么此结点理所应当就要安置摄像头,返回 1;
-
情况3:如果左右子结点有一个安了摄像头,那么此结点必然能被覆盖,返回 2;
-
特殊情况1:当前为空结点,返回 2(上面已经说明过为什么是 2,不再赘述);
-
特殊情况2:若root没被覆盖,要单独给root安一个摄像头。
实现
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
public class TX18监控二叉树 {
//状态:0没被监视覆盖,1:有摄像头,2:被监视覆盖
int result = 0;
private int traverse(TreeNode cur) {
if (cur == null) { //空结点假设是被监视状态
return 2;
}
int left = traverse(cur.left);
int right = traverse(cur.right);
//情况1:若左右子结点都有覆盖,那么便可以在它的上层父结点来安置摄像头,而此层不用安装
if (left == 2 && right == 2) {
return 0;
}
//情况2:只要左右子结点有一个没被覆盖,由于是从下向上覆盖,那么此结点理所应当就要安置摄像头
else if (left == 0 || right == 0) {
result++;
return 1;
}
//情况3:如果左右子结点有一个安了摄像头,那么此结点必然能被覆盖
else if (left == 1 || right == 1) {
return 2;
}
//为了不报编译错误而添加,理论上不会走到这里
return -1;
}
public int minCameraCover(TreeNode root) {
result = 0;
//若root没被覆盖,要单独给root安一个摄像头
if (traverse(root) == 0) {
result++;
}
return result;
}
}