diff --git a/content/posts/leetcode.md b/content/posts/leetcode.md index dcb6b08..f2a205c 100644 --- a/content/posts/leetcode.md +++ b/content/posts/leetcode.md @@ -20852,20 +20852,21 @@ For example, "ace" is a subsequence of "abcde". 如此反复直到遍历到数组末尾或者头指针已经遇到了全部的26个字母为止。 ### 代码 -```cpp + +```cpp class Solution { public: int countPalindromicSubsequence(string s) { - vector visited(26, false); - int count = 0; + 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; @@ -20876,18 +20877,21 @@ public: } } result += middle.size(); - break; + break; } } } } - + return result; } }; ``` + ## day303 2025-01-05 -### 2381. Shifting Letters II + +### 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'). @@ -20897,54 +20901,56 @@ 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 + +```cpp class SegmentTree { private: - vector tree; - int n; + vector tree; + int n; void build(int node, int start, int end) { if (start == end) { - tree[node] = 0; + 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; + 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 (r < start || l > end) return; if (l <= start && end <= r) { - tree[node] += val; + 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); + 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]; + if (start == end) return tree[node]; int mid = (start + end) / 2; if (idx <= mid) { - return tree[node] + query(2 * node + 1, start, mid, idx); + return tree[node] + query(2 * node + 1, start, mid, idx); } else { - return tree[node] + query(2 * node + 2, mid + 1, end, idx); + 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); + tree.resize(4 * n); + build(0, 0, n - 1); } void rangeUpdate(int l, int r, int val) { @@ -20960,7 +20966,7 @@ class Solution { public: string shiftingLetters(string s, vector>& shifts) { int n = s.length(); - SegmentTree st(n); + SegmentTree st(n); for (const auto& shift : shifts) { int start = shift[0]; @@ -20981,27 +20987,28 @@ public: 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; + 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 +```cpp class Solution { public: string shiftingLetters(string s, vector>& shifts) { int n = s.length(); - vector prefix(n + 1, 0); + vector prefix(n + 1, 0); for (auto &shift : shifts) { int a = shift[0]; @@ -21015,7 +21022,7 @@ public: int currentShift = 0; for (int i = 0; i < n; i++) { currentShift = (currentShift + prefix[i]) % 26; - s[i] = 'a' + (s[i] - 'a' + currentShift + 26) % 26; + s[i] = 'a' + (s[i] - 'a' + currentShift + 26) % 26; } return s; @@ -21023,8 +21030,11 @@ public: }; ``` + ## day304 2025-01-06 -### 1769. Minimum Number of Operations to Move All Balls to Each Box + +### 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. @@ -21036,6 +21046,7 @@ 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的位置和当前位置的距离求和。 显然这样浪费了太多的信息,如即时只遍历一遍,其实也能确定各个位置之间的相对距离,因为第一遍遍历是以第一个盒子的位置为基准的,后面的任意两个盒子之间的距离都可以通过这两个盒子分别与第一个盒子的相对距离通过做差计算出来。这也可以使我们想起昨天做过的差分数组的问题,可以想到本题同样只需找出从前一个盒子到下一个盒子时的改变量,再在前一个盒子的结果已知的情况下增减改变量就能得出下一个盒子对应的结果。 @@ -21045,7 +21056,8 @@ Each answer[i] is calculated considering the initial state of the boxes. 通过这个例子可以发现,在从左向右遍历的过程中,先算出第一个位置的总距离并记录下为1的盒子的总数。再在向右遍历的过程中减去在该位置右面的为1的盒子个数,加上在该位置左面的盒子个数即得当前位置的总距离。用两个变量分别保存在左面和在右面为1的盒子个数,在遍历到为1的盒子时就将右面的个数减1,左面的个数加1。 ### 代码 -```cpp + +```cpp class Solution { public: vector minOperations(string boxes) { @@ -21073,8 +21085,11 @@ public: } }; ``` + ## day305 2025-01-07 -### 1408. String Matching in an Array + +### 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 @@ -21082,17 +21097,19 @@ 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 + +```cpp class Solution { public: const int MOD = 1000000007; const int BASE = 31; - + // 计算整个字符串的哈希值 long long getStringHash(const string& s) { long long hash = 0; @@ -21103,7 +21120,7 @@ public: } return hash; } - + // 计算文本串中长度为len的子串的哈希值 long long getWindowHash(const string& s, int start, int len) { long long hash = 0; @@ -21118,18 +21135,18 @@ public: 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)) { @@ -21138,12 +21155,12 @@ public: } } } - + if (isSubstring) { result.push_back(words[i]); } } - + return result; } }; @@ -21155,13 +21172,13 @@ public: 这也提醒我们在实际应用过程中,一定要注意具体场景和限制条件,不要把知识学死,算法更多的是提供一些巧妙的解决问题的思路,但并不一定适于所有场景,就好像芯片一样,专门针对具体领域设计的领域专用芯片在某个特定领域效果未必比最先进的通用芯片差。同样这也是在不同场景可能会有很多通用算法的变体如KMP变体,BM变体一样。根据具体场景分析不同情况下的最优解并根据限制条件优化算法需要我们一直铭记在心。 -```cpp +```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++) { @@ -21173,11 +21190,11 @@ public: } 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])) { @@ -21186,8 +21203,307 @@ public: } } } - + return result; } }; ``` + +## day306 2025-01-08 + +### 3042. Count Prefix and Suffix Pairs I + +You are given a 0-indexed string array words. + +Let's define a boolean function isPrefixAndSuffix that takes two strings, str1 and str2: + +isPrefixAndSuffix(str1, str2) returns true if str1 is both a +prefix +and a +suffix +of str2, and false otherwise. +For example, isPrefixAndSuffix("aba", "ababa") is true because "aba" is a prefix of "ababa" and also a suffix, but isPrefixAndSuffix("abc", "abcd") is false. + +Return an integer denoting the number of index pairs (i, j) such that i < j, and isPrefixAndSuffix(words[i], words[j]) is true. + +![01087uvaVMzNKhtB](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/01087uvaVMzNKhtB.png) + +### 题解 + +先按照题意构造isPrefixAndSuffix函数,本题isPrefixAndSuffix函数是关键,如何快速的比对str1是否是str2的前缀和后缀字符串影响整体的解答效率。本题使用字符串哈希或者直接根据str1的长度进行判断都是可行的。考虑题目条件给出的数据量不大,直接根据str1的长度通过字符比较确定str1是否是str2的前缀和后缀即可。 + +### 代码 + +```cpp +class Solution { +public: + bool isPrefixAndSuffix(string str1, string str2){ + int len = str1.size(); + int len2 = str2.size(); + for(int i=0;i& words) { + int result = 0; + int wordlen = words.size(); + for(int i=wordlen-1;i>=0;i--){ + for(int j=0;j wordSubsets(vector& words1, vector& words2) { + vector maxCount(26, 0); + + for (const string& word : words2) { + vector currCount(26, 0); + // 只统计当前字符串中出现的字符 + for (char c : word) { + currCount[c - 'a']++; + } + // 只更新出现过的字符的最大值 + for (char c : word) { + maxCount[c - 'a'] = max(maxCount[c - 'a'], currCount[c - 'a']); + } + } + + vector result; + for (const string& word : words1) { + vector count(26, 0); + for (char c : word) { + count[c - 'a']++; + } + + bool isUniversal = true; + for (int i = 0; i < 26; i++) { + if (count[i] < maxCount[i]) { + isUniversal = false; + break; + } + } + if (isUniversal) { + result.push_back(word); + } + } + + return result; +} +}; +``` +## day308 2025-01-11 +### 1400. Construct K Palindrome Strings +Given a string s and an integer k, return true if you can use all the characters in s to construct k palindrome strings or false otherwise. + +![0111YVANRmanuGki](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0111YVANRmanuGki.png) + +### 题解 +本题考虑如何构造回文字符串,注意单独的一个字符满足回文字符串的要求,因此如果想获得最多的回文串可以直接将整个字符串全部拆分为单个字符,就可以得到由单个字符组成的字符串长度个数的回文串,因此可以用贪心法,一个一个的取回文串,再将剩余的全部字符作为一个回文串。 + +但要注意,字符能构造成回文串的前提是剩余的字符中每个字符的个数中最多只有一个字符个数是奇数,如果多个字符个数是奇数则无法构成回文串,因为两个奇数的字符必定不能对称分布,因此可以统计出所有为奇数的字符再根据k从这些奇数字符中挨个取出一个使其变为偶数直到取到第k个,此时如果剩余字符满足不超过一个字符是奇数则可以构造出k个回文串,否则不能。也就是说,如果为奇数的字符个数小于等于k个则一定能构造成功,否则一定不能。 + +则本题先考虑字符串长度,如果字符串长度小于k显然不能构造出k个回文串,大于等于k时按照上面的思路尝试构造回文串,成功则返回true,否则返回false。 +### 代码 +```cpp +class Solution { +public: + bool canConstruct(string s, int k) { + if (s.length() < k) return false; + + if (s.length() == k) return true; + + vector count(26, 0); + for (char c : s) { + count[c - 'a']++; + } + + int oddCount = 0; + for (int i = 0; i < 26; i++) { + if (count[i] % 2 == 1) { + oddCount++; + } + } + + if (oddCount > k) return false; + + return true; + } +}; +``` +## day309 2025-01-12 +### 2116. Check if a Parentheses String Can Be Valid +A parentheses string is a non-empty string consisting only of '(' and ')'. It is valid if any of the following conditions is true: + +It is (). +It can be written as AB (A concatenated with B), where A and B are valid parentheses strings. +It can be written as (A), where A is a valid parentheses string. +You are given a parentheses string s and a string locked, both of length n. locked is a binary string consisting only of '0's and '1's. For each index i of locked, + +If locked[i] is '1', you cannot change s[i]. +But if locked[i] is '0', you can change s[i] to either '(' or ')'. +Return true if you can make s a valid parentheses string. Otherwise, return false. + +![0112lVVn6vsyWRUD](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0112lVVn6vsyWRUD.png) + +### 题解 +本题就是在常规的判断括号字符串是否匹配的基础上增加了锁,来确定某个位置的括号字符是否可以随意改变,如果有锁则不能改变,无锁则可以任意改变。首先确定当字符串的长度为奇数时一定不能匹配成功,仅在字符串长度为偶数的情况下才可能匹配成功。 + +考虑对于无锁的位置,因为可以随意改变,其实不用考虑,重点关注有锁的位置,对于有锁的位置,我们可以使用无锁的位置或者有锁的与其可以匹配的括号来匹配它,那么只要始终可以匹配上就说明最终可以匹配成功。而左右括号需要的可以被匹配的位置方向不同,对于有锁右括号来说,需要其左面有足够的剩余的左括号或者无锁位置才能保证右括号匹配成功,对于有锁左括号则相反。因此可以正反分别遍历一遍数组,一次用来检查右括号是否可以匹配成功,一次用来检查左括号是否可以匹配成功。都成功则最终可以成功,否则不成功。 + +### 代码 +```cpp +class Solution { +public: + bool canBeValid(string s, string locked) { + int n = s.length(); + if (n % 2) return false; + + // 从左到右检查右括号是否过多 + int balance = 0; + for (int i = 0; i < n; i++) { + if (locked[i] == '0' || s[i] == '(') { + balance++; + } else { + balance--; + } + if (balance < 0) return false; + } + + // 从右到左检查左括号是否过多 + balance = 0; + for (int i = n-1; i >= 0; i--) { + if (locked[i] == '0' || s[i] == ')') { + balance++; + } else { + balance--; + } + if (balance < 0) return false; + } + + return true; + } +}; +``` +## day310 2025-01-13 +### 3223. Minimum Length of String After Operations +You are given a string s. + +You can perform the following process on s any number of times: + +Choose an index i in the string such that there is at least one character to the left of index i that is equal to s[i], and at least one character to the right that is also equal to s[i]. +Delete the closest character to the left of index i that is equal to s[i]. +Delete the closest character to the right of index i that is equal to s[i]. +Return the minimum length of the final string s that you can achieve. + +![0113qhIkf1ekZWQH](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0113qhIkf1ekZWQH.png) + +### 题解 +本题考虑题中所述的操作,可以发现无需知道字符所在的具体位置,当一个字符在字符串中的个数大于等于3个时,处在中间位置的字符一定满足题目所述的条件并可以将两侧的字符删除,并可如此反复直到字符的个数小于3个,由此发现只需统计字符的个数,当字符个数为奇数时,则可以将中间位置的字符两侧不断删除直到只剩下正中间的字符,而为偶数时则不断删除后最终会剩余两个字符。再将所有字符的剩余个数加和即得结果。 + +### 代码 +```cpp +class Solution { +public: + int minimumLength(string s) { + vector chars(26,0); + for(const auto& ch : s){ + chars[ch-'a']++; + } + int result = 0; + for(const int& count : chars){ + if(count > 0){ + if(count % 2 == 0){ + result += 2; + }else{ + result += 1; + } + } + } + return result; + } +}; +``` +## day311 2025-01-14 +### 2657. Find the Prefix Common Array of Two Arrays +You are given two 0-indexed integer permutations A and B of length n. + +A prefix common array of A and B is an array C such that C[i] is equal to the count of numbers that are present at or before the index i in both A and B. + +Return the prefix common array of A and B. + +A sequence of n integers is called a permutation if it contains all integers from 1 to n exactly once. + +![0114gjhZRYENaC7W](https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0114gjhZRYENaC7W.png) + +### 题解 +本题考虑A和B都仅包含1~n的不重复数字,并且n的长度最大不超过50,要想快速了解A和B的前缀数组中包含哪些共同的数字,我们就需要将这些不重复数字想办法保存起来,每个数字的位置互不冲突,还能方便的了解到哪些数字是二者都有的,如果使用数组的话,满足位置互不冲突但不能快速的得出哪些数字是二者共有的,数组只能靠遍历来判断,此时可以考虑位运算,因为对于每个数字我们仅需要知道它在前缀数组中是否出现,统计共同数字的个数时也不需要知道共同的数字具体都是哪些,仅需要个数,则用0表示未出现,用1表示出现。这样做的好处在于在寻找共同数字的个数时可以通过对两个二进制数直接取按位与,相同位置的1会自动保留,即表示同时在A和B的前缀数组中出现过,统计个数仅需统计按位与后得到的数字中二进制位1的个数。 + +统计某个数字中二进制位1的个数最直接的方法就是移位直到为0,也可以使用Brian Kernighan算法,每次消除数字最右侧的1。这里使用Brian Kernighan算法。 + +注意n的范围到50,因此要使用一个64位的数字才能有足够的二进制位来表示每个数字,因此要使用unsigned long int。 + +### 代码 +```cpp +class Solution { +public: + // 使用Brian Kernighan算法计算二进制中1的个数 + int countOnes(unsigned long int n) { + int count = 0; + while (n) { + n = n & (n - 1); // 消除最右边的1 + count++; + } + return count; + } + + vector findThePrefixCommonArray(vector& A, vector& B) { + int n = A.size(); + vector C(n); + unsigned long int maskA = 0, maskB = 0; + + for (int i = 0; i < n; i++) { + maskA |= ((unsigned long int)1 << A[i]); + maskB |= ((unsigned long int)1 << B[i]); + + C[i] = countOnes(maskA & maskB); + } + + return C; + } +}; +```