Leetcode计划(全勤完结):2024年二月第四周
由于本人寒假摸鱼,大四上学期一直在考研,寒假又在摸(原)鱼(神),所以曾经熟练的c++又变得生疏了起来,Leetcode是时候启动了!在不启动考研机考都要慌了!目前的计划是:
每天一题,如果很简单的,要求用c++和go两种语言写,如果是很难的可以只用c++写。
周一
题目很简洁,给定二叉搜索树的根结点 root
,返回值位于范围 [low, high]
之间的所有结点的值的和。这一看就是一道递归的题目。题目也很亲切的给我们了模板。
1 2 3 4 5 6 class Solution {public : int rangeSumBST (TreeNode* root, int low, int high) { } };
具体来说根据区间[low, high]
还有一个点x=root->val
,可以考虑五种情况:
x > h i g h x>high x > hi g h , 那么我们就应该在左区间找
x = h i g h x = high x = hi g h ,那么我们就要把当前节点算上,然后在左子树边找
后面的以此类推…
此外,如果节点不可用是null,那么要返回0
就这样,我写出来第一版代码,然后发现是错误的!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution {public : int rangeSumBST (TreeNode* root, int low, int high) { if (root == nullptr ) return 0 ; else if (root->val > high) return rangeSumBST (root->left, low, high); else if (root->val == high) return root->val + rangeSumBST (root->left, low, high - 1 ); else if (root->val < high && root->val > low) return root->val + rangeSumBST (root->left, low, root->val - 1 ) + rangeSumBST (root->right, root->val + 1 , high); else if (root->val == low) return root->val + rangeSumBST (root->right, low + 1 , high); else if (root->val < low) return rangeSumBST (root->right, low, high); return 0 ; } };
仔细一看才找到问题的所在,原来是因为我忘记检查了l o w < = h i g h low<=high l o w <= hi g h 这个条件,如果出现l o w > h i g h low>high l o w > hi g h 的时候,肯定要直接返回0的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution {public : int rangeSumBST (TreeNode* root, int low, int high) { if (root == nullptr || low > high) return 0 ; else if (root->val > high) return rangeSumBST (root->left, low, high); else if (root->val == high) return root->val + rangeSumBST (root->left, low, high - 1 ); else if (root->val < high && root->val > low) return root->val + rangeSumBST (root->left, low, root->val - 1 ) + rangeSumBST (root->right, root->val + 1 , high); else if (root->val == low) return root->val + rangeSumBST (root->right, low + 1 , high); else if (root->val < low) return rangeSumBST (root->right, low, high); return 0 ; } };
成功通过,按照简单题两种语言的原理,把go的版本也放上来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution { public: int rangeSumBST(TreeNode* root, int low, int high) { if (root == nullptr || low > high) return 0 ; else if (root->val > high) return rangeSumBST(root->left, low, high); else if (root->val == high) return root->val + rangeSumBST(root->left, low, high - 1 ); else if (root->val < high && root->val > low) return root->val + rangeSumBST(root->left, low, root->val - 1 ) + rangeSumBST(root->right, root->val + 1 , high); else if (root->val == low) return root->val + rangeSumBST(root->right, low + 1 , high); else if (root->val < low) return rangeSumBST(root->right, low, high); return 0 ; } };
周二
1)插曲题
今天的题目有些难,涉及到一个质数的判断题(需要学会埃氏筛 )。埃氏筛 题目的就是给定整数 n
,返回 所有小于非负整数 n
的质数的数量。
首先我们从2开始,2是质数,如果2是质数,那么2 m ( m ≥ 2 ) 2m(m \geq 2) 2 m ( m ≥ 2 ) 都是合数,不是质数,我们标记为0
然后我们往后读取,假设这个数是X X X
如果标记为0,那就是合数(因为他标记0,肯定是来源前面的某个质数的倍数)
如果标记不是0,那一定是质数(为什么?因为X X X 是合数,那么一定是某个质数Y Y Y 的整数倍,X = k Y X = kY X = kY )而我们前面一定已经扫描过了Y Y Y ,Y Y Y 的倍数都被标记上了!所以我们他一定是质数
当然这里还可以继续优化,对于一个质数 x x x ,如果按上文说的我们从 2 x 2x 2 x 开始标记其实是冗余的,应该直接从 x × x x \times x x × x 开始标记,因为 2 x , 3 x 2x, 3x 2 x , 3 x 这些数一定在 x x x 之前就被其他数的倍数标记过了,例如 2 的所有倍数,3 的所有倍数等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution {public : int countPrimes (int n) { if (n <= 1 ) return 0 ; int result = 0 ; vector<int > resultData (n, 1 ) ; if (n >= 1 ) resultData[0 ] = 0 ; if (n >= 2 ) resultData[1 ] = 0 ; for (long long int i = 2 ; i < n; i++) { if (resultData[i] == 1 ) { result += 1 ; for (long long int j = i * i; j < (long long int )n; j += i) { resultData[j] = 0 ; } } } return result; } };
2)难度题
给你一棵 n
个节点的无向树,节点编号为 1
到 n
。给你一个整数 n
和一个长度为 n - 1
的二维整数数组 edges
,其中 edges[i] = [ui, vi]
表示节点 ui
和 vi
在树中有一条边。
请你返回树中的 合法路径数目 。
如果在节点 a
到节点 b
之间 恰好有一个 节点的编号是质数,那么我们称路径 (a, b)
是 合法的 。
注意:
路径 (a, b)
指的是一条从节点 a
开始到节点 b
结束的一个节点序列,序列中的节点 互不相同 ,且相邻节点之间在树上有一条边。
路径 (a, b)
和路径 (b, a)
视为 同一条 路径,且只计入答案 一次 。
说实话这个题目我看的就很懵,也很清楚这个题目就是个缝合怪,缝合了两个东西:
质数判断(肯定有一种比较快速判断一组数里面哪些质数的)
然后就是图相关的搜索、BFS、DFS(这两个我都不是很熟悉)
仔细读题,我们发现就是找路径,要找一个路径在节点 a
到节点 b
之间 恰好有一个 节点的编号是质数。那问题就简单了,我们要把所有的质数节点找出来,遍历一次,把这个节点对应的路径数量求出来,再求个和。
但是问题就来了,怎么把这个节点对应的路径数量求出来。如下图所示,我目前搜索的是中间质数节点 的有关的合法路径,现在发现他连着三个子图,还有一个右下角质数节点
首先右下角的质数节点不考虑,为什么?没必要,肯定不合法
然后我们依次考虑左上、右上、左下角的三个子图,我们以蓝色的合数节点分别为起点,做DFS搜索这个子图里面可以连通的节点的数量(只考虑合数节点)。
左上子图的对应的,可以连通的(只考虑合数节点)节点数量是X X X
右上的对应Y Y Y ,左下的对应Z Z Z
那么当前这个中心质数节点,所有的合法路径的数量就是X + Y + Z + X Y + X Z + Y Z X+Y+Z+XY+XZ+YZ X + Y + Z + X Y + XZ + Y Z
好了这个题目就写出来了
仔细把这个图想的更一般一些,这个图里面可以连通的合数节点 我们可以把他抽象成一个子图,然后对于这些子图,我们只需要做一次DFS就够了,这样可以节约时间,官方题解里面用了count数组,记录每个节点所在的子图中,节点的数量(反正都是连通的嘛)
最终的代码:
一部分埃氏筛,这是个Lambda 表达式[]() { /* code */ }
,整个 Lambda 表达式后面的 ()
表示立即执行该 Lambda 表达式。
一部分dfs,深度优先搜索,这个写过,但是容易忘了,核心思想是递归
然后就是图的路径搜索,最后需要整合再一块,需要对图理解清楚
总之上面的三个知识点都比较难,最难的是把前面两个整合起来,写出代码!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 const int N = 100001 ;bool isPrime[N];int init = []() { fill (begin (isPrime), end (isPrime), true ); isPrime[1 ] = false ; for (int i = 2 ; i * i < N; i++) { if (isPrime[i]) { for (int j = i * i; j < N; j += i) { isPrime[j] = false ; } } } return 0 ; }();class Solution {private : void dfs (const vector<vector<int >>& G, vector<int >& seen, int i, int pre) { seen.push_back (i); for (int j : G[i]) { if (j != pre && !isPrime[j]) { dfs (G, seen, j, i); } } }public : long long countPaths (int n, const vector<vector<int >>& edges) { fill (begin (isPrime), end (isPrime), true ); isPrime[1 ] = false ; for (int i = 2 ; i * i <= n; i++) { if (isPrime[i]) { for (int j = i * i; j <= n; j += i) { isPrime[j] = false ; } } } vector<vector<int >> G (n + 1 ); for (const auto & edge : edges) { int i = edge[0 ]; int j = edge[1 ]; G[i].push_back (j); G[j].push_back (i); } vector<int > seen; long long res = 0 ; vector<long long > count (n + 1 , 0 ) ; for (int i = 1 ; i <= n; i++) { if (!isPrime[i]) { continue ; } long long cur = 0 ; vector<long long > surrounding; for (int j : G[i]) { if (isPrime[j]) { continue ; } if (count[j] == 0 ) { seen.clear (); dfs (G, seen, j, 0 ); long long cnt = seen.size (); for (int k : seen) { count[k] = cnt; } } surrounding.push_back (count[j]); } for (int x = 0 ; x < surrounding.size (); x++){ cur += surrounding[x]; } int sum = 0 ; for (int x = 0 ; x < surrounding.size (); x++){ cur += sum * surrounding[x]; sum += surrounding[x]; } res += cur; } return res; } };
周三
今天这个题目的图有点恶心。先把题目搬上来。
给你一个整数 n
表示一棵 满二叉树 里面节点的数目,节点编号从 1
到 n
。根节点编号为 1
,树中每个非叶子节点 i
都有两个孩子,分别是左孩子 2 * i
和右孩子 2 * i + 1
。
树中每个节点都有一个值,用下标从 0 开始、长度为 n
的整数数组 cost
表示,其中 cost[i]
是第 i + 1
个节点的值。每次操作,你可以将树中 任意 节点的值 增加 1
。你可以执行操作 任意 次。
你的目标是让根到每一个 叶子结点 的路径值相等。请你返回 最少 需要执行增加操作多少次。
注意:
满二叉树 指的是一棵树,它满足树中除了叶子节点外每个节点都恰好有 2 个子节点,且所有叶子节点距离根节点距离相同。
路径值 指的是路径上所有节点的值之和。
比如下面的这个图
1 2 3 4 5 6 7 8 9 输入:n = 7, cost = [1,5,2,2,3,3,1] 输出:6 解释:我们执行以下的增加操作: - 将节点 4 的值增加一次。 - 将节点 3 的值增加三次。 - 将节点 7 的值增加两次。 从根到叶子的每一条路径值都为 9 。 总共增加次数为 1 + 3 + 2 = 6 。 这是最小的答案。
说实话这个给我一个误解,我看这个例子一下就以为,要把每个地方的左右节点,都要调整的权值完全一样!比如上面图里面的2号节点,对应权值是5,左右权值是2、3,那就是2+1变成3。然后对于根节点,也是啊,左右的节点也要调成权值一样。
后来我发现我想错了,不能这样。我那样的方法并不是正确的答案。我把题目的例子稍微改了一下,就是本来是3的,改成4,如下图框框所示:
首先肯定看叶子节点,因为每个叶子节点的兄弟节点,如果值不一样,肯定要把小的调成大的
这样把最底层的全部调完,比如左下角的4号节点,权值2一定要调成3,右边的7号节点,权值1一定要调成4。
调完之后,叶子节点的父亲,我就可以把权值给改了,因为他儿子节点权值都是3,所以我直接调成5+3=8
这样同理,右下角的黑框,3号节点,权值2,现在变成2+4=6。
最后看1号节点,左边儿子权值8,右边6,只需要把6再加2,就可以变成左右权值一样!
AC代码如下!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution {public : int getResult (int rootNode, vector<int >& cost, int & result) { if (rootNode >= cost.size () || rootNode < 0 ) return 0 ; int leftNode = rootNode * 2 ; int rightNode = rootNode * 2 + 1 ; if (leftNode < cost.size () && rightNode < cost.size ()){ int left = getResult (leftNode, cost, result); int right = getResult (rightNode, cost, result); int maxChild = max (left, right); result += abs (left - right); cost[rootNode] += maxChild; } return cost[rootNode]; } int minIncrements (int n, vector<int >& cost) { vector<int > cost2; cost2.push_back (0 ); for (int i = 0 ; i < cost.size (); i++){ cost2.push_back (cost[i]); } int result = 0 ; getResult (1 , cost2, result); return result; } };
浅说一下这题,例子没有给好,正好太特殊,底层全部变成3,就会给我一种错觉,所有的地方要把左右节点调整成一样的,实际上不是的,稍微举一反三就会找出正确的方法。
周四
Alice 有一棵 n
个节点的树,节点编号为 0
到 n - 1
。树用一个长度为 n - 1
的二维整数数组 edges
表示,其中 edges[i] = [ai, bi]
,表示树中节点 ai
和 bi
之间有一条边。
Alice 想要 Bob 找到这棵树的根。她允许 Bob 对这棵树进行若干次 猜测 。每一次猜测,Bob 做如下事情:
选择两个 不相等 的整数 u
和 v
,且树中必须存在边 [u, v]
。
Bob 猜测树中 u
是 v
的 父节点 。
Bob 的猜测用二维整数数组 guesses
表示,其中 guesses[j] = [uj, vj]
表示 Bob 猜 uj
是 vj
的父节点。
Alice 非常懒,她不想逐个回答 Bob 的猜测,只告诉 Bob 这些猜测里面 至少 有 k
个猜测的结果为 true
。
给你二维整数数组 edges
,Bob 的所有猜测和整数 k
,请你返回可能成为树根的 节点数目 。如果没有这样的树,则返回 0
。
这题目今天的我也没做出来,只能说参考了一下别人的思路然后自己写出来了。这个题目有涉及到下面的一些考点:
考点一:树的DFS(dfs这个我觉得得多写,不同的图的DFS还是略有区别,当然本质是递归)
比如要在二维的一个地图里面,跑DFS,就需要标记那些节点已经走过了
如果在一个树里面DFS,那就只需要标记来源的节点 (然后走的时候,只要不是来的父亲节点,都可以走)。因为我们在保存树的时候,是会保存A->B
,还有B->A
两个信息的(比如这个题,当然要是那些父子节点分明的,那就不需要)
考点二:hash map。这个题目需要快速的查询guesses,需要知道一个边是否在guesses里面,所以需要一个哈希map。
我个人写代码到时候习惯用二维数组,但是这个题目二维数组会超时!
map是基于红黑树实现,unordered_map是基于hash_table实现,把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间。这个题目里面会用到大量的查询,如果直接用map肯定是会寄的!
难点三:假设X的儿子是Y,理解当X是根节点(猜对的数量是a x a_x a x ),和翻转后Y是根节点(猜对的数量是a y a_y a y ),猜对的数量是会怎么变?
如果只存在[X->Y]
的guess,不存在[Y-X]
的guess,那么a y = a x − 1 a_y = a_x - 1 a y = a x − 1 (翻转后原来的X-Y猜测X是根节点,所以要减1)
如果只存在[Y->X]
的guess,不存在[X->Y]
的guess,那么a y = a x + 1 a_y = a_x + 1 a y = a x + 1
如果存在[Y->X]
的guess,也存在[X->Y]
的guess,那么a y = a x a_y = a_x a y = a x
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 int N = 0 ;int node0RootOknums = 0 ; map<int , map<int , bool >> hashGuesses; unordered_set<long long > s;class Solution {private :public : bool ifExistInGuess (long long int startNode,long long int endNode) { return s.count (startNode << 32 | endNode); } void insertIntoGuess (long long int startNode,long long int endNode) { long long int llst = startNode; long long int llend = endNode; s.insert (startNode << 32 | endNode); } void dfs (int curNode, vector<vector<int >>& tree, int fromNode) { vector<int > nextAvai = tree[curNode]; for (int i = 0 ; i < nextAvai.size (); i++){ int nextNode = nextAvai[i]; if (nextNode == fromNode){ continue ; } if (ifExistInGuess(curNode, nextNode)){ node0RootOknums++; } dfs (nextNode, tree, curNode); } } void dfsV2 (int curNode, vector<vector<int >>& tree, vector<int >& nodeGuessOkNum,int fromNode) { vector<int > nextAvai = tree[curNode]; for (int i = 0 ; i < nextAvai.size (); i++){ int nextNode = nextAvai[i]; if (nextNode == fromNode){ continue ; } nodeGuessOkNum[nextNode] = nodeGuessOkNum[curNode]; if (ifExistInGuess(curNode, nextNode)){ nodeGuessOkNum[nextNode]--; } if (ifExistInGuess(nextNode, curNode)){ nodeGuessOkNum[nextNode]++; } dfsV2 (nextNode, tree, nodeGuessOkNum, curNode); } } int rootCount (vector<vector<int >>& edges, vector<vector<int >>& guesses, int k) { N = edges.size () + 1 ; node0RootOknums = 0 ; hashGuesses.clear (); s.clear (); vector<int > emptyVec; vector<vector<int >> Tree (N, emptyVec); for (int i = 0 ; i < edges.size (); i++){ vector<int > edge = edges[i]; int startNode = edge[0 ]; int endNode = edge[1 ]; Tree[startNode].push_back (endNode); Tree[endNode].push_back (startNode); } for (int j = 0 ; j < guesses.size (); j++){ vector<int > oneGuess = guesses[j]; int startNode = oneGuess[0 ]; int endNode = oneGuess[1 ]; insertIntoGuess (startNode, endNode); } dfs (0 , Tree, -1 ); vector<int > nodeGuessOkNum (N, node0RootOknums) ; dfsV2 (0 , Tree, nodeGuessOkNum, -1 ); int result = 0 ; for (int j = 0 ; j < nodeGuessOkNum.size (); j++){ if (nodeGuessOkNum[j] >= k) result++; } return result; } };
周五
给你一个下标从 0 开始的整数数组 nums
,你必须将数组划分为一个或多个 连续 子数组。
如果获得的这些子数组中每个都能满足下述条件 之一 ,则可以称其为数组的一种 有效 划分:
子数组 恰 由 2
个相等元素组成,例如,子数组 [2,2]
。
子数组 恰 由 3
个相等元素组成,例如,子数组 [4,4,4]
。
子数组 恰 由 3
个连续递增元素组成,并且相邻元素之间的差值为 1
。例如,子数组 [3,4,5]
,但是子数组 [1,3,5]
不符合要求。
如果数组 至少 存在一种有效划分,返回 true
,否则,返回 false
。
今天也是小丑的一天,不知道为什么又没有做出来。这个题目是DP。
其实一点就通,这个题目是nums数组为有效数组,则下面的条件一定有一个满足:
最后三个元素是满足有效划分,且前面的部分满足有效划分
最后两个元素相等,且前面的部分满足有效划分
那这么下来,你可以说是个dp,也可以说是个分治法,或者说成是递归。那么代码也就很好写了。
我下面的代码里面写了两个possible变量,就是分开对上面两种情况求解。
最后我为了优化防止超时,如果第一个可以,直接就可以返回true了,不能再去递归第二个,省时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Solution {public : bool verifyThreeNum (int num1, int num2, int num3) { if (num1 == num2 && num1 == num3) return true ; if (num1 + 1 == num2 && num2 + 1 == num3) return true ; return false ; } bool validPartitionWithRange (vector<int >& nums, int start, int end) { if (end - start < 1 ) return false ; if (end - start == 1 ) return nums[start] == nums[end]; if (end - start == 2 ) return verifyThreeNum (nums[start], nums[start + 1 ], nums[start + 2 ]); bool possible1 = (nums[end] == nums[end - 1 ]) && validPartitionWithRange (nums, start, end - 2 ); if (possible1) return true ; bool possible2 = verifyThreeNum (nums[end - 2 ], nums[end - 1 ], nums[end]) && validPartitionWithRange (nums, start, end - 3 ); return possible2; } bool validPartition (vector<int >& nums) { return validPartitionWithRange (nums, 0 , nums.size () - 1 ); } };
周六
现有一棵由 n
个节点组成的无向树,节点编号从 0
到 n - 1
,共有 n - 1
条边。
给你一个二维整数数组 edges
,长度为 n - 1
,其中 edges[i] = [ai, bi]
表示树中节点 ai
和 bi
之间存在一条边。另给你一个整数数组 restricted
表示 受限 节点。
在不访问受限节点的前提下,返回你可以从节点 0
到达的 最多 节点数目*。*
注意,节点 0
不 会标记为受限节点。
说实话这是这周起码第二个dfs的树的题目,写一下就能知道。递归搜索能到的地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Solution {public : int dfs (int fromNode, int curNode,map<int , vector<int >>& data, unordered_map<int , bool >& forbiddenNode) { if (forbiddenNode.count (curNode) == 1 ) return 0 ; int res = 1 ; if (data.count (curNode) == 0 ) return res; vector<int > nextAvai = data[curNode]; for (int j = 0 ; j < nextAvai.size (); j++){ if (nextAvai[j] == fromNode) continue ; res += dfs (curNode, nextAvai[j], data, forbiddenNode); } return res; } int reachableNodes (int n, vector<vector<int >>& edges, vector<int >& restricted) { map<int , vector<int >> data; unordered_map<int , bool > forbiddenNode; for (int i = 0 ; i < edges.size (); i++){ int start = edges[i][0 ]; int end = edges[i][1 ]; data[start].push_back (end); data[end].push_back (start); } for (int j = 0 ; j < restricted.size (); j++){ forbiddenNode[restricted[j]] = true ; } return dfs (-1 , 0 , data, forbiddenNode); } };
周日
收尾题目,简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class MyStack {private : list<int > data;public : MyStack () { data.clear (); } void push (int x) { data.push_front (x); } int pop () { int res = data.front (); data.pop_front (); return res; } int top () { return data.front (); } bool empty () { return data.empty (); } };