给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。
(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)
视频讲解https://www.bilibili.com/video/BV1Kv4y1x7tP/?spm_id_from=333.788&vd_source=f98f2942b3c4cafea8907a325fc56a48文章讲解
https://programmercarl.com/0738.%E5%8D%95%E8%B0%83%E9%80%92%E5%A2%9E%E7%9A%84%E6%95%B0%E5%AD%97.html
- 思路:不满足单调递增时借位,尽可能从大往小取值
-
局部最优:遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]--,然后strNum[i]给为9,可以保证这两位变成最大单调递增整数。
-
全局最优:得到小于等于N的最大单调递增的整数。
-
遍历顺序:从低位到高位
-
例:332,i 为 2👉strNum[i - 1] > strNum[i]👉329
-
还需比较借位后的 2 和高位数字是否满足单调递增 329👉299
-
-
i - 1发生借位,则从 i 往后赋值为9
-
如果没发生借位,则不需要赋9,将flag初始化为strNum.size()
-
- 代码:
// 代码随想录:
class Solution {
public:
int monotoneIncreasingDigits(int N) {
string strNum = to_string(N);
// flag用来标记赋值9从哪里开始
// 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行
int flag = strNum.size();
for (int i = strNum.size() - 1; i > 0; i--) {
if (strNum[i - 1] > strNum[i] ) {
flag = i;
strNum[i - 1]--;
}
}
for (int i = flag; i < strNum.size(); i++) {
strNum[i] = '9';
}
return stoi(strNum);
}
};
// 自己:
class Solution {
public:
int monotoneIncreasingDigits(int n) {
if (n < 10) return n;
vector<int> bits; // 从低位到高位逐位存n
// 从低位到高位获取每一位数字
int result = 0;
while (n != 0) {
bits.push_back(n % 10);
n = n / 10;
}
// 从高位到低位遍历,如果都满足递增,则不发生借位
for (int i = bits.size() - 2; i >= 0; --i) {
if (bits[i] < bits[i + 1]) {
// 借位
bits[i + 1] -= 1;
// 借位后的数 与 高位 进行比较
while (i < bits.size() - 2 && bits[i + 1] < bits[i + 2]) {
bits[i + 2] -= 1;
++i;
}
// 低位都为9
for (int j = 0; j <= i; ++j) {
bits[j] = 9;
}
break;
}
}
// int数组👉int
for (int i = bits.size() - 1; i >= 0; --i) {
result += bits[i] * pow(10, i);
}
return result;
}
};
⭐将 n 转化为字符串操作更方便。
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用(每笔交易你只需支付一次手续费)。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。
- 思路:
- 有了手续费,需要考虑买卖利润可能不足以支付手续费的情况;
- 贪心策略:最低值买,最高值(如果算上手续费还盈利)就卖;
- 找到两个点,买入日期和卖出日期:
- 买入日期:其实很好想,遇到更低点就记录一下;
- 卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。
- 三种情况:
- 情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润;
- 情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格(相当于寻找下一次交易的买入点);
- 情况三:不作操作,保持原有状态(买入,卖出,不买不卖)。
- 代码:
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 + fee 买入
minPrice = prices[i] - fee; // 情况一,这一步很关键
}
}
return result;
}
};
⭐每笔交易付一次手续费,不妨设在买入时付手续费,相当于以 minPrice + fee 买入;
一旦 prices[i] 大于买入的花费,可以先收获利润 prices[i] - (minPrice + fee);如果 prices[i + 1] - prices[i] > 0,说明能继续收获利润,且手续费已经付过👉为保持以 minPrice + fee 买入,更新 minPrice = prices[i] - fee,也就是以 prices[i] 买入。
给定一个二叉树,我们在树的节点上安装摄像头。节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。计算监控树的所有节点所需的最小摄像头数量。
文章讲解https://programmercarl.com/0968.%E7%9B%91%E6%8E%A7%E4%BA%8C%E5%8F%89%E6%A0%91.html
局部最优:让叶子节点的父节点安摄像头,所用摄像头最少👉全局最优:全部摄像头数量最少!
- 思路:
- 返回值:节点状态(0:无摄像头且无覆盖 1:有摄像头 2:无摄像头但有覆盖)
- 后序遍历:左孩子状态、右孩子状态👉当前节点状态
- 情况1:左右孩子都 无摄像头但有覆盖,当前节点置为 0(待其父节点装摄像头)
- 情况2:有一个孩子为 0,不论另一个孩子是 0、1、2,当前节点置为 1
- 情况3:有一个孩子为 1,不论另一个孩子是 1、2,当前节点置为 2
- 由于 根节点没有父节点,如果根节点状态为 0 则需要单独处理,给 root 加个摄像头
- 代码:
// 版本一
class Solution {
private:
int result;
int traversal(TreeNode* cur) {
// 空节点,该节点有覆盖
if (cur == NULL) return 2;
int left = traversal(cur->left); // 左
int right = traversal(cur->right); // 右
// 情况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 左节点覆盖,右节点无覆盖
if (left == 0 || right == 0) {
result++;
return 1;
}
// 情况3
// left == 1 && right == 2 左节点有摄像头,右节点有覆盖
// left == 2 && right == 1 左节点有覆盖,右节点有摄像头
// left == 1 && right == 1 左右节点都有摄像头
// 其他情况前段代码均已覆盖
if (left == 1 || right == 1) return 2;
// 以上代码我没有使用else,主要是为了把各个分支条件展现出来,这样代码有助于读者理解
// 这个 return -1 逻辑不会走到这里。
return -1;
}
public:
int minCameraCover(TreeNode* root) {
result = 0;
// 情况4
if (traversal(root) == 0) { // root 无覆盖
result++;
}
return result;
}
};
// 版本二
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;
else if (left == 0 || right == 0) {
result++;
return 1;
} else return 2;
}
public:
int minCameraCover(TreeNode* root) {
result = 0;
if (traversal(root) == 0) { // root 无覆盖
result++;
}
return result;
}
};