[朝花夕拾] LeetCode每日一题
值得一提:
20220304 2104; 20220309 798; 20220330 1606;
20220228 1601. 最多可达成的换楼请求数目
方法一:DFS 枚举
枚举所有换楼请求的选择与不选择两种情况,最后判断是否满足题意,时间复杂度为 O(2 ^ m),m 为请求个数,依题意 m <= 16,符合要求。
1 #include <cstring> 2 3 const int N = 25; 4 5 class Solution { 6 public: 7 int a[N][N], b[N]; 8 int dfs(int o, int x, int n, int m) { 9 int res = 0; 10 if (o == m) { 11 int tot = 0; 12 for (int i = 0; i < n; i++) { 13 for (int j = 0; j < m; j++) 14 tot += (b[j] ? a[j][i] : 0); 15 if (tot != 0) return 0; 16 } 17 return x; 18 } 19 b[o] = 1; 20 res = dfs(o + 1, x + 1, n, m); 21 b[o] = 0; 22 res = max(res, dfs(o + 1, x, n, m)); 23 return res; 24 } 25 int maximumRequests(int n, vector<vector<int>>& requests) { 26 int m = requests.size(); 27 for (int i = 0; i < m; i++) 28 a[i][requests[i][0]] += -1, a[i][requests[i][1]] += 1; 29 memset(b, 0, sizeof(b)); 30 return dfs(0, 0, n, m); 31 } 32 };
其实不用转换为二维数组,可以节省空间,并且 DFS 过程中就可以维护是否满足条件而不需要最后用 for 循环判断,代码有优化空间。
方法二:二进制位运算枚举
由于对于请求只有选择与不选择两种可能,使用二进制位运算,会大幅精简代码,但需要再最后用 for 循环判断是否满足条件,故时间复杂度会达到 O(n * 2 ^ m)。
1 const int N = 25; 2 3 class Solution { 4 public: 5 int maximumRequests(int n, vector<vector<int>>& r) { 6 int a[N], m = r.size(), ans = 0; 7 for (int o = 0; o < (1 << m); o++) { 8 memset(a, 0, sizeof(a)); 9 int res = 0; 10 for (int i = 0; i < m; i++) 11 if (o & (1 << i)) 12 a[r[i][0]]--, a[r[i][1]]++, res++; 13 int tot = 0; 14 for (int i = 0; i < n; i++) 15 tot += (a[i] == 0); 16 if (tot == n) ans = max(res, ans); 17 } 18 return ans; 19 } 20 };
其他:最小费用最大流
由于数据量并不大,用网络流有点杀鸡用牛刀了。
20220301 6. Z 字形变换
方法:找规律构造
对需要输出的 Z 字形矩阵进行编号,找出规律即可直接构造。
1 class Solution { 2 public: 3 string convert(string s, int r) { 4 string a; 5 int l = s.length(), x = 0; 6 if (r == 1) return s; 7 for (int i = 0; i < r; i++) { 8 int o = i; 9 if (o >= l) return a; 10 while (1) { 11 if (i != r - 1) { 12 a += s[o]; 13 o += (r - i - 1) * 2; 14 if (o >= l) break; 15 } 16 if (i != 0) { 17 a += s[o]; 18 o += i * 2; 19 if (o >= l) break; 20 } 21 } 22 } 23 return a; 24 } 25 };
注意 numRows 为 1 时在这里需要单独考虑。
其他:官方也提供了二维矩阵模拟和压缩存储空间两种方法,但显然是构造最为简单。
20220302 564. 寻找最近的回文数
自己做的时候思路不太行,越做越麻烦。
看了官方的题解,温习和新学到了很多 C++ 特别是新版的用法:
> stoi, stol, to_string... 等字符串转换函数
> rbegin(), rend() 反向迭代器同样适用于 string
> i & 1 非常方便的区分奇数偶数的写法
> for (int i : {...}) 以及 for (auto& i: list)
1 class Solution { 2 public: 3 vector<long> getlist(const string& str) { 4 int l = str.length(); 5 vector<long> list = {(long)pow(10, l - 1) - 1, (long)pow(10, l) + 1}; 6 long pre = stol(str.substr(0, (l + 1) / 2)); 7 for (int i : {pre - 1, pre, pre + 1}) { 8 string pres = to_string(i); 9 string o = pres + string(pres.rbegin() + (l & 1), pres.rend()); 10 list.push_back(stol(o)); 11 } 12 return list; 13 } 14 15 string nearestPalindromic(string str) { 16 long n = stol(str), ans = -1; 17 const vector<long>& list = getlist(str); 18 for (auto& i : list) { 19 if (i != n) { 20 long a1 = abs(i - n), a2 = abs(ans - n); 21 if (ans == -1 || a1 < a2 || a1 == a2 && i < ans) 22 ans = i; 23 } 24 } 25 return to_string(ans); 26 } 27 };
20220303 258. 各位相加
方法一:暴力回溯
1 class Solution { 2 public: 3 int work(int num) { 4 if (num < 10) return num; 5 int res = 0; 6 while (num) 7 res += num % 10, num /= 10; 8 return work(res); 9 } 10 11 int addDigits(int num) { 12 return work(num); 13 } 14 };
方法二:规律
由于数据范围在 int 内,num 的位数不会超过 10,则在第一次相加后得到的 num 必然在 100 以内,所以我考虑用方法一打出 100 以内的表,这样就最多只需要一次相加。
实际上这也并非题目要求的 O(1),但打完表之后我们惊喜地发现,结果是 1 ~ 9 的循环,答案也就不言而喻了。
1 class Solution { 2 public: 3 int addDigits(int num) { 4 return (num - 1) % 9 + 1; 5 } 6 };
20220304 2104. 子数组范围和
在写线段树的我是真的没救了(
方法一:暴力枚举
1 class Solution { 2 public: 3 long long subArrayRanges(vector<int>& a) { 4 long long n = a.size(), ans = 0; 5 for (int i = 0; i < n; i++) { 6 int mx = a[i], mi = a[i]; 7 for (int j = i + 1; j < n; j++) { 8 mx = max(mx, a[j]); 9 mi = min(mi, a[j]); 10 ans += mx - mi; 11 } 12 } 13 return ans; 14 } 15 };
方法二:单调栈
假设区间 i ∈ [j, k] 中,且 a[i] 是区间所有元素中的最大值,则该数对最后的范围和贡献了 a[i] * (i - j) * (k - i);同理如果是最小值,则贡献值取负。
如何求得上述区间?当且仅当 [j, i - 1] ∪ [i + 1, k] 所有元素均 < a[i],且 a[j - 1] > a[i], a[k + 1] > a[i],即分别求左右侧距离 a[i] 最近的比 a[i] 大的元素。
求最近元素采用:单调栈。单调栈的特点就是栈里保存的元素是有序的,举个例子,对于 [6, 4, 3, 5, 4, 2],找最近的比目标大的元素:
入栈 6;当前栈顶为 6,比 4 大,记录,入栈 4;当前栈顶为 4,比 3 大,记录,入栈 3;当前栈顶为 3,比 5 小,出栈;为 4,比 5 小,出栈;为 6,比 5 大,记录,入栈 5……以此类推。
不难发现这个过程中,栈始终会保持从栈顶到栈底的元素依次递减,这种栈被称之为单调栈。
1 class Solution { 2 public: 3 long long subArrayRanges(vector<int>& a) { 4 int n = a.size(); 5 long long ans = 0; 6 stack<int> s1, s2; 7 vector<int> mil(n), mxl(n), mir(n), mxr(n); 8 for (int i = 0; i < n; i++) { 9 while (!s1.empty() && a[s1.top()] > a[i]) 10 s1.pop(); 11 mil[i] = s1.empty() ? -1 : s1.top(); 12 s1.push(i); 13 while (!s2.empty() && a[s2.top()] <= a[i]) 14 s2.pop(); 15 mxl[i] = s2.empty() ? -1 : s2.top(); 16 s2.push(i); 17 } 18 s1 = s2 = stack<int>(); 19 for (int i = n - 1; i >= 0; i--) { 20 while (!s1.empty() && a[s1.top()] >= a[i]) 21 s1.pop(); 22 mir[i] = s1.empty() ? n : s1.top(); 23 s1.push(i); 24 while (!s2.empty() && a[s2.top()] < a[i]) 25 s2.pop(); 26 mxr[i] = s2.empty() ? n : s2.top(); 27 s2.push(i); 28 } 29 for (int i = 0; i < n; i++) 30 ans += (long long)a[i] * (long long)((i - mxl[i]) * (mxr[i] - i) - (i - mil[i]) * (mir[i] - i)); 31 return ans; 32 } 33 };
细节:元素可能相等,可以对其中一侧允许包含相等,另一侧不允许。
20220305 521. 最长特殊序列 I
方法:脑筋急转弯
1 class Solution { 2 public: 3 int findLUSlength(string a, string b) { 4 return a == b ? -1 : max(a.size(), b.size()); 5 } 6 };
20220306 2100. 适合打劫银行的日子
方法:前缀和(LC官方写的是动态规划)
1 class Solution { 2 public: 3 vector<int> goodDaysToRobBank(vector<int>& a, int t) { 4 int n = a.size(); 5 vector<int> dec(n); 6 vector<int> inc(n); 7 for (int i = 1; i < n; i++) { 8 if (a[i] <= a[i - 1]) 9 dec[i] = dec[i - 1] + 1; 10 if (a[n - i - 1] <= a[n - i]) 11 inc[n - i - 1] = inc[n - i] + 1; 12 } 13 vector<int> ans; 14 for (int i = t; i < n - t; i++) { 15 if (dec[i] >= t && inc[i] >= t) { 16 ans.emplace_back(i); 17 } 18 } 19 return ans; 20 } 21 };
20220307 504. 七进制数
1 class Solution { 2 public: 3 string convertToBase7(int num) { 4 string ans; 5 int x; 6 if (num < 0) num = -num, ans.push_back('-'); 7 for (x = 1; x * 7 <= num; x *= 7); 8 while (x) { 9 int o = 0, i; 10 for (i = 0; o + x <= num; i++, o += x); 11 num -= o, x /= 7, ans.push_back((char)i + '0'); 12 } 13 return ans; 14 } 15 };
20220308 2055. 蜡烛之间的盘子
方法:预处理前缀和
看起来像 DP,但其实不需要,预处理出每个位置左右侧最近的蜡烛位置以及前 i 个位置的蜡烛个数即可。
1 class Solution { 2 public: 3 vector<int> platesBetweenCandles(string s, vector<vector<int>>& q) { 4 int len = s.length(); 5 vector<int> ans, l(len), r(len), sum(len); 6 sum[0] = s[0] == '|', l[0] = s[0] == '|' ? 0 : -1; 7 for (int i = 1; i < len; i++) { 8 sum[i] = sum[i - 1] + (s[i] == '|'); 9 l[i] = s[i] == '|' ? i : l[i - 1]; 10 } 11 r[len - 1] = s[len - 1] == '|' ? len - 1 : -1; 12 for (int i = len - 2; i >= 0; i--) 13 r[i] = s[i] == '|' ? i : r[i + 1]; 14 int tot = q.size(); 15 for (int i = 0; i < tot; i++) 16 if (l[q[i][1]] == -1 || r[q[i][0]] == -1) ans.push_back(0); 17 else ans.push_back(max(0, l[q[i][1]] - r[q[i][0]] + 1 - sum[q[i][1]] + (q[i][0] == 0 ? 0 : sum[q[i][0] - 1]))); 18 return ans; 19 } 20 };
20220309 798. 得分最高的最小轮调
方法:差分数组
对于元素 a[i] = x,当其轮调后的新下标 j >= x 时,会得到 1 分,即得分下标范围为 [x, n - 1],共 n - x 个;不得分范围为 [0, x - 1],共 x 个。
由于其初始下标为 i,轮调下标为 k,则新下标 j = (i - k + n) mod n,当且仅当 (i - k + n) mod n >= x 时得分,等价于 k <= (i - x + n) mod n;又得分下标共 n - x 个,故 k >= (i + 1) mod n。
综上,当 i < x 时,i + 1 <= k <= i - x + n;当 i >= x 时,k >= i + 1 或 k <= i - x。
创建数组 points,points[k] 表示轮调下标为 k 时的得分。对于 nums 的每个元素,直接将 points 的对应的得分下标范围所有元素 +1。完成后求得 points 中的最大值即可。这个过程本身和暴力一样也是 O(n ^ 2),但是由于所有的操作都是范围操作,使用差分数组即可降维。
比如针对 i < x,我们需要将 [i + 1, i - x + n] 所有元素 +1,定义差分数组 diffs[k] = points[k] - points[k - 1],直接 diffs[i + 1]++, diffs[i - x + n]-- 即可,最后计算 diffs 的前缀和,即可还原 points 的数据。
记得取模。
1 class Solution { 2 public: 3 int bestRotation(vector<int>& nums) { 4 int n = nums.size(), ans = 0, sum = 0, mx = 0; 5 vector<int> diffs(n); 6 for (int i = 0; i < n; i++) { 7 int l = (i + 1) % n, r = (i - nums[i] + n + 1) % n; 8 diffs[l]++, diffs[r]--; 9 if (l >= r) diffs[0]++; 10 } 11 for (int i = 0; i < n; i++) 12 if (mx < (sum += diffs[i])) 13 mx = sum, ans = i; 14 return ans; 15 } 16 };
20220310. N 叉树的前序遍历
1 class Solution { 2 public: 3 void dfs(const Node* root, vector<int> & res) { 4 if (root == nullptr) { 5 return; 6 } 7 res.emplace_back(root->val); 8 for (auto & ch : root->children) { 9 dfs(ch, res); 10 } 11 } 12 13 vector<int> preorder(Node* root) { 14 vector<int> res; 15 dfs(root, res); 16 return res; 17 } 18 };
20220311 2049. 统计最高分的节点数目
方法:DFS
题目第一眼看起来好麻烦,其实其得分乘积很容易得出:左子树结点数 * 右子树结点数 * (总结点数 - 以该结点为根的子树结点数),把父结点关系数组转化为左右子树关系后再 DFS 就很容易求出来了。
1 const int N = 1e5 + 5; 2 3 class Solution { 4 long long l[N], r[N], tl[N], tr[N], b[N]; 5 public: 6 int dfs(int o) { 7 if (!tl[o]) return 1; 8 l[o] = dfs(tl[o]); 9 if (tr[o]) r[o] = dfs(tr[o]); 10 return l[o] + r[o] + 1; 11 } 12 int countHighestScoreNodes(vector<int>& p) { 13 int n = p.size(), ans; 14 long long mx = 0; 15 for (int i = 1; i < n; i++) 16 if (tl[p[i]]) tr[p[i]] = i; 17 else tl[p[i]] = i; 18 int root = dfs(0); 19 for (int i = 0 ; i < n; i++) { 20 for (auto &j : {&l[i], &r[i], &(b[i] = root - l[i] - r[i] - 1)}) 21 if (!*j) *j = 1; 22 long long res = l[i] * r[i] * b[i]; 23 if (res > mx) mx = res, ans = 1; 24 else if (res == mx) ans++; 25 } 26 return ans; 27 } 28 } s;
其实求最大值的这个循环可以直接在 DFS 中完成,因为显然 DFS 每个结点也有且仅有一次遍历,在 return 前就可以进行比较了。
20220312 590. N 叉树的后序遍历
1 class Solution { 2 public: 3 void helper(const Node* root, vector<int> & res) { 4 if (root == nullptr) { 5 return; 6 } 7 for (auto & ch : root->children) { 8 helper(ch, res); 9 } 10 res.emplace_back(root->val); 11 } 12 13 vector<int> postorder(Node* root) { 14 vector<int> res; 15 helper(root, res); 16 return res; 17 } 18 };
20220313 393. UTF-8 编码验证
1 class Solution { 2 public: 3 bool validUtf8(vector<int>& data) { 4 int cnt = 0; 5 for(int x : data) { 6 if((x >> 7) & 1) { 7 if((x >> 6) & 1) { 8 if((x >> 5) & 1) { 9 if((x >> 4) & 1) { 10 if((x >> 3) & 1) { 11 return false; 12 } else { 13 if(cnt) { 14 return false; 15 } 16 cnt = 3; 17 } 18 } else { 19 if(cnt) { 20 return false; 21 } 22 cnt = 2; 23 } 24 } else { 25 if(cnt) { 26 return false; 27 } 28 cnt = 1; 29 } 30 } else { 31 cnt--; 32 } 33 } 34 } 35 36 return cnt == 0; 37 } 38 };
22020314 599. 两个列表的最小索引总和
1 class Solution { 2 public: 3 vector<string> findRestaurant(vector<string>& list1, vector<string>& list2) { 4 unordered_map<string, int> index; 5 for (int i = 0; i < list1.size(); i++) { 6 index[list1[i]] = i; 7 } 8 9 vector<string> ret; 10 int indexSum = INT_MAX; 11 for (int i = 0; i < list2.size(); i++) { 12 if (index.count(list2[i]) > 0) { 13 int j = index[list2[i]]; 14 if (i + j < indexSum) { 15 ret.clear(); 16 ret.push_back(list2[i]); 17 indexSum = i + j; 18 } else if (i + j == indexSum) { 19 ret.push_back(list2[i]); 20 } 21 } 22 } 23 return ret; 24 } 25 };
20220315 2044. 统计按位或能得到最大值的子集数目
1 class Solution { 2 public: 3 int countMaxOrSubsets(vector<int> const& nums) { 4 const int n = nums.size(), maxsum = accumulate(nums.begin(), nums.end(), 0, bit_or<int>{}); 5 const auto dfs = [&] (auto&& dfs, int p = 0, int sum = 0) { 6 if (sum == maxsum) return 1 << (n - p); 7 if (n == p) return 0; 8 assert(p < n); 9 return dfs(dfs, p + 1, sum) + dfs(dfs, p + 1, sum | nums[p]); 10 }; 11 return dfs(dfs); 12 } 13 };
20220316 432. 全 O(1) 的数据结构
方法:双向链表+哈希
1 class AllOne { 2 list<pair<unordered_set<string>, int>> l; 3 unordered_map<string, list<pair<unordered_set<string>, int>> :: iterator> mp; 4 public: 5 AllOne() {} 6 7 void inc(string key) { 8 if (mp.count(key)) { 9 auto cur = mp[key], nxt = next(cur); 10 if (nxt == l.end() || nxt->second > cur->second + 1) { 11 unordered_set<string> s({key}); 12 mp[key] = l.emplace(nxt, s, cur->second + 1); 13 } 14 else { 15 nxt->first.emplace(key); 16 mp[key] = nxt; 17 } 18 cur->first.erase(key); 19 if (cur->first.empty()) 20 l.erase(cur); 21 } else { 22 if (l.empty() || l.begin()->second > 1) { 23 unordered_set<string> s({key}); 24 l.emplace_front(s, 1); 25 } else { 26 l.begin()->first.emplace(key); 27 } 28 mp[key] = l.begin(); 29 } 30 } 31 32 void dec(string key) { 33 auto cur = mp[key]; 34 if (cur->second == 1) { 35 mp.erase(key); 36 } else { 37 auto pre = prev(cur); 38 if (cur == l.begin() || pre->second < cur->second - 1) { 39 unordered_set<string> s({key}); 40 mp[key] = l.emplace(cur, s, cur->second - 1); 41 } else { 42 pre->first.emplace(key); 43 mp[key] = pre; 44 } 45 } 46 cur->first.erase(key); 47 if (cur->first.empty()) { 48 l.erase(cur); 49 } 50 } 51 52 string getMaxKey() { 53 return l.empty() ? "" : *l.rbegin()->first.begin(); 54 } 55 56 string getMinKey() { 57 return l.empty() ? "" : *l.begin()->first.begin(); 58 } 59 };
20220317 720. 词典中最长的单词
方法一:排序+哈希
1 class Solution { 2 public: 3 string longestWord(vector<string>& words) { 4 sort(words.begin(), words.end(), [](const string &a, const string &b){ 5 return a.size() == b.size() ? a > b : a.size() < b.size(); 6 }); 7 string ans = ""; 8 unordered_set<string> s; 9 s.emplace(""); 10 for (auto & word : words) { 11 if (s.count(word.substr(0, word.size() - 1))) { 12 s.emplace(word); 13 ans = word; 14 } 15 } 16 return ans; 17 } 18 };
方法二:Trie 字典树
1 class Trie { 2 vector<Trie*> child; 3 bool isEnd; 4 public: 5 Trie() { 6 this->child = vector<Trie*>(26, nullptr); 7 this->isEnd = 0; 8 } 9 void insert(const string& word) { 10 Trie* node = this; 11 for (const auto &ch : word) { 12 int o = ch - 'a'; 13 Trie* nxt = node->child[o]; 14 if (node->child[o] == nullptr) 15 node->child[o] = new Trie(); 16 node = node->child[o]; 17 } 18 node->isEnd = 1; 19 } 20 bool dfs(const string& word) { 21 Trie* node = this; 22 for (const auto &ch : word) { 23 int o = ch - 'a'; 24 if (node->child[o] == nullptr || !node->child[o]->isEnd) 25 return 0; 26 node = node->child[o]; 27 } 28 return node != nullptr && node->isEnd; 29 } 30 }; 31 32 class Solution { 33 public: 34 string longestWord(vector<string>& words) { 35 Trie t; 36 for (const auto &word : words) 37 t.insert(word); 38 string ans = ""; 39 int mx = 0; 40 for (const auto &word : words) { 41 int l = word.size(); 42 if (t.dfs(word)) 43 if (l > mx || l == mx && word < ans) 44 ans = word, mx = l; 45 } 46 return ans; 47 } 48 };
中间因为准备毕设和复试中断了一段时间,现在恢复保护现场。
20220329 2024. 考试的最大困扰度
方法:双指针法
和第 3 题差不多,完全不需要用到 DP。
1 class Solution { 2 public: 3 int work(string s, int k) { 4 int l = 0, r = 0, ans = 0, o = 0, len = s.length(); 5 while (r < len) { 6 while (s[r] == 'T') r++; 7 while (s[r] == 'F' && o < k) 8 r++, o++; 9 ans = max(ans, r - l); 10 while (o == k && s[r] == 'F') 11 if (s[l] == 'F') l++, o--; 12 else while (s[l] == 'T') l++; 13 } 14 return ans; 15 } 16 int maxConsecutiveAnswers(string s, int k) { 17 int len = s.length(); 18 string t = s; 19 for (int i = 0; i < len; i++) 20 t[i] = (s[i] == 'F' ? 'T' : 'F'); 21 return max(work(s, k), work(t, k)); 22 } 23 };
20220330 1606. 找到处理最多请求的服务器
方法一:模拟
直接按题意要求,从第 i % k 个服务器开始依次查询是否繁忙,同时维护每个服务器处理请求个数,时间复杂度为 O(n * k),不满足题意。
方法:优先队列
模拟过程有个问题在于,每次会扫描所有服务器判断是否繁忙,重复操作相当多;
考虑将所有当前繁忙的服务器放入一个优先队列 priority_queue,按照处理结束时间从早到晚排序,每次接收一个新处理时,将所有结束时间早于该新处理开始时间的服务器取出,再放入一个空闲服务器的 set,再从 set 中取出大于 i % k 的服务器(主要服务器是环形,还要考虑绕一圈从服务器 0 开始的情况),这样每次查询都只有 log 的复杂度了,最后时间复杂度为 O(n log n) 或 O(n log k);
一道不错的 STL 综合运用。
1 class Solution { 2 public: 3 vector<int> busiestServers(int k, vector<int>& a, vector<int>& l) { 4 set<int> avail; 5 vector<int> tot(k), ans; 6 priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> busy; 7 int n = a.size(); 8 for (int i = 0; i < k; i++) 9 avail.insert(i); 10 for (int i = 0; i < n; i++) { 11 while (!busy.empty() && busy.top().first <= a[i]) 12 avail.insert(busy.top().second), busy.pop(); 13 if (avail.empty()) 14 continue; 15 auto o = avail.lower_bound(i % k); 16 if (o == avail.end()) 17 o = avail.begin(); 18 tot[*o]++; 19 busy.emplace(a[i] + l[i], *o); 20 avail.erase(o); 21 } 22 int mx = *max_element(tot.begin(), tot.end()); 23 for (int i = 0; i < k; i++) 24 if (tot[i] == mx) 25 ans.push_back(i); 26 return ans; 27 } 28 };
20220331 728. 自除数
1 class Solution { 2 public: 3 vector<int> selfDividingNumbers(int l, int r) { 4 vector<int> ans; 5 for (int i = l; i <= r; i++) { 6 int o = i, x = i, nob = 0; 7 while (x) { 8 if (!(x % 10) || o % (x % 10)) { 9 nob = 1; 10 break; 11 } 12 x /= 10; 13 } 14 if (!nob) ans.push_back(i); 15 } 16 return ans; 17 } 18 } s;