From 07ab2fd1d5e2dfe2a457cc36b79d47e61811ab2b Mon Sep 17 00:00:00 2001 From: gameloader Date: Tue, 7 Jan 2025 11:53:55 +0800 Subject: [PATCH] leetcode update --- content/posts/leetcode.md | 1064 ++++++++++++++++++++++++++++++++----- 1 file changed, 938 insertions(+), 126 deletions(-) diff --git a/content/posts/leetcode.md b/content/posts/leetcode.md index 67c5ebd..dcb6b08 100644 --- a/content/posts/leetcode.md +++ b/content/posts/leetcode.md @@ -19455,8 +19455,11 @@ public: } }; ``` + ## day281 2024-12-14 -### 2762. Continuous Subarrays + +### 2762. Continuous Subarrays + You are given a 0-indexed integer array nums. A subarray of nums is called continuous if: Let i, i + 1, ..., j be the indices in the subarray. Then, for each pair of indices i <= i1, i2 <= j, 0 <= |nums[i1] - nums[i2]| <= 2. @@ -19467,15 +19470,16 @@ A subarray is a contiguous non-empty sequence of elements within an array. ![1214xvWH0FkwSGld](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1214xvWH0FkwSGld.png) ### 题解 + 本题思考题目条件,要求子数组中任意两个数字之间差的绝对值不超过2。则可使用双指针来指示子数组当前的范围,同时使用两个变量分别记录当前子数组内的最大值和最小值。先将右指针向后移动,同时更新子数组内的最大值和最小值,判断二者的差是否小于等于2。等于则说明该子数组满足条件,要给结果加上子数组长度(相当于新加入的数字自身,加上数字和前面的各个子数组的组合)。 当新遍历的数使得数组内的最大值和最小值差大于2时,考虑两种情况,如上述情况数字范围为3~5,则若新数字为6,则之前的数组中若尾部仅包含4,5两个数字,4,5,6仍然符合题目要求的差的绝对值为2。故找到之前子数组的尾部仅包含4,5两个数字的部分保留。对于7同理。但若数字大于7,则之前的数组中不可能存在可以和7组合满足条件的数字,因此直接从7开始向后遍历即可。由此得出对于新数字使得子数组极差大于2时,若新数字和数组内的最大值或最小值差的绝对值不超过2,此时移动左指针直到子数组中的数字全部在新的给定范围内(具体实现上,只需超出范围的数字的个数为0即可)。否则直接从新数字开始向后遍历数组(将左右指针都设为该数字的位置)。 将每次新找到的满足条件的子数组长度加和即得最终结果。 - ### 代码 -```cpp + +```cpp class Solution { public: long long continuousSubarrays(vector& nums) { @@ -19486,29 +19490,29 @@ public: unordered_map count; int current_min = nums[0]; int current_max = nums[0]; - + while (right < n) { - if (right > left && - (abs(nums[right] - current_max) > 2 && + if (right > left && + (abs(nums[right] - current_max) > 2 && abs(nums[right] - current_min) > 2)) { count.clear(); left = right; current_max = nums[right]; current_min = nums[right]; } - + count[nums[right]]++; - + current_max = max(current_max, nums[right]); current_min = min(current_min, nums[right]); - + while (current_max - current_min > 2) { count[nums[left]]--; if (count[nums[left]] == 0) { count.erase(nums[left]); } left++; - + current_max = nums[left]; current_min = nums[left]; for (auto& pair : count) { @@ -19518,12 +19522,12 @@ public: } } } - + // 计算当前窗口内的所有有效子数组数量 result += (right - left + 1); right++; } - + return result; } }; @@ -19531,24 +19535,28 @@ public: ``` ## day282 2024-12-15 -### 1792. Maximum Average Pass Ratio + +### 1792. Maximum Average Pass Ratio + There is a school that has classes of students and each class will be having a final exam. You are given a 2D integer array classes, where classes[i] = [passi, totali]. You know beforehand that in the ith class, there are totali total students, but only passi number of students will pass the exam. You are also given an integer extraStudents. There are another extraStudents brilliant students that are guaranteed to pass the exam of any class they are assigned to. You want to assign each of the extraStudents students to a class in a way that maximizes the average pass ratio across all the classes. The pass ratio of a class is equal to the number of students of the class that will pass the exam divided by the total number of students of the class. The average pass ratio is the sum of pass ratios of all the classes divided by the number of the classes. -Return the maximum possible average pass ratio after assigning the extraStudents students. Answers within 10-5 of the actual answer will be accepted. +Return the maximum possible average pass ratio after assigning the extraStudents students. Answers within 10-5 of the actual answer will be accepted. ![1215H3eQAQ3kny1p](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1215H3eQAQ3kny1p.png) ### 题解 + 本题首先要了解一个分数自身的性质,对一个分数给分子分母同时加1,会使得分数的值向1靠近,意味着若分数小于1,则同时加1会使分数变大,分数大于1,同时加1会使分数变小。 本题学生的通过率都是小于等于1的,因此给通过率小于1的class分配一个通过的学生会使分子分母同时加1使得分数变大。本题要提高总体的平均通过率,需要使分配学生后通过率的增加幅度尽可能大,因此本题可使用最大堆,每次弹出在分子分母同时加1后通过率增加幅度最大的组合,给它分子分母同时加1并计算新的通过率增加幅度重新放入堆中。注意此处为了计算的精确性,需要保留通过率的浮点数和原始的分子分母的整数用于后续计算。 ### 代码 -```cpp + +```cpp class Solution { public: @@ -19556,33 +19564,33 @@ public: int pass; int total; double delta; - + Class(int p, int t) { pass = p; total = t; delta = (double)(p + 1) / (t + 1) - (double)p / t; } }; - + double maxAverageRatio(vector>& classes, int extraStudents) { // 按照delta降序排列的优先队列 auto comp = [](const Class& a, const Class& b) { return a.delta < b.delta; }; priority_queue, decltype(comp)> pq(comp); - + for (const auto& c : classes) { pq.push(Class(c[0], c[1])); } - + while (extraStudents--) { Class curr = pq.top(); pq.pop(); - + curr.pass++; curr.total++; curr.delta = (double)(curr.pass + 1) / (curr.total + 1) - (double)curr.pass / curr.total; - + pq.push(curr); } @@ -19593,28 +19601,33 @@ public: sum += (double)c.pass / c.total; pq.pop(); } - + return sum / n; } }; ``` + ## day283 2024-12-16 -### 3264. Final Array State After K Multiplication Operations I + +### 3264. Final Array State After K Multiplication Operations I + You are given an integer array nums, an integer k, and an integer multiplier. You need to perform k operations on nums. In each operation: Find the minimum value x in nums. If there are multiple occurrences of the minimum value, select the one that appears first. -Replace the selected minimum value x with x * multiplier. -Return an integer array denoting the final state of nums after performing all k operations. +Replace the selected minimum value x with x \* multiplier. +Return an integer array denoting the final state of nums after performing all k operations. -![1216vNu3jp82Fvij](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1216vNu3jp82Fvij.png) +![1216vNu3jp82Fvij](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1216vNu3jp82Fvij.png) ### 题解 + 本题使用最小堆可解。构造一个pair将数字和对应的下标存储起来,再将这些pair插入最小堆,此处注意定义最小堆的比较规则,当比较数值后,若数值相同还要比较下标。每次从最小堆中弹出顶部数字,按照下标将nums对应数字和multiplier相乘即可。 ### 代码 -```cpp + +```cpp class Solution { public: vector getFinalState(vector& nums, int k, int multiplier) { @@ -19640,24 +19653,28 @@ public: } }; ``` + ## day284 2024-12-17 -### 2182. Construct String With Repeat Limit + +### 2182. Construct String With Repeat Limit + You are given a string s and an integer repeatLimit. Construct a new string repeatLimitedString using the characters of s such that no letter appears more than repeatLimit times in a row. You do not have to use all characters from s. Return the lexicographically largest repeatLimitedString possible. -A string a is lexicographically larger than a string b if in the first position where a and b differ, string a has a letter that appears later in the alphabet than the corresponding letter in b. If the first min(a.length, b.length) characters do not differ, then the longer string is the lexicographically larger one. +A string a is lexicographically larger than a string b if in the first position where a and b differ, string a has a letter that appears later in the alphabet than the corresponding letter in b. If the first min(a.length, b.length) characters do not differ, then the longer string is the lexicographically larger one. ![12173l9LfAwG3dJo](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/12173l9LfAwG3dJo.png) ### 题解 + 本题先统计字符串s中各个字母的字数,再根据题意构造最大字符串,构造最大字符串需要将所有字符从最大字符开始排列,但排列时相同字符的连续重复次数不能超过repeatLimit。则每次当最大字符重复了repeatLimit后,需要插入一个次大字符,再继续插入最大字符,因此可以使用双指针,分别指示当前的最大字符和次大字符,再按照题目要求通过不断交替插入尽可能多的最大字符和用于分隔的次大字符来构造整个字符串。 这样得到的就是最大的字符串,对于剩余的多余字符,如剩下了很多a没有使用并不影响这个字符串的最大性,因为题目只要求使用s中的字符得到最大字符串,不要求将字符全部用完。 - ### 代码 -```cpp + +```cpp class Solution { public: string repeatLimitedString(string s, int repeatLimit) { @@ -19687,7 +19704,7 @@ public: count[maxindex]--; limit--; } - + if(count[maxindex] > 0){ if(secondindex >= 0){ if(count[secondindex] > 0){ @@ -19702,13 +19719,13 @@ public: break; } } - if(secondindex < 0) break; + if(secondindex < 0) break; result.push_back('a' + secondindex); count[secondindex]--; limit = repeatLimit; } } else { - break; + break; } } else { maxindex = secondindex; @@ -19726,10 +19743,12 @@ public: } }; ``` + ### 总结 + 这次代码写的有点丑陋了,但思路上是没问题的,可以参考下其他人写的思路基本一样的代码 -```cpp +```cpp class Solution { public: string repeatLimitedString(string s, int repeatLimit) { @@ -19768,8 +19787,11 @@ public: } }; ``` + ## day285 2024-12-18 -### 1475. Final Prices With a Special Discount in a Shop + +### 1475. Final Prices With a Special Discount in a Shop + You are given an integer array prices where prices[i] is the price of the ith item in a shop. There is a special discount for items in the shop. If you buy the ith item, then you will receive a discount equivalent to prices[j] where j is the minimum index such that j > i and prices[j] <= prices[i]. Otherwise, you will not receive any discount at all. @@ -19779,12 +19801,14 @@ Return an integer array answer where answer[i] is the final price you will pay f ![1218vjGm9A6dZR7z](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1218vjGm9A6dZR7z.png) ### 题解 + 本题要考虑j>i的下标j的情况,则可以从右向左遍历数组,这样当遍历到下标i时下标j及j右侧已经处理过了,必然可以拿到一些信息。考虑下标j,对每个下标j如果其右侧已经遍历过,则此时必然已知一个下标k使得k是大于j的价格小于等于j的最小下标,则对于i,如果j不满足条件,那么价格大于j的当然同样不满足条件,我们要找的是价格小于j的在j右侧的下标,则此时下标k正满足条件。如果k的价格仍不满足条件,则同样在遍历k时已知一个p满足题目条件,再继续找到p,如此链式查找直到找到一个数字的结果的下标就是其自身说明没有满足条件的数字,直接使i的价格为其自身。若找到满足条件的则设为满足条件的价格。 设定两个数组,一个用来保存找到的满足条件的打折扣的数字下标,另一个保存当前下标是否能打折扣。最终遍历数组,对于能打折扣的数字,减去其折扣值。 ### 代码 -```cpp + +```cpp class Solution { public: vector finalPrices(vector& prices) { @@ -19823,21 +19847,25 @@ public: ``` ## day286 2024-12-19 -### 769. Max Chunks To Make Sorted + +### 769. Max Chunks To Make Sorted + You are given an integer array arr of length n that represents a permutation of the integers in the range [0, n - 1]. We split arr into some number of chunks (i.e., partitions), and individually sort each chunk. After concatenating them, the result should equal the sorted array. -Return the largest number of chunks we can make to sort the array. +Return the largest number of chunks we can make to sort the array. ![1219QG0PhBWCU2Zr](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1219QG0PhBWCU2Zr.png) ### 题解 + 本题若要在分块后将块内排序,使得每个块内有序后整体自然有序,则假设块i之前的所有块包含的数字个数为k,则块i必须包含从k+1到出现的块内最大值的所有数字(如果第一个出现的数字是k+3,就要包含k+1~k+3,如果第一个出现的就是k+1,则只包含一个k+1即可)。这样才能保证块内排序后不会出现后面有数字小于块内最大值从而使整体不满足有序这样的问题。则用一个数字统计当前最大值m之前已经出现的数字个数,当数字个数达到m-1时,从上一块结尾开始到当前数字就可以被分为单独的一块。 此处我们并不需要知道每个块都有哪些数字,只要已经出现了当前最大值之前的全部数字,我们就知道前面的数字一定可以排列成有序的,否则后面还可能出现更小的数字使得整体不满足有序,因此可以排列成有序的时候就可以分为单独的一块。如果数组本身就是有序的,那么按照我们的算法,对每个数字,遍历到该数字时小于该数字的全部数字都已经出现了,则一定可以排列成有序的,因此这个数字本身就可以单独分为一块。 ### 代码 -```cpp + +```cpp class Solution { public: int maxChunksToSorted(vector& arr) { @@ -19855,8 +19883,11 @@ public: } }; ``` + ## day287 2024-12-20 -### 2415. Reverse Odd Levels of Binary Tree + +### 2415. Reverse Odd Levels of Binary Tree + Given the root of a perfect binary tree, reverse the node values at each odd level of the tree. For example, suppose the node values at level 3 are [2,1,3,4,7,11,29,18], then it should become [18,29,11,7,4,3,1,2]. @@ -19864,42 +19895,44 @@ Return the root of the reversed tree. A binary tree is perfect if all parent nodes have two children and all leaves are on the same level. -The level of a node is the number of edges along the path between it and the root node. +The level of a node is the number of edges along the path between it and the root node. ![1220F7U3F1wDq3QW](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1220F7U3F1wDq3QW.png) ### 题解 + 本题可使用层序遍历,用一个变量记录当前访问的层级,对于奇数层将该层的所有节点指针保存在数组中,再通过对首尾指针指向的节点值进行交换并不断移动指针直到到达数组的中间位置正好将所有的值交换完毕,完成了该层节点的值的逆序。 ### 代码 -```cpp + +```cpp class Solution { public: TreeNode* reverseOddLevels(TreeNode* root) { if (!root) return root; - + queue q; q.push(root); int level = 0; - + while (!q.empty()) { int size = q.size(); vector nodes; - + for (int i = 0; i < size; i++) { TreeNode* curr = q.front(); q.pop(); - + if (curr->left) { q.push(curr->left); q.push(curr->right); } - + if (level % 2 == 1) { nodes.push_back(curr); } } - + // 对奇数层进行首尾交换 if (level % 2 == 1) { int left = 0, right = nodes.size() - 1; @@ -19909,27 +19942,31 @@ public: right--; } } - + level++; } - + return root; } }; ``` + ## day288 2024-12-21 -### 2872. Maximum Number of K-Divisible Components + +### 2872. Maximum Number of K-Divisible Components + There is an undirected tree with n nodes labeled from 0 to n - 1. You are given the integer n and a 2D integer array edges of length n - 1, where edges[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the tree. You are also given a 0-indexed integer array values of length n, where values[i] is the value associated with the ith node, and an integer k. A valid split of the tree is obtained by removing any set of edges, possibly empty, from the tree such that the resulting components all have values that are divisible by k, where the value of a connected component is the sum of the values of its nodes. -Return the maximum number of components in any valid split. +Return the maximum number of components in any valid split. ![1221gSmFhuhmVag9](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1221gSmFhuhmVag9.png) ### 题解 + 题目中说明了给定的数据可以构成一棵无向树,无向树可以选择任意一个节点作为根节点展开,任意选择根节点的情况下无向树未必是一棵二叉树,但一定不存在环。 考虑在任意确定了根节点后,如何寻找满足条件的连通分量。可以从根开始向下,在找到一个连通分量后就删掉这个连通分量,但这样做会存在无向树的剩余部分可能无法构成满足条件的连通分量的问题,如仅有三个节点,节点值分别为6,2,4且值为6的节点与另外两个节点相连,如果k为6且选定了节点值为6的节点作为根节点,那么如果从根开始寻找满足条件的连通分量,6自身就满足条件,此时应该将根节点单独作为一个连通分量,将其与其他两个节点相连的边删去,但这样得到单独的4和单独的2都不满足条件。因此若从选定的根开始向下探索,则由于删掉一个父节点会使得两个子节点被迫分离开,影响了这两个子节点与其他节点构成连通分量。因此这种方法会导致出现不满足条件的连通分量。 @@ -19939,19 +19976,20 @@ Return the maximum number of components in any valid split. 理解这一点后,实现时任意选定一个根节点,使用dfs一直遍历到树的最底部,对于dfs自身,递归调用dfs遍历当前节点的所有子树并得到返回值,将返回值和当前节点的值相加,若能被k整除,则将连通分量总数加一并返回0,若不能则返回相加得到的和。递归的退出状态为,当节点没有子节点时,若能被k整除,则同样将总数加一返回0,不能直接返回节点的值。这样通过贪心,每次得到一个有效的连通分量就将其分离开,最终得到了个数最多的连通分量。 ### 代码 -```cpp + +```cpp class Solution { public: - vector> adj; - vector visited; - vector nodeValues; - int divisor; - int result; + vector> adj; + vector visited; + vector nodeValues; + int divisor; + int result; + - long long dfs(int node) { visited[node] = true; - long long currentSum = nodeValues[node]; + long long currentSum = nodeValues[node]; for (int neighbor : adj[node]) { if (!visited[neighbor]) { @@ -19961,10 +19999,10 @@ public: if (currentSum % divisor == 0) { result++; - return 0; + return 0; } - return currentSum; + return currentSum; } int maxKDivisibleComponents(int n, vector>& edges, vector& values, int k) { @@ -19979,15 +20017,18 @@ public: adj[edge[1]].push_back(edge[0]); } - // 从节点0开始遍历 + // 从节点0开始遍历 dfs(0); return result; } }; ``` + ## day289 2024-12-22 -### 2940. Find Building Where Alice and Bob Can Meet + +### 2940. Find Building Where Alice and Bob Can Meet + You are given a 0-indexed array heights of positive integers, where heights[i] represents the height of the ith building. If a person is in building i, they can move to any other building j if and only if i < j and heights[i] < heights[j]. @@ -19999,6 +20040,7 @@ Return an array ans where ans[i] is the index of the leftmost building where Ali ![1222i3wwqUMS3GRh](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1222i3wwqUMS3GRh.png) ### 题解 + 本题考虑Alice和Bob能相遇的情况,Alice和Bob都可以移动到自己初始所处的建筑i的右侧比所处建筑高的建筑,那么无论Alice和Bob谁在右侧,此处假设Alice在左,Bob在右,假如Bob所处的建筑高度已经比Alice高,那么二者直接移动到Bob所处的建筑即满足条件。若Bob所处的建筑不如Alice的高,那么二者需要移动到Bob右侧第一座比Alice所处建筑高度高的位置(这个建筑当然也满足比Bob高,因此Bob也可以移动到此处)。由此可以发现,最关键的就是每个位置的右侧所有比该位置高的建筑的下标和高度。 可以将每个位置右侧所有比该位置高度高的下标和高度全部保存下来,但这样会将同一个建筑重复保存很多遍。是否有更高效的方法能获取到某个建筑右侧所有比该建筑高的全部建筑呢,我们发现其实只需要保存每个位置处右侧比该位置高的第一个建筑的下标就可以了,随后通过链式遍历,就可以得到全部的位置右侧单调增的建筑下标。 @@ -20010,14 +20052,15 @@ Return an array ans where ans[i] is the index of the leftmost building where Ali 思路是正确的,但对于少数几个例子会超时,因此需要再继续优化一下,可以发现在构造单调栈的过程中完全可以一边构造一边找出结果,只需要先将每个位置对应的所有query(即query中靠右的下标为该query对应的查询位置)都保存下来,这样就可以在逆序构造单调栈的过程中,对该位置对应的所有query,在单调栈中查找满足该query的结果,查找结果可以使用二分法来加速查找,这样一边查找一边将该位置的建筑高度用于继续构造单调栈。这样在构造单调栈的同时实现了对结果的查找。 ### 代码 -```cpp + +```cpp class Solution { public: vector leftmostBuildingQueries(vector& heights, vector>& queries) { int n = heights.size(); int m = queries.size(); vector ans(m, -1); - + // 将查询按右端点分组 vector>> queryGroups(n); // {左端点, 查询索引} for (int i = 0; i < m; i++) { @@ -20030,9 +20073,9 @@ public: } queryGroups[b].push_back({a, i}); } - + vector> stack; // {高度, 下标} - + // 从右向左构建单调递减栈并处理查询 for (int i = n - 1; i >= 0; i--) { // 处理当前位置的所有查询 @@ -20040,7 +20083,7 @@ public: int left = query.first; int queryIndex = query.second; int targetHeight = max(heights[left], heights[i]); - + int l = 0, r = stack.size() - 1; int pos = -1; while (l <= r) { @@ -20052,117 +20095,126 @@ public: r = mid - 1; } } - + if (pos != -1) { ans[queryIndex] = stack[pos].second; } } - + // 维护单调递减栈 while (!stack.empty() && stack.back().first <= heights[i]) { stack.pop_back(); } stack.push_back({heights[i], i}); } - + return ans; } }; ``` + ## day290 2024-12-23 -### 2471. Minimum Number of Operations to Sort a Binary Tree by Level + +### 2471. Minimum Number of Operations to Sort a Binary Tree by Level + You are given the root of a binary tree with unique values. In one operation, you can choose any two nodes at the same level and swap their values. Return the minimum number of operations needed to make the values at each level sorted in a strictly increasing order. -The level of a node is the number of edges along the path between it and the root node. +The level of a node is the number of edges along the path between it and the root node. ![1223hMOXrD0HsyuY](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1223hMOXrD0HsyuY.png) ### 题解 + 本题可以通过bfs来对二叉树按层进行处理,对二叉树每一层,将该层的全部数字保存下来,因为二叉树中所有节点的值均不重复,因此可以构造一个大数组,数组下标为节点的值,数组的值为该数字在当前层数字中的下标位置(设整个大数组为tree,则例如在第一层中数字6是从左到右第二个,那么tree\[6\]=1,在前面的题目中多次使用过这种思路)。再将该层的原始数组排序,遍历有序数组,根据每个位置应该放置的数字将当前位置的数字和目标数字交换。并将在大数组中保存的数字对应的下标做相应交换调整。如果需要交换数字则给结果加1,不需要则继续向后遍历。 ### 代码 -```cpp + +```cpp class Solution { public: vector pos; Solution() : pos(100001) { } // 在构造函数中初始化 int minimumOperations(TreeNode* root) { if (!root) return 0; - + int result = 0; queue q; q.push(root); - + while (!q.empty()) { int size = q.size(); - vector level; - + vector level; + // 获取当前层的所有节点值 for (int i = 0; i < size; i++) { TreeNode* node = q.front(); q.pop(); level.push_back(node->val); - + if (node->left) q.push(node->left); if (node->right) q.push(node->right); } - + result += countSwaps(level); } - + return result; } - + private: int countSwaps(vector& arr) { int n = arr.size(); - + for (int i = 0; i < n; i++) { pos[arr[i]] = i; } - + vector sorted = arr; sort(sorted.begin(), sorted.end()); - + int swaps = 0; for (int i = 0; i < n; i++) { if (arr[i] != sorted[i]) { swaps++; - + int oldVal = arr[i]; int newVal = sorted[i]; - + arr[i] = newVal; arr[pos[newVal]] = oldVal; - + int temp = pos[oldVal]; pos[oldVal] = pos[newVal]; pos[newVal] = temp; } } - + return swaps; } }; ``` + ## day291 2024-12-24 -### 3203. Find Minimum Diameter After Merging Two Trees + +### 3203. Find Minimum Diameter After Merging Two Trees + There exist two undirected trees with n and m nodes, numbered from 0 to n - 1 and from 0 to m - 1, respectively. You are given two 2D integer arrays edges1 and edges2 of lengths n - 1 and m - 1, respectively, where edges1[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the first tree and edges2[i] = [ui, vi] indicates that there is an edge between nodes ui and vi in the second tree. You must connect one node from the first tree with another node from the second tree with an edge. Return the minimum possible diameter of the resulting tree. -The diameter of a tree is the length of the longest path between any two nodes in the tree. +The diameter of a tree is the length of the longest path between any two nodes in the tree. ![1224k4jcxOAzr8A0](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1224k4jcxOAzr8A0.png) ### 题解 + 本题的总体思路其实比较容易想到,要找到的是两棵树上的两个节点,使得将这两个节点连接后得到的新树的直径最短。原始的两棵树是固定的,若要在连接后直径最短,则用于连接的节点在原来的树上到其他任何一个节点距离的最大值应该尽可能小,问题在于这样的节点应该如何确定。 树的直径是树中任意两节点之间最长的简单路径,直径是整棵树中最长的路径,那么要想使得最大值尽可能小,就应该找直径的中点,对于直径是偶数,中点只有一个,直径是奇数则任选两个中点中的一个即可。其他节点都会存在到直径的两个端点的更长距离,因为其他节点到端点都要先经过中点,再到端点。 @@ -20172,7 +20224,8 @@ The diameter of a tree is the length of the longest path between any two nodes i 真的得到最终结果了吗,并没有,还要考虑这样的情况,即一棵树的直径非常短,则两棵树的直径的1/2相加后的结果小于其中一棵树自身的直径,此时由于是通过节点将两棵树相连构成一棵树,因此每棵树自身当然是包含在树中的,因此要取树自身的直径和相连后通过两棵树的中间节点计算出来的直径的最大值才能得到最终的正确答案。 ### 代码 -```cpp + +```cpp class Solution { private: void buildGraph(vector>& edges, vector>& graph) { @@ -20181,74 +20234,77 @@ private: graph[edge[1]].push_back(edge[0]); } } - + void dfs1(int node, int parent, int dist, int& maxDist, int& farthestNode, vector>& graph) { if (dist > maxDist) { maxDist = dist; farthestNode = node; } - + for (int next : graph[node]) { if (next != parent) { dfs1(next, node, dist + 1, maxDist, farthestNode, graph); } } } - + void dfs2(int node, int parent, int dist, int& maxDist, vector>& graph) { maxDist = max(maxDist, dist); - + for (int next : graph[node]) { if (next != parent) { dfs2(next, node, dist + 1, maxDist, graph); } } } - + int getDiameter(vector>& edges, int n) { vector> graph(n); buildGraph(edges, graph); - + // 第一次DFS找最远点 int maxDist = 0, farthestNode = 0; dfs1(0, -1, 0, maxDist, farthestNode, graph); - + // 第二次DFS找直径 maxDist = 0; dfs2(farthestNode, -1, 0, maxDist, graph); - + return maxDist; } - + public: int minimumDiameterAfterMerge(vector>& edges1, vector>& edges2) { int n = edges1.size() + 1; int m = edges2.size() + 1; - + // 分别计算两棵树的直径 int diameter1 = getDiameter(edges1, n); int diameter2 = getDiameter(edges2, m); int maxdia = max(diameter1,diameter2); - + return max(maxdia,(diameter1 + 1) / 2 + (diameter2 + 1) / 2 + 1); } }; ``` ## day292 2024-12-25 -### 515. Find Largest Value in Each Tree Row + +### 515. Find Largest Value in Each Tree Row Given the root of a binary tree, return an array of the largest value in each row of the tree (0-indexed). ![1225bPdlxmAvYcFk](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1225bPdlxmAvYcFk.png) ### 题解 + 本题是比较常规的遍历二叉树的题目,使用BFS进行层序遍历,并且在每一层遍历时保存当前层的最大值直到遍历完该层,将最大值放入结果数组中即可。 ### 代码 -```cpp + +```cpp /** * Definition for a binary tree node. @@ -20265,23 +20321,23 @@ class Solution { public: vector largestValues(TreeNode* root) { if (!root) return {}; - + vector result; queue q; q.push(root); - + while (!q.empty()) { int levelSize = q.size(); int maxVal = INT_MIN; // 初始化当前层的最大值 - + // 遍历当前层的所有节点 for (int i = 0; i < levelSize; i++) { TreeNode* node = q.front(); q.pop(); - + // 更新当前层的最大值 maxVal = max(maxVal, node->val); - + // 将下一层的节点加入队列 if (node->left) { q.push(node->left); @@ -20290,27 +20346,31 @@ public: q.push(node->right); } } - + // 将当前层的最大值加入结果数组 result.push_back(maxVal); } - + return result; } }; ``` + ## day293 2024-12-26 -### 494. Target Sum + +### 494. Target Sum + You are given an integer array nums and an integer target. You want to build an expression out of nums by adding one of the symbols '+' and '-' before each integer in nums and then concatenate all the integers. For example, if nums = [2, 1], you can add a '+' before 2 and a '-' before 1 and concatenate them to build the expression "+2-1". -Return the number of different expressions that you can build, which evaluates to target. +Return the number of different expressions that you can build, which evaluates to target. ![1226B0EYGDIq0D8o](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1226B0EYGDIq0D8o.png) ### 题解 + 本题是一道很经典的记忆化问题,考虑计算数字和的过程,每个数字都有加或者减两种选择,那么n个数字就有2^n种可能,但可以注意到在数字加和过程中的两个特点。第一个特点是通过不同的加减和组合方式有可能能得出相同的结果。最简单的例子,如三个1,可以通过+1+1-1的方式得到和1,也可以通过+1-1+1的方式得到和1。第二个特点是从某个下标开始到数组最后能得到的全部可能结果仅与到这个下标为止已经得到的和有关,而与得到这个和的具体路径无关。例如前面已经得到了和为6,那么后面就在得到的和6的基础上进行加减操作,具体6是如何来的并不重要。 那么我们就可以保存到某个下标m前面所有数字的全部可能的和,每个和对应的通过加减后面的数字直到数组末尾最终得到target的可能路径的个数。通过递归得出第一个数字到数组末尾得到target的可能的路径个数。 @@ -20318,29 +20378,30 @@ Return the number of different expressions that you can build, which evaluates t 递归过程中,对当前遍历到的数字,有两条路径,可以将当前数字加上之前数字的和再向后递归,也可以将之前的和减去当前数字作为新的和向后递归,如果当前数字对应的前面所有数字的和在之前的递归路径中已经被计算过,则可直接返回该和对应的最终能得到target的路径个数。 ### 代码 -```cpp + +```cpp class Solution { private: vector> dp; - int offset = 1000; - + int offset = 1000; + int dfs(vector& nums, int target, int index, int sum) { if (index == nums.size()) { return sum == target ? 1 : 0; } - + // 如果当前状态已计算过,直接返回 if (dp[index][sum + offset] != -1) { return dp[index][sum + offset]; } - + // 递归计算两种选择:加号和减号 dp[index][sum + offset] = dfs(nums, target, index + 1, sum + nums[index]) + dfs(nums, target, index + 1, sum - nums[index]); - + return dp[index][sum + offset]; } - + public: int findTargetSumWays(vector& nums, int target) { dp.assign(nums.size(), vector(2001, -1)); @@ -20348,23 +20409,28 @@ public: } }; ``` + ## day294 2024-12-27 -### 1014. Best Sightseeing Pair + +### 1014. Best Sightseeing Pair + You are given an integer array values where values[i] represents the value of the ith sightseeing spot. Two sightseeing spots i and j have a distance j - i between them. The score of a pair (i < j) of sightseeing spots is values[i] + values[j] + i - j: the sum of the values of the sightseeing spots, minus the distance between them. -Return the maximum score of a pair of sightseeing spots. +Return the maximum score of a pair of sightseeing spots. ![1227W5w7glO9LWAG](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1227W5w7glO9LWAG.png) ### 题解 + 本题涉及两个变化因素,一个是数字本身的值,另一个就是数字之间的距离,在遍历数组的过程中,可以发现对于某个固定位置的数字,距离会自然发生递增或者递减的变化。但值是不确定的,对于有两个变化因素的问题,我们一般固定一个因素,再按照某种规律去改变另一个因素,这样就将难以处理的变化问题变成了只需考虑一个有规律的因素的问题。很多多因素问题就是通过对其中某些因素进行限制,或者固定,最终只剩余一个因素变化,从而得到O(n)的时间复杂度,或者说要想得到O(n)的复杂度,就要想办法构建只有一个变化因素的情况,这样只需处理一个因素,就可以通过一次遍历来解决。本题中,在向后遍历数组的过程中,前面的数字距离当前数字的距离是自然的递增的,每向后遍历一个数字距离加一,假设当前的下标为m,假设之前的某个数字的下标为i,则我们可以考虑这样一个量,即数字i对于m的真实价值,设为values[i]+i-m。即i自身的值减去i和m之间的距离。此处相当于固定了两个加和的数字中后面的数字为m,只需考虑前面数字中如何取得最大值即可。我们在遍历的时候保存到当前位置的数字的真实价值的最大值,用当前数字的值与这个最大值相加即得当前数字和前面的数字能够取得的数对的最大值。 每当遍历到一个新数字时,我们将之前的最大真实值减一和当前数字的值做比较并更新最大真实值,这样计算的是对于后面的数字来说,前面的数字中的最大真实值,如此只需遍历一遍数组即可得数组中数对的最大值。 ### 代码 -```cpp + +```cpp class Solution { public: int maxScoreSightseeingPair(vector& values) { @@ -20379,3 +20445,749 @@ public: } }; ``` + +## day295 2024-12-28 + +### 689. Maximum Sum of 3 Non-Overlapping Subarrays + +Given an integer array nums and an integer k, find three non-overlapping subarrays of length k with maximum sum and return them. + +Return the result as a list of indices representing the starting position of each interval (0-indexed). If there are multiple answers, return the lexicographically smallest one. + +![1228xpkcYJKuFfzn](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1228xpkcYJKuFfzn.png) + +### 题解 + +本题是一道难题。首先考虑长度为k的子数组的和,整个数组中全部长度为k的子数组的和是固定的,为了避免后续其他处理过程中重复计算这些子数组的和,可以先将全部长为k的子数组和计算出来并保存,后续直接对这些和而不是对原数组进行处理。计算子数组和可以使用滑动窗口。 + +得到全部的子数组和后,后续就是从子数组和中任选三个加和,求加和的最大值。但为了避免数组之间重叠,挑选的数字之间需要有一定的距离,即数字之间距离为k。则题目变成了,从一个数组中任选三个数字,数字和数字之间的距离大于等于k,求能得到的和的最大值。 + +那么可以先将这个问题再退化成一个更简单的问题,即从数字中任选两个数字之间距离大于等于k的数字,求可以得到的和的最大值是多少。这个题目可以使用动态规划求解,可以先遍历数组并将到下标i的前缀数组中的最大值保存在dp\[i\]处,再从下标k开始遍历数组,若当前下标为m,则当前下标对应的最大值为num\[m]+dp\[m-k\]。即只考虑m和前面的数字相加的情况,将m和前面能与m相加的数字中的最大值相加就得到以m作为后面的数字的两数和的最大值。同时我们发现获取前缀数组最大值和计算两数和的最大值可以同步进行,使用两个指针,一个指向前面遍历到的前缀数组的末尾(计算最大值只需将到该指针为止的最大值保存即可,前面的最大值可以丢弃),另一个指向后面需要加和的数字,只需遍历一遍即可解决。 + +解决了两个数字的问题,再来解决三个数字的问题,可以发现这只是对两个数字的问题的推广,思路仍可以采用两个数字时解题的思路,对前两个数字采用原来的两个数字时解题的思路,再对后两个数字采用原来的两个数字时的解题思路,这时我们可以发现可以先将最左边的数字的前缀数组最大值求出来,再将右边数组的后缀数组最大值求出来,再遍历中间的数字,当数字下标m时,将数字与m-k的前缀最大值和m+k的后缀最大值相加即得三个数字的最大值。 + +同时注意本题要求返回三个子数组的起始下标,因此除了保存最大值也要保存取得最大值的对应的子数组的起始下标(就是已经求好的子数组和数组的下标)。 + +### 代码 + +```cpp +class Solution { +public: + vector maxSumOfThreeSubarrays(vector& nums, int k) { + int n = nums.size(); + vector sums(n - k + 1); + int windowSum = 0; + + for (int i = 0; i < k; i++) { + windowSum += nums[i]; + } + sums[0] = windowSum; + + for (int i = k; i < n; i++) { + windowSum = windowSum - nums[i - k] + nums[i]; + sums[i - k + 1] = windowSum; + } + + // 记录左侧最大和的位置 + vector leftMax(n - k + 1); + int maxIndex = 0; + for (int i = 0; i < sums.size(); i++) { + if (sums[i] > sums[maxIndex]) { + maxIndex = i; + } + leftMax[i] = maxIndex; + } + + // 记录右侧最大和的位置 + vector rightMax(n - k + 1); + maxIndex = sums.size() - 1; + for (int i = sums.size() - 1; i >= 0; i--) { + if (sums[i] >= sums[maxIndex]) { + maxIndex = i; + } + rightMax[i] = maxIndex; + } + + vector result = {0, k, 2*k}; + int maxSum = 0; + + // 中间的子数组的起始位置从k到n-2k + for (int i = k; i <= n - 2*k; i++) { + int left = leftMax[i - k]; // 左侧最大和的位置 + int right = rightMax[i + k]; // 右侧最大和的位置 + int totalSum = sums[left] + sums[i] + sums[right]; + + if (totalSum > maxSum) { + maxSum = totalSum; + result = {left, i, right}; + } + } + + return result; + } +}; +``` + +## day296 2024-12-29 + +### 1639. Number of Ways to Form a Target String Given a Dictionary + +You are given a list of strings of the same length words and a string target. + +Your task is to form target using the given words under the following rules: + +target should be formed from left to right. +To form the ith character (0-indexed) of target, you can choose the kth character of the jth string in words if target\[i] = words\[j]\[k]. +Once you use the kth character of the jth string of words, you can no longer use the xth character of any string in words where x <= k. In other words, all characters to the left of or at index k become unusuable for every string. +Repeat the process until you form the string target. +Notice that you can use multiple characters from the same string in words provided the conditions above are met. + +Return the number of ways to form target from words. Since the answer may be too large, return it modulo 109 + 7. + +![1229l4wuZkxjk5rI](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1229l4wuZkxjk5rI.png) + +### 题解 + +本题是一道难题,注意题目给的比较特别的条件,所有的单词的长度都相等,根据题面,在选择了任意一个单词的第k个字符后,所有单词中下标小于等于k的字符都不能再使用了,因此我们可以从下标的角度考虑这个问题。先统计每个下标对应的所有单词中的字符,此处可使用二维数组,构造length\*26的数组用于记录每个下标对应的各个英文字符的个数。 + +考虑本题要求构造目标字符串有多少种解法,这个场景很适合动态规划,如果知道了在下标n处构造target字符串中的前m个字符有p种可能的解法,那么在下标n+1处构造前m+1个字符只需将p和n+1处的第m+1个字符可能的取值情况(不同字符串的同一个下标位置可能是相同的字母如都是a)数量相乘。 + +那么可以再构造一个长度为target单词长度+1的数组用于存储到当前下标时可以构造的target单词的某部分长度的全部可能数量。当从下标n遍历到下标n+1时,遍历target数组,对target数组(下称target_array)下标为i的数字,在下标n+1对应的字符数组中搜索target\[i+1\](此处是target字符串中找到字符)是否存在。若存在则将target_array\[i]和该字符对应的个数相乘再与之前的target_array\[i+1]相加作为新的target_array\[i+1]。当遍历到target长度(设为j)-1时,计算出新的target_array\[j]并和之前的target_array\[j]相加作为新的target_array\[j],最终返回target_array\[j]即得结果。 + +### 代码 + +```cpp +class Solution { +public: + int numWays(vector& words, string target) { + const int MOD = 1e9 + 7; + int m = words[0].length(); + int n = target.length(); + + vector> count(m, vector(26, 0)); + for(const string& word : words){ + for (int i = 0; i < m; i++) { + count[i][word[i] - 'a']++; + } + } + + vector dp(n + 1, 0); + dp[0] = 1; + for (int i = 0; i < m; i++) { + for (int j = n - 1; j >= 0; j--) { + char curr = target[j] - 'a'; + dp[j + 1] = (dp[j + 1] + (dp[j] * count[i][curr]) % MOD) % MOD; + } + } + return dp[n]; + } +}; +``` + +## day297 2024-12-30 + +### 2466. Count Ways To Build Good Strings + +Given the integers zero, one, low, and high, we can construct a string by starting with an empty string, and then at each step perform either of the following: + +Append the character '0' zero times. +Append the character '1' one times. +This can be performed any number of times. + +A good string is a string constructed by the above process having a length between low and high (inclusive). + +Return the number of different good strings that can be constructed satisfying these properties. Since the answer can be large, return it modulo 109 + 7. + +![1230xkxHzBn9CzOa](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1230xkxHzBn9CzOa.png) + +### 题解 + +本题仍是动态规划问题,考虑每次都可以append zero次0或者one次1,则在新append后可以得到的新数字的组合个数取决于append前的组合个数,即假设当前的下标为m,设到下标p的可能的组合方法有dp\[p\]种,则到下标m处可能的组合数量为dp\[m-zero\]+dp\[m-one\]。 + +从头开始遍历数组,对每个下标处的组合个数都使用上面的公式进行计算,对于小于0的下标则直接忽略。注意最终结果要模给定的大整数。 + +### 代码 + +```cpp +class Solution { +public: + int countGoodStrings(int low, int high, int zero, int one) { + vectordp(high+1,0); + dp[0] = 1; + for(int i=1;i<=high;i++){ + if(i-zero >= 0){ + dp[i] += dp[i-zero]; + } + if(i-one >= 0){ + dp[i] += dp[i-one] % (long long int)(1e9+7); + } + } + int result = 0; + for(int i=low;i<=high;i++){ + result = (result + dp[i])%(long long int)(1e9+7); + } + return result; + } +}; +``` + +## day298 2024-12-31 + +### 983. Minimum Cost For Tickets + +You have planned some train traveling one year in advance. The days of the year in which you will travel are given as an integer array days. Each day is an integer from 1 to 365. + +Train tickets are sold in three different ways: + +a 1-day pass is sold for costs[0] dollars, +a 7-day pass is sold for costs[1] dollars, and +a 30-day pass is sold for costs[2] dollars. +The passes allow that many days of consecutive travel. + +For example, if we get a 7-day pass on day 2, then we can travel for 7 days: 2, 3, 4, 5, 6, 7, and 8. +Return the minimum number of dollars you need to travel every day in the given list of days. + +![1231CwQSVal46EHX](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/1231CwQSVal46EHX.png) + +### 题解 + +本题仍是一道动态规划的问题,考虑的思路仍和昨天的问题类似,到第i天时要花费的成本取决于i-1,i-7,i-30这几个日期花费的最小成本再加上三种不同类型的票的价格,取三者中的最小值。本题中要旅行的日期不一定是连续的,则在上一个旅行的日期和下一个旅行的日期之间的日期的花费都和上一个旅行日期的花费保持一致。因为一共只有365天,因此对i-7,i-30这样的日期可以直接从当前位置反向遍历数组来找到其对应的前一个在days数组中的有效日期,并使用该日期来计算成本。 + +动态规划问题的关键在于,我们在处理后面的某个问题时已经处理过了前面的相同结构的子问题并得出了结果。本题中,因为日期是严格升序的,在考虑第i天花费的成本时前面的日期花费的成本已经在之前计算过了,直接使用即可。而这里需要一个初始状态,即所有小于等于0的日期花费的成本均为0。 + +### 代码 + +```cpp +class Solution { +public: + int mincostTickets(vector& days, vector& costs) { + vector dp(days.size(), 0); + dp[0] = min({costs[0], costs[1], costs[2]}); + + for (int i = 1; i < days.size(); i++) { + // 1天票价 + int cost1 = dp[i-1] + costs[0]; + + // 7天票价 + int j = i - 1; + while (j >= 0 && days[i] - days[j] < 7) j--; + int cost7 = (j >= 0 ? dp[j] : 0) + costs[1]; + + // 30天票价 + j = i - 1; + while (j >= 0 && days[i] - days[j] < 30) j--; + int cost30 = (j >= 0 ? dp[j] : 0) + costs[2]; + + // 取三种方案的最小值 + dp[i] = min({cost1, cost7, cost30}); + } + + return dp.back(); + } +}; +``` + +## day299 2025-01-01 + +### 1422. Maximum Score After Splitting a String + +Given a string s of zeros and ones, return the maximum score after splitting the string into two non-empty substrings (i.e. left substring and right substring). + +The score after splitting a string is the number of zeros in the left substring plus the number of ones in the right substring. + +![0101BEFUS2aanexW](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0101BEFUS2aanexW.png) + +### 题解 + +本题是一道简单题,直接统计字符串中1的总数,随后遍历字符串,统计当前位置左侧子字符串中0的个数和右侧子字符串中1的个数并加和,与保存的最大值比较并更新最大值。最终返回最大值即可。注意因为子字符串是非空的,因此第一位处的1和最后一位的0都不能计入最终的总和中(要保留一个字符作为一个单独的字符串,不能把整个字符串当成一个)。 + +### 代码 + +```cpp +class Solution { +public: + int maxScore(string s) { + int leftzero = 0; + int rightone = 0; + for(const auto& ch : s){ + if (ch == '1'){ + rightone++; + } + } + int n = s.size(); + int result = s[0]=='1'?rightone-1:rightone; + for(int i=0;i vowelStrings(vector& words, vector>& queries) { + int n = words.size(); + int m = queries.size(); + + vector isVowelStr(n); + for (int i = 0; i < n; i++) { + if (isVowel(words[i][0]) && isVowel(words[i].back())) { + isVowelStr[i] = 1; + } + } + + vector prefixSum(n + 1, 0); + for (int i = 0; i < n; i++) { + prefixSum[i + 1] = prefixSum[i] + isVowelStr[i]; + } + + vector ans(m); + for (int i = 0; i < m; i++) { + ans[i] = prefixSum[queries[i][1] + 1] - prefixSum[queries[i][0]]; + } + + return ans; + } +}; +``` + +## day301 2025-01-03 + +### 2270. Number of Ways to Split Array + +You are given a 0-indexed integer array nums of length n. + +nums contains a valid split at index i if the following are true: + +The sum of the first i + 1 elements is greater than or equal to the sum of the last n - i - 1 elements. +There is at least one element to the right of i. That is, 0 <= i < n - 1. +Return the number of valid splits in nums. + +![0103NP67JtmhZ1y1](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0103NP67JtmhZ1y1.png) + +### 题解 + +看题就可以想到,先求出数组总和,再分别用变量表示前面数字的和后后面数字的和,将后面数字的和初始化为数组和,前面数字和初始化为0,再遍历数组的同时分别将前面数字和与当前数字相加,后面与当前数字相减,比较二者的大小并记录符合要求的个数即可。(也可以用前缀和,在计算总和的同时计算出各个位置的前缀和,用总和与前缀和相减) + +### 代码 + +```cpp +class Solution { +public: + int waysToSplitArray(vector& nums) { + long long leftSum = 0, rightSum = 0; + + for (int num : nums) { + rightSum += num; + } + int count = 0; + for (int i = 0; i < nums.size() - 1; i++) { + leftSum += nums[i]; + rightSum -= nums[i]; + + if (leftSum >= rightSum) { + count++; + } + } + return count; + } +}; +``` + +## day302 2025-01-04 + +### 1930. Unique Length-3 Palindromic Subsequences + +Given a string s, return the number of unique palindromes of length three that are a subsequence of s. + +Note that even if there are multiple ways to obtain the same subsequence, it is still only counted once. + +A palindrome is a string that reads the same forwards and backwards. + +A subsequence of a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters. + +For example, "ace" is a subsequence of "abcde". + +![0104qi3mONxjHayo](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0104qi3mONxjHayo.png) + +### 题解 + +本题注意是找子序列而不是子数组,不需要连续,只需要相对位置的先后和数组中相同即可。考虑三个字符的回文串,首尾字符必定相同,中间可以是任意字符,因此可使用双指针,一个从头遍历数组,一个从尾部遍历数组,同时用一个布尔数组记录从头遍历数组过程中已经遇到的字母。一个变量记录当前已经遇到的字母个数。 + +在遍历过程中,为了找到回文串,对头部指针指向的字符,如果该字母之前没有遍历过,则尾部指针从尾部开始遍历数组找到和头部指针指向字母相同的字母(需在头部指针当前位置的后面),再遍历两个指针中间的字母,统计出现的不同字母的个数即为首尾为当前指针指向的字母的情况下能组成的不同的三字母回文序列的个数。此处可做简单优化,当统计的字母个数达到26个时即可停止遍历。 + +如此反复直到遍历到数组末尾或者头指针已经遇到了全部的26个字母为止。 + +### 代码 +```cpp +class Solution { +public: + int countPalindromicSubsequence(string s) { + vector visited(26, false); + int count = 0; + int result = 0; + + for (int i = 0; i < s.length() && count < 26; i++) { + char curr = s[i]; + if (!visited[curr - 'a']) { + visited[curr - 'a'] = true; + count++; + + for (int j = s.length() - 1; j > i; j--) { + if (s[j] == curr) { + unordered_set middle; + for (int k = i + 1; k < j; k++) { + middle.insert(s[k]); + if(middle.size() == 26){ + break; + } + } + result += middle.size(); + break; + } + } + } + } + + return result; + } +}; +``` +## day303 2025-01-05 +### 2381. Shifting Letters II +You are given a string s of lowercase English letters and a 2D integer array shifts where shifts[i] = [starti, endi, directioni]. For every i, shift the characters in s from the index starti to the index endi (inclusive) forward if directioni = 1, or shift the characters backward if directioni = 0. + +Shifting a character forward means replacing it with the next letter in the alphabet (wrapping around so that 'z' becomes 'a'). Similarly, shifting a character backward means replacing it with the previous letter in the alphabet (wrapping around so that 'a' becomes 'z'). + +Return the final string after all such shifts to s are applied. + +![010523U95OIXuCVA](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/010523U95OIXuCVA.png) + +### 题解 +本题最直观的方法就是遍历shifts数组,根据shifts数组的内容来改变s中对应范围的字符。 + +但考虑如果shift中的范围有重叠,并且在前一个shift中方向为0,而在后一个shift中方向为1,则重叠范围内的字符实际上并未发生变化。因此为了避免在这种情况下来回变化字符,可以想办法记录每个位置处字符的变化量,这里以方向1的变化为+1,以方向0的变化为-1,这样只需根据最终记录的变化量的值直接变化对应字符即可。为了避免每次记录某个范围内的变化值时都要遍历该范围内的所有记录并改变数值,可以使用线段树,使用线段树可以只记录某个范围内的变化量而不用直接将范围内所有数字的具体值算出来,到shift全部遍历完成后再一边遍历s字符串一边计算每个位置具体的变化量并改变字符。 + +### 代码 +```cpp +class SegmentTree { +private: + vector tree; + int n; + + void build(int node, int start, int end) { + if (start == end) { + tree[node] = 0; + return; + } + int mid = (start + end) / 2; + build(2 * node + 1, start, mid); + build(2 * node + 2, mid + 1, end); + tree[node] = 0; + } + + void update(int node, int start, int end, int l, int r, int val) { + if (r < start || l > end) return; + if (l <= start && end <= r) { + tree[node] += val; + return; + } + int mid = (start + end) / 2; + update(2 * node + 1, start, mid, l, r, val); + update(2 * node + 2, mid + 1, end, l, r, val); + } + + int query(int node, int start, int end, int idx) { + if (start == end) return tree[node]; + int mid = (start + end) / 2; + if (idx <= mid) { + return tree[node] + query(2 * node + 1, start, mid, idx); + } else { + return tree[node] + query(2 * node + 2, mid + 1, end, idx); + } + } + +public: + SegmentTree(int size) { + n = size; + tree.resize(4 * n); + build(0, 0, n - 1); + } + + void rangeUpdate(int l, int r, int val) { + update(0, 0, n - 1, l, r, val); + } + + int pointQuery(int idx) { + return query(0, 0, n - 1, idx); + } +}; + +class Solution { +public: + string shiftingLetters(string s, vector>& shifts) { + int n = s.length(); + SegmentTree st(n); + + for (const auto& shift : shifts) { + int start = shift[0]; + int end = shift[1]; + int direction = shift[2]; + int val = (direction == 1) ? 1 : -1; // 方向1为+1,方向0为-1 + st.rangeUpdate(start, end, val); // 更新区间 + } + + // 遍历字符串,根据线段树中的变化量调整字符 + for (int i = 0; i < n; ++i) { + int shiftAmount = st.pointQuery(i); // 查询当前字符的变化量 + s[i] = shiftChar(s[i], shiftAmount); // 调整字符 + } + + return s; + } + +private: + char shiftChar(char c, int shift) { + shift = shift % 26; + if (shift < 0) shift += 26; + int newChar = c - 'a' + shift; + newChar = newChar % 26; + if (newChar < 0) newChar += 26; + return 'a' + newChar; + } +}; +``` + +### 总结 +这种需要频繁处理区间变化的问题还可以使用差分数组,本题如果使用差分数组则效率要高得多,因为差分数组的实现和处理更简单。差分数组的思想建立在前缀和的基础上,我们构建一个差分数组diff,数组中每个位置的数字表示当前位置的数字比前一个位置大多少,如diff\[i]=3表示i比i-1的数字大3,差分数组为什么可以很方便的用于处理区间变化问题呢,我们考虑一个简单情况,差分数组初始化为全0表示所有位置的数字都一样,此时假如将diff\[i]变为3,那么i比i-1大3,此时我们可以发现,i后面的位置虽然在差分数组中的值仍然为0,但由于i发生了变化,则后面的数字在差分数组为0的情况下表示和i的大小相同也就自然跟随i发生了变化。这样就实现了大于等于i的全部位置都比i-1大3的效果。那么如果要只将中间某一段变为比i前面的数字大3应该怎么办呢。假如我们想让i~j的数字加3,j以后的数字不变,则在diff\[i]加3的情况下,让j+1位置不加3,则只需让diff\[j+1]减3,抵消掉前面的加3带来的效果即可(大于j+1的位置也就同样跟随着j+1自然产生了抵消效果)。最终计算每个位置的变化量时只需计算diff数组的前缀和即可,是非常巧妙的思路。 + +这里的思想是我们只需知道变化的路径就可以知道最终的值相当于初始值的总体变化量,在一些特定场景下,相对变化量变化频繁,可以不必每次都记下绝对变化量,仅将相对变化全部记录下来,最终再去计算出绝对变化量会大大增加计算效率。当然这需要有一个固定的初始值,比如本题其实默认在开始之前字符是没发生变化的,即初始为0。 + +```cpp +class Solution { +public: + string shiftingLetters(string s, vector>& shifts) { + int n = s.length(); + vector prefix(n + 1, 0); + + for (auto &shift : shifts) { + int a = shift[0]; + int b = shift[1]; + int c = shift[2]; + prefix[a] += (2 * c - 1); + prefix[b + 1] -= (2 * c - 1); + } + + + int currentShift = 0; + for (int i = 0; i < n; i++) { + currentShift = (currentShift + prefix[i]) % 26; + s[i] = 'a' + (s[i] - 'a' + currentShift + 26) % 26; + } + + return s; + } +}; + +``` +## day304 2025-01-06 +### 1769. Minimum Number of Operations to Move All Balls to Each Box +You have n boxes. You are given a binary string boxes of length n, where boxes[i] is '0' if the ith box is empty, and '1' if it contains one ball. + +In one operation, you can move one ball from a box to an adjacent box. Box i is adjacent to box j if abs(i - j) == 1. Note that after doing so, there may be more than one ball in some boxes. + +Return an array answer of size n, where answer[i] is the minimum number of operations needed to move all the balls to the ith box. + +Each answer[i] is calculated considering the initial state of the boxes. + +![0106FLjfNN7WT45O](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0106FLjfNN7WT45O.png) + +### 题解 +本题要求出将全部球移动到各个位置需要的移动次数,最直观的可以直接暴力求解,对每个位置从头遍历数组将全部为1的位置和当前位置的距离求和。 + +显然这样浪费了太多的信息,如即时只遍历一遍,其实也能确定各个位置之间的相对距离,因为第一遍遍历是以第一个盒子的位置为基准的,后面的任意两个盒子之间的距离都可以通过这两个盒子分别与第一个盒子的相对距离通过做差计算出来。这也可以使我们想起昨天做过的差分数组的问题,可以想到本题同样只需找出从前一个盒子到下一个盒子时的改变量,再在前一个盒子的结果已知的情况下增减改变量就能得出下一个盒子对应的结果。 + +改变量如何计算呢,可以从一个简单例子出发,如00111,对第一个盒子来说,其距离总和为2+3+4=9。移动到第二个盒子时,距离总和为1+2+3=6。可以发现因为向右移动了一位,因此所有在该位置右面的为1的盒子到这个盒子的距离都减少了1,即总距离减少了右边为1的盒子的个数。移动到第三个盒子时同理,第三个盒子的总距离为3。在移动到第四个盒子时,注意此时有一个盒子到了左面,因为是从左向右遍历的,则左面的盒子的距离会逐渐增加,此时包含自身和右面有两个盒子,左面有一个盒子,左面盒子的距离增加,右面盒子的距离减少,总距离变为2。 + +通过这个例子可以发现,在从左向右遍历的过程中,先算出第一个位置的总距离并记录下为1的盒子的总数。再在向右遍历的过程中减去在该位置右面的为1的盒子个数,加上在该位置左面的盒子个数即得当前位置的总距离。用两个变量分别保存在左面和在右面为1的盒子个数,在遍历到为1的盒子时就将右面的个数减1,左面的个数加1。 + +### 代码 +```cpp +class Solution { +public: + vector minOperations(string boxes) { + int rightones = 0; + int leftones = 0; + int sumdistance = 0; + int currentdis = 0; + for(const auto& box : boxes){ + if(box == '1'){ + rightones++; + sumdistance += currentdis; + } + currentdis++; + } + vector result; + for(const auto& box : boxes){ + result.push_back(sumdistance); + if(box == '1'){ + leftones++; + rightones--; + } + sumdistance = sumdistance - rightones + leftones; + } + return result; + } +}; +``` +## day305 2025-01-07 +### 1408. String Matching in an Array +Given an array of string words, return all strings in words that is a substring of another word. You can return the answer in any order. + +A substring is a contiguous sequence of characters within a string + +![0107lUtkjOdxYtdh](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0107lUtkjOdxYtdh.png) + +### 题解 +本题是简单题,但只是说题面比较简单,思路也比较直接,因为题目限制words最多100个,因此直接暴力求解也是没有问题的。 + +但我们仍然希望能有一种方法可以使得求解速度快一些,判定一个字符串是不是另一个字符串的子字符串有很多种方法,像经典的kmp,BM算法,Rabin-Karp算法,此处我们使用Rabin-Karp算法,使用该算法的好处在于可以一次算出所有字符串的哈希值保存起来,后续就不用重复计算模式串的哈希值了,只需根据字符串长度去字符串中使用滑动窗口计算哈希值尝试匹配即可。 + +### 代码 +```cpp +class Solution { +public: + const int MOD = 1000000007; + const int BASE = 31; + + // 计算整个字符串的哈希值 + long long getStringHash(const string& s) { + long long hash = 0; + long long power = 1; + for (char c : s) { + hash = (hash + (c - 'a' + 1) * power) % MOD; + power = (power * BASE) % MOD; + } + return hash; + } + + // 计算文本串中长度为len的子串的哈希值 + long long getWindowHash(const string& s, int start, int len) { + long long hash = 0; + long long power = 1; + for (int i = 0; i < len; i++) { + hash = (hash + (s[start + i] - 'a' + 1) * power) % MOD; + power = (power * BASE) % MOD; + } + return hash; + } + + vector stringMatching(vector& words) { + int n = words.size(); + vector result; + + for (int i = 0; i < n; i++) { + bool isSubstring = false; + int len1 = words[i].length(); + long long patternHash = getStringHash(words[i]); + + for (int j = 0; j < n && !isSubstring; j++) { + if (i == j) continue; + + int len2 = words[j].length(); + if (len1 > len2) continue; + + // 在较长的字符串中滑动窗口 + for (int k = 0; k + len1 <= len2; k++) { + if (patternHash == getWindowHash(words[j], k, len1)) { + isSubstring = true; + break; + } + } + } + + if (isSubstring) { + result.push_back(words[i]); + } + } + + return result; + } +}; +``` + +## 总结 + +其实这种算法在本题中表现并不好,尝试直接暴力解法可以发现反而快得多,此时要注意题目中的限制条件,待匹配的字符串不超过100个,而且每个字符串的长度不超过30,因此直接暴力求解即便在最差情况下也不过执行几十万次循环,考虑大部分题目本身的数据量就已经达到10万,遍历一遍就要10万次循环,这个最差情况是完全可以接受的,而暴力也有其好处在于简洁快速,不需要任何预处理,这也是为什么现代一般编程语言中类似find这样查找字符串,进行字符串匹配的库函数在具体实现时对于长文本可能会使用一些比较高级的算法如BM算法或其变体,而在字符串比较短时往往直接使用暴力求解,这样可以得到最佳的实际效果。 + +这也提醒我们在实际应用过程中,一定要注意具体场景和限制条件,不要把知识学死,算法更多的是提供一些巧妙的解决问题的思路,但并不一定适于所有场景,就好像芯片一样,专门针对具体领域设计的领域专用芯片在某个特定领域效果未必比最先进的通用芯片差。同样这也是在不同场景可能会有很多通用算法的变体如KMP变体,BM变体一样。根据具体场景分析不同情况下的最优解并根据限制条件优化算法需要我们一直铭记在心。 + +```cpp +class Solution { +public: + bool isSubstring(const string& pattern, const string& text) { + int n = text.length(); + int m = pattern.length(); + + for (int i = 0; i <= n - m; i++) { + int j; + for (j = 0; j < m; j++) { + if (text[i + j] != pattern[j]) { + break; + } + } + if (j == m) return true; + } + return false; + } + + vector stringMatching(vector& words) { + vector result; + int n = words.size(); + + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + if (i != j && isSubstring(words[i], words[j])) { + result.push_back(words[i]); + break; + } + } + } + + return result; + } +}; +```