记录一些有意思的题。
经典算法
二分搜索
二分搜索的加强版
算是非常经典又非常复杂的一道题了。我的算法和官方题解稍有不同,可以康康(我的题解):
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 findNum(const vector<int>& nums1, const vector<int>& nums2, int seq) { int left = 0, right = nums1.size() - 1; while (left <= right) { int mid = (left + right) / 2; int mid_2 = seq - mid; if (mid_2 < 0 || (mid_2 < (int)nums2.size() && nums2[mid_2] < nums1[mid])) { right = mid - 1; } else if (mid_2 - 1 >= (int)nums2.size() || (mid_2 - 1 >= 0 && nums2[mid_2 - 1] > nums1[mid])) { left = mid + 1; } else return nums1[mid]; }
return findNum(nums2, nums1, seq); } double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) { int n1 = nums1.size(), n2 = nums2.size(); if ((n1 + n2) % 2 == 1) return findNum(nums1, nums2, (n1 + n2) / 2); else return (findNum(nums1, nums2, (n1 + n2) / 2) + findNum(nums1, nums2, (n1 + n2) / 2 - 1)) / 2.0; } };
|
线性数据结构
时间线、区间调度问题
这类问题一般是给定很多时间区间(区间的起始时间、终止时间),讨论一定的问题。
这类问题一般可以用贪心/动态规划解,贪心之前需要进行排序。
求覆盖完整个范围所需子区间的最小值。
结论:按结束时间降序排序,然后从前往后遍历(遍历的时候需要同时考虑其结束时间是否超过了当前开始时间、其开始时间是否比下一次的开始时间更远)。
结论:按开始时间升序排序,能够合并的区间一定连续。
还有一种巧妙的做法,注意到线段重合等价于 a[1] >= b[0] || b[1] >= a[0]
,于是我们重载 set
的比较类:
1 2 3 4 5 6
| struct Cmp { bool operator() (const vector<int>& a, const vector<int>& b) const { return a[1] < b[0]; } };
|
用这个比较类定义的 set 中,如果两个线段重合,这两个线段在 set 中是被认为相等的!
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
| struct Cmp { bool operator() (const vector<int>& a, const vector<int>& b) const { return a[1] < b[0]; } };
class Solution { public: vector<vector<int>> merge(vector<vector<int>>& intervals) { set<vector<int>, Cmp> s; for (auto interval : intervals) { auto iter = s.find(interval); while (iter != s.end()) { interval[0] = min(interval[0], (*iter)[0]); interval[1] = max(interval[1], (*iter)[1]); s.erase(iter); iter = s.find(interval); } s.insert(interval); } return vector<vector<int>>(s.begin(), s.end()); } };
|
并查集
树
树的遍历(非递归)
先序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| vector<int> preorderTraversal(TreeNode* root) { vector<int> ans; stack<TreeNode*> s; s.push(root); while(!s.empty()) { TreeNode * cur = s.top(); s.pop(); if (cur == nullptr) continue; ans.push_back(cur->val); s.push(cur->right); s.push(cur->left); } return ans; }
|
中序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| vector<int> inorderTraversal(TreeNode* root) { vector<int> ans; stack<TreeNode*> s; TreeNode* p = root; while(p != nullptr) { s.push(p); p = p->left; } while(!s.empty()) { p = s.top(); s.pop(); ans.push_back(p->val); p = p->right; while(p != nullptr) { s.push(p); p = p->left; } } return ans; }
|
后序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
vector<int> postorderTraversal(TreeNode* root) { list<int> ans_reverse; vector<int> ans; stack<TreeNode*> s; s.push(root); while(!s.empty()) { TreeNode * p = s.top(); s.pop(); if (p == nullptr) continue; ans_reverse.push_front(p->val); s.push(p->left); s.push(p->right); } for (int i: ans_reverse) ans.push_back(i); return ans; }
|
统一写法
还有一种写法,统一了三种遍历,只需改变入栈的顺序。
链接:https://leetcode-cn.com/problems/binary-tree-postorder-traversal/comments/495211
动态规划
动态规划的题,emmm 做多了就明白套路了,做少了的话,还是多看答案,然后自己写吧。注意勤复习。
两人博弈取石子,dp[i][j]
指取完 i
到 j
(闭区间)的石子后,先手比后手高的分数。dp 转移方程中由于有先后手的交换,会有 dp[][] = piles[] - dp[][]
的出现。
NOI 1995 石子合并,非常经典的区间 dp。dp[i][j]
表示将 i
到 j
(闭区间)的石子合并为一堆所需的最小总代价,而 for
中还需要一维表示在哪里合并。
类似于合并石子的区间 dp,但注意由于这道题的 dp 转移方程中包含两端的气球值,所以 dp 转移方程有些不同,但同样是需要三个 for
的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int maxCoins(vector<int>& nums) { const int n = nums.size(); nums.insert(nums.begin(), 1); nums.push_back(1); vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0)); for (int len = 0; len < n; len++) for (int start = 1, end = start + len; end <= n; start++, end++) for (int i = start; i <= end; i++) dp[start][end] = max( dp[start][end], dp[start][i - 1] + dp[i + 1][end] + nums[start - 1] * nums[i] * nums[end + 1] ); return dp[1][n]; }
|
一道印象很深的题,因为题面非常直观。
题面很像接雨水。
另外 85. 最大矩形 也用到了这个题的算法。
数学题
线性筛
204. 计数质数
每个合数 t
只会在 j=t最大的质因数, i=t/j
时被筛掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Solution { public: int countPrimes(int n) { vector<bool> is_prime(n, 1); vector<int> primes; for (int i = 2; i < n; i++) { if (is_prime[i]) primes.push_back(i); for (int j : primes) { if (i*j >= n) break; is_prime[i*j] = 0; if (i % j == 0) break; } } return primes.size(); } };
|
不过面试一般不会要求这么高吧,会基础的埃氏筛应该就可以了;而且
巧妙的算法
置换法
实现上需要注意细节,如需要使用 while
反复交换(我第一次使用的是 if
);交换的终止条件是 arr[n] != arr[arr[n]-1]
而不是 n != arr[n]-1
(后者会被 [1, 1]
这种数组卡掉)。
这个题虽然可以写一堆 if
,但是官方题解的有限自动机让我眼前一亮。有空一定实现一次。
巧妙的 C++ 语法
std::stoi
C 提供 int atoi(const char *)
,而对于 string 类型,想要使用 aoti
只能使用 const char * string::c_str()
方法。不过,也可以使用 int std::stoi(const string &)
:
1 2
| string str = "-233"; atoi(str.c_str()) == stoi(str);
|
string::substr
1
| string substr (size_t pos = 0, size_t len = npos) const;
|
总是要忘记它的定义。string::npos
的值为从 pos
到字符串最后的长度。
集合的交集
349. 两个数组的交集
Python 版本就非常简洁:
1 2
| def intersection(nums1, nums2): return list(set(nums1) & set(nums2))
|
没想到 C++ 也自带集合求交,见set-集合运算。
巧妙的数据结构
单调栈
简单、好用的数据结构。
数据结构上,递减栈为从栈底往栈顶依次递减的栈(栈里可存元素,也可以存数组下标)。算法上,一个数入栈前,将小于等于它的数全部出栈。
对于某些要计算“某数后面第一个比他大/小的数”的题目,有奇效,如 739. 每日温度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Solution { public: vector<int> dailyTemperatures(vector<int>& T) { stack<int> s; vector<int> ans(T.size()); for (int i = T.size() - 1; i >= 0; i--) { while(!s.empty() && T[s.top()] <= T[i]) { s.pop(); } ans[i] = s.empty() ? 0 : s.top() - i; s.push(i); } return ans; } };
|
单调队列
和单调栈相对应的就是单调队列了。
数据结构上,递减队列为从队首往队尾依次递减的双向队列(队列里存数组下标),其本质是类似于单调栈,但是由于题目常常加了一个长度限制,所以也需要让元素从前面出队。
算法上,一个数 i
入队列前,
- (如果有,)将超出边界的队首元素出队
pop_front()
;
- 将小于等于
i
的队尾元素全部出队 pop_back()
。
计算滑动窗口(一个固定大小的区间)的最值有奇效,如 239. 滑动窗口最大值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Solution { public: vector<int> maxSlidingWindow(vector<int>& nums, int k) { deque<int> q; vector<int> ans; for (int i = 0; i < nums.size(); i++) { if (!q.empty() && q.front() <= i - k) q.pop_front(); while (!q.empty() && nums[q.back()] <= nums[i]) q.pop_back(); q.push_back(i); if (i >= k - 1) ans.push_back(nums[q.front()]); } return ans; } };
|
双堆:数据流中位数
对于求数据流/滑动窗口的题目,可以维护 大顶堆放小一半的数 + 小顶堆放大一半的数(保证两堆大小相等或大顶堆比小顶堆大 1),两堆的顶就是中位数。
对于滑动窗口,由于有删除的需求,可以使用 multiset
替代堆。
在维护左右大小的时候,如果分情况讨论会非常复杂,不如 push/remove 的时候先保证两堆大小相等或大顶堆比小顶堆大 1
,然后再检查是否符合大顶小于小顶
,不符合就交换。这样思路更清晰,代价时间复杂度的常数变大了。
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
| class MedianFinder { public: priority_queue<int> big_heap; priority_queue<int, vector<int>, greater<int>> small_heap; void swap_top() { int b = big_heap.top(); big_heap.pop(); int s = small_heap.top(); small_heap.pop(); big_heap.push(s); small_heap.push(b); }
void addNum(int num) { if (big_heap.size() == small_heap.size()) big_heap.push(num); else small_heap.push(num); assert(big_heap.size() == small_heap.size() || big_heap.size() == small_heap.size() + 1);
if (small_heap.size() > 0 && big_heap.top() > small_heap.top()) swap_top(); assert(small_heap.empty() || big_heap.top() <= small_heap.top()); } double findMedian() { if (big_heap.size() == small_heap.size()) return ((double)big_heap.top() + (double)small_heap.top()) / 2; else return big_heap.top(); } };
|
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
| class MedianFinder { public: multiset<int, greater<int>> big_heap; multiset<int> small_heap; void make_balance() { if (small_heap.size() > 0 && *big_heap.begin() > *small_heap.begin()) { big_heap.insert(big_heap.end(), *small_heap.begin()); small_heap.insert(small_heap.end(), *big_heap.begin()); big_heap.erase(big_heap.begin()); small_heap.erase(small_heap.begin()); } assert(small_heap.empty() || *big_heap.begin() <= *small_heap.begin()); }
void insert(int num) { if (big_heap.size() == small_heap.size()) big_heap.insert(num); else small_heap.insert(num); assert(big_heap.size() == small_heap.size() || big_heap.size() == small_heap.size() + 1);
make_balance(); }
void remove(int num) { if (num > *big_heap.begin()) { assert(small_heap.find(num) != small_heap.end()); small_heap.erase(small_heap.find(num)); if (big_heap.size() - 2 == small_heap.size()) { small_heap.insert(small_heap.end(), *big_heap.begin()); big_heap.erase(big_heap.begin()); } } else { assert(big_heap.find(num) != big_heap.end()); big_heap.erase(big_heap.find(num)); if (big_heap.size() + 1 == small_heap.size()) { big_heap.insert(big_heap.end(), *small_heap.begin()); small_heap.erase(small_heap.begin()); } } assert(big_heap.size() == small_heap.size() || big_heap.size() == small_heap.size() + 1);
make_balance(); } double findMedian() { if (big_heap.size() == small_heap.size()) return ((double)*big_heap.begin() + (double)*small_heap.begin()) / 2; else return *big_heap.begin(); } };
class Solution { public: vector<double> medianSlidingWindow(vector<int>& nums, int k) { MedianFinder mf; vector<double> ans; for (int i = 0; i < k; i++) mf.insert(nums[i]); ans.push_back(mf.findMedian()); for (int window = 0; window + k < nums.size(); window++) { mf.remove(nums[window]); mf.insert(nums[window+k]); ans.push_back(mf.findMedian()); } return ans; } };
|
最后更新时间: