leetcode 738.单调递增的数字
给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。
(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)
示例 1:
输入: N = 10
输出: 9
示例 2:
输入: N = 1234
输出: 1234
示例 3:
输入: N = 332
输出: 299
暴力
暴力解法就是从给定的数n开始,用for循环依次遍历从其值开始的一次减一的数,再对这些数进行“各个位数上的数字是否单调递增”的判断。
代码如下:
class Solution {
private:
bool check(int n){ //check whether the number in each position increases monotonically
int curValue = 10;
while(n){
int num = n % 10;
if(curValue >= num)
curValue = num;
else
return false;
n /= 10;
}
return true;
}
public:
int monotoneIncreasingDigits(int n) {
for(int i = n; i > 0; i--){
if(check(i))
return i;
}
return 0;
}
};
时间复杂度:O(n × m) m为n的数字长度
空间复杂度:O(1)
leetcode超时。
贪心算法
贪心算法就是要避免像暴力那样每种情况都详详细细的考虑到,这样就做了很多无用功,算法的时间效率也很低下。我们要做的是当给定的数字n不满足各位置单调递增的情况下,不是去遍历小于它的所有数来找到符合要求的数值最大者,而是总结出一种变换的方法能直接将原给定数n变成符合要求的最大数。
先考察各种情况:
n为个位数:个位数的n其本身就是小于或等于n的最大整数。
n为两位的数字:当我们有一个两个数,将其转化为字符串。若num[0] > num[1]时,此时只需将num[0]--并赋值num[1] = 9,就找到了符合题意的数。举个例子:
96 -> "9" "6" -> "8" "9" -> 89
与实际情况相符,逻辑也很容易理解。
n为多位数:此时我们要进行两个数两个数的比较,方法同n为两位数字的情况。但是需要注意的是应该从左向右进行比较还是从右向左进行比较?由于题目要求是”单调递增“,对于一个三位数来说,如果前两位需要进行”操作“,那么第三位无论值为多少都可以直接赋9,同理对于一个四位数、五位数来说,只要前面的两位进行了操作,后面的值都可以直接赋9。
所以这就决定了我们不能从左到右进行比较,例如遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2],如332。虽然后面的数可以直接赋9,但它导致了中间的数的错乱
当从右向左进行比较时,就不需要考虑中间的数的问题,它可以始终保持前面的数单增、后面的数均为9的特性。
我们在代码中需要定义一个标记,来表示最前的不符合单增原理的需要进行”操作“的两个数的后一个数所在下标,然后对这两个数进行操作后,将其后面的所有数均赋9即可。
整体代码如下:
class Solution {
public:
int monotoneIncreasingDigits(int n) {
int flag = -1;
string num = to_string(n); // int -> string
for(int i = num.size() - 1; i > 0; i--){
if(num[i - 1] > num[i]){
flag = i;
num[i - 1]--;
}
}
for(int i = flag; i < num.size(); i++){
num[i] = '9';
}
return stoi(num); // string -> int
}
};
时间复杂度:O(n),n 为数字长度
空间复杂度:O(n),需要一个字符串,转化为字符串操作更方便
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.
贪心策略,就是最低值买,最高值(如果算上手续费还盈利)就卖。
无非就是要找到两个点,买入日期,和卖出日期。
买入日期:其实很好想,遇到更低点就记录一下。
卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。
本题在遍历prices数组时会遇到以下几种情况:
该买入时:向后遍历遇到比前面记录的minPrice更小的值就更新minPrice,相当于买入了一个股票。
该卖出时:如果碰到prices[i] >= minPrice就考虑卖出,此时:
同时prices[i] <= minPrice + fee,此时卖出并不赚,我们就跳过这个值继续向后寻找。
同时prices[i] > minPrice + fee,这时就可以计算利润,但是需要注意的是这个利润不一定是最终的利润,因为我们可能进入到了一个获利区间内,在这个区间内的所有值都可以使我们获利,我们当前遍历的值不一定是区间内的最大值,如果此时”卖出“,那么获利就会打折扣。如下:
prices = [1, 3, 2, 8, 9] , fee = 2
我们在1买入,此后3、2均不符合卖出的条件,当遍历到8时,符合卖出的条件,但是观察后面会发现在后方的9卖出会获得更大的利润。所以我们在遍历到8时要先做一个记录:
if(prices[i] > minPrice + fee){
result += prices[i] - minPrice - fee;
minPrice = prices[i] - fee;
}
从代码中可以看出如果还在收获利润的区间里,并不是真正的卖出,而计算利润每次都要减去手续费,所以要让minPrice = prices[i] - fee;,这样在明天收获利润的时候,才不会多减一次手续费,而代入后:
// 8 (i)
result = prices[i] - minPrice - fee; // result = 8 - 1 - 2 = 5
minPrice = prices[i] - fee; // minPrice = 8 - 2 = 6
// 9 (i + 1)
result += prices[i + 1] - (prices[i] - fee) - fee; // result = 8 - 1 - 2 + (9 - 8) = 6
minPrice = prices[i + 1] - fee; // minPrice = 9 - 2 = 7
可见我们使用minPrice = prices[i] - fee的操作可以实现在获利区间中逐步更新result(相当于一直变换”持有“的股票并记录其利润)。当遍历到prices[i]< minPrice时,就记录新的minPrice,相当于对前面的”持有“的股票进行了”卖出“的操作并”买入“了新的股票。
整体代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int result = 0;
int minPrice = prices[0];
for(int i = 1; i < prices.size(); i++){
if(prices[i] < minPrice){
minPrice = prices[i];
}
if(prices[i] >= minPrice && prices[i] <= minPrice + fee){
continue;
}
if(prices[i] > minPrice + fee){
result += prices[i] - minPrice - fee;
minPrice = prices[i] - fee;
}
}
return result;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
leetcode 968.监控二叉树
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例 1:

