【738.单调递增的数字】中等题
方法一 从左往右遍历(自己做的,稍微抽象)
思路:
1、编写getIncreasing(int num, int idx)方法,用于根据处理后的非递增的开始索引,返回处理后的最大递增数。(注意:是在原方法中处理非递增的开始索引再通过形参传入)
- 例如12351,非递增的开始是数字5,非递增的开始索引是3,那么就得到times为10,对12351先除10再乘10,就得到12350,再减1,就得到想要的结果12349。
- 例如9999899,非递增的开始是第4个数字9,非递增的开始索引是3,但是因为非递增的开始数字是9,前面还是相同的数字9,不处理直接减就会得到9998999,不符合题意,因此要将非递增的开始索引向左移动到相同数字的开始索引,即0,则得到的times是1000000,返回的值为(9999899 / 1000000)* 1000000 - 1 = 8999999
2、原方法的处理逻辑:
将数字转换成字符串,从左到右遍历,寻找非递增的开始索引。
- 找到非递增的开始索引后,如果该索引需要处理,则将处理后的索引传入getIncreasing方法,如果不需要处理,直接往getIncreasing方法传入索引即可,getIncreasing方法返回的值就是结果。
- 如果没有找到非递归索引,证明这个数本身就是单调递增的,直接返回原来的数即可。
class Solution {
public int monotoneIncreasingDigits(int n) {
String s = Integer.toString(n);
for (int i = 0; i < s.length() - 1; i++){
char c1 = s.charAt(i); // 当前遍历数字
char c2 = s.charAt(i+1); // 下一个遍历的数字
// 找到非递增的开始数字
if (c1 - '0' > c2 - '0'){
// 如果是9999899这种情况,遍历到8前面的9前面还是9,则进入下面的if分支,那么需要定位到第一个9
if (i > 0 && c1 == s.charAt(i-1)) {
int idx = i;
while (idx > 0){
if (s.charAt(idx-1) == c1) idx--;
else break;
}
return getIncreasing(n, idx);
}
// 其余情况12351,定位到5,i为3,times为10,获取12350,再减1,就是答案12349
return getIncreasing(n, i);
}
}
return n; // 如果找到非递增的情况,证明都是递增的,直接返回原来的数字
}
// 根据非递增的开始索引,返回处理后的最大递增数
public int getIncreasing(int num, int idx){
int times = (int) Math.pow(10, Integer.toString(num).length() - 1 - idx);
return (num / times) * times - 1;
}
}
- 时间复杂度: O(logn),数字n的位数是logn,遍历所有位数一次
- 空间复杂度: O(logn),字符串的长度等于数字n的位数logn
方法二 从右往左遍历
思路:当前遍历的数字和前面的数字为非递增关系,则前面的数字-1,start标记要开始变成9的索引。
例子:332
从右往左遍历:
字符串为332,遍历 i = 2,32是非递增,则3-1=2,332变成322,start = 2
字符串为322,遍历 i = 1,32是非递增,则3-1=2,322变成222,start = 1
遍历结束,开始变9:
从start = 1开始的数字都是9,所以222变成299
返回结果299
易错点:不能遇到非递增就把当前数字变为9,把前面数字-1
例子:124428
如果按这个逻辑,遍历到2的时候,发现42不是递增,则变成124398,再遍历到3时,遇到43不是递增,则变成123998,后序没有非递增,则返回123998,明显结果是错的,对的结果是123999。
class Solution {
public int monotoneIncreasingDigits(int n) {
String s = Integer.toString(n);
StringBuffer sb = new StringBuffer(s);
int start = s.length();
for (int i = sb.length() - 1; i > 0; i--){
char cur = sb.charAt(i);
char pre = sb.charAt(i - 1);
// 遇到非递增,将前一个数字减1,更新开始变9的索引
if (cur - 'a' < pre - 'a'){
sb.setCharAt(i-1, (char) (pre - 1));
start = i;
}
}
// 从开始变9的索引遍历,将后面的都变成9
for (int i = start; i < s.length(); i++){
sb.setCharAt(i, '9');
}
return Integer.parseInt(sb.toString());
}
}
- 时间复杂度: O(logn),数字n的位数是logn,遍历所有位数一次
- 空间复杂度: O(logn),字符串的长度等于数字n的位数logn
【968.监控二叉树】困难题
难点:需要考虑三种不同的状态
- 状态0:未被覆盖(无摄像头)
- 状态1:有摄像头
- 状态2:已被覆盖(无摄像头)
(一开始很容易只考虑了两种状态,即有摄像头和无摄像头,造成题解无法通过)
思路:
1、利用后序递归遍历二叉树,根据子节点的状态确定当前节点的状态。子节点的状态和当前节点的状态关系如下(子节点状态无序):
- (子节点状态, 子节点状态) -> 当前节点状态
- (0, 0) -> 1
- (0, 1) -> 1
- (0, 2) -> 1
- (1, 1) -> 2
- (1, 2) -> 2
- (2, 2) -> 0
解释:
- 只要有子节点有0,即存在未被覆盖的子节点,则当前节点必须装摄像头,即确定当前节点的状态是1。
- 子节点没有0的情况下,只要有子节点有1,证明当前节点一定会被覆盖,可以不装摄像头,即确定当前节点的状态是2。
- 如果子节点都是2,即都已被覆盖状态,而当前节点就无法被监测,贪心算法为了减少摄像头数,可以不在当前节点装摄像头,等上面的父节点装,即当前节点是状态0。
2、确定叶子节点和空节点的状态
- 贪心算法想尽量减少摄像头的个数,则避免在叶子节点放置摄像头,以尽可能地扩大监测范围。因此,叶子节点不能是状态1,加上叶子节点可以让父节点放置摄像头进行检测,因此叶子节点应该设置为状态0。
- 根据状态转换关系,只有两个都是状态2的子节点,当前节点才能为状态0,因此空节点应该设置为状态2。
class Solution {
int num = 0;
public int minCameraCover(TreeNode root) {
// 如果只有一个节点,那么返回的状态为0,需要装1个摄像头
if (dfs(root) == 0) num++; // 在判断的同时调用了dfs方法进行二叉树的后序递归遍历
return num;
}
// 1、确定参数和返回值:传入节点,返回当前节点的状态值
public int dfs(TreeNode root){
// 2、确定终止条件
// 如果是空节点,返回2
if (root == null) return 2;
// 3、确定单层递归逻辑
int left = dfs(root.left);
int right = dfs(root.right);
if (left == 0 || right == 0) { // (0, 0) (0, 1) (0, 2)
num++;
return 1;
}
else if (left == 2 && right == 2) return 0; // (2, 2) - 叶子节点
else return 2; // (1, 1) (1, 2)
}
}
- 时间复杂度: O(n),n为节点个数,相当于遍历一次所有节点
- 空间复杂度: O(n),递归栈的深度最坏为n
【贪心算法总结】
1、本质:
局部最优 -> 全局最优
2、需要重点掌握的三类题型:
- 二维权衡问题
- 股票问题
- 重叠区间问题
(根据代码随想录的贪心算法总结篇对各类题型进行复习)