算法学习记录~2023.X.XX~章节DayX~题目号.题目标题 & 题目号.题目标题
738.单调递增的数字
题目链接
思路
一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]–,然后strNum[i] 设为9。
至于遍历顺序:
如果从前向后遍历,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2]。
比如数字:332,从前向后遍历的话,那么就把变成了329,此时2又小于了第一位的3了,真正的结果应该是299。
从后向前遍历的话,就可以重复利用上次比较得出的结果,从后向前遍历332的数值变化为:332 -> 329 -> 299
需要注意的是,由于要取最大值,所以从后向前遍历遍历时,需要找到最大的不符合大小关系的位置,也就是最前面的一处,从这开始后面全设为 9 。
如果从后往前边遍历边修改当前值,那么会出现后面也有部分是符合的,从而没有被修改,而无法获取最大值。
比如:100 -> 90
代码
class Solution {
public:
int monotoneIncreasingDigits(int n) {
string strN = to_string(n); //将数值转为字符串方便取坐标处理
int flag = strN.size(); //记录从哪里开始往后都是9 。 设置这个默认值防止第二个 for 循环在flag没有被赋值情况下进行
for (int i = strN.size() - 1; i >= 1; i--){
if (strN[i] < strN[i - 1]){
flag = i; //记录 9 开始位置
strN[i - 1]--; //前一位减 1
}
}
for (int i = flag; i < strN.size(); i++)
strN[i] = '9';
int result = stoi(strN); //转换为 int
return result;
}
};
总结
968.监控二叉树
题目链接
思路1:自己想的(错误思路)(又是企图枚举)
直觉上感觉一定是每 3 层为一个周期,因为一个摄像头能把本节点和上下两层都覆盖到。
因此就采用层序遍历,分别记录每 3n、3n + 1、3n + 2的节点各自总量count0、count1、count2,接着对于总层数对3进行取模,然后分情况讨论使用哪些节点总量。
(事实上没跑通,因为感觉情况想的还是不是很全也不太对,此处只是记录一下当时想法)
代码
class Solution {
public:
int minCameraCover(TreeNode* root) {
queue<TreeNode*> que;
que.push(root);
int count0 = 1; //3n 层,初始化为第一层的 1
int count1 = 0; //3n + 1层
int count2 = 0; //3n + 2层
int judge = 0; //判断是哪一层,设定第一层为0。对3取模来判断是3n + x层
while (!que.empty()){
int size = que.size(); //先记录本层总共个数,方便本层处理完改变judge
for (int i = 0; i < size; i++){
TreeNode* node = que.front();
que.pop();
if (node -> left){
que.push(node -> left);
if (judge % 3 == 1)
count2++;
else if (judge % 3 == 2)
count0++;
else
count1++;
}
if (node -> right){
que.push(node -> right);
if (judge % 3 == 1)
count2++;
else if (judge % 3 == 2)
count0++;
else
count1++;
}
}
judge ++; //记录一共有多少层了,实际层数需要加1
}
judge++; //记录一共有多少层了,实际层数需要加 1 所以这里加上
if (judge % 3 == 0){
if (count1 == 0)
return 1;
return count1;
}
else if (judge % 3 == 1){
return count0;
}
else{
if (count1 == 0)
return 1;
return count1;
}
}
};
思路2:贪心算法
从下往上看,叶子节点都没放摄像头,因为摄像头可以覆盖三层,所以放在叶子节点上会浪费,而且显然越往上节点是越少的,因为如果本层确定有节点,那父节点只会小于或等于本层,因此优先叶子节点的父节点。
大体思路就是,从下往上,先给叶子节点的父节点放个摄像头,然后每隔两个节点放一个摄像头,知道二叉树头节点。
有两个难点:
- 二叉树的遍历
- 怎样每隔两个节点放一个摄像头
问题1:二叉树的遍历
想从下向上,可以后序遍历左右中,这样就可以在回溯过程中从下到上推导了。
int traversal(TreeNode* cur) {
// 空节点,该节点有覆盖
if (终止条件) return ;
int left = traversal(cur->left); // 左
int right = traversal(cur->right); // 右
逻辑处理 // 中
return ;
}
注意在以上代码中我们取了左孩子的返回值,右孩子的返回值,即left 和 right, 以后推导中间节点的状态
问题2:怎样每隔两个节点放一个摄像头
需要状态转移的公式。
首先一共有3 种状态:
- 该节点无覆盖 --> 使用 0 表示
- 该节点有摄像头 --> 使用 1 表示
- 该节点有覆盖 --> 使用 2 表示
遍历树时会遇到空节点,如何处理?
回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。
那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了。
空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。
所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了。
接下来是递推关系。
首先是递推的终止条件,是遇到了空节点,也就是对应上面说的 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;
注意并不需要考虑一个摄像头一个无覆盖的情况,因为已经在情况 2 中讨论了,因此情况 3 只考虑没有无覆盖的 3 种情况即可。
- 情况4:头节点没有覆盖
以上都处理完了,递归结束之后,可能头节点还有一个无覆盖的情况
所以递归结束之后,还要判断根节点,如果没有覆盖,result++
int minCameraCover(TreeNode* root) {
result = 0;
if (traversal(root) == 0) { // root 无覆盖
result++;
}
return result;
}
代码
// 版本一
class Solution {
public:
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,该节点加摄像头,result++
// 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;
}
int minCameraCover(TreeNode* root) {
result = 0;
// 情况4,头节点加一个摄像头,result++
if (traversal(root) == 0)
result++;
return result;
}
};
//精简版,在版本一的基础上使用else直接覆盖掉一些情况
// 版本二
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;
}
};
总结
不愧是hard题.jpg