输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。
本题很容易发现摄像头可以监控其父节点、本身节点以及其左右孩子。
如果把摄像头放在叶子节点上,就浪费的一层的覆盖。所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。那么为什么是从叶子节点开始分配摄像头的覆盖呢?因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。
局部最优:让叶子节点的父节点安摄像头,所用摄像头最少;整体最优:全部摄像头数量所用最少。
所以大致思路就是从叶子节点开始,给其父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。
既然要从叶子节点一直向上遍历,那么毫无疑问我们将选用后序遍历(左右中)。
每个节点可能有如下三种状态:
该节点无覆盖
本节点有摄像头
本节点有覆盖
我们分别有三个数字来表示:
0:该节点无覆盖
1:本节点有摄像头
2:本节点有覆盖
在遍历树的过程中,很有可能会遇到空节点的情况,那么应该给空节点赋什么值呢?
回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上,同时也会使得摄像头数量增多。
所以要把空节点的状态设为”有覆盖“。
递归三部曲
确定递归函数的参数和返回值
题目要求计算监控树的所有节点所需的最小摄像头数量,那么将递归函数类型设置为int型。
同时参数为传入二叉树的根节点。
int traversal(TreeNode* cur)
确定终止条件
那么递归的终止条件应该是遇到了空节点,此时应该返回2(有覆盖)。
if(cur == NULL)
return 2;
确定单层递归的逻辑
情况1:左右节点都有覆盖
左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该就是无覆盖的状态了。
如图:

if(left == 2 && right == 2) return 0;
情况2:左右节点至少有一个无覆盖的情况
如果是以下情况,则中间节点(父节点)应该放摄像头:
left == 0 && right == 0 左右节点无覆盖
left == 1 && right == 0 左节点有摄像头,右节点无覆盖
left == 0 && right == 1 左节点无覆盖,右节点摄像头
left == 0 && right == 2 左节点无覆盖,右节点覆盖
left == 2 && right == 0 左节点覆盖,右节点无覆盖
这个不难理解,毕竟有一个孩子没有覆盖,父节点就应该放摄像头。
此时摄像头的数量要加一,并且return 1,代表中间节点放摄像头。
代码如下:
if(left == 0 || right == 0){
result++;
return 1;
}
情况3:左右节点至少有一个有摄像头
如果是以下情况,其实就是 左右孩子节点有一个有摄像头了,那么其父节点就应该是2(覆盖的状态)
left == 1 && right == 2 左节点有摄像头,右节点有覆盖
left == 2 && right == 1 左节点有覆盖,右节点有摄像头
left == 1 && right == 1 左右节点都有摄像头
代码如下:
if(left == 1 || right == 1){
return 2;
}
情况4:头结点没有覆盖
以上都处理完了,递归结束之后,可能头结点还有一个无覆盖的情况,如图

int minCameraCover(TreeNode* root) {
result = 0;
if (traversal(root) == 0) { // root 无覆盖
result++;
}
return result;
}
整体代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
int result;
int traversal(TreeNode* cur){
if(cur == NULL) return 2;
int left = traversal(cur->left);
int right = traversal(cur->right);
if(left == 2 && right == 2)
return 0;
if(left == 0 || right == 0){
result++;
return 1;
}
if(left == 1 || right == 1)
return 2;
return -1; // return -1 虽然逻辑不会到达这里,但是必须加上,把情况补充完整
}
public:
int minCameraCover(TreeNode* root) {
result = 0;
if(traversal(root) == 0)
result++;
return result;
}
};