思路小集
重看算法总发现很多问题曾经知道但思路难以立刻闪现,终是少记了思路的“啊哈”点。
试着把散落的算法思路再遇到时记下来,先乱糟糟地记着吧,记得多了思路间就有了关联。
—— Yang Le
一些参考:
- ita - Introduction to Algorithm, 2nd
- pp - Programming Perals, 2nd
- ctci - Crack the Coding Interview, 6th
比较和的阶数,哪个大取哪个,一样大乘上个;
取的阶需要:加号左边代入后的 < 右边的
泰勒级数,知道函数在某点的导数值,就能将函数在这点处展开成多项式
比如在0点展开:
在a点展开:
二阶递推式 ,先构造再求参数。
假设 ,,设,,有,。
叫做特征方程,解方程得特征根和,将可以用和表示成。代入初始值和,求得c和d。
Catalan数:
比如n对括号的加括号方式数
用杨辉三角计算组合数nCk
vector<vector<int>> C(N, vector<int>(N));
C[0][0] = 1;
for (int i = 1; i < N; i++) {
C[i][0] = 1;
for (int j = 1; j <= i; j++) {
C[i][j] = C[i-1][j-1] + C[i-1][j]; // 等于上、上左的和
}
}
调和级数:
Sprague-Grundy
Follow(x):x的下一状态集合
mex, minimum excluded value: mex(list)指没被包含在list中的最小非负数。如 mex({2,4,5,6})=0,mex({0,1,2,6})=3
SG数:SG(x) = mex({ SG(y), y∈Follow(x) }),从x可达各状态的SG值集的mex。这是个递归定义,递归基础是SG(0)=0。
Sprague-Grundy定理:大游戏的SG值等于各子游戏SG值的异或和。SG(x1 . . .xn) = SG(x1) ⊕ . . . ⊕ SG(xn)
单堆的nim有x个球,每步可以取1到x个,下一步状态集合Follow(x)=[0..x-1]个。从递归基础SG(0)=0一步步反推:SG(1) = mex({SG(0)}) = 1,SG(2) = mex({SG(0), SG(1)}) = mex({0,1}) = 2,...,所以单堆的nim游戏SG(x)=x,即单堆的nim-sum为球数x。
n堆的nim,可以认为是n个 单堆的子游戏,大游戏的SG值是各子游戏SG值的异或和。
n堆的nim游戏
两人轮流、两人待遇相同(impartial)、没有平局的游戏,先手赢(normal play)的策略:player1每步都使SG值为0。
两人待遇相同(impartial):两人的差别只在player1先走。黑白棋、象棋等不是impartial,因为只能动自己的棋子、不能动别人的。
先手赢(normal play):自己走最后一步,让别人无步可走。
In normal play, the winning strategy is to finish every move with a nim-sum of 0. This is always possible if the nim-sum is not zero before the move.
To find out which move to make, let X be the nim-sum of all the heap sizes. Find a heap where 'the-heap-size ⊕ X = new-heap-size < heap-size' - the winning strategy is to play in such a heap, reducing that heap to the 'new-heap-size'. In the example above, taking the nim-sum of the sizes is X = 3 ⊕ 4 ⊕ 5 = 2. The nim-sums of the heap sizes A=3, B=4, and C=5 with X=2 are
A⊕X= 3⊕2 = 1 [Since (011)⊕(010) = 001 ] B⊕X= 4⊕2 = 6 C⊕X= 5⊕2 = 7The only heap that is reduced is heap A, so the winning move is to reduce the size of heap A to 1 (by removing two objects).
要在节点前插入,可以使用dummy节点,或者使用指向指针的指针;如果两种都不用,只能特殊处理头节点前的插入。
可以使用指向指针的指针,是因为虽然遍历时修改了指针的值,但指针的地址不变,前一个节点仍能指向修改后的指针。
记住要更新头指针,修改链表的函数或者返回新的头指针,或者更新头指针的指针。
使用`while (node->next)`循环的情况:
// 停在尾节点
while (node->next) {
node = node->next;
}
// 删除节点
while (node->next) {
// 循环里node->next是当前节点,node是前一节点
...
}
链表排序
// 插入排序
ListNode* insertionSortList(ListNode* head) {
ListNode dummy(-1);
while (head) {
auto next = head->next;
auto prev = &dummy;
while (prev->next && head->val >= prev->next->val) {
prev = prev->next;
}
// 循环结束时找到插入位置
head->next = prev->next;
prev->next = head;
head = next;
}
return dummy.next;
}
反转链表
把节点一个个取出,插入另一个链表头。观察循环中的赋值顺序,刚好就是个轮转:head->next => list => head => next。
ListNode* reverseList(ListNode* head) {
ListNode *list = NULL;
while (head) {
auto next = head->next;
head->next = list;
list = head;
head = next;
}
return list;
}
删除倒数第n个节点
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode dummy(-1); // 可能删除头节点
dummy.next = head;
auto fast = &dummy;
// 找倒数第n节点前一节点:
// fast先走n步、后面while(fast->next)是唯一正确写法,可以配合!fast检测n值合法
// <del>fast先走n+1步、后面while(fast)的写法,若配合!fast检测n值会出错</del>
for (int i = 0; i < n && fast; i++) fast = fast->next;
if (!fast) return head;
auto slow = &dummy;
while (fast->next) { // fast最后停在尾节点
fast = fast->next;
slow = slow->next;
}
// slow是待删除节点的前一节点
auto next = slow->next->next;
delete slow->next;
slow->next = next;
return dummy.next;
}
快慢指针
- https://leetcode.com/problems/middle-of-the-linked-list/
- https://leetcode.com/problems/palindrome-linked-list/
- https://leetcode.com/problems/convert-sorted-list-to-binary-search-tree/
前进条件while (fast && fast->next) { ... },不管fast初始走没走一步,slow在链表长奇数时总指向正中间节点。
若fast初始先走一步(记这种写法),slow在链表长偶数时指向前半段尾节点(由两节点情况推得)。由单节点情况可知,fast==NULL可判断链表奇数长。
if (!head) return;
auto fast = head->next, slow = head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
}
若fast初始没有先走,slow在链表长偶数时指向后半段头节点(由两节点情况推得)。由单节点情况可知,fast!=NULL可判断链表奇数长。比fast初始先走写法麻烦在:如需前半段尾节点,比如需要切断链表,得用个prev保存最近slow。
if (!head) return;
auto fast = head, slow = head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
}
链表环的入口点
先快慢指针,若fast、slow相遇则有环;相遇后让一指针回到链表头,再同速跑,再相遇处就是入口点。
这是因为,假设链表头到入口点要a步,入口点到相遇点要b步,slow进环后不到一圈就被fast追上,从开始到相遇slow走了a+b步,fast走了a+b+k*LoopSize=2*(a+b)步,a=k*LoopSize-b。一指针回到链表头再同速跑,再跑a=k*LoopSize-b步,环中相当于倒退b步,再相遇处就是入口点。
ListNode *detectCycle(ListNode *head) {
auto fast = head, slow = head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) { // 相遇,再同速跑
fast = head;
while (fast != slow) {
fast = fast->next;
slow = slow->next;
}
return fast;
}
}
return NULL;
}
扩展:快慢指针应用在数组
两链表的交点
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
// pa跑完链表A跑链表B,pb跑完链表B跑链表A,
// 最终跑到同一交点(有交点到交点,无交点到NULL节点)
auto pa = headA, pb = headB;
while (pa != pb) {
pa = pa ? pa->next : headB;
pb = pb ? pb->next : headA;
}
return pa;
}
链表右旋k步
不用"一指针先走k步、再两指针同步走"的办法,同样的指针移动操作数,逻辑更复杂。
ListNode* rotateRight(ListNode* head, int k) {
if (!head) return NULL;
// 先变循环链表
auto tail = head;
int len = 1;
while (tail->next) {
tail = tail->next;
len++;
}
tail->next = head;
// 再断开
k %= len;
for (int i = 0; i < len - k; i++)
tail = tail->next;
auto newHead = tail->next;
tail->next = NULL;
return newHead;
}
在循环有序链表中插入
ListNode* insert(ListNode* node, int x) {
if (!node) {
auto nn = new ListNode(x);
nn->next = nn;
return nn;
}
// 能否在某位置后插入:
// 以node为头的话,循环中只判断非尾的之前位置;
// 这些位置都不行时,通过画图例可知,尾位置总是可行。
auto p = node;
while (p->next != node) {
if (p->val <= x && x <= p->next->val) break;
if (p->val > p->next->val && (x >= p->val || x <= p->next->val)) break;
p = p->next;
}
// 这时在p后插入
auto nn = new ListNode(x);
nn->next = p->next;
p->next = nn;
return nn;
}
链表按层展平
链表展平:遍历第一层,每遇到子链表就把它接到尾指针的后面,这样随着尾指针后移就按层展平了。
取消展平:链表展平并没有丢失父子关系,只是增加了前后关系,只要取消这些前后关系。可以反向遍历,把子节点指针所指的与它前面一个分离。
将组内第[2..k]元素依次插入到"上一组尾"prev之后。
子段反转
- https://leetcode.com/problems/reverse-linked-list-ii/
ListNode* reverseBetween(ListNode* head, int m, int n) {
if (!head) return NULL;
ListNode dummy(-1);
dummy.next = head;
auto prev = &dummy;
for (int i = 1; i < m; i++) {
prev = prev->next;
}
// 组头是反转后的组尾,将[m+1..n]的节点插入prev之后
auto newtail = prev->next, curr = newtail->next;
for (int i = m + 1; i <= n; i++) {
newtail->next = curr->next;
curr->next = prev->next;
prev->next = curr;
curr = newtail->next;
}
return dummy.next;
}
k个元素一组地反转
- https://leetcode.com/problems/reverse-nodes-in-k-group/
ListNode* reverseKGroup(ListNode* head, int k) {
int len = 0;
for (auto p = head; p; p = p->next) len++;
ListNode dummy(-1);
dummy.next = head;
auto prev = &dummy; // 组前的指针
for (; len >= k; len -= k) {
// 组头是反转后的组尾newtail,将组内第[2..k]元素插入到prev之后
auto newtail = prev->next, curr = newtail->next;
for (int i = 2; i <= k; i++) {
newtail->next = curr->next;
curr->next = prev->next;
prev->next = curr;
curr = newtail->next;
}
prev = newtail;
}
return dummy.next;
}
链表数加上1
Given a non-negative number represented as a singly linked list of digits, plus one to the number.
The digits are stored such that the most significant digit is at the head of the list.
Example:
Input: 1->2->3 Output: 1->2->4
ListNode* plusOne(ListNode* head) {
auto dummy = new ListNode(0);
dummy->next = head;
// 找最后一个!=9(不进位)的位置
ListNode *found;
for (auto p = dummy; p; p = p->next) {
if (p->val != 9) found = p;
}
// found位置加1,后面位置全变0
found->val += 1;
for (auto p = found->next; p; p = p->next) {
p->val = 0;
}
// 如果dummy后全是9,found=dummy,dummy->val==1
if (dummy->val != 0) return dummy;
auto ans = dummy->next;
delete dummy;
return ans;
}
可取最大值的栈
再用个maxs栈保存当前nums栈的最大值。
class MaxStack {
stack<int> nums;
stack<int> maxs;
public:
/** initialize your data structure here. */
MaxStack() {
}
void push(int x) {
if (maxs.empty() || x >= maxs.top()) maxs.push(x);
nums.push(x);
}
int pop() {
if (nums.top() == maxs.top()) maxs.pop();
int ans = top(); nums.pop();
return ans;
}
int top() {
return nums.top();
}
int peekMax() {
return maxs.top();
}
int popMax() {
int ans = peekMax();
stack<int> buffer;
while (top() != ans) {
buffer.push(pop());
}
maxs.pop();
nums.pop();
while (!buffer.empty()) {
push(buffer.top());
buffer.pop();
}
return ans;
}
};
扩展:可取最大值的队列
用到最大值放队头的"单调递减队列",见单调队列
频率栈
按频率分桶,类似O(1)时间修改问题,但频率增加时小频率项保留
class FreqStack {
// 每个频率一个桶;"有多个最大频率时,后进先出",所以该桶是栈,栈中存放该频率的数
unordered_map<int, stack<int>> buckets; // freq=>stack
unordered_map<int, int> freq; // num=>freq
int maxfreq = 0;
public:
FreqStack() {
}
void push(int x) {
buckets[++freq[x]].push(x);
if (freq[x] > maxfreq) maxfreq = freq[x];
}
int pop() {
int x = buckets[maxfreq].top(); buckets[maxfreq].pop();
freq[x]--;
if (buckets[maxfreq].empty()) maxfreq--;
return x;
}
};
只用栈操作,把栈排序成栈顶最小的递减序列
void sort(stack<int> &A) {
// 先把B排成递增序列 <=> 找下一个更小的数
stack<int> B;
while (!A.empty()) {
int a = A.top(); A.pop();
while (!B.empty() && a < B.top()) {
A.push(B.top());
B.pop();
}
B.push(a);
}
// 把B倒回A
while (!B.empty()) {
A.push(B.top());
B.pop();
}
}
单调栈,对应博弈论中消除被占优选项后的未被占优选项(undominated)。
递减栈 <=> 找波谷localMin <=> 数组中下一个更大的数
找下一个更大的数,当前数>栈顶时就弹出栈顶,弹不动了再入栈。当前数入栈时<=栈顶,栈中是个递减序列,栈顶是最小值。弹出数是被占优的数,表示找到了"下一个更大的数"。栈中数是未被占优的数,表示还没有找到“下一个更大的数"。
考查当前数、弹出数、新栈顶三个数:
- 新栈顶>=弹出数<当前数,弹出数是波谷、局部最小值。
- 弹出数<当前数,这是硬约束。若继续弹出,弹出数是不断"抬高"的波谷,最后的弹出数是左侧<当前数的最大值。新栈顶会变大,最后新栈顶是>=当前数的最小值。即紧邻的三个数:最后弹出数 < 当前数 <= 最后新栈顶,半开半闭。对应下面的代码,
注:正常在当前数>栈顶时弹出,留下(非严格)单调递减栈(相等数都留下);若当前数>=栈顶时弹出,留下严格单调递减栈(相同数留下最右边的);偶尔,若要相同数留下最左边的,那么:1. 当前数>栈顶时弹出;2. 当前数!=栈顶才push()入栈。
- https://leetcode.com/problems/next-greater-element-i/
- https://leetcode.com/problems/online-stock-span/
- https://leetcode.com/problems/daily-temperatures
vector<int> dailyTemperatures(vector<int>& T) {
const int N = T.size();
vector<int> ans(N, 0);
stack<int> stk;
for (int i = 0; i < N; i++) {
while (!stk.empty() && T[i] > T[stk.top()]) {
int pop = stk.top(); stk.pop();
ans[pop] = i - pop;
}
stk.push(i);
}
return ans;
}
循环数组中下一个更大的数
循环数组的通用处理是把数组拼两次
vector<int> nextGreaterElements(vector<int>& nums) {
const int N = nums.size();
vector<int> ans(N, -1);
stack<int> stk; // 找下一个更大元素,保存下标
// 循环数组,查找两遍nums[]
for (int i = 0; i < 2 * N; i++) {
while (!stk.empty() && nums[i % N] > nums[stk.top()]) {
ans[stk.top()] = nums[i % N];
stk.pop();
}
if (i < N) stk.push(i); // 前N个数才找
}
return ans;
}
132模式
- https://leetcode.com/problems/132-pattern/
bool find132pattern(vector<int>& nums) {
// 132模式:顺序三个数a1、a2、a3,a1<a3<a2。
// 对于第二个数a2,在右边找下一个更小的数a3,再看左边是否有数<a3。
// a2右边下一个更小的数a3 <=等价于=> 从右往左,a3找下一个更大的数a2
const int N = nums.size();
int a3 = INT_MIN;
stack<int> stk;
for (int i = N - 1; i >= 0; i--) {
if (nums[i] < a3) return true; // i--后进新循环,nums[i]是a1
while (!stk.empty() && nums[i] > stk.top()) { // nums[i]是a2
a3 = stk.top();
stk.pop();
}
stk.push(nums[i]);
}
return false;
}
延伸:123模式简单得多
bool increasingTriplet(vector<int>& nums) {
// 从左到右扫描,若开始设置第三小,说明前面已有min1和min2
int min1 = INT_MAX, min2 = INT_MAX;
for (int num : nums) {
if (num <= min1) {
min1 = num;
} else if (num <= min2) {
min2 = num;
} else {
return true;
}
}
return false;
}
数组对应的最小代价树
int mctFromLeafValues(vector<int> &arr) {
// 题目:数组对应叶节点值的中序遍历,内节点代价=左右子树最大叶节点之积,
// 总代价=内节点代价之和,求所有树结构中的最小总代价。
// 转化:每次从两个邻居节点a和b中删除min(a,b),代价为a*b,
// 求所有删除顺序中代价最小的。
// 每次删除局部最小值(波谷)
int ans = 0;
stack<int> stk;
stk.push(INT_MAX); // 左哨兵
for (int val : arr) {
while (!stk.empty() && val > stk.top()) {
int localMin = stk.top();
stk.pop();
ans += localMin * min(stk.top(), val);
}
stk.push(val);
}
// 递减栈,栈顶是最小值
while (stk.size() > 2) {
int localMin = stk.top();
stk.pop();
ans += localMin * stk.top();
}
return ans;
}
柱状图储水问题
递增栈 <=> 找波峰localMax <=> 数组中下一个更小的数
同样有结论:
- 弹出数是波峰、局部最大值
- 最后弹出数是左侧>当前数的最小值
- 紧邻的三个数:最后弹出数 > 当前数 >=最后新栈顶,半开半闭
柱状图中的最大矩形
最大矩形找"波峰",刚好形状相似。储水问题找“波谷”,刚好也形状相似。
参见:全是1的子矩阵
最远正序对
int maxWidthRamp(vector<int>& A) {
// 找相距最远的两个递增数
// 对第一个数,若A[i]<A[j],A[j]、A[k]递增,则A[i]、A[k]是更远的递增,
// A[i]占优于A[j],即几何上的左下点占优,所以从左到右扫描,<栈顶的占优
// 对第二个数,若A[j]<A[k],A[i]、A[j]递增,则A[i]、A[k]是更远的递增,
// A[k]占优于A[j],即几何上的右上点占优,所以从右往左扫描,>栈顶的占优
// 两指针法
const int N = A.size();
stack<int> stk;
for (int i = 0; i < N; i++) {
if (stk.empty() || A[i] < A[stk.top()]) { // 第一个数的占优选择
stk.push(i);
}
}
int ans = 0;
for (int j = N - 1; j >= 0; j--) { // 从右往左扫描
while (!stk.empty() && A[j] >= A[stk.top()]) { // 第二个数的占优选择
ans = max(ans, j - stk.top());
stk.pop();
}
}
return ans;
}
柱状图储水问题
方法一:某bar上的储水 = min(它左侧(含)最高的bar高,它右侧(含)最高的bar高) - 它自己的bar高
int trap(vector<int>& height) {
if (height.empty()) return 0;
// 某bar上的储水 = min(它左侧(含)最高的bar高,它右侧(含)最高的bar高) - 它自己的bar高
const int N = height.size();
vector<int> leftMax(N, 0);
leftMax[0] = height[0];
for (int i = 1; i < N; i++) {
leftMax[i] = max(height[i], leftMax[i-1]);
}
vector<int> rightMax(N, 0);
rightMax[N-1] = height[N-1];
for (int i = N - 2; i >= 0; i--) {
rightMax[i] = max(height[i], rightMax[i+1]);
}
int ans = 0;
for (int i = 0; i < N; i++) {
ans += min(leftMax[i], rightMax[i]) - height[i];
}
return ans;
}
方法二:"波谷"的左右都比它高,贡献一个水层
int trap(vector<int>& height) {
// 用栈找波谷
int ans = 0;
stack<int> stk;
for (int i = 0; i < height.size(); i++) {
while (!stk.empty() && height[i] > height[stk.top()]) {
int valley = stk.top(); stk.pop();
if (stk.empty()) break;
int left = stk.top();
// "波谷"的右i和左left都比它高,贡献一个水层
int w = i - left - 1;
int h = min(height[i], height[left]) - height[valley];
ans += w * h;
}
stk.push(i);
}
return ans;
}
二维柱状图储水问题
含水高度最小的先出队处理,看它的邻居。若邻居的含水高度更小,该邻居柱上可储水,储水量为高度差。
int trapRainWater(vector<vector<int>>& heightMap) {
// 可视化解释:https://www.youtube.com/watch?v=cJayBq38VYw
// 用优先队列的遍历算法
if (heightMap.empty()) return 0;
const int R = heightMap.size(), C = heightMap[0].size();
// 队列元素int[3]: { row, col, heightWithWater }
auto cmp = [](vector<int> &a, vector<int> &b) {
return a[2] > b[2]; // 高度最小的先出队处理
};
priority_queue<vector<int>, vector<vector<int>>, decltype(cmp)> pq(cmp);
vector<vector<bool>> visited(R, vector<bool>(C, false));
// 四条边先入队
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
if (r == 0 || r == R - 1 || c == 0 || c == C - 1) {
pq.push({r, c, heightMap[r][c]});
visited[r][c] = true;
}
}
}
int ans = 0;
vector<vector<int>> dirs = { {-1, 0}, {0,1}, {1,0}, {0,-1} };
while (!pq.empty()) {
auto elem = pq.top(); pq.pop();
int r = elem[0], c = elem[1], heightWithWater = elem[2];
for (auto &dir : dirs) {
int nr = r + dir[0], nc = c + dir[1];
if (nr < 0 || nr >= R || nc < 0 || nc >= C || visited[nr][nc]) continue;
// 若邻居的高度更小,该邻居柱上可储水,储水量为高度差
int diff = heightWithWater - heightMap[nr][nc];
if (diff > 0) ans += diff;
pq.push({nr, nc, max(heightWithWater, heightMap[nr][nc])});
visited[nr][nc] = true;
}
}
return ans;
}
删除k个数字,使剩余数字串最小
- https://leetcode.com/problems/remove-k-digits
string removeKdigits(string num, int k) {
// 使剩余数字串最小,需要栈中留下递增序列 <=> 找下一个更小的数
// 在此基础上,考虑限制条件:
// 当栈内个数+剩余可压栈个数>needed时才可pop(),栈内个数<needed时才可push()
const int N = num.size(), needed = N - k;
string s;
for (int i = 0; i < N; i++) {
while (!s.empty() && num[i] < s.back() && s.size() + (N - i) > needed) { // [i,N)是剩余可压栈的数字
s.pop_back();
}
if (s.size() < needed) s.push_back(num[i]);
}
// 删除开头的0
int pos = 0;
while (pos < s.size() && s[pos] == '0') pos++;
return pos < s.size() ? s.substr(pos) : "0";
}
两个单数字数组保持组内顺序取k个数字,拼成最大的数
vector<int> maxNumber(vector<int>& nums1, vector<int>& nums2, int k) {
// 一个数组取i个数拼成最大数,另一个数组取k-i个数拼成最大数,将两个最大数归并成两数组的最大数。
// 0<=i<=M, 0<=k-i<=N => 0<=i<=M, k-N<=i<=k => max(0,k-N)<=i<=min(k,M)
vector<int> ans;
const int M = nums1.size(), N = nums2.size();
int minI = max(0, k - N), maxI = min(k, M);
for (int i = minI; i <= maxI; i++) {
auto maxNum1 = maxNumber(nums1, i);
auto maxNum2 = maxNumber(nums2, k - i);
auto merged = merge(maxNum1, maxNum2);
if (isDigitallyGreaterOrEqual(merged, 0, ans, 0)) ans = merged;
}
return ans;
}
// 单数字数组保持组内顺序取k个数字,拼成最大的数
vector<int> maxNumber(vector<int>& nums, int k) {
// 拼成最大的数:栈中留下的是递减序列 <=> 找下一个更大的数
// 取k个数:当栈内数字个数+剩余可压栈个数>k时才可pop(),栈内数字个数<k时才可push()
const int N = nums.size();
vector<int> stk;
for (int i = 0; i < N; i++) {
while (!stk.empty() && nums[i] > stk.back()
&& stk.size() + (N - i) > k) { // [i,N)是剩余可压栈的数字
stk.pop_back();
}
if (stk.size() < k) stk.push_back(nums[i]);
}
return stk;
}
vector<int> merge(vector<int> &nums1, vector<int> &nums2) {
vector<int> ans;
int size = nums1.size() + nums2.size();
for (int i = 0, j = 0, k = 0; k < size; k++) {
if (isDigitallyGreaterOrEqual(nums1, i, nums2, j)) ans.push_back(nums1[i++]);
else ans.push_back(nums2[j++]);
}
return ans;
}
// 按位比较nums1[i1..]和nums2[i2..]的大小
bool isDigitallyGreaterOrEqual(vector<int> &nums1, int i1, vector<int> &nums2, int i2) {
while (i1 < nums1.size() && i2 < nums2.size() && nums1[i1] == nums2[i2]) {
i1++;
i2++;
}
return (i2 == nums2.size() || (i1 < nums1.size() && nums1[i1] > nums2[i2]));
}
删除重复字母、使剩余串的字典序最小
string removeDuplicateLetters(string s) {
// 要使剩余串的字典序最小<=>递增栈<=>找下一个更小的数
// 限制条件:1.已入栈的不再选择 2.某字母有富余时才可pop()
vector<int> cnt(128, 0); // 各字母计数
for (char c : s) cnt[c]++;
vector<bool> picked(128, false);
string stk;
for (char c : s) {
cnt[c]--;
if (picked[c]) continue; // 条件1
while (!stk.empty() && c < stk.back()
&& cnt[stk.back()] > 0) { // 条件2
int top = stk.back(); stk.pop_back();
picked[top] = false;
}
stk.push_back(c);
picked[c] = true;
}
return stk;
}
构造笛卡尔树
以数组最大值作根、左边最大值作左子节点、右边最大值作右子节点,如此递归构造的树叫做笛卡尔树。
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
// 以最大值为根构造二叉树,正好对应问题"找下一个更大数"
// 从左往右扫描,当前数num>stk.top()时弹栈顶。
// 1. 最后的弹出数是num左边<num的最大数,是num的左子节点;
// 2. 新栈顶是num左边>num的最小数,
// => num是新栈顶右边<新栈顶的最大数,是新栈顶的右子节点。
// 最后,找下一个更大的数 => 留下递减栈 => 最大值在stk[0]
vector<TreeNode *> stk;
for (int num : nums) {
auto curr = new TreeNode(num);
while (!stk.empty() && num > stk.back()->val) {
curr->left = stk.back();
stk.pop_back();
}
if (!stk.empty()) stk.back()->right = curr;
stk.push_back(curr);
}
return !stk.empty() ? stk.front() : NULL;
}
下图示例,已构造好子问题[10 8 6 4],再插入个7:

另:若改成留下递增栈,则构造的是以子段最小值为根的笛卡尔树。
所有子段最小值的和
int sumSubarrayMins(vector<int>& A) {
// 求所有子段最小值的和。
// A[i]什么时候是子段的最小值?
// 从位置i往左扩展到下一个<A[i]的位置i1,往右扩展到下一个<A[i]的位置i2,
// 以A[i]作最小值的子段往左有(i-i1)个、往右有(i2-i)个,总共(i-i1)*(i2-i)个。
// 找下一个更小的数 <=> 找波峰,波峰两侧都是下一个<波峰的数
const int MOD = 1e9 + 7;
A.insert(A.begin(), 0); // 左右哨兵
A.insert(A.end(), 0);
int ans = 0;
stack<int> stk; // 递增栈,保存idx
for (int i2 = 0; i2 < A.size(); i2++) {
while (!stk.empty() && A[i2] < A[stk.top()]) {
int i = stk.top();
stk.pop();
int i1 = stk.top();
ans = (ans + (long)A[i] * (i - i1) * (i2 - i)) % MOD;
}
stk.push(i2);
}
return ans;
}
验证字符串是 abc 的多次嵌套
bool isValid(string S) {
// 用栈,从左到右扫,遇到c时弹出末尾的b和a
string stk;
for (char c : S) {
if (c == 'c') {
if (stk.empty() || stk.back() != 'b') return false;
stk.pop_back();
if (stk.empty() || stk.back() != 'a') return false;
stk.pop_back();
} else {
stk.push_back(c);
}
}
return stk.empty();
}
不断删除连续 k 个相同字母
string removeDuplicates(string s, int k) {
vector<pair<char, int>> stk;
for (char c : s) {
if (stk.empty() || stk.back().first != c) {
stk.push_back({c, 1});
} else if (++stk.back().second == k) {
stk.pop_back();
}
}
string ans;
for (auto &e : stk) {
ans += string(e.second, e.first);
}
return ans;
}
反转括号中的串,括号不输出
string reverseParentheses(string s) {
string ans;
stack<int> open;
for (char c : s) {
if (c == '(') {
open.push(ans.size());
} else if (c == ')') {
int pos = open.top(); open.pop();
reverse(ans.begin() + pos, ans.end());
} else {
ans += c;
}
}
return ans;
}
单调队列的写法和单调栈一样,O(n),只是使用deque。
单调队列可取当前队列的最值。功能上类似优先队列,但多了删除功能,比如超出滑动窗口长度限制时做删除。用multiset能粗略模拟单调队列,O(nlgn)。
滑动窗口的最大值
- https://leetcode.com/problems/sliding-window-maximum/
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
// 用deque实现单调队列,队头保存当前窗口最大值的下标
// 递减队列 <=> 找下一个更大的数
vector<int> ans;
deque<int> dq;
for (int i = 0; i < nums.size(); i++) {
while (!dq.empty() && nums[i] > nums[dq.back()]) {
dq.pop_back();
}
dq.push_back(i);
if (i - dq[0] + 1 > k) dq.pop_front(); // 控制住窗口长
if (i - k + 1 >= 0) ans.push_back(nums[dq[0]]);
}
return ans;
}
可取最大值的队列
class MaxQueue {
queue<int> nums;
deque<int> maxs; // 单调递减队列
public:
void push(int x) {
nums.push(x);
while (!maxs.empty() && x > maxs.back())
maxs.pop_back();
maxs.push_back(x);
}
void pop() {
if (nums.front() == maxs.front()) maxs.pop_front();
nums.pop();
}
int getMax() {
return maxs.front();
}
};
滑动窗口最大最小值相差<=limit,求最长窗口长
- https://leetcode.com/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/
int longestSubarray(vector<int>& nums, int limit) {
// 子段max(A)-min(A)<=limit
// 同时保留滑动窗口的最大最小值
// 用deque实现单调队列,O(n)
const int N = nums.size();
int ans = 0, lo = 0;
deque<int> maxq, minq;
for (int hi = 0; hi < N; hi++) {
while (!maxq.empty() && nums[hi] > maxq.back())
maxq.pop_back();
maxq.push_back(nums[hi]);
while (!minq.empty() && nums[hi] < minq.back())
minq.pop_back();
minq.push_back(nums[hi]);
while (maxq[0] - minq[0] > limit) {
if (maxq[0] == nums[lo]) maxq.pop_front();
if (minq[0] == nums[lo]) minq.pop_front();
lo++;
}
ans = max(ans, hi - lo + 1);
}
return ans;
}
或者用multiset
int longestSubarray(vector<int>& nums, int limit) {
// 子段max(A)-min(A)<=limit
// 同时保留滑动窗口的最大最小值
// 用multiset,O(nlogn)
const int N = nums.size();
multiset<int> win;
int ans = 0, lo = 0;
for (int hi = 0; hi < N; hi++) {
win.insert(nums[hi]);
while (*win.rbegin() - *win.begin() > limit) {
win.erase(win.find(nums[lo++]));
}
ans = max(ans, hi - lo + 1);
}
return ans;
}
有负数数组,和>=T的最短子段
参见同名笔记
子序列项间隔<=k,求最大子序列和
int constrainedSubsetSum(vector<int>& nums, int k) {
// 设dp[i]表示以nums[i]结尾的nums[0..i]的最大子序列和,
// dp[i] = nums[i] + max{ dp[i-k], ..., dp[i-2], dp[i-1], 0 }
// 用可取最大值的单调递减队列q,保存dp前k项[i-k..i-1]
// 初始dp[0]=nums[0]
const int N = nums.size();
vector<int> dp(N);
dp[0] = nums[0];
deque<int> q = {{dp[0]}};
int ans = dp[0];
for (int i = 1; i < N; i++) {
if (i-k-1 >= 0 && q[0] == dp[i-k-1]) q.pop_front();
dp[i] = nums[i] + max(q[0], 0);
while (!q.empty() && dp[i] > q.back()) { // 递减队列
q.pop_back();
}
q.push_back(dp[i]);
ans = max(ans, dp[i]);
}
return ans;
}
对于一些点,xi<xj,最大化(yi-xi)+(yj+xj)
int findMaxValueOfEquation(vector<vector<int>>& points, int k) {
// 因为xi<xj,要最大化(yi-xi)+(yj+xj)
// 从左往右扫描,对于特定的j点,要最大化它前面点的yi-xi
// 求[j-k..j)滑动窗口内的最大yi-xi值,用单调递减队列
int ans = INT_MIN;
deque<array<int, 2>> dq; // 保存{yi-xi, xi}
for (const auto& p : points) {
while (!dq.empty() && p[0] - dq[0][1] > k) { // xj-xi
dq.pop_front();
}
// 有效的k长窗口
if (!dq.empty()) {
ans = max(ans, dq[0][0] + p[1] + p[0]);
}
while (!dq.empty() && p[1] - p[0] > dq.back()[0]) {
dq.pop_back();
}
dq.push_back({p[1] - p[0], p[0]});
}
return ans;
}
分成一些k长连续整数
- https://leetcode.com/problems/hand-of-straights/
- https://leetcode.com/problems/divide-array-in-sets-of-k-consecutive-numbers/
bool isPossibleDivide(vector<int>& nums, int k) {
map<int, int> cnt; // num=>count
for (int num : nums) {
cnt[num]++;
}
for (auto &e : cnt) {
int count = e.second;
if (count > 0) { // [num..num+k)都减count
int num = e.first;
for (int i = 0; i < k; i++) {
cnt[num+i] -= count;
if (cnt[num+i] < 0) return false;
}
}
}
return true;
}
涉及到优先选择时用到堆,比如贪心法、k路归并、类dijkstra算法
堆的数组实现
数组x[1..n]下标从1开始(不使用x[0]),这样节点i的lchild = 2*i, rchild = 2*i+1, parent = i/2
最小堆性质:父节点<=它的子节点
关键函数siftup(n)用于在x[n]不满足堆性质时进行调整,调整只发生在i和它的父节点间,i不断向上;关键函数siftdown(n)用于在x[1]不满足堆性质时进行调整,调整只发生在i和它的较小子节点间,i不断向下。
void siftup(n) {
int i = n;
while (true) {
if (i == 1) break;
int p = i / 2;
if (x[p] <= x[i]) break;
swap(i, p);
i = p;
}
}
void siftdown(n) {
int i = 1;
while (true) {
int c = 2 * i;
if (c > n) break;
if (c + 1 <= n && x[c+1] < x[c])
c++;
if (x[i] <= x[c]) break;
swap(i, c);
i = c;
}
}
堆排序
用最大堆,将堆顶调换到数组尾、然后在去掉数组尾的堆上"整堆”
for i = [2,n]
siftup(i)
for i = [n,2]
swap(1, i)
siftdown(i-1)
siftdown(l,u)这种实现就是将siftdown()中的i=1变成i=l、跟n的比较变成跟u的比较
for i = [n/2, 1]
siftdown(i, n)
for i = [n,2]
swap(1, i)
siftdown(1, i-1)
Fenwick树(二叉索引树)
Fenwick用于数组经常更新时,前缀和(进而范围和)的快速查询。
class FenwickTree {
static inline int lowbit(int x) { return x & -x; }
vector<int> _nums;
vector<int> _tree; // 使用_tree[1..n]
public:
FenwickTree(const vector<int> &nums)
: _nums(nums.size()), _tree(nums.size() + 1) {
for (int i = 0; i < _nums.size(); i++) {
update(i, nums[i]);
}
}
void update(int i, int val) {
int delta = val - _nums[i];
_nums[i] = val;
i++;
while (i < _tree.size()) {
_tree[i] += delta;
i += lowbit(i);
}
}
// 返回_nums[0..i]的和
int query(int i) const {
i++;
int sum = 0;
while (i) {
sum += _tree[i];
i -= lowbit(i);
}
return sum;
}
};
线段树用于动态的区间更新和查询(区间最大值、最小值、加和、最大公约数、最小公倍数等)
各节点除了保存下标区间,还应保存该区间对应的聚合值。叶节点对应单点区间,内节点对应虚拟的合并区间。若某内节点表示区间[i..j],那左子节点表示区间[i..(i+j)/2],右子节点表示区间[(i+j)/2+1..j]。
数组实现
用n个叶节点保存n个数组元素,还需要n-1个内节点。类似堆的数组实现,数组大小2n,不用tree[0],叶节点从n开始。父子关系:p=i/2、lchild=2*i、rchild=2*i+1。每个叶节点(i>=n)表示区间nums[i..i],每个内节点(i<n)表示左子节点和右子节点的合并区间。各节点表示的区间是固定的,节点中不再显式记录,只记录聚合值。三个操作:构建、更新、查询。
class SegmentTree {
int _n;
vector<int> _tree;
public:
SegmentTree(const vector<int> &nums)
: _n(nums.size()), _tree(2 * _n) {
build(nums);
}
void build(const vector<int> &nums) {
// 叶节点是原数组元素
for (int i = _n; i < 2 * _n; i++) {
_tree[i] = nums[i-_n];
}
// 内节点计算聚合信息
for (int i = _n - 1; i >= 1; i--) {
_tree[i] = _tree[2*i] + _tree[2*i+1];
}
}
void update(int i, int val) {
i += _n;
_tree[i] = val;
for (int j = i/2; j >= 1; j /= 2) {
_tree[j] = _tree[2*j] + _tree[2*j+1];
}
}
// 返回nums[i..j]的和
int query(int i, int j) {
int sum = 0;
for (i += _n, j += _n; i <= j; i /= 2, j /= 2) {
if (i % 2 == 1) sum += _tree[i++];
if (j % 2 == 0) sum += _tree[j--];
}
return sum;
}
};
query()的解释见http://codeforces.com/blog/entry/18051
General idea is the following. If l, the left interval border, is odd (which is equivalent to l&1) then l is the right child of its parent. Then our interval includes node l but doesn't include it's parent. So we add t[l] and move to the right of l's parent by setting l = (l + 1) / 2. If l is even, it is the left child, and the interval includes its parent as well (unless the right border interferes), so we just move to it by setting l = l / 2. Similar argumentation is applied to the right border. We stop once borders meet.
递归实现
递归树实现
递归数组实现
每个treeIdx对应范围[lo,hi],lo==hi是叶节点,build、query、update都是用后序遍历merge两子树解。因为只用递归向下计算子节点,不用计算父节点,可以使用tree[0],子节点变为:lchild=2*i+1、rchild=2*i+2。因为递归到叶节点结束,算空节点凑成完全二叉树,最多4n个节点(叶节点n,最底层空叶节点最多n,全部叶节点最多2n,全部节点最多4n)。
正常线段树一次更新一个元素,并把更新沿着"祖父=>叶"的路径传播。要想一次更新一个范围,用lazy propagation技术,意为:对子节点的更新延迟处理,先记录到与tree[]同样大小的lazy[]数组中。更新时先应用本节点的lazy[i]更新(并把本该传播给子节点的更新记录到lazy[lchild]、lazy[rchild],lazy[i]清零),再应用本次更新调用的更新(并把本该传播给子节点的更新记录到lazy[lchild]、lazy[rchild]);查询时,先应用本节点的lazy[i]更新(并把本该传播给子节点的更新记录到lazy[lchild]、lazy[rchild],lazy[i]清零),然后递归查询。
都要用个哈希表将查找值=映射到=>存储位置,而存储位置=也要能映射回=>查找值。
O(1)时间插入删除
无重复值:值=>值在数组的idx;有重复值:值=>值在数组的idx集合。在数组末增减元素。
O(1)时间增减频率值
class AllOne {
// 映射表table按值分行
struct ValueRow { int value; unordered_set<string> keys; };
list<ValueRow> table;
unordered_map<string, list<ValueRow>::iterator> rowPtrs;
private:
void deleteKey(const string &key, list<ValueRow>::iterator &row) {
row->keys.erase(key);
if (row->keys.empty()) table.erase(row);
rowPtrs.erase(key);
}
void insertKey(const string &key, list<ValueRow>::iterator &row) {
row->keys.insert(key);
rowPtrs[key] = row;
}
void moveKey(const string &key, list<ValueRow>::iterator &from, list<ValueRow>::iterator &to) {
deleteKey(key, from);
insertKey(key, to);
}
public:
/** Initialize your data structure here. */
AllOne() {
}
/** Inserts a new key <Key> with value 1. Or increments an existing key by 1. */
void inc(string key) {
if (!rowPtrs.count(key)) { // 先插入0,待会儿和其他情况一起增1
rowPtrs[key] = table.insert(table.begin(), {0, { key }});
}
auto currRow = rowPtrs[key], nextRow = next(currRow);
int nextValueNeeded = currRow->value + 1;
if (nextRow == table.end() || nextRow->value != nextValueNeeded) { // 插入新行
nextRow = table.insert(nextRow, {nextValueNeeded, { }});
}
moveKey(key, currRow, nextRow);
}
/** Decrements an existing key by 1. If Key's value is 1, remove it from the data structure. */
void dec(string key) {
if (!rowPtrs.count(key)) return;
auto currRow = rowPtrs[key];
if (currRow->value == 1) {
deleteKey(key, currRow);
return;
}
auto prevRow = prev(currRow);
int prevValueNeeded = currRow->value - 1;
if (currRow == table.begin() || prevRow->value != prevValueNeeded) {
prevRow = table.insert(currRow, {prevValueNeeded, { }});
}
moveKey(key, currRow, prevRow);
}
/** Returns one of the keys with maximal value. */
string getMaxKey() {
return table.empty() ? "" : *table.rbegin()->keys.begin();
}
/** Returns one of the keys with Minimal value. */
string getMinKey() {
return table.empty() ? "" : *table.begin()->keys.begin();
}
};
LRU cache
比按频率分行的情况简单,只需1维链表。
class LRUCache{
// 把entry{key,value}用链表串起来,
// 每次访问把entry移到表头,删除lru时删除表尾
struct Entry { int key; int value; };
list<Entry> l; // 实际存储
unordered_map<int, list<Entry>::iterator> entryPtrs; // key=>entry
int capacity;
private:
void touch(int key) {
// toList.splice(toListIterator, fromList, fromListSingleIterator)
l.splice(l.begin(), l, entryPtrs[key]);
entryPtrs[key] = l.begin();
}
public:
LRUCache(int capacity) {
this->capacity = capacity;
}
int get(int key) {
if (!entryPtrs.count(key)) return -1;
touch(key);
return entryPtrs[key]->value;
}
void put(int key, int value) {
if (capacity == 0) return;
if (!entryPtrs.count(key)) {
if (l.size() == capacity) { // 删除表尾
entryPtrs.erase(l.back().key);
l.pop_back();
}
l.push_front({key, value});
entryPtrs[key] = l.begin();
} else {
entryPtrs[key]->value = value;
touch(key);
}
}
};
LFU Cache
class LFUCache {
// 映射表table按频率分行,每行行首放最近使用项
struct Entry { int key; int value; };
struct FreqRow { int freq; list<Entry> entries; };
list<FreqRow> table;
struct EntryPtr { list<FreqRow>::iterator row; list<Entry>::iterator entry; };
unordered_map<int, EntryPtr> entryPtrs;
int capacity;
private:
void moveKey(int key, list<FreqRow>::iterator &to) {
auto from = entryPtrs[key].row;
to->entries.splice(to->entries.begin(), from->entries, entryPtrs[key].entry);
if (from->entries.empty()) table.erase(from);
entryPtrs[key] = { to, to->entries.begin() };
}
// 将entry从原行删除、插入"freq+1"行
void incFreq(int key) {
auto currRow = entryPtrs[key].row, nextRow = next(currRow);
int nextFreq = currRow->freq + 1;
if (nextRow == table.end() || nextRow->freq != nextFreq) { // 插入新行
nextRow = table.insert(nextRow, { nextFreq, { }});
}
moveKey(key, nextRow);
}
// 删除最小频率lfu的行中最少最近使用lru项
void evict() {
if (table.empty()) return;
auto lfu = table.begin();
entryPtrs.erase(lfu->entries.back().key);
lfu->entries.pop_back();
if (lfu->entries.empty()) table.erase(lfu);
}
public:
LFUCache(int capacity) {
this->capacity = capacity;
}
int get(int key) {
if (!entryPtrs.count(key)) return -1;
incFreq(key);
return entryPtrs[key].entry->value;
}
void put(int key, int value) {
if (capacity == 0) return;
if (!entryPtrs.count(key)) {
if (entryPtrs.size() == capacity) evict();
// 先插入freq=0的行,待会儿和其他情况一起增1
auto row = table.insert(table.begin(), { 0, {{key, value}} });
entryPtrs[key] = { row, row->entries.begin() };
} else {
entryPtrs[key].entry->value = value;
}
incFreq(key);
}
};
二叉树问题通常有个dfs函数,该函数返回一个值、用引用参数修改另一个值。
回溯的参数是传值时,相当于自动在调用后做了参数还原;回溯的参数是引用时,要在调用后做参数还原。
值唯一的子树个数
int countUnivalSubtrees(TreeNode* root) {
int ans = 0;
isUnivalue(root, ans);
return ans;
}
bool isUnivalue(TreeNode *root, int &ans) {
if (!root) return true;
auto left = isUnivalue(root->left, ans);
auto right = isUnivalue(root->right, ans);
if (!left || (root->left && root->left->val != root->val)) return false;
if (!right || (root->right && root->right->val != root->val)) return false;
ans++;
return true;
}
N枚硬币不均匀散布在二叉树中,每次可在父子间移动一枚硬币,要让硬币均匀分布,需要几次移动
int distributeCoins(TreeNode* root) {
int ans = 0;
excessCoins(root, ans);
return ans;
}
// 返回:某子树有多少多余硬币
int excessCoins(TreeNode *root, int &ans) {
if (!root) return 0;
auto left = excessCoins(root->left, ans);
auto right = excessCoins(root->right, ans);
ans += abs(left) + abs(right); // 在本节点和左右子节点间移动多余硬币
return left + right + root->val - 1;
}
祖父节点是偶数的所有节点之和
int sumEvenGrandparent(TreeNode* root) {
int ans = 0;
dfs(root, false, false, ans);
return ans;
}
void dfs(TreeNode *root, bool pEven, bool gpEven, int &ans) {
if (!root) return;
if (gpEven) ans += root->val;
bool even = root->val % 2 == 0;
dfs(root->left, even, pEven, ans);
dfs(root->right, even, pEven, ans);
}
最大的层宽度
- https://leetcode.com/problems/maximum-width-of-binary-tree/
int widthOfBinaryTree(TreeNode* root) {
int ans = INT_MIN;
vector<int> leftMost; // 各层最左节点的id
dfs(root, 0, 1, leftMost, ans);
return ans;
}
void dfs(TreeNode *root, int depth, int id, vector<int> &leftMost, int &ans) {
if (!root) return;
if (depth == leftMost.size()) leftMost.push_back(id);
ans = max(ans, id - leftMost[depth] + 1);
dfs(root->left, depth + 1, id * 2, leftMost, ans);
dfs(root->right, depth + 1, id * 2 + 1, leftMost, ans);
}
分成等和的两树
Given a binary tree with
nnodes, your task is to check if it's possible to partition the tree to two trees which have the equal sum of values after removing exactly one edge on the original tree.Example 1:
Input: 5 / \ 10 10 / \ 2 3 Output: True Explanation: 5 / 10 Sum: 15 10 / \ 2 3 Sum: 15Example 2:
Input: 1 / \ 2 10 / \ 2 20 Output: False Explanation: You can't split the tree into two trees with equal sum after removing exactly one edge on the tree.
bool checkEqualTree(TreeNode* root) {
unordered_map<int, int> mp; // sum=>count
int sum = getSum(root, mp);
if (sum == 0) return mp[0] > 1;
return sum % 2 == 0 && mp.count(sum / 2);
}
int getSum(TreeNode *root, unordered_map<int, int> &mp) {
if (!root) return 0;
int sum = root->val + getSum(root->left, mp) + getSum(root->right, mp);
mp[sum]++;
return sum;
}
可返回随机节点的二叉树
每个节点知道以自己为根的子树size,这就是顺序统计树,可以O(lgn)时间找第k个节点。
顺序统计树参考:
public class RankNode {
public int data;
public RankNode left, right;
public int size = 1;
public RankNode(int d) {
data = d;
}
public void insert(int d) {
if (d <= data) {
if (left != null) left.insert(d);
else left = new RankNode(d);
} else {
if (right != null) right.insert(d);
else right = new RankNode(d);
}
size++;
}
int getRank(int d) { // 0-based
if (d == data) {
return left == null ? 0 : left.size();
} else if (d < data) {
return left == null ? -1 : left.getRank(d);
} else {
int rightRank = right == null ? -1 : right.getRank(d);
if (rightRank == -1) return -1; // 要这么写,因为right.getRank()可能返回-1
int leftSize = left == null ? 0 : left.size();
return leftSize + 1 + rightRank;
}
}
}
类似的,见ctci p413
节点上的相机可看到节点本身、相连的父节点和子节点,为看到全树最少装多少相机
class Solution {
const int NO_CAMERA_NOT_COVERED = 0;
const int NO_CAMERA_BUT_COVERED = 1;
const int CAMERA_HERE = 2;
public:
int minCameraCover(TreeNode* root) {
int ans = 0;
if (dfs(root, ans) == NO_CAMERA_NOT_COVERED) ans++;
return ans;
}
int dfs(TreeNode *root, int &ans) {
if (!root) return NO_CAMERA_BUT_COVERED;
int left = dfs(root->left, ans), right = dfs(root->right, ans);
if (left == NO_CAMERA_NOT_COVERED || right == NO_CAMERA_NOT_COVERED) {
ans++;
return CAMERA_HERE;
}
if (left == CAMERA_HERE || right == CAMERA_HERE) {
return NO_CAMERA_BUT_COVERED;
}
return NO_CAMERA_NOT_COVERED;
}
};
路径的排列是回文
int pseudoPalindromicPaths(TreeNode* root) {
// 用count的第i位记录数字x的奇偶,1<=x<=9
int count = 0, ans = 0;
dfs(root, count, ans);
return ans;
}
void dfs(TreeNode *root, int count, int &ans) {
if (!root) return;
count ^= (1 << root->val);
if (!root->left && !root->right) {
ans += (count & (count - 1)) == 0; // 最多1位1
}
dfs(root->left, count, ans);
dfs(root->right, count, ans);
}
用栈中序遍历二叉树
用curr作为额外的栈顶的写法
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
stack<TreeNode *> stk;
auto curr = root; // curr是额外栈顶
while (curr || !stk.empty()) {
while (curr) { // pushLeft
stk.push(curr);
curr = curr->left;
}
auto node = stk.top(); stk.pop();
ans.push_back(node->val); // 出栈时访问
curr = node->right;
}
return ans;
}
- https://leetcode.com/problems/binary-search-tree-iterator/
- https://leetcode.com/problems/recover-binary-search-tree/
- https://leetcode.com/problems/all-elements-in-two-binary-search-trees/
用栈前序遍历二叉树
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
stack<TreeNode *> stk;
auto curr = root; // curr是额外栈顶
while (curr || !stk.empty()) {
while (curr) { // pushLeft
ans.push_back(curr->val); // 进栈前访问
stk.push(curr);
curr = curr->left;
}
auto node = stk.top(); stk.pop();
curr = node->right;
}
return ans;
}
前序遍历还可用图遍历写法:
vector<int> preorderTraversal(TreeNode* root) {
// 使用栈的图遍历
vector<int> ans;
stack<TreeNode *> stk;
if (root) stk.push(root);
while (!stk.empty()) {
auto node = stk.top(); stk.pop();
ans.push_back(node->val);
if (node->right) stk.push(node->right);
if (node->left) stk.push(node->left);
}
return ans;
}
用栈后序遍历二叉树
- https://leetcode.com/problems/binary-tree-postorder-traversal/
后序遍历要用prev节点来判断是否从右子树返回
vector<int> postorderTraversal(TreeNode* root) {
vector<int> ans;
stack<TreeNode *> stk;
auto curr = root; // curr是额外栈顶
TreeNode *prev = nullptr;
while (curr || !stk.empty()) {
while (curr) { // pushLeft
stk.push(curr);
curr = curr->left;
}
auto node = stk.top();
if (node->right && prev != node->right) { // 不是从右子树返回,访问右子树
curr = node->right;
} else { // 从右子树返回,出栈并访问
stk.pop();
ans.push_back(node->val);
prev = node;
}
}
return ans;
}
有父指针,中序遍历的下个节点
- 有右儿子:右儿子的最左儿子为下个节点
- 无右儿子:当前节点是右儿子时不断上移,最终父节点为下个节点
(停下来时父节点为空或当前节点是左儿子)
TreeLinkNode* inorderSucc(TreeLinkNode* node) {
if (!node) return nullptr;
if (node->right) {
auto curr = node->right;
while (curr->left) {
curr = curr->left;
}
return curr;
}
while (node->parent && node == node->parent->right) {
node = node->parent;
}
return node->parent;
}
有父指针,中序遍历的上个节点
同样地,
- 有左儿子:左儿子的最右儿子为下个节点
- 无左儿子:当前节点是左儿子时不断上移,最终父节点为下个节点
扩展:
分层遍历
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> ans;
queue<TreeNode *> q;
if (root) q.push(root);
while (!q.empty()) {
vector<int> row;
for (int sz = q.size(); sz > 0; sz--) {
auto node = q.front(); q.pop();
row.push_back(node->val);
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
ans.push_back(row);
}
return ans;
}
或两个队列换着用
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> ans;
vector<TreeNode *> curr;
if (root) curr.push_back(root);
while (!curr.empty()) {
vector<int> row;
vector<TreeNode *> next;
for (auto node : curr) {
row.push_back(node->val);
if (node->left) next.push_back(node->left);
if (node->right) next.push_back(node->right);
}
ans.push_back(row);
swap(curr, next);
}
return ans;
}
扩展:之字形分层遍历
是不是完全二叉树
- https://leetcode.com/problems/check-completeness-of-a-binary-tree/
bool isCompleteTree(TreeNode* root) {
queue<TreeNode *> q;
q.push(root);
// BFS,在弹出nullptr后不应再有实际节点
bool seenNull = false;
while (!q.empty()) {
auto node = q.front(); q.pop();
if (!node) {
seenNull = true;
} else {
if (seenNull) return false;
q.push(node->left);
q.push(node->right);
}
}
return true;
}
ref: 是不是二叉搜索树
分层反向编号的满二叉树,从根节点到本节点的路径
inline int pow2(int n) {
return 1 << n;
}
vector<int> pathInZigZagTree(int label) {
// 第h层(0-based)的label范围[2^h..2^(h+1)-1]
int h = 0;
while (pow2(h+1) - 1 < label) {
h++;
}
vector<int> ans;
while (label) {
ans.push_back(label);
// 每层都与上一层方向相反
label = (pow2(h) + pow2(h+1) - 1 - label) / 2;
h--;
}
reverse(ans.begin(), ans.end());
return ans;
}
同层节点用next指针连起来
Node* connect(Node *root) {
Node nextRow(0); // 下层起点
auto last = &nextRow;
auto curr = root;
while (curr) { // 遍历当前层,当前层已用next指针连接
if (curr->left) {
last->next = curr->left;
last = curr->left;
}
if (curr->right) {
last->next = curr->right;
last = curr->right;
}
curr = curr->next;
if (!curr) {
curr = nextRow.next;
nextRow.next = NULL;
last = &nextRow;
}
}
return root;
}
二叉树的前序遍历串是否合法
bool isValidSerialization(string preorder) {
// 想象扫描串并构建树,在扫描串的过程中要确保"叶节点数<内节点数+1",
// 一旦"叶节点数=内节点数+1"则树已构建好不能再扩展,要确保扫描完。
// 设diff=叶节点数-内节点数,扫描过程中确保diff<1,一旦diff==1确保扫描完。
const int N = preorder.size();
int diff = 0, i = 0;
while (i < N) {
if (preorder[i++] == '#') diff++;
else diff--;
if (diff == 1) break;
// 向前直到','
while (i < N && preorder[i] != ',') i++;
if (i < N) i++;
}
return i == N && diff == 1;
}
可交换节点的左右子树,为使前序遍历结果符合给定顺序,要在哪些节点做交换
vector<int> flipMatchVoyage(TreeNode* root, vector<int>& voyage) {
vector<int> ans;
int idx = 0; // 类似表达式解析问题,有个idx记录解析位置
if (match(root, voyage, idx, ans)) return ans;
return {-1};
}
bool match(TreeNode* root, vector<int>& voyage, int &idx, vector<int> &ans) {
if (!root) return true;
if (root->val != voyage[idx++]) return false;
if (root->left && root->left->val != voyage[idx]) {
ans.push_back(root->val); // flip
return match(root->right, voyage, idx, ans)
&& match(root->left, voyage, idx, ans);
}
return match(root->left, voyage, idx, ans)
&& match(root->right, voyage, idx, ans);
}
扩展:从前序遍历数组构造bst
二叉树的边界
Given a binary tree, return the values of its boundary in anti-clockwise direction starting from root. Boundary includes left boundary, leaves, and right boundary in order without duplicate nodes.
Left boundary is defined as the path from root to the left-most node. Right boundary is defined as the path from root to the right-most node. If the root doesn't have left subtree or right subtree, then the root itself is left boundary or right boundary. Note this definition only applies to the input binary tree, and not applies to any subtrees.
The left-most node is defined as a leaf node you could reach when you always firstly travel to the left subtree if exists. If not, travel to the right subtree. Repeat until you reach a leaf node.
The right-most node is also defined by the same way with left and right exchanged.
Example 1
Input: 1 \ 2 / \ 3 4 Ouput: [1, 3, 4, 2] Explanation: The root doesn't have left subtree, so the root itself is left boundary. The leaves are node 3 and 4. The right boundary are node 1,2,4. Note the anti-clockwise direction means you should output reversed right boundary. So order them in anti-clockwise without duplicates and we have [1,3,4,2].Example 2
Input: ____1_____ / \ 2 3 / \ / 4 5 6 / \ / \ 7 8 9 10 Ouput: [1,2,4,7,8,9,10,6,3] Explanation: The left boundary are node 1,2,4. (4 is the left-most node according to definition) The leaves are node 4,7,8,9,10. The right boundary are node 1,3,6,10. (10 is the right-most node). So order them in anti-clockwise without duplicate nodes we have [1,2,4,7,8,9,10,6,3].
vector<int> boundaryOfBinaryTree(TreeNode *root) {
// 输出根节点、左子树的非叶左边界、叶节点、右子树的非叶右边界
if (!root) return {};
vector<int> ans;
ans.push_back(root->val);
leftBoundary(root->left, ans);
leaves(root, ans);
rightBoundary(root->right, ans);
return ans;
}
void leftBoundary(TreeNode *root, vector<int> &ans) {
if (!root || (!root->left && !root->right)) return;
ans.push_back(root->val);
if (root->left) leftBoundary(root->left, ans);
else leftBoundary(root->right, ans);
}
void leaves(TreeNode *root, vector<int> &ans) {
if (!root) return;
if (!root->left && !root->right) {
ans.push_back(root->val);
} else {
leaves(root->left, ans);
leaves(root->right, ans);
}
}
void rightBoundary(TreeNode *root, vector<int> &ans) {
if (!root || (!root->left && !root->right)) return;
if (root->right) rightBoundary(root->right, ans);
else rightBoundary(root->left, ans);
ans.push_back(root->val);
}
两树对称
bool isSymmetric(TreeNode* root) {
if (!root) return true;
return isSymmetric(root->left, root->right);
}
bool isSymmetric(TreeNode *left, TreeNode *right) {
if (!left && !right) return true;
if (!left || !right) return false;
return left->val == right->val
&& isSymmetric(left->left, right->right)
&& isSymmetric(left->right, right->left);
}
是另一棵的子树
bool isSubtree(TreeNode* s, TreeNode* t) {
if (!t) return true;
if (!s) return false;
if (s->val == t->val && isSame(s, t)) return true;
return isSubtree(s->left, t) || isSubtree(s->right, t);
}
bool isSame(TreeNode* s, TreeNode* t) {
if (!s && !t) return true;
if (!s || !t) return false;
if (s->val != t->val) return false;
return isSame(s->left, t->left) && isSame(s->right, t->right);
}
bt两节点的最近公共祖先
TreeNode *lowestCommonAncestor(TreeNode *root, TreeNode *p, TreeNode *q) {
// 在子树中找p或q,找到一个就返回
if (!root || root == p || root == q) return root;
auto left = lowestCommonAncestor(root->left, p, q);
auto right = lowestCommonAncestor(root->right, p, q);
if (left && right) return root; // p和q分别在左右子树,root就是lca
return left ? left : right; // p和q都在某个子树
}
bst两节点的最近公共祖先
TreeNode* lowestCommonAncestor(TreeNode *root, TreeNode *p, TreeNode *q) {
// 找值在p、q间的节点
if (!root || !p || !q) return NULL;
if (p->val > q->val) swap(p, q); // p.val < q.val
while (root) {
if (root->val < p->val) {
root = root->right;
} else if (root->val > q->val) {
root = root->left;
} else {
break;
}
}
return root;
}
含所有最深叶节点的最近公共祖先
- https://leetcode.com/problems/lowest-common-ancestor-of-deepest-leaves/
- https://leetcode.com/problems/smallest-subtree-with-all-the-deepest-nodes/
TreeNode* lcaDeepestLeaves(TreeNode* root) {
return getHeight(root).second;
}
// 返回:(height, lcaWithDeepest)
pair<int, TreeNode*> getHeight(TreeNode *root) {
if (!root) return {0, NULL};
auto left = getHeight(root->left);
auto right = getHeight(root->right);
int h1 = left.first, h2 = right.first;
if (h1 > h2) return {1 + h1, left.second};
if (h1 < h2) return {1 + h2, right.second};
return {1 + h1, root};
}
经过单一值的最大边数
int longestUnivaluePath(TreeNode* root) {
int ans = 0;
dfs(root, ans);
return ans;
}
int dfs(TreeNode *root, int &ans) {
if (!root) return 0;
int left = dfs(root->left, ans);
int right = dfs(root->right, ans);
int leftEdge = 0, rightEdge = 0;
if (root->left && root->left->val == root->val) leftEdge = 1 + left;
if (root->right && root->right->val == root->val) rightEdge = 1 + right;
ans = max(ans, leftEdge + rightEdge); // 经过当前节点的最大边数
return max(leftEdge, rightEdge);
}
二叉树的直径
int diameterOfBinaryTree(TreeNode* root) {
int ans = 0;
dfs(root, ans);
return ans;
}
int dfs(TreeNode *root, int &ans) {
if (!root) return 0;
int left = dfs(root->left, ans);
int right = dfs(root->right, ans);
ans = max(ans, left + right); // 经过当前节点的直径长
return 1 + max(left, right);
}
沿路径向下的最大节点差值
int maxAncestorDiff(TreeNode* root) {
int ans = 0;
dfs(root, INT_MAX, INT_MIN, ans);
return ans;
}
void dfs(TreeNode *root, int mn, int mx, int &ans) {
if (!root) return;
mn = min(mn, root->val);
mx = max(mx, root->val);
dfs(root->left, mn, mx, ans);
dfs(root->right, mn, mx, ans);
ans = max(ans, mx - mn);
}
最长连续递增的向下路径
int longestConsecutive(TreeNode* root) {
int ans = 0;
dfs(root, ans);
return ans;
}
int dfs(TreeNode *root, int &ans) {
if (!root) return 0;
auto left = dfs(root->left, ans);
auto right = dfs(root->right, ans);
int len = 1;
if (root->left && root->left->val == root->val + 1)
len = max(len, 1 + left);
if (root->right && root->right->val == root->val + 1)
len = max(len, 1 + right);
ans = max(ans, len);
return len;
}
最长连续递增或递减的任意路径
Given a binary tree, you need to find the length of Longest Consecutive Path in Binary Tree.
Especially, this path can be either increasing or decreasing. For example, [1,2,3,4] and [4,3,2,1] are both considered valid, but the path [1,2,4,3] is not valid. On the other hand, the path can be in the child-Parent-child order, where not necessarily be parent-child order.
Example 1:
Input: 1 / \ 2 3 Output: 2 Explanation: The longest consecutive path is [1, 2] or [2, 1].Example 2:
Input: 2 / \ 1 3 Output: 3 Explanation: The longest consecutive path is [1, 2, 3] or [3, 2, 1].Note: All the values of tree nodes are in the range of [-1e7, 1e7].
class Solution {
struct Info {
int incCnt;
int decCnt;
};
public:
int longestConsecutive(TreeNode* root) {
int ans = 0;
dfs(root, ans);
return ans;
}
// 从root开始往下递增和递减的节点数
Info dfs(TreeNode *root, int &ans) {
if (!root) return {0, 0};
auto left = dfs(root->left, ans);
auto right = dfs(root->right, ans);
int incCnt = 1, decCnt = 1;
if (root->left) {
if (root->left->val == root->val + 1) {
incCnt = max(incCnt, left.incCnt + 1);
} else if (root->left->val == root->val - 1) {
decCnt = max(decCnt, left.decCnt + 1);
}
}
if (root->right) {
if (root->right->val == root->val + 1) {
incCnt = max(incCnt, right.incCnt + 1);
} else if (root->right->val == root->val - 1) {
decCnt = max(decCnt, right.decCnt + 1);
}
}
ans = max(ans, incCnt + decCnt - 1);
return {incCnt, decCnt};
}
};
最长ZigZag向下路径长
int longestZigZag(TreeNode* root) {
return dfs(root)[2];
}
// 返回{leftLongest, rightLongest, subLongest}
// 表示从当前节点向左最长、向右最长,当前子树所含最长
array<int,3> dfs(TreeNode *root) {
if (!root) return {-1,-1,-1};
auto left = dfs(root->left);
auto right = dfs(root->right);
int toL = 1 + left[1], toR = 1 + right[0];
int sub = max({toL, toR, left[2], right[2]});
return { toL, toR, sub };
}
以下写法中,height是从叶节点往上的节点数,空节点为0、叶节点为1;depth与height不区分。 都不像教科书上的,height定义为离叶节点的边数,叶节点高度为0;depth定义为离根节点的边数,根节点深度为0。
满二叉树的节点数
int countNodes(TreeNode* root) {
if (!root) return 0;
int leftH = getHeight(root->left);
int rightH = getHeight(root->right);
if (leftH == rightH) { // 左子树是满二叉树,左子树和根节点共(2^leftH-1)+1
return (1 << leftH) + countNodes(root->right);
} else { // 右子树是满二叉树
return (1 << rightH) + countNodes(root->left);
}
}
int getHeight(TreeNode *root) {
int height = 0;
while (root) {
height++;
root = root->left;
}
return height;
}
把叶节点一遍遍剥离
先剥离高度为0的节点,再剥离高度为1的节点……
vector<vector<int>> findLeaves(TreeNode* root) {
vector<vector<int>> ans;
getHeight(root, ans);
return ans;
}
int getHeight(TreeNode *root, vector<vector<int>> &ans) {
// 高h节点放到ans[h]中,让叶节点高为0,故定义空节点高为-1
if (!root) return -1;
auto left = getHeight(root->left, ans);
auto right = getHeight(root->right, ans);
int h = 1 + max(left, right);
if (h == ans.size()) ans.push_back({});
ans[h].push_back(root->val);
return h;
}
无向图的哪些节点提作树中心,可使树高度最小
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
// 叶节点到这些“根”的距离最长,从外往内一圈圈删除叶节点,最后剩下的可作为根。
// bfs不断删除度为1的叶节点,根据最长路径的奇偶性,可能剩下1个或2个节点。
vector<vector<int>> adj(n);
for (auto &edge : edges) {
adj[edge[0]].push_back(edge[1]);
adj[edge[1]].push_back(edge[0]);
}
vector<int> leaves;
vector<int> degree(n);
for (int i = 0; i < n; i++) {
degree[i] = adj[i].size();
if (degree[i] == 0 || degree[i] == 1) { // 单个节点的度为0
leaves.push_back(i);
degree[i] = 0; // 删除叶节点
}
}
int count = leaves.size();
while (count < n) {
vector<int> newLeaves;
for (int u : leaves) {
for (int v : adj[u]) {
if (--degree[v] == 1) newLeaves.push_back(v);
}
degree[u] = 0; // 删除叶节点
}
swap(leaves, newLeaves);
count += leaves.size();
}
return leaves;
}
最大路径和,路径不必经过根节点
int maxPathSum(TreeNode* root) {
int ans = INT_MIN;
arrowSum(root, ans);
return ans;
}
// 返回从root往下的最大路径和
int arrowSum(TreeNode *root, int &ans) {
if (!root) return 0;
int left = arrowSum(root->left, ans);
int right = arrowSum(root->right, ans);
int arrow = root->val + max({0, left, right});
// 经过root的最大路径和
int pathSum = root->val + max(0, left) + max(0, right);
ans = max(ans, pathSum);
return arrow;
}
向下路径和为target的路径有多少条
平均O(nlgn),最坏O(n^2)
int pathSum(TreeNode* root, int sum) {
if (!root) return 0;
return searchFrom(root, sum)
+ pathSum(root->left, sum)
+ pathSum(root->right, sum);
}
int searchFrom(TreeNode *root, int sum) {
if (!root) return 0;
return (root->val == sum ? 1 : 0)
+ searchFrom(root->left, sum - root->val)
+ searchFrom(root->right, sum - root->val);
}
子段和的runningSum解法,O(n)
int pathSum(TreeNode *root, int target) {
unordered_map<int, int> presum = {{0,1}}; // sum=>count
int runningSum = 0;
int ans = 0;
search(root, target, runningSum, presum, ans);
return ans;
}
void search(TreeNode *root, int target, int runningSum,
unordered_map<int, int> &presum, int &ans) {
if (!root) return;
// 以当前节点结尾的子数组,希望能找到数组子段和runningSum-toFind=target
runningSum += root->val;
int toFind = runningSum - target;
if (presum.count(toFind)) ans += presum[toFind];
presum[runningSum]++;
search(root->left, target, runningSum, presum, ans);
search(root->right, target, runningSum, presum, ans);
presum[runningSum]--; // 回溯,为其他搜索路径准备
}
删除只含0值的子树
TreeNode* pruneTree(TreeNode* root) {
if (!root) return NULL;
root->left = pruneTree(root->left);
root->right = pruneTree(root->right);
if (!root->left && !root->right && root->val == 0) return NULL;
return root;
}
将二叉树右转90度
TreeNode* upsideDownBinaryTree(TreeNode* root) {
// 已知右节点为叶节点、左节点非空,或右节点为空。
// 此题要将二叉树右转90度:左节点=>父节点、右节点=>左节点、父节点=>右节点。
// 假设本函数能处理好左子树,只需考虑父节点、左节点、右节点这三者的关系。
if (!root || !root->left) return root;
auto newRoot = upsideDownBinaryTree(root->left);
// 右转90度
root->left->left = root->right;
root->left->right = root;
// 右节点已知为叶节点
root->left = root->right = NULL;
return newRoot;
}
插入一行值v
TreeNode* addOneRow(TreeNode* root, int v, int d) { // d是1-based
TreeNode dummy(0);
dummy.left = root;
insert(&dummy, 0, v, d);
return dummy.left;
}
void insert(TreeNode* root, int depth, int v, int d) {
if (!root) return;
// 找到d-1层,在其子节点插入
if (depth < d - 1) {
insert(root->left, depth + 1, v, d);
insert(root->right, depth + 1, v, d);
} else {
auto ln = new TreeNode(v);
ln->left = root->left;
root->left = ln;
auto rn = new TreeNode(v);
rn->right = root->right;
root->right = rn;
}
}
删除节点并返回森林
vector<TreeNode*> delNodes(TreeNode* root, vector<int>& to_delete) {
unordered_set<int> toDel(begin(to_delete), end(to_delete));
vector<TreeNode*> ans;
root = dfs(root, toDel, ans);
if (root) ans.push_back(root);
return ans;
}
TreeNode* dfs(TreeNode *root, unordered_set<int> &toDel, vector<TreeNode*> &ans) {
if (!root) return nullptr;
root->left = dfs(root->left, toDel, ans);
root->right = dfs(root->right, toDel, ans);
if (!toDel.count(root->val)) return root;
if (root->left) ans.push_back(root->left);
if (root->right) ans.push_back(root->right);
return nullptr;
}
树中各节点到所有其他节点的距离之和
vector<int> sumOfDistancesInTree(int N, vector<vector<int>>& edges) {
// 设subtreeDist(x)表示x到所有x子树节点的距离之和,nodeCount(x)表示子树x的节点数,
// dist(x)表示x到所有其他节点的距离之和。考虑父节点p和子节点c,
// 1. 已知subtreeDist(NULL)==0,后序遍历求subtreeDist(p)
// subtreeDist(p) = sum( subtreeDist(c[i])+nodeCount(c[i]) )
// 最终 subtreeDist(root) == dist(root)
// 2. 已知dist(p),前序遍历求dist(c)。设树中除子树c外的含p的其他部分为o,
// 从dist(p)(所有其他节点到p)变为dist(c)(所有其他节点到c),o中的节点距离+1,c中的节点距离-1,
// 所以 dist(c) = dist(p)+nodeCount(o)-nodeCount(c) = dist(p)+N-2*nodeCount(c)
vector<vector<int>> adj(N);
for (auto &e : edges) {
adj[e[0]].push_back(e[1]);
adj[e[1]].push_back(e[0]);
}
vector<bool> visited1(N, false), visited2(N, false);
vector<int> ans(N, 0), nodeCount(N, 0);
postOrder(0, adj, visited1, nodeCount, ans);
preOrder(0, adj, visited2, nodeCount, N, ans);
return ans;
}
void postOrder(int root, vector<vector<int>>& adj, vector<bool> &visited,
vector<int> &nodeCount, vector<int> &subtreeDist) {
visited[root] = true;
for (int child : adj[root]) {
if (visited[child]) continue;
postOrder(child, adj, visited, nodeCount, subtreeDist);
nodeCount[root] += nodeCount[child];
subtreeDist[root] += subtreeDist[child] + nodeCount[child];
}
nodeCount[root] += 1;
}
void preOrder(int root, vector<vector<int>>& adj, vector<bool> &visited,
vector<int> &nodeCount, int N, vector<int> &dist) {
visited[root] = true;
for (int child : adj[root]) {
if (visited[child]) continue;
dist[child] = dist[root] + N - 2 * nodeCount[child];
preOrder(child, adj, visited, nodeCount, N, dist);
}
}
bst中序遍历的下个节点
TreeNode* inorderSuccessor(TreeNode* root, TreeNode* p) {
if (!root || !p) return NULL;
// bst的中序遍历是个有序数组,这题相当于要找第一个>p.val的节点
// 类似二分搜索,root就是二分搜索中的mid
TreeNode *succ = NULL;
while (root) {
if (root->val > p->val) { // root作候选,看左边有没更小的
succ = root;
root = root->left;
} else { // root值太小,排除root和左子树
root = root->right;
}
}
return succ;
}
bst中删除值为key的节点
TreeNode* deleteNode(TreeNode* root, int key) {
if (!root) return nullptr;
if (key < root->val) {
root->left = deleteNode(root->left, key);
return root;
}
if (key > root->val) {
root->right = deleteNode(root->right, key);
return root;
}
if (!root->right) {
auto left = root->left;
delete root;
return left;
}
// 找到后继节点
auto succ = root->right;
while (succ->left) {
succ = succ->left;
}
succ->left = root->left;
auto right = root->right;
delete root;
return right;
}
已知数组从左到右扫描所得的bst样子,求对应数组的可能排列数
class Solution {
vector<vector<int>> C;
const int MOD = 1e9 + 7;
public:
int numOfWays(vector<int>& nums) {
const int N = nums.size();
// 组合数nCk为杨辉三角中C[n][k]
C = vector<vector<int>>(N, vector<int>(N));
C[0][0] = 1;
for (int i = 1; i < N; i++) {
C[i][0] = 1;
for (int j = 1; j <= i; j++) {
C[i][j] = (C[i-1][j-1] + C[i-1][j]) % MOD;
}
}
return ways(nums) - 1; // 其他排列
}
int ways(vector<int>& nums) {
const int N = nums.size();
if (N <= 1) return 1;
vector<int> left, right;
for (int i = 1; i < N; i++) {
if (nums[i] < nums[0]) left.push_back(nums[i]);
else right.push_back(nums[i]);
}
return weave(left, right);
}
int weave(vector<int>& A, vector<int>& B) {
// 左右两数组交织,可保持二叉树不变
int nCk = C[A.size() + B.size()][A.size()];
long left = ways(A), right = ways(B);
return (((left * right) % MOD) * nCk) % MOD;
}
};
已知数组从左到右扫描所得的bst样子,求对应数组的所有可能排列
根节点是数组首元素,左子树是小于它的元素们、右子树是大于它的元素们,但左右子树的元素哪个先对应到数组元素不确定。左右子树分别对应的数组有多种排列,取左子树的一组排列{a1, a2, a3, ...}和右子树的一组排列{b1, b2, b3, ...},怎么由它们得到原数组的排列?用"交织"操作:保持组内的相对次序不变,组间的次序(先插入左子树元素还是右子树元素)无所谓。递归:取{a1},后面是{a2, a3, ...}和{b1, b2, b3, ...}的交织;或取{b1},后面是{a1, a2, a3, ...}和{b2, b3, ...}的交织;当一组为空时,另一组直接拼上。见ctci p262。
// 简写起见,List指LinkedList<Integer>,List[]指ArrayList<LinkedList<Integer>>
void weaveLists(List prefix, List l1, List l2, List[] ans) {
if (l1.size() == 0 || l2.size() == 0) {
List list = prefix.clone();
list.add(l1);
list.add(l2);
ans.add(list);
return;
}
// l1取头放到prefix中,递归后还原
int f1 = l1.removeFirst();
prefix.addLast(f1);
weaveList(prefix, l1, l2, ans);
prefix.removeLast();
l1.addFirst(f1);
// l2一样处理
int f2 = l2.removeFirst();
prefix.addLast(f2);
weaveList(prefix, l1, l2, ans);
prefix.removeLast();
l2.addFirst(f2);
}
List[] allSequences(TreeNode node) {
List[] ans = new ArrayList<LinkedList<Integer>>();
if (!node) return ans;
List prefix = new LinkedList<Integer>();
prefix.add(node.data); // 根节点=>数组首元素
List[] leftSeq = allSequences(node.left);
List[] rightSeq = allSequences(node.right);
for (List left : leftSeq) {
for (List right : rightSeq) {
weaveList(prefix, left, right, ans);
}
}
return ans;
}
是不是二叉搜索树
- https://leetcode.com/problems/validate-binary-search-tree/
bool isValidBST(TreeNode* root) {
return isValidBST(root, LONG_MIN, LONG_MAX);
}
bool isValidBST(TreeNode *root, long lower, long upper) {
if (!root) return true;
return (lower < root->val && root->val < upper)
&& isValidBST(root->left, lower, root->val)
&& isValidBST(root->right, root->val, upper);
}
ref: 是不是完全二叉树
bst前序遍历数组是否合法
bool verifyPreorder(vector<int>& preorder) {
return verify(preorder, 0, preorder.size(), INT_MIN, INT_MAX);
}
// 用值区间(lower,upper)判断A[start..end)
bool verify(vector<int> &A, int start, int end, int lower, int upper) {
if (start >= end) return true;
int val = A[start];
if (val <= lower || val >= upper) return false;
int i = start + 1;
while (i < end && A[i] < val) i++;
if (!verify(A, start + 1, i, lower, val)) return false;
if (!verify(A, i, end, val, upper)) return false;
return true;
}
从前序遍历数组构造bst
TreeNode* bstFromPreorder(vector<int>& A) {
int idx = 0; // 有个idx记录扫描位置,在每个位置检查是否满足upper
return bstFromPreorder(A, idx, INT_MAX);
}
TreeNode* bstFromPreorder(vector<int>& A, int &idx, int upper) {
if (idx >= A.size() || A[idx] > upper) return NULL;
int val = A[idx++];
auto node = new TreeNode(val);
node->left = bstFromPreorder(A, idx, val);
node->right = bstFromPreorder(A, idx, upper);
return node;
}
bst修剪到[L,R]范围
TreeNode* trimBST(TreeNode* root, int L, int R) {
if (!root) return NULL;
if (root->val > R) return trimBST(root->left, L, R);
if (root->val < L) return trimBST(root->right, L, R);
root->left = trimBST(root->left, L, R);
root->right = trimBST(root->right, L, R);
return root;
}
最大和的bst子树
int maxSumBST(TreeNode* root) {
int ans = 0;
dfs(root, ans);
return ans;
}
// 返回{sum, min, max}
// 空节点作左儿子时,要让"父节点的val > 空节点的max"总成立,空节点的max=INT_MIN;
// 同理,空节点的min=INT_MAX;所以,空节点值区间[INT_MAX,INT_MIN]
// {0,INT_MAX,INT_MIN}表示空节点,{0,INT_MIN,INT_MAX}表示无效节点
array<int,3> dfs(TreeNode *root, int &ans) {
if (!root) return { 0, INT_MAX, INT_MIN };
auto left = dfs(root->left, ans);
auto right = dfs(root->right, ans);
if (left[2] < root->val && root->val < right[1]) {
int sum = left[0] + root->val + right[0];
ans = max(ans, sum);
return { sum, min(root->val, left[1]), max(root->val, right[2]) };
}
return { 0, INT_MIN, INT_MAX };
}
最大bst子树
解法同上题,dfs返回{size, min, max}
Given a binary tree, find the largest subtree which is a Binary Search Tree (BST), where largest means subtree with largest number of nodes in it.
Note:
A subtree must include all of its descendants.
Here's an example:10 / \ 5 15 / \ \ 1 8 7The Largest BST Subtree in this case is the highlighted one.
The return value is the subtree's size, which is 3.Hint:
- You can recursively use algorithm similar to 98. Validate Binary Search Tree at each node of the tree, which will result in O(nlogn) time complexity.
Follow up:
Can you figure out ways to solve it with O(n) time complexity?
bst中最接近t的k个数
Given a non-empty binary search tree and a target value, find k values in the BST that are closest to the target.
Note:
- Given target value is a floating point.
- You may assume k is always valid, that is: k ≤ total nodes.
- You are guaranteed to have only one unique set of k values in the BST that are closest to the target.
Follow up:
Assume that the BST is balanced, could you solve it in less than O(n) runtime (where n = total nodes)?Hint:
- Consider implement these two helper functions:
i. getPredecessor(N), which returns the next smaller node to N.
ii. getSuccessor(N), which returns the next larger node to N.- Try to assume that each node has a parent pointer, it makes the problem much easier.
- Without parent pointer we just need to keep track of the path from the root to the current node using a stack.
- You would need two stacks to track the path in finding predecessor and successor node separately.
用中序遍历和k长队列,O(n)解法
vector<int> closestKValues(TreeNode* root, double target, int k) {
deque<int> q;
inorder(root, target, k, q);
return vector<int>(q.begin(), q.end());
}
void inorder(TreeNode *root, double target, int k, deque<int> &q) {
if (!root) return;
// 用中序遍历和k长队列
inorder(root->left, target, k, q);
if (q.size() < k) {
q.push_back(root->val);
} else if (abs(root->val - target) < abs(q.front() - target)) {
q.pop_front();
q.push_back(root->val);
} else return;
inorder(root->right, target, k, q);
}
找前驱和后继,O(klgn)解法
vector<int> closestKValues(TreeNode* root, double target, int k) {
// 前驱栈pred和后继栈succ
stack<TreeNode *> pred, succ;
auto p = root;
while (p) {
if (target < p->val) {
succ.push(p);
p = p->left;
} else {
pred.push(p);
p = p->right;
}
}
vector<int> ans;
while (k--) {
// 往两端的两指针
if (succ.empty() || (!pred.empty() && target - pred.top()->val < succ.top()->val - target)) { // 选前驱
ans.push_back(pred.top()->val);
getPredecessor(pred);
} else {
ans.push_back(succ.top()->val);
getSuccessor(succ);
}
}
return ans;
}
// 类似i--,到左子树最右儿子的路径
void getPredecessor(stack<TreeNode *> &pred) {
auto node = pred.top(); pred.pop();
auto p = node->left;
while (p) {
pred.push(p);
p = p->right;
}
}
// 类似j++,到右子树最左儿子的路径
void getSuccessor(stack<TreeNode *> &succ) {
auto node = succ.top(); succ.pop();
auto p = node->right;
while (p) {
succ.push(p);
p = p->left;
}
}
将bst分裂成<=V、>V两颗树
Given a Binary Search Tree (BST) with root node
root, and a target valueV, split the tree into two subtrees where one subtree has nodes that are all smaller or equal to the target value, while the other subtree has all nodes that are greater than the target value. It's not necessarily the case that the tree contains a node with valueV.Additionally, most of the structure of the original tree should remain. Formally, for any child C with parent P in the original tree, if they are both in the same subtree after the split, then node C should still have the parent P.
You should output the root TreeNode of both subtrees after splitting, in any order.
Example 1:
Input: root = [4,2,6,1,3,5,7], V = 2 Output: [[2,1],[4,3,6,null,null,5,7]] Explanation: Note that root, output[0], and output[1] are TreeNode objects, not arrays. The given tree [4,2,6,1,3,5,7] is represented by the following diagram: 4 / \ 2 6 / \ / \ 1 3 5 7 while the diagrams for the outputs are: 4 / \ 3 6 and 2 / \ / 5 7 1Note:
- The size of the BST will not exceed
50.- The BST is always valid and each node's value is different.

vector<TreeNode*> splitBST(TreeNode* root, int V) {
if (!root) return {NULL, NULL};
if (root->val <= V) { // root分在左边
auto splitted = splitBST(root->right, V);
root->right = splitted[0];
splitted[0] = root;
return splitted;
} else {
auto splitted = splitBST(root->left, V);
root->left = splitted[1];
splitted[1] = root;
return splitted;
}
}
沿路径向上第k个祖先
class TreeAncestor {
// 记住到祖先的捷径
// P[node][level]表示node的第2^level个祖先
vector<vector<int>> P;
public:
TreeAncestor(int n, vector<int>& parent) {
P = vector<vector<int>>(n, vector<int>(20, -1));
const int N = parent.size();
for (int node = 0; node < N; node++) {
P[node][0] = parent[node];
}
for (int node = 0; node < N; node++) {
for (int level = 1; level < 20; level++) {
int pNode = P[node][level-1];
if (pNode == -1) break;
P[node][level] = P[pNode][level-1];
}
}
}
int getKthAncestor(int node, int k) {
if (node == -1) return -1;
for (int level = 0; level < 20; level++) {
if (k & (1 << level)) {
node = P[node][level];
if (node == -1) return -1;
}
}
return node;
}
};
'''
并查集,查两点是否连通
参考:https://www.cs.princeton.edu/courses/archive/spring13/cos423/lectures/UnionFind.pdf
并查集只有两个操作,查(find)和并(unite)
int find(int x) {
if (parent[x] != x)
parent[x] = find(parent[x]); // 路径压缩
return parent[x];
}
void unite(int x, int y) {
int px = find(x), py = find(y);
if (px == py) return;
parent[px] = py;
}
合并时可以优化。用size[]记录集合大小,初始全为1,合并时以size较大集合作为父集合。
void unite(int x, int y) {
int px = find(x), py = find(y);
if (px == py) return;
if (size[px] < size[py]) swap(px, py);
size[px] += size[py];
parent[py] = px;
}
或者用rank[]记录集合的高,初始全为0,合并时以rank较大集合作为父集合。之所以叫rank[]不叫height[],因为确切来说rank[]并不是高度,“路径压缩”后height[x]<=rank[x]。
void unite(int x, int y) {
int px = find(x), py = find(y);
if (px == py) return;
if (rank[px] < rank[py]) swap(px, py);
if (rank[px] == rank[py]) rank[px]++;
parent[py] = px;
}
也有用str作key,用unordered_map<string, string>作parent的。
初始赋值可以写在find()中
string find(const string &s, unordered_map<string, string> &parent) {
if (!parent.count(s)) parent[s] = s;
if (parent[s] != s)
parent[s] = find(parent[s], parent);
return parent[s];
}
连通子图的个数
int countComponents(int n, vector<pair<int, int>>& edges) {
// 并查集
vector<int> parent(n);
for (int i = 0; i < n; i++) {
parent[i] = i;
}
// unite
for (auto &edge : edges) {
int x = find(edge.first, parent);
int y = find(edge.second, parent);
if (x != y) {
parent[x] = y;
n--;
}
}
return n;
}
int find(int x, vector<int> &parent) {
if (parent[x] != x)
parent[x] = find(parent[x], parent);
return parent[x];
}
冗余的连接
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
// 并查集
const int N = edges.size(); // 已知节点数N为输入数组长
vector<int> parent(N + 1, 0);
for (int i = 1; i <= N; i++) {
parent[i] = i;
}
// unite
for (auto &edge : edges) {
int pu = find(edge[0], parent);
int pv = find(edge[1], parent);
if (pu == pv) return edge;
parent[pu] = pv;
}
return {};
}
int find(int x, vector<int> &parent) {
if (parent[x] != x) {
parent[x] = find(parent[x], parent);
}
return parent[x];
}
最多能去掉多少石头
int removeStones(vector<vector<int>>& stones) {
// 同行或同列的要在一个并查集,这里的技巧是,
// 把行或列作为某种资源点,石头位置连接了行列资源点,
// 将列j+10000以区别于行i资源点
unordered_map<int, int> uf;
int islands = 0;
for (auto &stone : stones) {
unite(stone[0], stone[1] + 10000, uf, islands);
}
return stones.size() - islands;
}
int find(int x, unordered_map<int, int> &uf, int &islands) {
if (!uf.count(x)) {
uf[x] = x;
islands++;
}
if (uf[x] != x) uf[x] = find(uf[x], uf, islands);
return uf[x];
}
void unite(int x, int y, unordered_map<int, int> &uf, int &islands) {
int px = find(x, uf, islands), py = find(y, uf, islands);
if (px != py) {
uf[py] = px;
islands--;
}
}
树多了条冗余边
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
// 两种可并存的情况:
// 冗余边指向非根节点 => 该节点有两个父节点
// 指向祖父节点 => 有环
//
// 先找有两个父节点的节点,找到时把两条父边作为冗余候选,从图中去掉一条。
// 再在“没有两个父节点的节点”的图上用并查集找环,边的两端属于同一集合时有环。
// 1. 若这一步有环,上一步有候选,冗余边是 保留的候选边
// 2. 有 无 并查集中刚导致有环的边
// 3. 无 有 去掉的侯选边
const int N = edges.size();
vector<int> parent(N + 1, 0); // 节点是1-based
vector<int> candA, candB;
for (auto &edge : edges) {
if (parent[edge[1]] == 0) {
parent[edge[1]] = edge[0];
} else { // edge[1]有两个父节点,把两条父边作为冗余候选
candA = {parent[edge[1]], edge[1]};
candB = edge;
edge[1] = -1; // 从图中去掉candB
}
}
// 并查集找环
for (int i = 1; i <= N; i++) parent[i] = i;
for (auto &edge : edges) {
if (edge[1] == -1) continue; // 上一步中去掉的边
int pu = find(edge[0], parent), pv = find(edge[1], parent);
if (pu == pv) { // 有环
if (!candA.empty()) return candA; // case 1
return edge; // case 2
}
parent[pu] = pv; // unite
}
return candB; // case 3
}
int find(int x, vector<int> &parent) {
if (parent[x] != x) {
parent[x] = find(parent[x], parent);
}
return parent[x];
}
消除某方块后将掉落的方块数
已知方块直接或间接与顶部相连时不会掉落
class Solution {
class UnionFind {
vector<int> parent;
vector<int> size;
public:
UnionFind(int sz) : parent(sz), size(sz, 1) {
for (int i = 0; i < sz; i++)
parent[i] = i;
}
int find(int x) {
if (parent[x] != x)
parent[x] = find(parent[x]);
return parent[x];
}
void unite(int x, int y) {
int px = find(x), py = find(y);
if (px == py) return;
if (size[px] < size[py]) swap(px, py);
size[px] += size[py];
parent[py] = px;
}
int topBricksCount() { // 与"顶部"相连的砖块数
return size[find(parent.size() - 1)] - 1;
}
};
public:
vector<int> hitBricks(vector<vector<int>>& grid, vector<vector<int>>& hits) {
// 把时间反转,从hits[]全应用的局面开始,一步步恢复被消除的砖块,
// 看与"顶部"相连的砖块数的变化
if (grid.empty()) return {};
const int R = grid.size(), C = grid[0].size();
vector<vector<int>> erased(grid);
for (auto &hit : hits) { // 先是hits[]全应用的局面
erased[hit[0]][hit[1]] = 0;
}
UnionFind uf(R * C + 1); // idx=R*C是特殊的"顶部"节点
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
if (!erased[r][c]) continue;
int idx = r * C + c;
// idx与上下左右连接(连接就是并查集unite)
// 从上往下、从左往右遍历,只要考虑与上左的连接
if (r == 0) uf.unite(idx, R * C); // 首行砖块与"顶部"连接
if (r > 0 && erased[r-1][c]) uf.unite(idx, (r - 1) * C + c);
if (c > 0 && erased[r][c-1]) uf.unite(idx, r * C + (c - 1));
}
}
int prev = uf.topBricksCount();
const int N = hits.size();
vector<int> ans(N, 0);
vector<vector<int>> dirs = {{0, -1}, {-1, 0}, {0, 1}, {1, 0}};
// 一步步恢复被消除的砖块
for (int i = N - 1; i >= 0; i--) {
int r = hits[i][0], c = hits[i][1];
if (!grid[r][c]) continue;
erased[r][c] = 1;
int idx = r * C + c;
// 让idx与上下左右连接
if (r == 0) uf.unite(idx, R * C);
for (auto &dir : dirs) {
int nr = r + dir[0], nc = c + dir[1];
if (0 <= nr && nr < R && 0 <= nc && nc < C && erased[nr][nc]) {
uf.unite(idx, nr * C + nc);
}
}
int curr = uf.topBricksCount();
// 若curr>prev,掉落curr-prev-1块;若curr==prev,掉落0块
ans[i] = max(curr - prev - 1, 0);
prev = curr;
}
return ans;
}
};
账号合并
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
// 直接用mail作并查集的键,同用户的mail在同一集合
unordered_map<string, string> parent;
unordered_map<string, string> owner;
for (auto &account : accounts) {
for (int i = 1; i < account.size(); i++) {
auto &mail = account[i];
parent[mail] = mail; // init
owner[mail] = account[0];
}
}
for (auto &account : accounts) {
auto px = find(account[1], parent);
for (int i = 2; i < account.size(); i++) {
auto py = find(account[i], parent);
if (px != py) parent[py] = px; // unite
}
}
// 同一集合的mail放入数组
unordered_map<string, vector<string>> mp;
for (auto &e : parent) {
auto &x = e.first;
auto px = find(e.second, parent);
mp[px].push_back(x);
}
vector<vector<string>> ans;
for (auto &e : mp) {
auto &mails = e.second;
sort(mails.begin(), mails.end());
mails.insert(mails.begin(), owner[e.first]);
ans.push_back(mails);
}
return ans;
}
string find(const string &mail, unordered_map<string, string> &parent) {
if (parent[mail] != mail)
parent[mail] = find(parent[mail], parent);
return parent[mail];
}
两句子相似
Given two sentences
words1, words2(each represented as an array of strings), and a list of similar word pairspairs, determine if two sentences are similar.For example,
words1 = ["great", "acting", "skills"]andwords2 = ["fine", "drama", "talent"]are similar, if the similar word pairs arepairs = [["great", "good"], ["fine", "good"], ["acting","drama"], ["skills","talent"]].Note that the similarity relation is transitive. For example, if "great" and "good" are similar, and "fine" and "good" are similar, then "great" and "fine" are similar.
Similarity is also symmetric. For example, "great" and "fine" being similar is the same as "fine" and "great" being similar.
Also, a word is always similar with itself. For example, the sentences
words1 = ["great"], words2 = ["great"], pairs = []are similar, even though there are no specified similar word pairs.Finally, sentences can only be similar if they have the same number of words. So a sentence like
words1 = ["great"]can never be similar towords2 = ["doubleplus","good"].Note:
- The length of
words1andwords2will not exceed1000.- The length of
pairswill not exceed2000.- The length of each
pairs[i]will be2.- The length of each
words[i]andpairs[i][j]will be in the range[1, 20].
bool areSentencesSimilarTwo(vector<string>& words1, vector<string>& words2, vector<pair<string, string>> pairs) {
if (words1.size() != words2.size()) return false;
// 直接用word作并查集的键
unordered_map<string, string> parent;
for (auto &p : pairs) {
auto pw1 = find(p.first, parent), pw2 = find(p.second, parent);
if (pw1 != pw2) parent[pw1] = pw2; // unite
}
for (int i = 0; i < words1.size(); i++) {
auto &w1 = words1[i], &w2 = words2[i];
if (w1 != w2 && find(w1, parent) != find(w2, parent)) return false;
}
return true;
}
string find(const string &s, unordered_map<string, string> &parent) {
if (!parent.count(s)) parent[s] = s;
if (parent[s] != s)
parent[s] = find(parent[s], parent);
return parent[s];
}
trie树
前缀树从宏观角度考虑,就是建立了各个前缀到单词的映射。可认为是个map,只是比map省空间。
实现上,root节点可类比链表的dummy表头,一层层向下类比链表的遍历。
前缀后缀搜索
class WordFilter {
struct TrieNode {
vector<int> wordIdx;
vector<TrieNode *> child;
TrieNode() : child(26, nullptr) { }
};
class Trie {
TrieNode root;
public:
void insert(const string &word, int index) {
auto p = &root;
p->wordIdx.push_back(index); // ""前缀可对应单词
for (char c : word) {
int idx = c - 'a';
if (!p->child[idx]) p->child[idx] = new TrieNode();
p = p->child[idx];
p->wordIdx.push_back(index);
}
}
vector<int> search(const string &prefix) {
auto p = &root;
for (char c : prefix) {
int idx = c - 'a';
if (!p->child[idx]) return {};
p = p->child[idx];
}
return p->wordIdx;
}
};
Trie forward;
Trie backward;
public:
WordFilter(vector<string> words) {
for (int i = 0; i < words.size(); i++) {
forward.insert(words[i], i);
reverse(words[i].begin(), words[i].end());
backward.insert(words[i], i);
}
}
/* */
int f(string prefix, string suffix) {
auto wordIdx1 = forward.search(prefix);
reverse(suffix.begin(), suffix.end());
auto wordIdx2 = backward.search(suffix);
// 找出这两个列表的交集中的最大值
int i = wordIdx1.size() - 1, j = wordIdx2.size() - 1;
while (i >= 0 && j >= 0) {
if (wordIdx1[i] == wordIdx2[j]) return wordIdx1[i];
if (wordIdx1[i] > wordIdx2[j]) i--;
else j--;
}
return -1;
}
};
字母矩阵中找单词
class Solution {
struct TrieNode {
string word;
vector<TrieNode *> child;
TrieNode() : child(26, nullptr) {}
};
TrieNode root;
public:
vector<string> findWords(vector<vector<char>> &board, vector<string> &words) {
// 将单词表存成trie结构以同时搜索整个单词表,在矩阵中回溯搜索
if (board.empty()) return {};
const int R = board.size(), C = board[0].size();
vector<vector<bool>> visited(R, vector<bool>(C, false));
for (auto &word : words) {
insertToTrie(word);
}
vector<string> ans;
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
search(r, c, board, visited, &root, ans);
}
}
return ans;
}
void insertToTrie(const string &word) {
auto p = &root;
for (char c : word) {
int idx = c - 'a';
if (!p->child[idx]) p->child[idx] = new TrieNode();
p = p->child[idx];
}
p->word = word;
}
void search(int r, int c, const vector<vector<char>> &board,
vector<vector<bool>> &visited, TrieNode *node, vector<string> &ans) {
const int R = board.size(), C = board[0].size();
if (r < 0 || r >= R || c < 0 || c >= C || visited[r][c]) return;
// 回溯法,在trie中搜索board[r][c]
int idx = board[r][c] - 'a';
auto p = node->child[idx];
if (!p) return;
if (!p->word.empty()) { // 找到一个词
ans.push_back(p->word);
p->word.clear(); // 不用再找这个词
}
visited[r][c] = true;
search(r - 1, c, board, visited, p, ans);
search(r + 1, c, board, visited, p, ans);
search(r, c - 1, board, visited, p, ans);
search(r, c + 1, board, visited, p, ans);
visited[r][c] = false;
}
};
相同元素的向上多层指针是个数组,越上层的某些元素构成的更稀疏链表
查找时先从最上层开始找,找过头时回头去下一层,继续找
插入时先找到在最底层的插入位置,在最底层插入后以1/2概率提升到上一层,提升后再以1/2概率提升到再上一层,等等
能保证查找、插入等都是O(lgn)
双向bfs
双向bfs的两个搜索方向各自维护,除了queue队列待扩展它的邻居节点、还有个enqueued映射记录当前方向已遇到哪些节点(不要用toVisit、visited这样容易混淆的命名),这样在一个方向搜索时就能知道另一个方向是否遇到过当前节点。queue和enqueued都是关于PathNode类型的,PathNode要在遍历时记录路径链表:{ personId, previous: PathNode },因此跟bfs遍历相关的BFSData是:{ queue: queue
// srcData、destData指BFSData
while (!srcData.queue.isEmpty() && !destData.queue.isEmpty()) {
Integer collision = searchOneLevel(srcData, destData, mapFromIdToPerson); // srcData搜索一层
if (collision != null) return mergePaths(srcData, destData, collision);
collision = searchOneLevel(destData, srcData, mapFromIdToPerson); // destData搜索一层
if (collision != null) return mergePaths(srcData, destData, collision);
}
return null;
Integer searchOneLevel(BFSData first, BFSData second, HasMap<Integer, Person> mapFromIdToPerson) {
int count = first.queue.size(); // 只搜索一层
for (int i = 0; i < count; i++) {
PathNode node = first.queue.poll();
int personId = node.getPersonId();
if (second.enqueued.containsKey(personId)) { // 节点有碰撞
return personId;
}
// 处理当前节点
Person person = mapFromIdToPerson.get(personId);
ArrayList<Integer> friendIds = person.getFriendIds();
for (int friendId : friendIds) {
if (!first.enqueued.containsKey(friendId)) {
PathNode next = new PathNode(friendId, node);
first.enqueued.put(friendId, next);
first.queue.add(next);
}
}
}
return null;
}
另一例子见ctci p607,其中PathNode:{ word, previous: PathNode },BFSData:{ queue: queue
红黑树4个性质
1. 节点要么红要么黑
2. 根节点和空节点是黑
3. 红节点的父节点一定是黑(或说红节点的两个子节点一定是黑,或说不能有两个红节点为父子节点)
4. 从根到空节点的黑节点数相同
插入
插入新节点时,新节点是红,只可能违反性质3。考虑新节点N、父节点P、叔节点U、祖节点G,这时N、P是红(违反性质3),G是黑(不违反性质3),根据U是红或黑、P和N是左子节点或右子节点分情况讨论,实际上只有三种情况:
1. U是红。这时把P和U变黑、G变红就行
接下来U是黑,分两种情况:
2. 从N到P、从P到G的路线是z形的
3. 从N到P、从P到G的路线是直的
情况2,经过一次P上的旋转,可以捋直变成情况3
情况3,只要G上一次旋转,然后交换P和G的颜色
现在G的子树满足红黑树性质,而G自己可能变红违反红黑树性质,但我们把N的问题向上推到了G。依样继续,最后的最后把问题推到根节点,不管根节点是什么颜色,直接变成黑色就行。
见ctci p639。情况2和3的旋转是标准的旋转操作,根据路线是z形的还是直的来操作,AVL树中也用这种旋转。
最大流
每条边上有(flow/capacity)两个数。每条边都引入一条容量为0的反向边(residual edge,初始为0/0),所得的新图叫做流图(flow graph),也叫残留图(residual graph)。增广路径(augmenting path)是指流图上从源点->汇点的剩余容量(=capacity-flow)大于0的路径。反向边的引入是为了undo“坏的”、没能算出最大流的增广路径。
Ford-Fulkerson算法不断在流图上找增广路径、并增广路径上的流。怎么增广?记路径的瓶颈流量为bottleneck,对于路径上的每条原始边增加bottleneck流量,每条反向边减少bottleneck流量。找不到增广路径时算法结束,最大流=sum(所有增广路径的bottleneck),或=sum(流入汇点的flow)。
Ford-Fulkerson只是通用的算法框架,没有指定找增广路径的方法。找增广路径,可以dfs、bfs(Edmonds-Karp)、优先选大容量的启发式(capacity scaling)、bfs+dfs(Dinic's)等。参见WilliamFiset的图论视频
最大二分匹配
变为最大流问题:添加源点s到左半边节点的容量为1的边、添加右半边节点到汇点t的容量为1的边,找最大流。
这里因为已是二分且容量为0或1,实现上可简化。用数组matchR记录找到的匹配某右边节点的左边节点,不断用dfs找匹配左边某节点的右边节点(相当于找增广路径)。
最佳会面地点
A group of two or more people wants to meet and minimize the total travel distance. You are given a 2D grid of values 0 or 1, where each 1 marks the home of someone in the group. The distance is calculated usingManhattan Distance, where distance(p1, p2) =
|p2.x - p1.x| + |p2.y - p1.y|.For example, given three people living at
(0,0),(0,4), and(2,2):1 - 0 - 0 - 0 - 1 | | | | | 0 - 0 - 0 - 0 - 0 | | | | | 0 - 0 - 1 - 0 - 0The point
(0,2)is an ideal meeting point, as the total travel distance of 2+2+2=6 is minimal. So return 6.Hint:
- Try to solve it in one dimension first. How can this solution apply to the two dimension case?
int minTotalDistance(vector<vector<int>>& grid) {
// 分维度考虑,在某个维度让各点相遇的路程
if (grid.empty()) return 0;
const int R = grid.size(), C = grid[0].size();
vector<int> rows, cols;
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
if (grid[r][c]) {
rows.push_back(r);
cols.push_back(c);
}
}
}
return minDist(rows) + minDist(cols);
}
int minDist(vector<int> &v) {
int ans = 0;
sort(v.begin(), v.end());
int i = 0, j = (int)v.size() - 1;
while (i < j) {
ans += v[j--] - v[i++];
}
return ans;
}
令牌袋得分
int bagOfTokensScore(vector<int>& tokens, int P) {
// token有两种用法:-tokens[i]、+1分,+tokens[i]、-1分
// 贪心法,尽量多加分少减分,让单位token加分最多减分最少。
// 加分时用最小的tokens[i]、减分时用最大的tokens[i]
sort(tokens.begin(), tokens.end());
int i = 0, j = (int)tokens.size() - 1;
int point = 0, ans = 0;
while (i <= j) {
if (P >= tokens[i]) {
P -= tokens[i++];
ans = max(ans, ++point);
} else if (point > 0) {
P += tokens[j--];
point--;
} else {
break;
}
}
return ans;
}
2sum,找两数之和等于t
- https://leetcode.com/problems/two-sum/
数组无序,可以用个unordered_map记录出现过的数,对当前数看map中是否存在toFind=t-nums[i]。
- https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/
数组有序,可以两指针相向遍历。若a[i]+a[j]==t则找到,<t则右移i,>t则左移j。
- https://leetcode.com/problems/two-sum-iv-input-is-a-bst/
在bst中找两数之和等于t,可以直接像无序数组那样用个unordered_set记录出现过的数,dfs一遍。
某数能否分解成两数的平方和
[1..sqrt(num)]上,两指针相向遍历
4个数组各取一数之和等于0,有多少种取法
取两数组作为一组,用cnt[]给和计数,类似无序数组的2sum问题
3sum,找三数之和等于t
数组排序,在2sum问题的外层加个循环遍历取第一个数。
- https://leetcode.com/problems/3sum/
- https://leetcode.com/problems/3sum-closest/
- https://leetcode.com/problems/3sum-smaller/
- https://leetcode.com/problems/valid-triangle-number/
Given an array of n integers nums and a target, find the number of index triplets
i, j, kwith0 <= i < j < k < nthat satisfy the conditionnums[i] + nums[j] + nums[k] < target.For example, given nums =
[-2, 0, 1, 3], and target = 2.Return 2. Because there are two triplets which sums are less than 2:
[-2, 0, 1] [-2, 0, 3]Follow up:
Could you solve it in O(n^2) runtime?
int threeSumSmaller(vector<int>& nums, int target) {
// 两指针法
sort(nums.begin(), nums.end());
const int N = nums.size();
int ans = 0;
for (int i = 0; i < N - 2; i++) {
int j = i + 1, k = N - 1;
while (j < k) {
if (nums[i] + nums[j] + nums[k] < target) {
// j&k, j&(k-1), j&(k-2) ... j&(j+1) 的数对都满足
ans += k - j;
j++;
} else {
k--;
}
}
}
return ans;
}
可作三角形边长的三数有多少组
- https://leetcode.com/problems/valid-triangle-number/ 类似3sums-smaller
int triangleNumber(vector<int>& nums) {
sort(nums.begin(), nums.end());
const int N = nums.size();
int ans = 0;
for (int k = 2; k < N; k++) {
int i = 0, j = k - 1;
while (i < j) {
if (nums[i] + nums[j] > nums[k]) {
// (i,j) & (i+1,j) & ... & (j-1,j) 都符合
ans += j - i;
j--;
} else {
i++;
}
}
}
return ans;
}
k数之和,动态规划
数组中找重复值
int findDuplicate(vector<int>& nums) {
// 由鸽笼原理,一定有重复
// 把nums[i]看做下标i的next下标,相当于链表的next指针,
// 找重复的数变成链表中找环的入口点,快慢指针法
int fast = 0, slow = 0;
while (true) {
fast = nums[nums[fast]];
slow = nums[slow];
if (fast == slow) break;
}
fast = 0;
while (fast != slow) {
fast = nums[fast];
slow = nums[slow];
}
return fast;
}
循环数组中找单向循环
快慢指针找循环,将够不成循环的路径上的点都设为0。
bool circularArrayLoop(vector<int>& nums) {
const int N = nums.size();
// 快慢指针找循环
for (int i = 0; i < N; i++) {
if (nums[i] == 0) continue; // 无循环路径上的点已设为0
// 看从i出发是否构成单向循环
int fast = i, slow = i;
while (true) {
int next = getNext(fast, nums);
if (nums[i] * nums[next] < 0) break; // 循环方向变了
fast = next, next = getNext(fast, nums);
if (nums[i] * nums[next] < 0) break;
fast = next;
slow = getNext(slow, nums);
if (fast == slow) { // 有环
if (getNext(fast, nums) == fast) break; // 循环长度==1
return true;
}
}
// 至此从i出发够不成循环,将路径上的点都设为0
int sign = nums[i], curr = i;
while (sign * nums[curr] > 0) {
int next = getNext(curr, nums);
nums[curr] = 0;
curr = next;
}
}
return false;
}
int getNext(int idx, vector<int> &nums) {
const int N = nums.size();
int ans = (idx + nums[idx]) % N;
if (ans < 0) ans += N;
return ans;
}
更多快慢指针的例子见链表
实际是两指针法,要利用某种单调性。[lo,hi]是子段,每次hi移动一步,lo移动0步或多步。
- 常见伸缩滑动窗口,写法关键是使窗口保持有效条件。
- 求最长窗口长,可用不收缩滑动窗口:窗口无效时lo只移动一步(不用while而用if),窗口大小保持不变、整体右移一步。窗口内暂时打破了有效条件,但循环结束时能确定找到最长窗口长N-lo。
- 用滑动数组解子段和问题,需要数组全部非负数,即
presum[]数组单调递增
包含某些字符的最小窗口
- https://leetcode.com/problems/minimum-window-substring/
- https://leetcode.com/problems/find-all-anagrams-in-a-string/
- https://leetcode.com/problems/replace-the-substring-for-balanced-string/
- https://leetcode.com/problems/permutation-in-string/
- https://leetcode.com/problems/substring-with-concatenation-of-all-words/
string minWindow(string s, string t) {
if (s.empty() || s.size() < t.size()) return "";
unordered_map<char, int> cnt;
for (char c : t) cnt[c]++;
int distinct = cnt.size();
int minWidth = INT_MAX, ansLo;
for (int hi = 0, lo = 0; hi < s.size(); hi++) {
if (--cnt[s[hi]] == 0) distinct--;
while (distinct == 0) { // 有效窗口
if (hi - lo + 1 < minWidth) {
minWidth = hi - lo + 1;
ansLo = lo;
}
if (cnt[s[lo]]++ == 0) distinct++;
lo++;
}
}
return minWidth != INT_MAX ? s.substr(ansLo, minWidth) : "";
}
相关:含某些字符的最小窗口用滑动窗口,含子序列的最小窗口用动态规划。
可替换<=k个字符,连续单字符的最长子段长
int characterReplacement(string s, int k) {
const int N = s.size();
unordered_map<char, int> count;
int maxCnt = 0, lo = 0;
for (int hi = 0; hi < N; hi++) {
// 每一步都尝试将窗口长度推到极限 maxCnt+k
// 不收缩滑动窗口,窗口不符合条件时整体右移一步
maxCnt = max(maxCnt, ++count[s[hi]]);
if (hi - lo + 1 > maxCnt + k) {
count[s[lo]]--;
lo++;
}
}
return N - lo;
}
含全部三种字符的子段数
int numberOfSubstrings(string s) {
const int N = s.size();
unordered_map<int, int> cnt; // char=>count
int ans = 0;
for (int hi = 0, lo = 0; hi < N; hi++) {
cnt[s[hi]]++;
while (cnt['a'] && cnt['b'] && cnt['c']) {
cnt[s[lo++]]--;
}
// 现在[lo..hi]里字符不够,但前面的lo=[0..lo-1]都够
ans += lo; // 关键是这里,在不合法状态计算合法状态数
}
return ans;
}
满意的顾客数
int maxSatisfied(vector<int>& customers, vector<int>& grumpy, int X) {
// 滑动窗口记录X分钟内的不满意者xUnsatis
const int N = customers.size();
int satis = 0, xUnsatis = 0, maxUnsatis = 0;
for (int hi = 0; hi < N; hi++) {
if (!grumpy[hi]) satis += customers[hi];
if (grumpy[hi]) xUnsatis += customers[hi];
if (hi >= X && grumpy[hi-X]) xUnsatis -= customers[hi-X];
maxUnsatis = max(maxUnsatis, xUnsatis);
}
return satis + maxUnsatis;
}
滑动窗口写法"hi移一步、lo移多步",可以解"至多K"问题atMost(K)
- ”刚好K“问题exactly(K)=atMost(K)-atMost(K-1)
联想:atLeast(K)问题只能解全正数数组的最短子段,需要 1.全正数 2.atLeastK 3.求最短。见子段和>=T
无重复字符的最长子段
窗口内至多一个
- https://leetcode.com/problems/longest-substring-without-repeating-characters/
int lengthOfLongestSubstring(string s) {
const int N = s.size();
unordered_map<char, int> cnt;
int ans = 0;
for (int hi = 0, lo = 0; hi < N; hi++) {
cnt[s[hi]]++;
while (cnt[s[hi]] > 1) {
cnt[s[lo]]--;
lo++;
}
ans = max(ans, hi - lo + 1);
}
return ans;
}
至多K个不同字符的最长子段
- https://leetcode.com/problems/longest-substring-with-at-most-k-distinct-characters/
int lengthOfLongestSubstringKDistinct(string s, int k) {
unordered_map<int, int> cnt; // char=>count
int ans = 0;
for (int hi = 0, lo = 0; hi < s.size(); hi++) {
if (cnt[s[hi]]++ == 0) k--;
while (k < 0) {
if (--cnt[s[lo]] == 0) k++;
lo++;
}
ans = max(ans, hi - lo + 1);
}
return ans;
}
把01数组的<=K位0改成1,连续1的最长子段长
- https://leetcode.com/problems/max-consecutive-ones-iii/
- https://leetcode.com/problems/longest-subarray-of-1s-after-deleting-one-element/
int longestOnes(vector<int>& A, int K) {
const int N = A.size();
int zeroCnt = 0, ans = INT_MIN;
for (int hi = 0, lo = 0; hi < N; hi++) {
if (A[hi] == 0) zeroCnt++;
while (zeroCnt > K) {
if (A[lo++] == 0) zeroCnt--;
}
ans = max(ans, hi - lo + 1);
}
return ans;
}
含K个不同数的子段个数
- https://leetcode.com/problems/subarrays-with-k-different-integers/
- https://leetcode.com/problems/binary-subarrays-with-sum/
- https://leetcode.com/problems/count-number-of-nice-subarrays
int subarraysWithKDistinct(vector<int>& A, int K) {
return atMost(A, K) - atMost(A, K-1);
}
int atMost(vector<int>& A, int K) {
const int N = A.size();
unordered_map<int, int> cnt; // num=>count
int ans = 0;
for (int hi = 0, lo = 0; hi < N; hi++) {
if (cnt[A[hi]]++ == 0) K--;
while (K < 0) {
if (--cnt[A[lo]] == 0) K++;
lo++;
}
ans += hi - lo + 1;
}
return ans;
}
长为K的最小和子段
简单的情况,可不用atMost(K)-atMost(K-1)写法
int maxScore(vector<int>& cardPoints, int k) {
// 变为:找长=N-k的最小和子段
const int N = cardPoints.size();
int totalSum = 0, subSum = 0, minSum = INT_MAX;
for (int hi = 0, lo = 0; hi < N; hi++) {
totalSum += cardPoints[hi];
subSum += cardPoints[hi];
if (hi - lo + 1 > N - k) {
subSum -= cardPoints[lo];
lo++;
}
if (hi - lo + 1 == N - k) {
minSum = min(minSum, subSum);
}
}
return totalSum - minSum;
}
全非负数意味着,preprod[]数组单调递增,可以用滑动窗口解法
全正数数组、乘积<K的子段数
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
// 因为 k<2^20, nums[i]<2^10,小于k的乘积prod*nums[i]<2^30不会溢出
if (k <= 1) return 0;
int prod = 1, ans = 0;
for (int hi = 0, lo = 0; hi < nums.size(); hi++) {
prod *= nums[hi];
while (prod >= k) {
prod /= nums[lo];
lo++;
}
// 以hi结尾的子段积都满足:[lo..hi], [lo+1,hi], ..., [hi,hi]
ans += hi - lo + 1;
}
return ans;
}
全正数数组、和>=K最短子段长
int minSubArrayLen(int k, vector<int>& nums) {
const int N = nums.size();
int sum = 0, ans = INT_MAX;
for (int hi = 0, lo = 0; hi < N; hi++) {
sum += nums[hi];
while (sum >= k) {
ans = min(ans, hi - lo + 1);
sum -= nums[lo];
lo++;
}
}
return ans != INT_MAX ? ans : 0;
}
扩展:有负数数组的子段和,可以用 子段和的presum解法
“子段和”的总结
- 最大子段和,用动态规划
- 数组全正数(意味着
presum[]数组递增),可以用滑动窗口 - 数组有负数(意味着
presum[]数组无序),用presum解法- 子段和
>=K、<=K、在范围[lower,upper],都能用presum解法
- 特例,子段和>=K的最短子段用了单调队列
- 子段和
另:使用presum数组时,k长子段=presum[i]-presum[i-k]一定是半开半闭
- 如果设
presum[i]=[0..i)(这时i也是区间长度),后开,k长子段也是后开(前闭) - 如果设
presum[i]=[0..i](这时空区间要单独对待),后闭,k长子段也是后闭(前开)
`最大子段和,动态规划
设dp[i]表示结束位置为 i 的最大子段和,若子段可空,dp[i]=max(dp[i-1]+nums[i], 0)。
dp[i]只依赖dp[i-1],降一维,将dp[i]记作currMax,有 pp p77 的如下算法。
currMax = 0, ans = INT_MIN
for (num : nums) {
currMax = max(currMax + num, 0)
ans = max(ans, currMax)
}
扩展:最大子段和,子段非空
dp[i] = max(dp[i-1]+nums[i], nums[i]) = nums[i] + max(dp[i-1], 0)
int maxSubArray(vector<int>& nums) {
int currMax = 0, ans = INT_MIN;
for (int num : nums) {
currMax = num + max(currMax, 0);
ans = max(ans, currMax);
}
return ans;
}
扩展:循环数组的最大子段和,子段非空
int maxSubarraySumCircular(vector<int>& A) {
// 1. 没跨越数组尾,= 最大子段和ansMax
// 2. 跨越数组尾,= arrSum - 最小子段和ansMin
// 特例:ansMax<0时,数组全是负数,arrSum==ansMin;要返回ansMax
int currMax = 0, ansMax = INT_MIN;
int currMin = 0, ansMin = INT_MAX;
int arrSum = 0;
for (int a : A) {
currMax = a + max(currMax, 0);
ansMax = max(ansMax, currMax);
currMin = a + min(currMin, 0);
ansMin = min(ansMin, currMin);
arrSum += a;
}
if (ansMax < 0) return ansMax;
return max(ansMax, arrSum - ansMin);
}
扩展:k 次重复数组的最大子段和
int kConcatenationMaxSum(vector<int>& arr, int k) {
// 最大子段和,如果arrSum<=0:
// 1. 在arr中间,k=1
// 2. 在两数组尾首,arr[suffix]+arr[prefix],k=2
// 如果arrSum>0:
// 1. 在arr中间 + (k-1)*arrSum
// 2. 在两数组尾首,arr[suffix]+arr[prefix]+(k-2)*arrSum
// 总结:
// 对k<=2,照常找最大子段和;
// 对k>2且arrSum>0,再加上(k-2)*arrSum
long maxendinghere = 0, maxsofar = 0;
for (int i = 0; i < min(k, 2); i++) {
for (int a : arr) {
maxendinghere = max(maxendinghere + a, 0L);
maxsofar = max(maxsofar, maxendinghere);
}
}
const int MOD = 1e9 + 7;
long arrSum = accumulate(begin(arr), end(arr), 0);
if (k > 2 && arrSum > 0) {
return (maxsofar + (k - 2) * arrSum) % MOD;
}
return maxsofar % MOD;
}
扩展:最大子矩阵和
对 MxN 矩阵列举所有行区间 O(M^2),对某个行区间将各行累加成一行,然后用最大子段和 O(N)算法求得该行区间的对应最大列区间。复杂度 O(M^2 * N),见 ctci p614。
另:对于子区域和,如果先预处理得到{(0,0),(r,c)}区域的"累加和"数组sum[r,c],那么区域和{(r1,c1), (r2,c2)}=sum[r2,c2]-sum[r2,c1-1]-sum[r1-1,c2]+sum[r1-1,c1-1]。而预处理时sum[r,c]=x[r][c]+sum[r-1,c]+sum[r,c-1]-sum[r-1,c-1]。整个算法 O(M^2 * N^2)。
联想:全是 1 的子矩阵
最大子段积
int maxProduct(vector<int>& nums) {
// 最大乘积可能来自负负得正,因此要保留最大乘积currMax和最小乘积currMin,
// 以nums[i]结尾子问题nums[0..i]的最大最小乘积来自 { currMax*nums[i], currMin*nums[i], nums[i] }
const int N = nums.size();
int currMax = 1, currMin = 1, ans = INT_MIN;
for (int num : nums) {
int cand1 = currMax * num, cand2 = currMin * num;
currMax = max({cand1, cand2, num});
currMin = min({cand1, cand2, num});
if (currMax > ans) ans = currMax;
}
return ans;
}
联想:"全正数数组、子段积<K 的个数"参考 滑动数组解法
可有 1 个删除的最大子段和
int maximumSum(vector<int>& arr) {
// 设dp[i][0]表示以arr[i]结尾、子段中间没有删除过数的最大子段和,
// dp[i][1]表示以arr[i]结尾、子段中间删除过一个数的最大子段和。
// dp[i][0] = max(dp[i-1][0]+arr[i], arr[i])
// dp[i][1] = max(dp[i-1][0], dp[i-1][1]+arr[i])
// 第i项只依赖i-1项,省掉i这维,注意赋值顺序,
// dp[1] = max(dp[0], dp[1]+arr[i])
// dp[0] = max(dp[0]+arr[i], arr[i])
const int N = arr.size();
vector<int> dp({arr[0], 0}); // i==0
int ans = dp[0]; // 子段要非空
for (int i = 1; i < N; i++) {
dp[1] = max(dp[0], dp[1] + arr[i]);
dp[0] = max(dp[0] + arr[i], arr[i]);
ans = max({ans, dp[0], dp[1]});
}
return ans;
}
找数组中两个不相交的子段 A 和 B,使|sum(A)-sum(B)|最大
使|sum(A)-sum(B)|最大,从中间某点 i 把数组分成两半,就是找 max{ abs(左半最大子段和-右半最小子段和), abs(左半最小子段和-右半最大子段和) },遍历分割点 i。
除了求最大子段和用的 O(n)动态规划,求其他满足“某种条件"的子段和可用”前缀和“解法。runningSum 对应“前缀和”数组的“后闭”模式。runningSum 可以认为是滑动窗口的特例。
和为 target 的子段数
- https://leetcode.com/problems/binary-subarrays-with-sum/
- https://leetcode.com/problems/subarray-sum-equals-k/
- https://leetcode.com/problems/maximum-size-subarray-sum-equals-k/
int numSubarraysWithSum(vector<int>& A, int target) {
unordered_map<int, int> presum = {{0,1}}; // sum=>count
int runningSum = 0;
int ans = 0;
for (int a : A) {
runningSum += a;
// 需S=runningSum-toFind
int toFind = runningSum - target;
if (presum.count(toFind)) ans += presum[toFind];
presum[runningSum]++;
}
return ans;
}
和为 target 的不重叠子段数
- https://leetcode.com/problems/maximum-number-of-non-overlapping-subarrays-with-sum-equals-target/
int maxNonOverlapping(vector<int>& nums, int target) {
const int N = nums.size();
// 判断重叠,使用sum=>idx、记录lastIdx
unordered_map<int, int> presum = {{0, -1}}; // sum=>idx
int runningSum = 0, lastIdx = -1, ans = 0;
for (int i = 0; i < N; i++) {
runningSum += nums[i];
// runningSum-toFind=target
int toFind = runningSum - target;
if (presum.count(toFind)) {
// 保证不重叠,presum相关是前开后闭区间
if (presum[toFind] >= lastIdx) {
ans++;
lastIdx = i;
}
}
// 多个相同值保留最右的,因为前面的已处理过
presum[runningSum] = i;
}
return ans;
}
最接近 target 的子段和
假设已把前面各累加和放到的 set 中,对当前 runningSum 要在 set 中找 toFind 使:runningSum-toFind 最接近 target,即找最接近 runningSum-target 的旧和值 toFind。设 x=runningSum-target,看备选 it=lower_bound(x)和 prev(it)哪个更接近 x。
java 代码可参考,jave 中 TreeSet 对应 c++的 std::set
落在范围[lower,upper]的子段和有多少
用 map 记录各累加和出现的次数。对当前 runningSum 要在 map 中查找 toFind 使 lower<=runningSum-toFind<=upper,runningSum-upper<=toFind<=runningSum-lower。
int countRangeSum(vector<int>& nums, int lower, int upper) {
map<long, int> cnt = {{0, 1}} // sum=>count
long runningSum = 0;
int ans = 0;
for (int num : nums) {
runningSum += num;
// 找数x使lower<= runningSum-x <=upper,runningSum-upper<= x <=runningSum-lower
auto it1 = cnt.lower_bound(runningSum - upper);
auto it2 = cnt.upper_bound(runningSum - lower);
for (auto it = it1; it != it2; it++) {
ans += it->second;
}
cnt[runningSum]++;
}
return ans;
}
可被 k 整除的子段和有多少
int subarraysDivByK(vector<int>& A, int K) {
unordered_map<int, int> cnt = {{0, 1}}; // sum=>count
int runningSum = 0;
int ans = 0;
for (int a : A) {
runningSum = (runningSum + a % K + K) % K; // a可能为负
// (runningSum-toFind)%K==0,(toFind==runningSum)%K
ans += cnt[runningSum];
cnt[runningSum]++;
}
return ans;
}
子段和等于 k 的倍数、且子段长>=2
bool checkSubarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp = {{0, -1}}; // sum=>idx
int runningSum = 0;
// 子段和是k的倍数,即子段和(runningSum-toFind)%k==0
// (toFind==runningSum)%K
// 为让子段尽量长,多个相同runningSum的只保留第一个
for (int i = 0; i < nums.size(); i++) {
runningSum += nums[i];
if (k) runningSum %= k;
if (!mp.count(runningSum)) {
mp[runningSum] = i;
} else if (i - mp[runningSum] >= 2) {
return true;
}
}
return false;
}
子段在重排和替换 k 个字符后,能够成回文串吗
vector<bool> canMakePaliQueries(string s, vector<vector<int>>& queries) {
// "前缀和"preOdds[i+1]对应s[0..i],各个位对应各字符的奇偶性
vector<int> preOdds = {0};
int runningOdds = 0;
for (char c : s) {
runningOdds ^= 1 << (c - 'a');
preOdds.push_back(runningOdds);
}
vector<bool> ans;
for (auto &query : queries) {
// 回文需要oddCnt/2<=k
int odds = preOdds[query[1] + 1] ^ preOdds[query[0]];
int oddCnt = bitset<26>(odds).count();
ans.push_back(oddCnt / 2 <= query[2]);
}
return ans;
}
两个不重叠子段的最大和
- https://leetcode.com/problems/maximum-sum-of-two-non-overlapping-subarrays/
int maxSumTwoNoOverlap(vector<int>& A, int l1, int l2) {
// 题目:两个不重叠子段,l1在l2前、或l2在l1前
const int N = A.size();
vector<int> presum(N + 1, 0);
for (int i = 0; i < N; i++) {
presum[i + 1] = presum[i] + A[i];
}
int l1max = 0, l2max = 0, ans = 0;
for (int i = l1 + l2; i <= N; i++) {
// l1在l2前
l1max = max(l1max, presum[i - l2] - presum[i - l1 - l2]);
ans = max(ans, l1max + (presum[i] - presum[i - l2]));
// l2在l1前
l2max = max(l2max, presum[i - l1] - presum[i - l1 - l2]);
ans = max(ans, l2max + (presum[i] - presum[i - l1]));
}
return ans;
}
有负数数组,意味着presum[]无序,先想到presum解法,可配合其他条件
有负数数组、和>=T的最短子段
用了presum[]和单调栈
int shortestSubarray(vector<int>& A, int T) {
// 求presum[i]-presum[j]>=T的最短子段长
// 对于左端点presum[j],如果在presum[]中能找到下一个更小(或相等)的数presum[k],
// 因为presum[i]-presum[k]更大(或相等)、且子段i-k更短,所以presum[k]占优。
// 只需考虑占优选项,用单调栈
// 配合两指针法
const int N = A.size();
vector<long> presum(N + 1, 0);
for (int i = 1; i <= N; i++) {
presum[i] = presum[i - 1] + A[i - 1];
}
int ans = INT_MAX;
deque<int> dq;
for (int k = 0; k <= N; k++) {
while (!dq.empty() && presum[k] <= presum[dq.back()]) { // 只考虑占优策略
dq.pop_back();
}
dq.push_back(k);
while (!dq.empty() && presum[k] - presum[dq[0]] >= T) {
ans = min(ans, k - dq[0]);
dq.pop_front(); // dp[0]对后面的k都不会取得更短子段
}
}
return ans != INT_MAX ? ans : -1;
}
用runningSum解法,至少O(NlgN),超时
// Time Limit Exceeded
int shortestSubarray(vector<int>& A, int K) {
map<int, int> mp; // sum=>lastIdx
int runningSum = 0;
mp[runningSum] = -1;
int ans = INT_MAX;
for (int i = 0; i < A.size(); i++) {
runningSum += A[i];
// 在旧runningSum集合mp中找toFind,
// 使runningSum-toFind>=K,toFind<=runningSum-K
int x = runningSum - K;
auto ub = mp.upper_bound(x);
for (auto it = mp.begin(); it != ub; it++) {
ans = min(ans, i - it->second);
}
mp[runningSum] = i;
}
return ans != INT_MAX ? ans : -1;
}
和都为k的两个不重叠子段,长度和最小
- < https://leetcode.com/problems/find-two-non-overlapping-sub-arrays-each-with-target-sum/>
int minSumOfLengths(vector<int>& arr, int target) {
// 全为正数,可用滑动数组
// 设dp[i]表示arr[0..i]的、和为target的、最小长度
// 若找到有效窗口[lo..hi],dp[hi]=min(hi-lo+1,dp[hi-1])
// 若没找到,dp[hi]=dp[hi-1]
const int N = arr.size(), INF = 1e9;
vector<int> dp(N, INF);
int sum = 0, ans = INF;
for (int hi = 0, lo = 0; hi < N; hi++) {
sum += arr[hi];
while (sum > target) {
sum -= arr[lo++];
}
if (sum == target) {
int len = hi - lo + 1;
dp[hi] = len;
// 不重叠子段,能求到两个长度最小的
if (lo > 0) ans = min(ans, len + dp[lo-1]);
}
if (hi > 0) dp[hi] = min(dp[hi], dp[hi-1]);
}
return ans == INF ? -1 : ans;
}
三个不重叠的k长子段的最大和
vector<int> maxSumOfThreeSubarrays(vector<int>& nums, int k) {
if (k * 3 > nums.size()) return { -1, -1, -1 };
// 先算出所有k长的子段和,构成数组W
vector<int> W(nums.size() - k + 1);
int sum = 0;
for (int i = 0; i < k; i++) sum += nums[i];
int iw = 0;
W[iw++] = sum;
for (int i = k; i < nums.size(); i++) {
sum += nums[i] - nums[i-k];
W[iw++] = sum;
}
// 问题变成从W中取3个数ia、ib、ic,使W[ia]+W[ib]+W[ic]最大,不重叠要求ia+k<=ib、ib+k<=ic。
// 三个数一般想法是固定住中间的数,故先选定中间数ib。那么ia只要在[0..ib-k]中找最大值的最左出现,
// ic只要在[ib+k..N-1]中找最大值的最左出现。选最左出现为满足字典序最小。
const int N = W.size();
vector<int> leftIdx(N);
int maxIdx = 0;
for (int i = 0; i < N; i++) {
if (W[i] > W[maxIdx]) maxIdx = i; // 最大值的最左出现
leftIdx[i] = maxIdx;
}
vector<int> rightIdx(N);
maxIdx = N - 1;
for (int j = N - 1; j >= 0; j--) {
if (W[j] >= W[maxIdx]) maxIdx = j; // 最大值的最左出现,注意这里>=号
rightIdx[j] = maxIdx;
}
// 选三个数{ia,ib,ic}, ia+k<=ib, ib+k<=ic
int maxSum = INT_MIN;
vector<int> ans = { -1, -1, -1 };
for (int ib = k; ib < N - k; ib++) {
int ia = leftIdx[ib-k], ic = rightIdx[ib+k];
int sum = W[ia] + W[ib] + W[ic];
if (sum > maxSum) {
maxSum = sum;
ans = { ia, ib, ic };
}
}
return ans;
}
奇数和子段的个数
int numOfSubarrays(vector<int>& arr) {
// 设odd[i]表示以arr[i]结尾的、arr[0..i]的"和为奇数"的、子段个数,
// even[i] 和为偶数
// 若arr[i]是奇数,
// odd[i]=even[i-1]+1 /*+1因为arr[i]单独作为一个子段*/
// even[i]=odd[i-1],
// 若arr[i]是偶数,
// odd[i]=odd[i-1]
// even[i]=even[i-1]+1 /*+1因为arr[i]单独作为一个子段*/,
// 所求为sum(odd[i])
// 第i项只依赖于第i-1项,省掉i这维,i仍从左往右遍历
// arr[i]是奇数时,even=odd,odd=even+1
// 偶数时,even+=1
const int MOD = 1e9 + 7;
int ans = 0;
vector<int> dp(2); // even,odd
for (int num : arr) {
if (num & 1) {
dp = { dp[1], dp[0] + 1};
} else {
dp[0]++;
}
ans = (ans + dp[1]) % MOD;
}
return ans;
}
除掉i、j、k,数组分成非空的四段,且这四段的和相等
划分的invariant
画invariant示意图,加上待处理共三段,两指针指向第一段最右、第三段最左。
从左到右划分:首元素t作pivot后,分三段:<t、>=t、待处理;初始化两指针使第一段<t为空 m=l、全是第三段待处理 i=l+1,主循环右移待处理指针i,见pp p112。记住这种写法。
|t| <t | >=t | ? |
l m i u
int t = a[l];
int m = l;
for (int i = l + 1; i <= u; i++) {
if (a[i] < t) swap(++m, i); // 往左抛
}
swap(m, l);
从右到左划分:首元素t作pivot后,分三段:待处理、<t、>=t;初始化两指针使第三段>=t为空 m=u+1、全是第一段待处理 i=u,主循环左移待处理指针i,见pp p210。不用记这种写法。
|t| ? | <t | >=t |
l i m u
int t = a[l];
int m = u + 1;
for (int i = u; i > l; i--) {
if (a[i] >= t) swap(--m, i); // 往右抛
}
swap(--m, l);
双向划分:首元素t作pivot后,分三段:<=t、待处理、>=t,两端范围都带等号;初始化两指针使第一段<=t为空 i=l、第三段>=t为空 j=u+1,loop中用do...while移动i、j指针,见pp p114。不用记这种写法。
|t| <=t | ? | >=t |
l i j u
int partition(int l, int u) {
if (l >= u) return;
swap(l, randint(l, u));
int t = a[l];
int i = l, j = u + 1;
while (true) {
do { i++; } while (i <= u && a[i] < t); // 停下时>=t
do { j--; } while (j > l && a[j] > t); // 停下时<=t
if (i > j) break;
swap(i, j);
}
swap(j, l);
return j;
}
loop中用while移动i、j指针,写法微调,记住这种写法。
// 双向划分
int partition(int l, int u) {
if (l >= u) return l;
swap(l, randint(l, u));
int t = a[l];
int i = l + 1, j = u; // 初始时多个步进
while (true) {
while (i <= u && a[i] < t) i++; // 停下时>=t
while (j > l && a[j] > t) j--; // 停下时<=t
if (i > j) break;
swap(i, j);
i++; j--; // 循环中也多个步进
}
swap(j, l);
return j;
}
三路划分:将数组分为<t、=t、>t三段
从左到右划分,遇到<t的往左抛,遇到>t的往右抛,=t的就会留在中间。
用i指向<t的待写入位置,j指向待处理位置,k指向>t的待写入位置。
|t| <t | =t | ? | >t |
l i j k u
int t = a[l];
int i = l+1, j = l+1, k = u;
while (j <= k) {
if (a[j] < t) { swap(j,i); i++; j++; }
else if (a[j] > t) { swap(j, k); k--; } // 因为从右边换过来的还待处理,不用j++
else { j++; }
}
swap(--i, l);
另:二路划分若用三路划分往两端抛的思路写,同样从左到右划分,但比标准写法繁琐:
int t = a[l];
int i = l + 1, j = u; // i指向<t的待写入部分,j指向>=t的待写入部分
while (i <= j) {
if (a[i] < t) { i++; }
else { swap(i, j); j--; }
}
swap(j, l);
快排划分找第k大的数
- https://leetcode.com/problems/kth-largest-element-in-an-array/
int findKthLargest(vector<int>& nums, int k) {
int l = 0, u = (int)nums.size() - 1;
k--; // k是1-based,变成下标作比较
while (l <= u) {
int p = partition(nums, l, u);
if (k == p) break;
if (k < p) u = p - 1;
else l = p + 1;
}
return nums[k];
}
int partition(vector<int> &nums, int l, int u) {
if (l >= u) return l;
// 单向划分
// |t| >t | <=t | ? |
// l m i u
int m = l;
for (int i = l + 1; i <= u; i++) {
if (nums[i] > nums[l]) swap(nums[i], nums[++m]);
}
swap(nums[m], nums[l]);
return m;
}
摆动排序
把数组排成“小-大-小-大”的样子。
Given an unsorted array nums, reorder it such that nums[0] < nums[1] > nums[2] < nums[3]...
按逆序按中位数划分两半;把[N/2..](后面小的一半或一半多1)放在偶位,再把[0..N/2)(前面大的一半)放在奇位。
比如 1 2 3 3 3 3 4 5,逆序 5 4 3 3 3 3 2 1
先把 3 3 2 1 逆序放在偶位:3 · 3 · 2 · 1
再把 5 4 3 3 逆序放在奇位:· 5 · 4 · 3 · 3
变成:3 5 3 4 2 3 1 3
这种把后一半坐标[N/2..]放到偶位、[0..N/2)放到奇位的下标映射是:idx => (idx*2+1)%(N|1)
比如 0 1 2 3 4 5 6 7
逆序 7 6 5 4 3 2 1 0
变成 3 7 2 6 1 5 0 4
下标 6 4 2 0 7 5 3 1
若把数组排成“大-小-大-小”的样子。
按中位数划分两半;把[N/2..](后面大的一半或一半多1)放在偶位,再把[0..N/2)(前面小的一半)放在奇位。
比如 1 2 3 3 3 3 4 5 先把 3 3 4 5 放在偶位:3 · 3 · 4 · 5 再把 1 2 3 3 放在奇位:· 1 · 2 · 3 · 3 变成:3 1 3 2 4 3 5 3
可以把按中位数划分、按奇偶位摆放两个过程整合,在“三路划分”的过程中用虚拟索引摆放到位,这样只要O(1)空间。
void wiggleSort(vector<int>& nums) {
const int N = nums.size();
// 按逆序处理,下面先找media、再按>median三路划分
// 可自己实现findKthLargest,其中k是1-based,中位数k=(N+1)/2
// int median = findKthLargest(nums, (N + 1) / 2);
// 见 https://leetcode.com/problems/kth-largest-element-in-an-array/
// 这里也可以调用nth_element(..., greater<int>()),其中midptr是0-based
auto midptr = nums.begin() + (N - 1) / 2; // 0-based偏移中位数(N+1)/2-1
nth_element(nums.begin(), midptr, nums.end(), greater<int>());
int median = *midptr;
// 把后一半坐标[N/2..]放到偶位、前一半坐标[0..N/2)放到奇位的下标映射:i => (2*i+1) % (N|1)
// 见 https://leetcode.com/problems/wiggle-sort-ii/discuss/77677/O(n)+O(1)-after-median-Virtual-Indexing
auto idx = [&](int i) { return (2*i+1) % (N|1); };
// 三路划分:| >median | =median | ? | <median |
// 0 i j k N-1
// i指向>median待写入位置,j指向待处理位置,k指向<median待写入位置
int i = 0, j = 0, k = N - 1;
while (j <= k) {
if (nums[idx(j)] > median) {
swap(nums[idx(j)], nums[idx(i)]);
i++, j++;
} else if (nums[idx(j)] < median) {
swap(nums[idx(j)], nums[idx(k)]);
k--;
} else {
j++;
}
}
}
Given an unsorted array nums, reorder it in-place such that nums[0] <= nums[1] >= nums[2] <= nums[3]....
条件更宽松,<=、>=,只要把不符合要求的相邻数对换
void wiggleSort(vector<int>& nums) {
for (int i = 0; i + 1 < nums.size(); i++) {
if ((i % 2 == 0 && nums[i] > nums[i+1]) || (i % 2 == 1 && nums[i] < nums[i+1])) {
swap(nums[i], nums[i+1]);
}
}
}
最长摆动子序列、最长摆动子段只需用贪心法
有序数组中找值t
若存在,t一定在范围[l,u]中,x[l]<=t<=x[u]。初始l=0, u=n-1。写起来思路是排除不可能包含t的范围。
有序数组中找第一个出现的值t
变成找第一个>=t的值,看它是不是t。不变式x[l]<t<=x[u],当l+1=u时x[u]就是第一个>=t的值。 所以主循环是while (l+1 < u) {},循环结束时x[u]是第一个>=t的值。
要初始化l=-1, u=n。因为循环中l+1<u,实际访问的m=(l+u)/2一定l<m<u,不会访问数组最两端的l和u,可以假想x[l]=-∞、x[n]=∞。见pp p80
l = -1; u = n;
while l + 1 < u
m = (l + u) / 2
if x[m] >= t // 参照不变式x[l]<t<=x[u]的x[u]>=t部分
u = m
else
l = m
// x[u]是第一个>=t的值
p = u
if p >= n || x[p] != t
p = -1
上述不变式思路最清晰。还有另一种写法理解下不用记,关注在排除不可能范围,而不是保持不变式。初始范围l=0, u=n-1,需要循环结束时l和u刚好交错,l=u+1, x[l]>=t, x[u]<t,x[l]是第一个>=t的值。两种写法其实一样,正说反说的区别,把l=l'-1, u=u'+1代入标准式就得下式。
l = 0; u = n - 1;
while l <= u
m = (l + u) / 2
if x[m] >= t
u = m - 1
else
l = m + 1
// x[l]是第一个>=t的值
p = l
if p >= n || x[p] != t
p = -1
见 http://ranger.uta.edu/~weems/NOTES2320/binarySearchRange.c
有时还有种写法知道下不用记,与标准写法比较,看初始时是改了l还是u。比如改了l,就把l相关的都改动,把l=l'-1代入标准式可得。
l = 0; u = n;
while l < u
m = (l + u) / 2
if x[m] >= t
u = m
else
l = m + 1
// x[u]是第一个>=t的值
p = u
if p >= n || x[p] != t
p = -1
综上,l和u两端跟实际区间比,两端开区间是标准写法。哪端闭区间哪端作改动:比如第二种写法,两端闭区间,l和u相关的作改动;第三种写法,前闭后开,l相关的作改动。
为了防止死循环,总是检查2个元素时的执行能将范围减1。如果mid的取值(比如取小的中位数的mid=lo+(hi-lo)/2)和if...else...某分支的取值(比如mid=lo)相同,那就会死循环。
二分搜索本质:在 [0 ... 0 1 1 ...]的数组中找第一个1的位置
比如在有序数组中找第一个>=t的数,cond(x) { x>=t }。u总是满足条件的位置,l总是不满足条件的位置。
l = -1; u = n;
while l + 1 < u
m = (l + u) / 2
if cond(m) // 满足条件
u = m
else
l = m
...
二分搜索常用来找满足条件的最值。先根据题意列出各变量关系,只要表达式是关于某变量x的单调函数expr(x),就能用二分搜索解。
- 如果expr(x)关于x递增↑,条件式cond(x){ expr(x)>=某常量 }
- 如果expr(x)关于x递减↓,条件式cond(x){ expr(x)<=某常量 }
二分搜索可用在,比如:
- 找最大值的最小值
- 找最小值的最大值
- 找均值的最大值
- 找第k小的数x,表达式count(x){ <=x的个数 }关于x递增,条件式guess(x){ count(x)>=k }
- 找第k大的数x,表达式count(x){ >=x的个数 }关于x递减,条件式guess(x){ count(x)<=k }
有序数组中找最后一个出现的值t
只要找第一个>t的数,然后看它左边的数是不是t。不变式x[l]<=t<x[u],当l+1=u时x[l]就是最后一个<=t的值。 修改标准的二分搜索写法:1. 条件式由>=变成>,去掉等号,2. 最终要找的为u前面一位的l。
猴子吃香蕉的最小吃速
int minEatingSpeed(vector<int>& piles, int H) {
int lo = 1, hi = 1e9;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (enough(mid, piles, H)) {
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return lo;
}
bool enough(int K, vector<int> &piles, int H) {
// 吃完piles[i]需要(piles[i]+K-1)/K小时
// sum{ (piles[i]+K-1)/K }是K的递减函数,
// sum<=H 符合二分搜索[0 0 .. 0 1 1 ..]的条件
int sum = 0;
for (int pile : piles) {
sum += (pile + K - 1) / K;
}
return sum <= H;
}
D天内运完的最小运力
int shipWithinDays(vector<int>& weights, int D) {
int maxW = 0, sumW = 0;
for (int w : weights) {
maxW = max(maxW, w);
sumW += w;
}
int l = maxW, u = sumW;
while (l <= u) {
int m = l + (u - l) / 2;
if (enough(m, weights, D)) {
u = m - 1;
} else {
l = m + 1;
}
}
return l;
}
bool enough(int capacity, vector<int>& weights, int D) {
// 运载力capacity、天数days的关系
int weight = 0, days = 1;
for (int i = 0; i < weights.size(); i++) {
weight += weights[i];
if (weight > capacity) {
days++;
weight = weights[i];
}
}
// capacity越大、days越小、days<=D越返回1
return days <= D;
}
各子段和的最大值最小化
int splitArray(vector<int>& nums, int m) {
// 各子段和的最大值x在值范围[max(nums), sum(nums)]
int mx = 0;
long sum = 0;
for (int num : nums) {
mx = max(mx, num);
sum += num;
}
// 二分搜素猜子段和的最大值x
long l = mx, u = sum;
while (l <= u) {
long mid = l + (u - l) / 2;
if (enough(mid, nums, m)) {
u = mid - 1;
} else {
l = mid + 1;
}
}
return l;
}
bool enough(long x, vector<int> &nums, int m) {
// 子段和的最大值x和子段数m的关系,是关于x的递减函数count(x)
// 条件式count(x)<=m满足二分搜索的条件形式[0 0 ... 0 1 1 ...]
int count = 1;
long sum = 0;
for (int num : nums) {
sum += num;
if (sum > x) {
count++;
sum = num;
if (count > m) return false;
}
}
return true;
}
制造花束的最小等待天数
int minDays(vector<int>& bloomDay, int m, int k) {
// 要在bloomDay中找出m个k长的子段
if (bloomDay.size() < m * k) return -1;
// 等待天数wait为各子段最大值的最小值,值范围[min(bloomDay),max(bloomDay)]
int mn = INT_MAX, mx = INT_MIN;
for (int day : bloomDay) {
mn = min(mn, day);
mx = max(mx, day);
}
// 二分搜索猜等待天数
int lo = mn, hi = mx;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (enough(mid, bloomDay, m, k)) {
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return lo;
}
bool enough(int wait, vector<int>& bloomDay, int m, int k) {
// 等待天数wait一确定,bloomDay根据开没开花变成10数组,
// 就能统计有多少个k长的连续1的子段,count(wait)是关于wait的递增函数,
// count(wait)>=m满足二分搜索的条件形式[0..0 1 1..]
int adjacent = 0, count = 0;
for (int day : bloomDay) {
if (day <= wait) {
adjacent++;
if (adjacent >= k) {
count++;
adjacent = 0;
}
} else {
adjacent = 0;
}
}
return count >= m;
}
使放小球的位置尽可能分散
int maxDistance(vector<int>& position, int m) {
// 在允许的不同位置放球,使它们尽可能分散
// 最小距离dist的取值在[1..position[-1]-1]
// 二分搜索猜dist,找满足条件最后一个,修改标准二分搜索写法:
// 1. 条件式中去掉等号,2. 最终要找的位置为lo
sort(position.begin(), position.end());
int lo = 0, hi = position.back();
int ans = 0;
while (lo + 1 < hi) {
int mid = lo + (hi - lo) / 2;
if (enough(mid, position, m)) {
hi = mid;
} else {
lo = mid;
}
}
return lo; // 修改2
}
bool enough(int dist, vector<int>& position, int m) {
// 两球间的最小距离dist一确定,position[]中能放的球数count也确定
// count(dist)是关于dist的递减函数,
// count(dist)<m符合二分搜索的条件形式[0..0 1 1..],
// 用<m而不是<=m,因为要找满足条件的最后一个,条件式中去掉等号
const int N = position.size();
int nextPos = 0, count = 0;
for (int i = 0; i < N; i++) {
if (position[i] >= nextPos) {
count++;
nextPos = position[i] + dist;
}
}
return count < m; // 修改1
}
将>=value的全改成value,使数组之和最接近target的最小value
int findBestValue(vector<int>& arr, int target) {
const int N = arr.size();
sort(begin(arr), end(arr));
vector<int> presum(N + 1, 0); // presum[i]表示sum(arr[0..i))
int mx = INT_MIN;
for (int i = 0; i < N; i++) {
presum[i+1] = presum[i] + arr[i];
mx = max(mx, arr[i]);
}
int l = 0, u = mx + 1; // 题目条件
while (l + 1 < u) {
int m = l + (u - l) / 2;
// 满足二分搜索的条件形式[0..0 1..1]
if (getSum(m, arr, presum) >= target) {
u = m;
} else {
l = m;
}
}
// 找最接近target的数,u和u-1作为候选
int diff1 = abs(getSum(u, arr, presum) - target);
int diff2 = abs(getSum(u-1, arr, presum) - target);
return diff2 <= diff1 ? u-1 : u;
}
int getSum(int value, vector<int>& arr, const vector<int> &presum) {
// 将>=value的值全变成value
int idx = lower_bound(begin(arr), end(arr), value) - begin(arr);
// [0..idx)为原数,[idx..N)变为value,sum(value)是关于value的递增函数
int sum = presum[idx] + (arr.size() - idx) * value;
return sum;
}
设第k小的数为x,表达式count(x){<=x的个数}是关于x的递增函数,条件式enough(x){count(x)>=k}满足二分搜索条件形式[0 0 ... 0 1 1 ...]。
同理,若找第k大的数,设第k大的数为x,表达式count(x){>=x的个数}是关于x的递减函数,条件式enough(x){count(x)<=k}满足二分搜索条件形式[0 0 ... 0 1 1 ...]。
复杂度:T(n)=T(n/2)+f(n),f(n)是条件式的复杂度。根据主方法,看O(1)和O(f(n))哪个阶数高。若f(n)=O(n), T(n)=O(n);若f(n)=O(1), T(n)=O(lgn)。
从行列有序的矩阵中找第k小的数,可用二分法猜结果,或者用最小堆从多个有序行中找。
乘法表中找第k小的数
int findKthNumber(int m, int n, int k) {
// 猜第k小的数x,x在范围[1..m*n]
int l = 0, u = m * n + 1;
while (l + 1 < u) {
int mid = l + (u - l) / 2;
if (enough(mid, m, n, k)) {
u = mid;
} else {
l = mid;
}
}
return u;
}
bool enough(int x, int m, int n, int k) {
// count(x){ <=x的个数 }是关于x的递增函数,
// enough(x){ count(x)>=k }符合二分搜索的条件形式[0..0 1..1]
int count = 0;
for (int r = 1; r <= m; r++) {
// 乘法表一行行看该行有多少乘积<=x
count += min(x / r, n);
}
return count >= k;
}
相似:https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix
bool enough(int value, vector<vector<int>>& matrix, int k) {
int count = 0;
for (auto &row : matrix) {
// 看每行<=value的数有多少
count += upper_bound(row.begin(), row.end(), value) - row.begin();
}
return count >= k;
}
数组两数间有距离,找第k小的距离
int smallestDistancePair(vector<int>& nums, int k) {
// 两数的距离与数的位置无关,先排序数组
sort(nums.begin(), nums.end());
// 猜第k小的距离m,m的值范围[0, max(nums)-min(nums)]
int l = 0, u = nums.back() - nums[0];
while (l <= u) {
int mid = l + (u - l) / 2;
if (enough(mid, nums, k)) {
u = mid - 1;
} else {
l = mid + 1;
}
}
return l;
}
bool enough(int m, vector<int> &nums, int k) {
// count(m){ <=m的距离个数 }是关于m的递增函数,
// enough(m){ count(m)>=k }满足二分搜索的条件形式[0..0 1..1]
int count = 0;
for (int j = 1; j < nums.size(); j++) {
// 找 nums[j]-nums[i] <= m,nums[i] >= nums[j]-m
int i = lower_bound(nums.begin(), nums.end(), nums[j] - m) - nums.begin();
count += j - i; // 距离对:(i,j),(i+1,j),...,(j-1,j)
}
return count >= k;
}
第N小的A,B倍数
int nthMagicalNumber(int N, int A, int B) {
// 猜第N小的A,B倍数为x,x的取值范围为[min(A,B), N*min(A,B)]
const int MOD = 1e9 + 7;
int lcm = A / __gcd(A, B) * B;
long l = min(A, B), u = (long)N * min(A, B);
while (l <= u) {
long mid = l + (u - l) / 2;
if (enough(mid, A, B, lcm, N)) {
u = mid - 1;
} else {
l = mid + 1;
}
}
return l % MOD;
}
bool enough(long x, int A, int B, int lcm, int N) {
// count(x){ <=x的A,B倍数的个数}是关于x的递增函数
// enough(x){ count(x)>=N }满足二分搜索的条件形式[0..0 1..1]
// 而 count(x) = x/A + x/B - x/lcm(A,B)
return x / A + x / B - x / lcm >= N;
}
长>=k子段的最大均值
Given an array consisting of
nintegers, find the contiguous subarray whose length is greater than or equal tokthat has the maximum average value. And you need to output the maximum average value.Example 1:
Input: [1,12,-5,-6,50,3], k = 4 Output: 12.75 Explanation: when length is 5, maximum average value is 10.8, when length is 6, maximum average value is 9.16667. Thus return 12.75.Note:
- 1 <=
k<=n<= 10,000.- Elements of the given array will be in range [-10,000, 10,000].
- The answer with the calculation error less than 10-5 will be accepted.
double findMaxAverage(vector<int>& nums, int k) {
// 最大均值一定在min(nums)和max(nums)之间
double l = INT_MAX, u = INT_MIN;
for (double num : nums) {
l = min(l, num);
u = max(u, num);
}
// 二分搜索猜
while (u - l > 1e-5) {
double mid = (l + u) / 2;
if (enough(mid, nums, k)) u = mid;
else l = mid;
}
return u;
}
bool enough(double m, vector<int> &nums, const int k) {
// "所有"长>=k的子段均值 (a[i]+a[i+1]+...+a[j])/(j−i+1) <= m
// a[i]+a[i+1]+...+a[j] <= m*(j−i+1),
// (a[i]−m)+(a[i+1]−m)+...+(a[j]−m)<=0 (1),
// (1)式左侧是关于m的递减函数,(1)式满足二分搜索的条件形式[0..0 1..1]
// (1)式可用累加数组sum[i]=(a[0]-m)+(a[1]-m)...+(a[i]-m)计算
// 对"所有"长>=k的子段,要 sum[j]-sum[i-1] <= 0,j-i+1>=k,
// 即要sum[j]-sum[ <=j-k ] <= 0,sum[j]-min{ sum[ <=j-k ] } <= 0
// 记currSum=sum[j],minPrevSum=min{ sum[ <=j-k ] },要currSum-minPrevSum<=0
double currSum = 0;
for (int j = 0; j < k; j++) {
currSum += nums[j] - m;
}
if (currSum > 0) return false;
double prevSum = 0, minPrevSum = 0;
for (int j = k; j < nums.size(); j++) {
currSum += nums[j] - m;
prevSum += nums[j-k] - m;
minPrevSum = min(minPrevSum, prevSum);
if (currSum - minPrevSum > 0) return false;
}
return true;
}
有序数组中找x[i]==i的数i
若数组中无重复值,索引值以1增加、数组值以>=1增加。当midVal<midIdx时搜右边[midIdx+1, u],当midVal>midIdx时搜左边[l, midIdx-1]。
若数组中有重复值,索引值以1增加、数组值以>=0增加。当midVal<midIdx时除了搜右边[midIdx+1, u],因为左边可以同值重复,也要搜左边[l, midVal]。同理,midVal>midIdx时除了搜左边[l, midIdx-1],也要搜右边[midVal, u]。
简化:当midVal!=midIdx时,搜索的左边范围可统一成[l, min{midIdx-1, midVal}]。因为当midVal<midIdx时,min{midIdx-1, midVal}=midVal;当midVal>midIdx时,min{midIdx-1, midVal}=midIdx-1。同理,右边范围可统一成[max{ midIdx+1, midVal }, u]。
所以,当midVal!=midIdx时,要搜[l, min{midIdx-1, midVal}]和[max{ midIdx+1, midVal }, u]。直观上,合并使搜索范围尽量小。
有序数组中找值t,该有序数组不知size()只知elementAt()
不知size()就没法取得中位数,可假设该数组后部空位上都是∞,令elementAt()返回-1表示该位置的值太大。见ctci p400
int search(Listy list, int value) {
int idx = 1;
while (list.elementAt(idx) != -1 && list.elementAt(idx) < value) {
idx *= 2;
}
return binarySearch(list, value, idx / 2, idx);
}
int binarySearch(Listy list, int target, int l, int u) {
while (l <= u) {
int m = l + (u - l) / 2;
int val = list.elementAt(m);
if (val == -1 || val > target) {
u = m - 1;
} else if (val < target) {
l = m + 1;
} else {
return m;
}
}
return -1;
}
有序数组中散布着无用数据
若尝试的mid位置是无用数据,则mid一圈圈往左右扩展,用最近的有用元素替代。见ctci p401
int mid = l + (u - l) / 2;
if (strs[mid].isEmpty()) { // mid是无用数据
int left = mid - 1, right = mid + 1;
while (true) {
if (left < l && right > u) { // 找不到有用数据
return -1;
}
if (left >= l && !strs[left].isEmpty()) { // 左看一步
mid = left;
break;
}
if (right <= u && !strs[right].isEmpty()) { // 右看一步
mid = right;
break;
}
left--;
right++;
}
}
...
x的平方根
int mySqrt(int x) {
if (x <= 1) return x;
// 找满足r^2<=x的最大整数r,就是找满足r^2>x的最小整数r的前一个数
int l = 1, u = x;
while (l + 1 < u) {
int mid = l + (u - l) / 2;
if (mid > x / mid) { // mid*mid > x
u = mid;
} else {
l = mid;
}
}
return l;
}
num是完全平方数
bool isPerfectSquare(int num) {
// 找到使x*x>=num的第一个x,二分搜索
int l = 1, u = num;
while (l <= u) {
int mid = l + (u - l) / 2;
if (mid >= num / mid) { // mid*mid >= num
u = mid - 1;
} else {
l = mid + 1;
}
}
return l * l == num;
}
找数组中任一峰值
int findPeakElement(vector<int>& nums) {
// 只要不断往高处走,就一定有波峰,这是一种单调性
// 找波峰,找第一个nums[i]>nums[i+1]的位置i
// i二分搜索的范围是[0..N-2]
const int N = nums.size();
int l = -1, u = N - 1;
while (l + 1 < u) {
int mid = l + (u - l) / 2;
if (nums[mid] > nums[mid+1]) {
u = mid;
} else {
l = mid;
}
}
return u;
}
在先升后降的山坡数组中找数
int findInMountainArray(int target, MountainArray &mountainArr) {
// 二分搜索,先找峰值,再分别找左右半数组
// 找第一个A[m]>A[m+1]的位置m,m在[1..N-2]
const int N = mountainArr.length();
int l = 0, u = N - 1;
while (l + 1 < u) {
int m = l + (u - l) / 2;
if (mountainArr.get(m) > mountainArr.get(m + 1)) {
u = m;
} else {
l = m;
}
}
// u是波峰
int ans = binarySsearch(target, mountainArr, 0, u, true);
if (ans != -1) return ans;
return binarySsearch(target, mountainArr, u, N - 1, false);
}
int binarySsearch(int target, MountainArray &mountainArr,
int lower, int upper, bool asc) {
int l = lower, u = upper;
while (l <= u) {
int m = l + (u - l) / 2;
int val = mountainArr.get(m);
if (target == val) return m;
if ((target < val) == asc) {
u = m - 1;
} else {
l = m + 1;
}
}
return -1;
}
满足x!末尾刚好K个0的数x有多少个
int preimageSizeFZF(int K) {
// 题目:满足x!末尾刚好K个0的数x有多少个?
// x!末尾0的个数等于因子5的个数,因子5的个数每连续5个数相等。
// 因此,满足条件的x若存在就有5个,若不存在就0个。
// 二分搜索猜x是否存在,x的上限取5K,因为5K可至少贡献5K/5=K个因子5。
long l = 0, u = 5l * K;
while (l <= u) {
long mid = l + (u - l) / 2;
int count = countZeros(mid);
if (count == K) return 5;
if (count > K) u = mid - 1;
else l = mid + 1;
}
return 0;
}
int countZeros(long n) {
int ans = 0;
while (n /= 5) {
ans += n;
}
return ans;
}
有序数组中找k个最接近x的元素
vector<int> findClosestElements(vector<int>& arr, int k, int x) {
// 设要找的子段为 arr[i..i+k-1],对位置i做二分搜索
// 比较 arr[i] 和 子段后的arr[i+k]:
// 若 x - arr[i] > arr[i+k] - x,右移子段起始i
int lo = 0, hi = (int)arr.size() - k;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (x - arr[mid] > arr[mid + k] - x) {
lo = mid + 1;
} else {
hi = mid;
}
}
return vector<int>(begin(arr) + lo, begin(arr) + lo + k);
}
旋转数组中找值t
旋转数组对半分后,一半是有序数组、一半是旋转数组(两半都是有序数组的情况也符合讨论,因为有序数组是特殊的旋转数组)。这“一半是有序数组”的条件是该利用的。见ctci p399
怎么判断哪一半是有序数组?比较x[mi]和x[hi]。用mi和hi比较更健壮,因为mi一定!=hi(len>=2时)。若用lo和mi比较,因为mi向下取整,mi可能==lo,从而使判断if (x[lo]<x[mid])被绕过,会增加边界情况复杂度。
bool search(vector<int>& nums, int target) {
// 旋转数组对半分后,一半是有序数组、一半是旋转数组
int lo = 0, hi = (int)nums.size() - 1;
while (lo <= hi) {
int mi = lo + (hi - lo) / 2;
if (nums[mi] == target) return true;
if (nums[mi] < nums[hi]) { // 右半有序
if (nums[mi] < target && target <= nums[hi]) {
lo = mi + 1;
} else {
hi = mi - 1;
}
} else if (nums[mi] > nums[hi]) { // 左半有序
if (nums[lo] <= target && target < nums[mi]) {
hi = mi - 1;
} else {
lo = mi + 1;
}
} else { // nums[mi] == nums[hi]
hi--;
}
}
return false;
}
旋转数组的最小值
int findMin(vector<int>& nums) {
// 旋转数组对半分,一半旋转一半有序,最小值在旋转那一半
const int N = nums.size();
int lo = 0, hi = N - 1;
while (lo < hi) { // len([lo..hi])>=2
int mi = lo + (hi - lo) / 2;
// 由于mi向下取整,可能mi==lo,
// 若用if (nums[lo] < nums[mi]),当mi==lo时这个if将被绕过
// 用mi和hi就没这个问题,mi!=hi
if (nums[mi] > nums[hi]) { // 右半旋转,且m位置不是min
lo = mi + 1;
} else { // 左半旋转
hi--;
}
}
return nums[lo];
}
在行列有序的矩阵中查找
- https://leetcode.com/problems/search-a-2d-matrix-ii/
- http://articles.leetcode.com/searching-2d-sorted-matrix-part-ii,或 ctci p410。
按步搜:从行列有序的"中间"位置(右上角或左下角)开始线性搜索。 四分法:值t跟中心点比较,可排除掉左上或右下块,剩下三块子矩阵。 二分法:先在中间行(或中间列或主对角线)上找第一个>=t的数,若它等于t直接返回;否则就是第一个>t的数,可排除掉右下块;再根据它左边相邻数<t排除掉左上块,只剩下两块子矩阵。
bool searchMatrix(vector<vector<int>> &matrix, int target) {
// 从行列有序的”中间“位置(右上角或左下角)开始线性查找,O(m+n)
if (matrix.empty()) return false;
const int R = matrix.size();
const int C = matrix[0].size();
int r = 0, c = C - 1;
while (r < R && c >= 0) {
if (target == matrix[r][c]) {
return true;
} else if (target > matrix[r][c]) {
r++;
} else {
c--;
}
}
return false;
}
行列有序的矩阵中负数的个数
int countNegatives(vector<vector<int>>& grid) {
// 从行列有序的"中间"位置(右上角或左下角)开始线性查找,O(m+n)
if (grid.empty()) return false;
const int R = grid.size(), C = grid[0].size();
int r = 0, c = C - 1;
int ans = 0;
while (r < R && c >= 0) {
if (grid[r][c] < 0) { // 类似找0的位置
c--;
ans += R - r;
} else {
r++;
}
}
return ans;
}
无序数组中第k小的数
把快排划分找第k大的数的partition函数改成小的往前抛,二分搜索函数相同。
int findKthSmallest(vector<int>& nums, int k) {
int l = 0, u = (int)nums.size() - 1;
k--; // k是1-based,变成下标作比较
while (l <= u) {
int p = partition(nums, l, u);
if (k == p) break;
if (k < p) u = p - 1;
else l = p + 1;
}
return nums[k];
}
多个有序数组中第k小的数
设x是第k小的数,二分搜索条件enough(x)表示"<=x的个数"count>=k。当猜的x不断变大时,二分搜索输出[0 0 ... 0 1 1 ...]。
若是找第k大的数:设x是第k大的数,二分搜索条件enough(x)表示">=x的个数"count<=k。
两个有序数组中找第k小的数
上题的特例。或者另种方式的二分搜索,见中位数
用k元堆找第k小的数
无序数组中找第k小用最大堆。数据流不断插入并弹出,保持k元堆,弹出N-k次。
作为对比,多个有序数组中找第k小用最小堆,只插入各行最小的数,弹出k-1次。
动态有序集中第k小的节点
扩展红黑树(bst),节点要记录以当前节点为根的子树大小,这就相当于做好了数组中的划分操作。
1~n数字的字典序
vector<int> lexicalOrder(int n) {
// 十叉树的前序遍历
vector<int> ans;
for (int i = 1; i <= 9; i++) {
preorder(i, n, ans);
}
return ans;
}
void preorder(int x, int n, vector<int> &ans) {
if (x > n) return;
ans.push_back(x);
for (int i = 0; i <= 9; i++) {
preorder(x * 10 + i, n, ans);
}
}
[1,n]中字典序第k小的数
int findKthNumber(int n, int k) {
int curr = 1;
while (k > 1) {
int count = countNums(curr, n);
if (k > count) { // 走右兄弟节点
curr++;
k -= count;
} else { // 走最左子节点
curr *= 10;
k--;
}
}
return curr;
}
// 想象十叉树中,以数p为根的子树含多少[1,n]间的数?
// 可累加[p,p+1)、[p*10,(p+1)*10)、[p*100,(p+1)*100)、... 与[1,n]重合的部分
int countNums(int p, long n) {
int count = 0;
long start = p, end = p + 1;
while (start <= n) {
count += min(end, n + 1) - start;
start *= 10, end *= 10;
}
return count;
}
多个有序数组中找第k小的数
用最小堆保存行索引,用colIdx[]保存各行的列索引
int kthSmallest(vector<vector<int>>& matrix, int k) {
const int N = matrix.size();
vector<int> colIdx(N, 0);
auto cmp = [&](int r1, int r2) { // 哪一行的当前元素较小
if (colIdx[r1] == N) return true; // 相当于r1行元素无穷大
if (colIdx[r2] == N) return false;
return matrix[r1][colIdx[r1]] > matrix[r2][colIdx[r2]];
};
priority_queue<int, vector<int>, decltype(cmp)> pq(cmp);
for (int i = 0; i < N; i++) {
pq.push(i);
}
while (true) {
int minR = pq.top();
pq.pop();
k--;
if (k == 0) return matrix[minR][colIdx[minR]];
colIdx[minR]++;
pq.push(minR);
}
}
注:这题也可用二分搜索解。
两个有序数组的前k个最小和对
把两有序数组一个视作行指示、一个视作列指示,和值作为矩阵元素,就变成行列分别有序的矩阵,求其中前k个最小数。
vector<pair<int, int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
if (nums1.empty() || nums2.empty()) return {};
// nums1的数作行、nums2的数作列、两数和作为矩阵元素,构成行列分别有序的矩阵
// 用最小堆保存行列索引
auto cmp = [&](pair<int, int> &p1, pair<int, int> &p2) {
return nums1[p1.first] + nums2[p1.second] > nums1[p2.first] + nums2[p2.second];
};
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> pq(cmp);
const int M = nums1.size(), N = nums2.size();
for (int i = 0; i < M; i++) pq.push({ i, 0 }); // 第一列先入队
vector<pair<int, int>> ans;
while (!pq.empty() && k--) {
auto p = pq.top(); pq.pop();
ans.push_back({ nums1[p.first], nums2[p.second] });
if (p.second + 1 < N) pq.push({ p.first, p.second + 1 });
}
return ans;
}
至少包含各有序数组一个数的最小范围
vector<int> smallestRange(vector<vector<int>>& nums) {
const int N = nums.size();
vector<int> idx(N, 0); // idx[]存各数组的当前下标,数组i的当前元素是nums[i][idx[i]]
auto cmp = [&](int i, int j) { // 最小堆
return nums[i][idx[i]] > nums[j][idx[j]];
};
priority_queue<int, vector<int>, decltype(cmp)> pq(cmp);
int rangeEnd = INT_MIN;
for (int i = 0; i < N; i++) {
if (nums[i].empty()) break;
pq.push(i);
rangeEnd = max(rangeEnd, nums[i][0]);
}
if (pq.size() < N) return { INT_MIN, INT_MAX };
vector<int> ans;
int minRangeSize = INT_MAX;
while (true) {
int minQ = pq.top(); pq.pop();
int rangeBegin = nums[minQ][idx[minQ]];
int rangeSize = rangeEnd - rangeBegin + 1;
if (rangeSize < minRangeSize) {
minRangeSize = rangeSize;
ans = { rangeBegin, rangeEnd };
}
// 移动指针,下一元素
idx[minQ]++;
if (idx[minQ] == nums[minQ].size()) break;
pq.push(minQ);
rangeEnd = max(rangeEnd, nums[minQ][idx[minQ]]);
}
return ans;
}
大数组中包含小数组所有元素的最小范围
为小数组中每个元素记录大数组中的位置链表num=>idxList,变成求至少包含各有序链表一个数的最小范围。见ctci p589。
开花位置相隔k个空槽
There is a garden with
Nslots. In each slot, there is a flower. TheNflowers will bloom one by one inNdays. In each day, there will beexactlyone flower blooming and it will be in the status of blooming since then.Given an array
flowersconsists of number from1toN. Each number in the array represents the place where the flower will open in that day.For example,
flowers[i] = xmeans that the unique flower that blooms at dayiwill be at positionx, whereiandxwill be in the range from1toN.Also given an integer
k, you need to output in which day there exists two flowers in the status of blooming, and also the number of flowers between them iskand these flowers are not blooming.If there isn't such day, output -1.
Example 1:
Input: flowers: [1,3,2] k: 1 Output: 2 Explanation: In the second day, the first and the third flower have become blooming.Example 2:
Input: flowers: [1,2,3] k: 1 Output: -1Note:
- The given array will be in the range [1, 20000].
用set查看前后位置,O(nlgn)
int kEmptySlots(vector<int> &flowers, int k) {
const int N = flowers.size();
set<int> st;
for (int i = 1; i <= N; i++) {
int pos = flowers[i-1];
st.insert(pos);
// 看前后位置是否相隔k
auto it = st.find(pos);
if (it != st.begin() && pos - *prev(it) - 1 == k) return i;
if (next(it) != st.end() && *next(it) - pos - 1 == k) return i;
}
return -1;
}
桶排序,O(n)
int kEmptySlots(vector<int> &flowers, int k) {
const int N = flowers.size();
// 桶排序,每个桶对应k+1个位置,
// 这样所找的k个空槽两端有花的情况就不可能出现在桶内,而只能在桶间
// 每个桶保留最大最小值
unordered_map<int, vector<int>> buckets; // bucketIdx=>[minOfBucket,maxOfBucket]
for (int i = 1; i <= N; i++) {
int pos = flowers[i-1];
int idx = pos / (k + 1);
if (buckets.count(idx)) {
buckets[idx][0] = min(buckets[idx][0], pos);
buckets[idx][1] = max(buckets[idx][1], pos);
} else {
buckets[idx] = {pos, pos};
}
// 查看左右桶边界
if (pos == buckets[idx][0] && buckets.count(idx-1) && pos - buckets[idx-1][1] - 1 == k) return i;
if (pos == buckets[idx][1] && buckets.count(idx+1) && buckets[idx+1][0] - pos - 1 == k) return i;
}
return -1;
}
前k个数中有某数与当前数相差<=t
滑动窗口内的数放入set,O(nlgk):
bool containsNearbyAlmostDuplicate(vector<int> &nums, int k, int t) {
if (t < 0) return false;
set<long> st; // 把前k个数放入set中
for (int i = 0; i < nums.size(); i++) {
// 在st中找x,abs(nums[i]-x)<=t,nums[i]-t<=x<=nums[i]+t
// 只要找>=nums[i]-t的最小数,看它是否<=nums[i]+t
auto it = st.lower_bound((long)nums[i] - t);
if (it != st.end() && *it <= (long)nums[i] + t) return true;
st.insert(nums[i]);
if (st.size() > k) st.erase(nums[i-k]);
}
return false;
}
桶排序法,O(n):
bool containsNearbyAlmostDuplicate(vector<int> &nums, int k, int t) {
if (t < 0) return false;
unordered_map<long, long> buckets; // bucketIdxOfNum=>num
// buckets保存前k个数,buckets.size()<=k
for (int i = 0; i < nums.size(); i++) {
auto idx = bucketIdx(nums[i], t);
// 又落在自己桶
if (buckets.count(idx)) return true;
// 检查相邻两桶
if (buckets.count(idx-1) && nums[i] - buckets[idx-1] <= t) return true;
if (buckets.count(idx+1) && buckets[idx+1] - nums[i] <= t) return true;
buckets[idx] = nums[i];
if (buckets.size() > k) buckets.erase(bucketIdx(nums[i-k], t));
}
return false;
}
// |x-y|<=t有[0..t]个值,桶长t+1时落在一个桶中的值肯定重复
long bucketIdx(long num, long t) {
return (num - INT_MIN) / (t + 1);
}
归并过程一般这么写
while (i <= u1 && j <= u2) {
if (nums[i] < nums[j]) {
merged[k++] = nums[i++];
} else {
merged[k++] = nums[j++];
}
}
while (i <= u1) {
merged[k++] = nums[i++];
}
while (j <= u2) {
merged[k++] = nums[j++];
}
有时见这么写
while (i <= u1 || j <= u2) {
if (j > u2 || (i <= u1 && nums[i] < nums[j]) {
merged[k++] = nums[i++];
} else {
merged[k++] = nums[j++];
}
}
可在两有序数组的公共元素间跳跃前进,求最大路径和
int maxSum(vector<int>& A, vector<int>& B) {
// 数组有些公共元素,公共元素间的路径两数组各有一条,选大的那条
const int MOD = 1e9 + 7;
const int M = A.size(), N = B.size();
int i = 0, j = 0;
long sumA = 0, sumB = 0;
while (i < M || j < N) {
if (j == N || (i < M && A[i] < B[j])) {
sumA += A[i++];
} else if (i == M || (j < N && A[i] > B[j])) {
sumB += B[j++];
} else {
sumA = sumB = max(sumA, sumB) + A[i];
i++, j++;
}
}
return max(sumA, sumB) % MOD;
}
逆序数
用归并排序的归并过程来统计,逆序数=左子问题+右子问题+跨数组逆序数。为统计跨数组逆序数,从数组末开始,一旦左数组末的a[i]>右数组末的b[j](逆序),就知a[i]>b[j]>=b[j-1]>=...>=b[0],关于a[i]的跨组逆序数是j+1。或者从数组头开始,一旦右数组头的b[j]<左数组头的a[i](逆序),就知b[j]<a[i]<=a[i+1]<=...<=a[n1],关于b[j]的跨组逆序数是n1-i+1。
- https://leetcode.com/problems/reverse-pairs/
int reversePairs(vector<int>& nums) {
return mergeSort(nums, 0, (int)nums.size() - 1);
}
int mergeSort(vector<int>& nums, int l, int h) {
if (l >= h) return 0;
int mid = l + (h - l) / 2;
int ans = mergeSort(nums, l, mid) + mergeSort(nums, mid + 1, h);
// 统计组间逆序数
for (int j = mid + 1; j <= h; j++) {
// nums[l..mid]中找第一个>2*nums[j]的i
auto it = upper_bound(begin(nums) + l, begin(nums) + mid + 1, 2l * nums[j]);
int i = it - begin(nums);
ans += mid - i + 1; // 位置[i..mid]与i逆序
}
merge(nums, l, mid, h);
return ans;
}
// 归并已排序的 nums[l..mid] 和 nums[mid+1..h]
void merge(vector<int>& nums, int l, int mid, int h) {
vector<int> merged(h - l + 1);
int i = l, j = mid + 1, k = 0;
while (i <= mid && j <= h) {
if (nums[i] < nums[j]) {
merged[k++] = nums[i++];
} else {
merged[k++] = nums[j++];
}
}
while (i <= mid) merged[k++] = nums[i++];
while (j <= h) merged[k++] = nums[j++];
for (int k = 0; k < merged.size(); k++) {
nums[l + k] = merged[k];
}
}
每个数右边比它小的数有多少个
vector<int> countSmaller(vector<int>& nums) {
// 要看数的右边,所以不能移动数字,在索引数组idx上归并排序
const int N = nums.size();
vector<int> idx;
for (int i = 0; i < N; i++) {
idx.push_back(i);
}
vector<int> ans(N, 0);
mergeSort(nums, idx, 0, N - 1, ans);
return ans;
}
void mergeSort(vector<int> &nums, vector<int> &idx, int l, int r, vector<int> &ans) {
if (l >= r) return;
int mid = l + (r - l) / 2;
mergeSort(nums, idx, l, mid, ans);
mergeSort(nums, idx, mid + 1, r, ans);
vector<int> merged(r - l + 1);
// 两指针同向遍历,从两数组末端开始比较
int i = mid, j = r, k = r - l;
while (i >= l && j > mid) {
if (nums[idx[i]] > nums[idx[j]]) {
ans[idx[i]] += j - mid; // 位置[mid+1..j]与i逆序
merged[k--] = idx[i--];
} else {
merged[k--] = idx[j--];
}
}
while (i >= l) merged[k--] = idx[i--];
while (j > mid) merged[k--] = idx[j--];
for (int k = 0; k < merged.size(); k++) {
idx[l + k] = merged[k];
}
}
全局逆序数==相邻逆序数
bool isIdealPermutation(vector<int>& A) {
// 全局逆序数==相邻逆序数,除了相邻、没有逆序,
// 即max(A[0..i-2])<=A[i]
int theMax = -1;
for (int i = 2; i < A.size(); i++) {
theMax = max(theMax, A[i-2]);
if (theMax > A[i]) return false;
}
return true;
}
[1..n]间的数,有的出现2次、有的1次、有的缺失,找缺失的那些数(in-place算法)
vector<int> findDisappearedNumbers(vector<int>& nums){
// 把数x当作下标(1-based),将nums[x-1]标记为负数
for (int num : nums) {
int x = abs(num);
if (nums[x-1] > 0) nums[x-1] *= -1;
}
vector<int> ans;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > 0) ans.push_back(i+1);
}
return ans;
}
第一个缺失的正整数(数组有负数)
比如[1,2,0]返回3,[3,4,-1,1]返回2
int firstMissingPositive(vector<int>& nums) {
// 期望变成数组[1..n]。位置i应放数i+1,即nums[i]==i+1
// => nums[i]-1==i,nums[nums[i]-1]==nums[i]
// 旋转置换:若nums[nums[i]-1]!=nums[i],交换这两处的值
const int N = nums.size();
for (int i = 0; i < N; i++) {
while (1 <= nums[i] && nums[i] <= N && nums[nums[i]-1] != nums[i]) {
swap(nums[nums[i]-1], nums[i]);
}
}
// first missing
for (int i = 0; i < N; i++) {
if (nums[i] != i + 1) return i + 1;
}
return N + 1;
}
第k个缺失的正整数(有序正数组)
int findKthPositive(vector<int>& arr, int k) {
// 题目:找第k小的缺失自然数
// 假设arr[i]全部>k,所求就是k;从左往右遇到arr[i]<=k,就递增k
for (int num : arr) { // arr[]是递增数组
if (num <= k) k++;
else break;
}
return k;
}
也可用二分搜索解决
int findKthPositive(vector<int>& arr, int k) {
// 题目:找第k小的缺失自然数
// 数组arr和缺失数组mis可拼成自然数序列,
// 值arr[m]是自然数序列的第arr[m]个元素,又是数组arr的第m+1个元素,
// 所以<=arr[m]的缺失数count(m)=arr[m]-(m+1)
// 因为arr[m]比m增加得快,count(m)是关于m的递增函数,
// 条件式enough(m){count(m)>=k}满足二分搜索的条件形式[0..0 1..1]
int l = -1, u = arr.size();
while (l + 1 < u) {
int m = l + (u - l) / 2;
if (enough(m, arr, k)) {
u = m;
} else {
l = m;
}
}
// <=arr[u]的缺失数首次有>=k个。
// <=arr[u-1]的缺失数有count(u-1)=arr[u-1]-u个,
// 比起需要的k个,还差k-(arr[u-1]-u)个,
// 第k个缺失数为 arr[u-1]+(k-(arr[u-1]-u)) = u+k
return u + k;
}
bool enough(int m, vector<int>& arr, int k) {
return arr[m] - (m + 1) >= k;
}
给数组补一些数,使[1..n]的数都能由数组数相加获得
贪心法
int minPatches(vector<int>& nums, int n) {
// 设[1..missing)已能由数组数相加获得,尝试扩展上边界missing
long missing = 1;
int i = 0, ans = 0;
while (missing <= n) { // 最终missing>n,[1..missing)包含[1..n]
// nums[i]已能由相加获得(<missing)或在数组中(=missing)
if (i < nums.size() && nums[i] <= missing) {
missing += nums[i];
i++;
} else {
missing += missing; // 补上数missing
ans++;
}
}
return ans;
}
[1..n]间的数,有的出现2次、有的1次、有的缺失,找重复的那些数(in-place算法)
vector<int> findDuplicates(vector<int>& nums) {
// 把数x当作下标(1-based),将nums[x-1]标记为负数
vector<int> ans;
for (int num : nums) {
int x = abs(num);
if (nums[x-1] < 0) ans.push_back(x);
else nums[x-1] *= -1;
}
return ans;
}
[1..n]间的数,有一个数重复、一个数缺失,找这两个数
同上的in-place算法
vector<int> findErrorNums(vector<int>& nums) {
const int N = nums.size();
int dup = -1, missing = -1;
// 把数x当作下标(1-based),将nums[x-1]标记为负数
for (int num : nums) {
int x = abs(num);
if (nums[x-1] < 0) {
dup = x;
} else {
nums[x-1] *= -1;
}
}
// 哪个下标位置仍是正数
for (int i = 0; i < N; i++) {
if (nums[i] > 0) missing = i + 1;
}
return {dup, missing};
}
或者用位操作
vector<int> findErrorNums(vector<int>& nums) {
const int N = nums.size();
int x = 0;
for (int num : nums) x ^= num;
for (int i = 1; i <= N; i++) x ^= i;
// x = duplicate^missing
int lastBit = x & -x;
int xor0 = 0, xor1 = 0;
for (int num : nums) {
if (num & lastBit) xor1 ^= num;
else xor0 ^= num;
}
for (int i = 1; i <= N; i++) {
if (i & lastBit) xor1 ^= i;
else xor0 ^= i;
}
// xor0和xor1 是 duplicate和missing,不知哪个是哪个
for (int num : nums) {
if (num == xor0) {
return { xor0, xor1 };
}
}
return { xor1, xor0 };
}
长为n的数组中放[1..n]间的数,有的出现多次、有的缺失,in-place统计各出现的次数
vector<int> statArray(vector<int> &nums) {
// 把数x当作下标(1-based),在下标处计数nums[x-1]++,计数最多增N;
// 为不影响nums[x-1]原先的值,先将所有数*=(N+1),
// 这时数x的下标处计数为nums[x/(N+1)-1]++。
const int N = nums.size();
for (int &x : nums) {
x *= (N + 1);
}
for (int x : nums) {
nums[x / (N + 1) - 1]++;
}
vector<int> ans;
for (int x : nums) {
// x/(N+1) => nums[x/(N+1)-1]%(N+1)
ans.push_back(nums[x / (N + 1) - 1] % (N + 1));
}
return ans;
}
长为n+1的数组中放[1,n]间的数,至少有个数重复,找任一重复的数
不修改数组的 快慢指针法
是否有m长子段连续重复k次
bool containsPattern(vector<int>& arr, int m, int k) {
// 是否有m长子段连续重复k次
const int N = arr.size();
int count = 0;
for (int i = 0; i + m < N; i++) {
if (arr[i] != arr[i+m]) {
count = 0;
} else {
count++;
if (count == m * (k-1)) return true;
}
}
return false;
}
其他数出现3次、一个数出现1次,求这单个数
int singleNumber(vector<int>& nums) {
// 泛化成:其他数出现k次,某个数出现p次(p不能被k整除),找这单个数
// bit运算各个位相互独立,下面用int同时操作所有位,逻辑上把整个int当作一个bit(nums[i]也当作一个bit)
// 要统计某位上出现1的次数,需要个mod k计数器(该计数器共m>=lgk位),设bm,..,b2,b1表示该计数器的位,
// 1. 进位规则:从最高位开始更新,后面位和新数num都为1时进位
// ..., b3^=b2&b1&num, b2^=b1&num, b1^=num
// 2. 计数为k时直接清零:
// 当bm,..,b1的各个位分别等于k的各个位km,..,k1时计数为k,判断计数为k的表达式
// 为expr_count_k=(bm_km)&..&(b1_k1)。分量(bj_kj)表示于kj==1时取bj、kj==0时取~bj
// 清零的 mask = ~(expr_count_k),清零:..., b3&=mask, b2&=mask, b1&=mask
// 3. 最终各逻辑位”合并或“:b1|b2|b3...
int b1 = 0, b2 = 0;
int mask = 0;
for (int num : nums) {
b2 ^= b1 & num;
b1 ^= num;
mask = ~(b1 & b2); // k =3 =(11)_2
b2 &= mask;
b1 &= mask;
}
return b1 | b2;
}
还有种方法设计mod 3计数器:用数电知识,画出x1,x2,nums[i]的卡诺图真值表、化简x1,x2表达式。只是该方法泛化能力差,计数器位数多了以后表达式复杂。
其他数出现2次,两个数出现1次,求这两个数
vector<int> singleNumber(vector<int>& nums) {
// 设x和y是数组中只出现1次的两个数
int xXorY = 0;
for (auto num : nums) {
xXorY ^= num;
}
// xXorY中任意一位1表示x和y该处不同,不妨取最后一位1来区分x和y
int lastOne = xXorY & -xXorY;
int x = 0;
for (auto num : nums) {
if (num & lastOne) x ^= num;
}
int y = x ^ xXorY;
return {x, y};
}
有序数组中其他数出现两次、某个数出现一次,求这单个数
int singleNonDuplicate(vector<int>& nums) {
// 由题知N是奇数,对于[0..N-2]的一对对,找第一对nums[m]!=nums[m^1]
// nums[m]!=nums[m^1]作二分搜索条件满足[0 ... 0 1 1 ...]形式
int l = -1, u = (int)nums.size() - 1;
while (l + 1 < u) {
int mid = l + (u - l) / 2;
if (nums[mid] != nums[mid ^ 1]) u = mid;
else l = mid;
}
return nums[u];
}
出现次数>N/2的数
用碰撞抵消法找出一数,最后再数一遍验证是不是。比如从{2,3,1}中找到1,还要验证1是不是。若已知众数存在就不用验证。
已知众数存在时,也可用快速划分法直接找中位数。
int majorityElement(vector<int>& nums) {
int cand = INT_MIN;
int cnt = 0;
for (int num : nums) {
if (num == cand) {
cnt++;
} else if (cnt == 0) {
cand = num;
cnt++;
} else {
cnt--;
}
}
return cand;
}
所有出现次数>N/k的数
vector<int> majorityElement(vector<int>& nums) {
// 一般化,找出现次数>N/k的元素
// 用个map统计各候选的出现次数,当map.size()==k时删掉k个不同元素,
// 最后剩下的为候选
unordered_map<int, int> cnt;
for (auto num : nums) {
cnt[num]++;
if (cnt.size() == 3) {
auto it = cnt.begin();
while (it != cnt.end()) {
it->second--;
if (it->second == 0) {
it = cnt.erase(it);
} else {
it++;
}
}
}
}
// 需要验证候选的出现次数
for (auto &e : cnt) {
e.second = 0; // 给候选重新计数
}
for (auto num : nums) {
if (cnt.count(num)) cnt[num]++;
}
vector<int> ans;
for (auto &e : cnt) {
if (e.second > nums.size() / 3) {
ans.push_back(e.first);
}
}
return ans;
}
bst中找众数
vector<int> findMode(TreeNode* root) {
// bst中找众数,中序遍历找连续出现最多的数
vector<int> ans;
int prev = INT_MIN;
int count = 0, maxCount = INT_MIN;
inorder(root, prev, count, maxCount, ans);
return ans;
}
void inorder(TreeNode *root, int &prev, int &count, int &maxCount, vector<int> &ans) {
if (!root) return;
inorder(root->left, prev, count, maxCount, ans);
if (root->val != prev) count = 1;
else count++;
if (count > maxCount) {
maxCount = count;
ans = { root->val };
} else if (count == maxCount) {
ans.push_back(root->val);
}
prev = root->val;
inorder(root->right, prev, count, maxCount, ans);
}
中位数堆
最大堆负责左半、最小堆负责右半,保持左半跟右半等大或大1。见ctci p595
class MedianFinder {
priority_queue<int> lo; // 左半边最大堆
priority_queue<int, vector<int>, greater<int>> hi; // 右半边最小堆
public:
// 保持左半堆与右半堆等大或大1
void addNum(int num) {
if (lo.size() == hi.size()) {
hi.push(num);
lo.push(hi.top());
hi.pop();
} else { // 左半堆大1
lo.push(num);
hi.push(lo.top());
lo.pop();
}
}
double findMedian() {
if (lo.empty()) return 0;
if (lo.size() == hi.size()) return (lo.top() + hi.top()) * 0.5;
return lo.top();
}
};
中位数队列
class MedianQueue {
// 最大堆负责左半,最小堆负责右半
// 因为c++的priority_queue没法随意删除元素,用multiset替代
multiset<int, greater<int>> lo;
multiset<int> hi;
void moveOneToLo() {
lo.insert(*hi.begin());
hi.erase(hi.begin());
}
void moveOneToHi() {
hi.insert(*lo.begin());
lo.erase(lo.begin());
}
public:
// 保证lo比hi大1或等大
void push(int num) {
if (lo.size() > hi.size()) {
lo.insert(num);
moveOneToHi();
} else {
hi.insert(num);
moveOneToLo();
}
}
void remove(int num) {
if (num <= *lo.begin()) {
lo.erase(lo.find(num));
if (lo.size() < hi.size()) {
moveOneToLo();
}
} else {
hi.erase(hi.find(num));
if (lo.size() - hi.size() > 1) {
moveOneToHi();
}
}
}
double median() {
if (lo.size() > hi.size()) {
return *lo.begin();
} else {
return ((double)*lo.begin() + *hi.begin()) / 2;
}
}
};
两不等长有序数组的中位数
double findMedianSortedArrays(vector<int>& a, vector<int>& b) {
// a分成a[0..i-1]、a[i..M-1],b分成b[0..j-1]、b[j..N-1],
// 令k=(M+N+1)/2,划分i要满足i+j=k,且a[i-1]<=b[j] && b[j-1]<=a[i]
// 1.要在a、b的较短数组中搜索划分i
// 由0<=i<=M(i==M时后半为空),j=k-i => k-M<=j<=k;要使0<=j<=N,需要k-M>=0,k<=N => M<=N
// 2.最终M+N为奇数时,lMax=max(a[i-1],b[j-1])为中位数;
// M+N为偶数时,rMin=min(a[i],b[j]),(lMax+rMin)/2为中位数
const int M = a.size(), N = b.size();
if (M > N) return findMedianSortedArrays(b, a);
const int k = (M + N + 1) / 2;
int lo = 0, hi = M;
while (lo <= hi) {
int i = lo + (hi - lo) / 2; // 划分点i
int j = k - i;
int la = (i > 0) ? a[i - 1] : INT_MIN;
int lb = (j > 0) ? b[j - 1] : INT_MIN;
int ra = (i < M) ? a[i] : INT_MAX;
int rb = (j < N) ? b[j] : INT_MAX;
if (la > rb) { // 划分点i太靠右了,要往左移,排除划分点[i..]
hi = i - 1;
} else if (lb > ra) {
lo = i + 1;
} else { // 有效的划分
int lMax = max(la, lb);
if ((M + N) % 2 == 1) return lMax;
int rMin = min(ra, rb);
return (lMax + rMin) * 0.5;
}
}
return -1;
}
两等长有序数组的上中位数
int findMedianinTwoSortedAray(vector<int>& arr1, vector<int>& arr2) {
const int N = arr1.size(); // 上中位数是在合并数组中第N小的数
int lo = 0, hi = N - 1;
while (lo < hi) {
int mid1 = lo + (hi - lo) / 2;
// 划分出N个元素 arr1[0..mid1]、arr2[0..mid2],有 mid1+1 + mid2+1 == N
int mid2 = N - 2 - mid1;
int a1 = arr1[mid1], a2 = arr2[mid2];
if (a1 < a2) {
// 一共划分出N个元素,arr1[mid1]较小,不可能是第N个元素,排除 arr1[0..mid1]
lo = mid1 + 1;
} else if (a1 > a2) {
// N个元素的划分之外的 arr1[mid1+1..],可以排除
hi = mid1;
} else {
return a1;
}
}
// 合并数组中 lo+x == N-1,x = N-1-lo
return min(arr1[lo], arr2[N - 1 - lo]);
}
丑数
int nthUglyNumber(int n) {
// 每个因子x对应一个要相乘的已知丑数seq[ix],以生成下个丑数 min{ seq[ix]*x }
// 当生成了下个丑数后,所有参与生成的因子的索引ix++
vector<int> seq;
seq.push_back(1);
int i2 = 0, i3 = 0, i5 = 0;
for (int i = 1; i < n; i++) {
int next = min({ seq[i2] * 2, seq[i3] * 3, seq[i5] * 5 });
seq.push_back(next);
if (next == seq[i2] * 2) i2++;
if (next == seq[i3] * 3) i3++;
if (next == seq[i5] * 5) i5++;
}
return seq.back();
}
超级丑数
上面算法的扩展
int nthSuperUglyNumber(int n, vector<int>& primes) {
vector<int> seq;
seq.push_back(1);
// 每个因子对应有一个要相乘的已知丑数,以生成下个丑数
// 第j个因子primes[j]对应的丑数是seq[idx[j]]
const int M = primes.size();
vector<int> idx(M, 0);
for (int i = 1; i < n; i++) {
int next = INT_MAX;
for (int j = 0; j < M; j++) {
next = min(next, seq[idx[j]] * primes[j]);
}
seq.push_back(next);
for (int j = 0; j < M; j++) {
if (next == seq[idx[j]] * primes[j]) idx[j]++;
}
}
return seq.back();
}
注意:丑数可以无限生成下去,因此不是在行列有序的矩阵中找第k小数的问题。
最多交换一次数字、使数尽量大
int maximumSwap(int num) {
// 从左往右看各位置,看有没有比当前数字尽量大的数字在最后面,有就交换
string s = to_string(num);
const int N = s.size();
vector<int> lastIdx(10, -1);
for (int i = 0; i < N; i++) {
lastIdx[s[i] - '0'] = i;
}
for (int i = 0; i < N; i++) {
for (int x = 9; x > s[i] - '0'; x--) {
if (lastIdx[x] > i) {
swap(s[i], s[lastIdx[x]]);
return stoi(s);
}
}
}
return num;
}
一组数最多改一个数,能否变成非递减(<=)
bool checkPossibility(vector<int>& nums) {
// 贪心法。找到违反<=关系的x[i]>x[i+1],优先改小x[i]:x[i]=x[i+1]。
// (后面是否满足<=待后面循环去检查,前面满足要还需x[i-1]<=x[i+1])
// 无法改小时改大x[i+1]:x[i+1]=x[i]。
int cnt = 0;
for (int i = 0; i + 1 < nums.size(); i++) {
if (nums[i] <= nums[i+1]) continue;
if (cnt++ > 0) return false;
// 优先改小x[i]
if (i-1 < 0 || nums[i-1] <= nums[i+1]) {
nums[i] = nums[i+1];
} else {
nums[i+1] = nums[i];
}
}
return true;
}
有多少相差k的不同数对
int findPairs(vector<int>& nums, int k) {
if (k < 0) return 0;
unordered_map<int, int> cnt;
for (int num : nums) cnt[num]++;
int ans = 0;
for (auto &e : cnt) {
if (k == 0) ans += e.second >= 2;
else ans += cnt.count(e.first + k);
}
return ans;
}
最多连续数的子集
int longestConsecutive(vector<int>& nums) {
// 把数都塞到集合中,然后取出无x-1的数x
// 看后序的x+1,x+2,...是否也在集合中
unordered_set<int> st;
for (int num : nums)
st.insert(num);
int ans = 0;
for (int num : nums) {
if (st.count(num - 1)) continue;
int len = 1;
while (st.count(num + len))
len++;
ans = max(ans, len);
}
return ans;
}
每个数最多可重复两次,把多的去掉
int removeDuplicates(vector<int>& nums) {
const int N = nums.size(), k = 2;
if (N <= k) return N;
int out = k;
for (int in = k; in < N; in++) {
if (nums[in] != nums[out-k]) {
nums[out++] = nums[in];
}
}
return out;
}
一组数的总hamming距离
int totalHammingDistance(vector<int>& nums) {
// 两数的hamming距离指两数二进制的不同比特数。
// 可以总体考虑一组数的某一位,该位有ones个1、(N-ones)个零,
// 那么这组数在该位上的hamming距离是ones*(N-ones)。累加所有位。
int ans = 0;
for (int i = 0; i < 32; i++) {
// 考查整组数的第i位
int ones = 0;
for (int num : nums) {
if (num & (1 << i)) ones++;
}
ans += ones * (nums.size() - ones);
}
return ans;
}
任意两点的最大hamming距离
int maxAbsValExpr(vector<int>& x, vector<int>& y) {
// |arr1[i] - arr1[j]| + |arr2[i] - arr2[j]| + |i - j| (1)
// 如果没有绝对值,式(1)=(arr1[i]+arr2[i]+i)-(arr1[j]+arr2[j]+j)
// 令arr3[i]=arr1[i]+arr2[i]+i,式(1)=arr3[i]-arr3[j],在arr3[]中找两数的最大差值
// 现在加回绝对值,前两项各两种可能、第三项只一种可能,变成共有四种可能
// 相当于在四种arr3[]中找两数的最大差值
const int N = x.size();
int ans = 0;
vector<int> P{{-1, 1}};
for (auto p1 : P) {
for (auto p2 : P) {
// 四种arr3[],(x,y) => (p1*x, p2*y)
// 每种都找其中两数的最大差值
int theMin = INT_MAX, theMax = INT_MIN;
for (int i = 0; i < N; i++) {
int val = p1 * x[i] + p2 * y[i] + i;
theMin = min(theMin, val);
theMax = max(theMax, val);
ans = max(ans, theMax - theMin);
}
}
}
return ans;
}
到某字符的最短距离
vector<int> shortestToChar(string S, char C) {
// 正反各扫一遍
// 正向看距离左边e的最小距离,反向看距离右边e的最小距离
const int N = S.size();
vector<int> ans(N);
int idx = -N;
for (int i = 0; i < N; i++) {
if (S[i] == C) idx = i;
ans[i] = i - idx;
}
for (int i = idx - 1; i >= 0; i--) {
if (S[i] == C) idx = i;
ans[i] = min(ans[i], idx - i);
}
return ans;
}
分糖果
int candy(vector<int>& ratings) {
const int N = ratings.size();
vector<int> candies(N, 1);
for (int i = 1; i < N; i++) { // 只考虑比左侧分高的
if (ratings[i] > ratings[i-1]) {
candies[i] = candies[i-1] + 1;
}
}
for (int i = N - 2; i >= 0; i--) { // 只考虑比右侧分高的
if (ratings[i] > ratings[i+1]) {
candies[i] = max(candies[i], candies[i+1] + 1);
}
}
return accumulate(begin(candies), end(candies), 0);
}
翻转成“先0后1的数”的minFlips
可用动态规划
int minFlipsMonoIncr(string S) {
// 设子问题S[0..i]以'0'结尾时minFlips为dp[i][0],以'1'结尾时minFlips为dp[i][1]
// dp[i][0] = dp[i-1][0] + (S[i]=='1')
// dp[i][1] = min(dp[i-1][0], dp[i-1][1]) + (S[i]=='0')
// 递推式在i这维上只依赖i-1项,省掉i这维:
// dp[1] = min(dp[0], dp[1]) + (S[i]=='0')
// dp[0] += (S[i]=='1') // dp[0]覆盖放后面
int f0 = 0, f1 = 0;
for (char c : S) {
f1 = min(f0, f1) + (c == '0');
f0 += (c == '1');
}
return min(f0, f1);
}
也可用前缀后缀数组
int minFlipsMonoIncr(string S) {
const int N = S.size();
vector<int> pre(N + 1); // pre[i]表示S[..i-1]以0结尾
for (int i = 0; i < N; i++) {
pre[i+1] = pre[i] + (S[i] == '1');
}
vector<int> suf(N + 1); // suf[i]表示S[i..]以1开头
for (int i = N - 1; i >= 0; i--) {
suf[i] = suf[i+1] + (S[i] == '0');
}
int ans = INT_MAX;
for (int i = 0; i <= N; i++) {
ans = min(ans, pre[i] + suf[i]);
}
return ans;
}
山峰数组
int longestMountain(vector<int>& A) {
const int N = A.size();
vector<int> inc(N), dec(N);
for (int i = 1; i < N; i++) {
if (A[i] > A[i-1])
inc[i] = inc[i-1] + 1;
}
for (int i = N - 2; i >= 0; i--) {
if (A[i] > A[i+1])
dec[i] = dec[i+1] + 1;
}
int ans = 0;
for (int i = 1; i < N - 1; i++) {
if (inc[i] && dec[i])
ans = max(ans, inc[i] + dec[i] + 1);
}
return ans;
}
也可一遍扫描统计
int longestMountain(vector<int>& A) {
// 统计一段上升和下降的最大长度
int up = 0, down = 0, ans = 0;
for (int i = 1; i < A.size(); i++) {
if (A[i] > A[i-1]) {
if (down > 0) up = down = 0; // 下降结束,开始新一段的统计
up++;
} else if (A[i] < A[i-1]) {
down++;
} else {
up = down = 0; // 开始新一段的统计
}
if (up > 0 && down > 0) {
ans = max(ans, 1 + up + down); // 1来自统计开始处的元素
}
}
return ans;
}
各项为除了自己外的数组乘积
vector<int> productExceptSelf(vector<int>& nums) {
const int N = nums.size();
vector<int> ans(N, 1);
// 从左到右扫一遍
for (int i = 0, prodL = 1; i < N; i++) {
ans[i] *= prodL;
prodL *= nums[i];
}
// 从右到左扫一遍
for (int i = N - 1, prodR = 1; i >= 0; i--) {
ans[i] *= prodR;
prodR *= nums[i];
}
return ans;
}
数组分段排序后连接、总体也有序,最多可分多少段
数组数任意可重复
int maxChunksToSorted(vector<int>& arr) {
// maxL[i]表示arr[0..i)的最大值,minR[i]表示arr[i..]的最小值
// maxL[i]<=minR[i]时可分
const int N = arr.size();
vector<int> maxL(N + 1, INT_MIN), minR(N + 1, INT_MAX);
for (int i = 0; i < N; i++) {
maxL[i+1] = max(maxL[i], arr[i]);
}
int ans = 0;
for (int i = N - 1; i >= 0; i--) {
minR[i] = min(minR[i+1], arr[i]);
if (maxL[i] <= minR[i]) ans++;
}
return ans;
}
上题解法可用。因为数组是[1,n]的排列,还可以简化
int maxChunksToSorted(vector<int>& arr) {
// 数组是[1,n]的排列,若maxL[i]==i,则在i后可断开左右
int ans = 0;
int maxL = INT_MIN;
for (int i = 0; i < arr.size(); i++) {
maxL = max(maxL, arr[i]);
if (maxL == i) ans++;
}
return ans;
}
最短无序子段,该子段排序后、整个数组也有序
int findUnsortedSubarray(vector<int>& nums) {
// 若数组有序,对任意nums[i]有:maxL[i]==nums[i]==minR[i]
// 从左往右检查,最右的违反maxL[i]==nums[i]的i是右边界
// 从右往左检查,最左的违反nums[i]==minR[i]的i是左边界
const int N = nums.size();
int maxL = INT_MIN, minR = INT_MAX;
int lo = 0, hi = -1; // 无序子段的左右边界
for (int i = 0, j = N - 1; i < N; i++, j--) {
maxL = max(maxL, nums[i]);
if (nums[i] != maxL) hi = i;
minR = min(minR, nums[j]);
if (nums[j] != minR) lo = j;
}
return hi - lo + 1;
}
螺旋打印矩阵
由外向内螺旋
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if (matrix.empty()) return {};
const int R = matrix.size(), C = matrix[0].size();
const vector<vector<int>> dirs = {{0,1},{1,0},{0,-1},{-1,0}}; // 右下左上
vector<int> numSteps = {C, R-1};
int r = 0, c = -1;
int cur = 0; // 方向,4种
vector<int> ans;
while (numSteps[cur % 2]) {
for (int i = 0; i < numSteps[cur % 2]; i++) {
r += dirs[cur][0], c += dirs[cur][1];
ans.push_back(matrix[r][c]);
}
numSteps[cur % 2]--;
cur = (cur + 1) % 4;
}
return ans;
}
由内向外螺旋
vector<vector<int>> spiralMatrixIII(int R, int C, int r0, int c0) {
// 往右下左上方向不断走1,1,2,2,3,3,4,4,5,5,...步
// 从索引i(0-based)生成步数的通式是 i/2+1
const vector<vector<int>> dirs = {{0,1}, {1,0}, {0,-1}, {-1,0}};
vector<vector<int>> ans;
ans.push_back({r0, c0});
int r = r0, c = c0;
for (int i = 0; ans.size() < R * C; i++) {
auto &dir = dirs[i%4]; // 方向
for (int k = 0; k < i / 2 + 1; k++) { // 走i/2+1步
r += dir[0], c += dir[1];
if (0 <= r && r < R && 0 <= c && c < C) ans.push_back({r, c});
}
}
return ans;
}
来回遍历次对角线方向
同一次对角线上两个点r、c一增一减,r+c相等。
vector<int> findDiagonalOrder(vector<vector<int>>& matrix) {
if (matrix.empty()) return {};
const int R = matrix.size(), C = matrix[0].size();
vector<vector<int>> diags(R + C - 1);
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
diags[r+c].push_back(matrix[r][c]);
}
}
vector<int> ans;
for (int i = 0; i < diags.size(); i++) {
if (i % 2 == 0) {
ans.insert(end(ans), rbegin(diags[i]), rend(diags[i]));
} else {
ans.insert(end(ans), begin(diags[i]), end(diags[i]));
}
}
return ans;
}
联想:同一主对角线上两点r、c同增减,r-c相等
NxN矩阵顺时针旋转90度

void rotate(vector<vector<int>>& matrix) {
// 旋转90度跟"转置"有关
// 顺时针旋转90度 <=等价=> 先把行倒排、再转置
// 1 2 3 7 8 9 7 4 1
// 4 5 6 => 4 5 6 => 8 5 2
// 7 8 9 1 2 3 9 6 3
reverse(begin(matrix), end(matrix));
const int N = matrix.size();
for (int i = 0; i < N; i++) {
for (int j = 0; j < i; j++) {
swap(matrix[i][j], matrix[j][i]);
}
}
}
如果是左转90度,那就是先先转置、再把行倒排。
01矩阵中各点到最近0值的距离
两遍扫描更新距离矩阵dist[][],第一遍从上到下从左到右、第二遍从下到上从右到左。
vector<vector<int>> updateMatrix(vector<vector<int>>& matrix) {
const int INF = 1e7;
const int R = matrix.size(), C = matrix[0].size();
vector<vector<int>> dist(R, vector<int>(C, INF));
// 上=>下、左=>右
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
if (matrix[r][c] != 0) {
if (r > 0) dist[r][c] = min(dist[r][c], dist[r-1][c] + 1);
if (c > 0) dist[r][c] = min(dist[r][c], dist[r][c-1] + 1);
} else {
dist[r][c] = 0;
}
}
}
// 下=>上、右=>左
for (int r = R - 1; r >= 0; r--) {
for (int c = C - 1; c >= 0; c--) {
if (matrix[r][c] != 0) {
if (r + 1 < R) dist[r][c] = min(dist[r][c], dist[r+1][c] + 1);
if (c + 1 < C) dist[r][c] = min(dist[r][c], dist[r][c+1] + 1);
}
}
}
return dist;
}
全是1的最大子矩阵
int maximalRectangle(vector<vector<char>>& matrix) {
// 把R行C列的矩阵看作R个以第r行为底的直方图
if (matrix.empty()) return 0;
const int R = matrix.size(), C = matrix[0].size();
int ans = 0;
vector<int> h(C, 0);
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
if (matrix[r][c] == '0') h[c] = 0;
else h[c]++;
}
ans = max(ans, largestRectangleArea(h));
}
return ans;
}
int largestRectangleArea(vector<int>& h) {
// 同https://leetcode.com/problems/largest-rectangle-in-histogram/
// 找波峰,对应找下一个更小的数
h.push_back(0); // 右哨兵
const int N = h.size();
int ans = 0;
stack<int> stk; // 栈中保存坐标
for (int i = 0; i < N; i++) {
while (!stk.empty() && h[i] < h[stk.top()]) {
int peak = stk.top(); stk.pop();
int left = stk.empty() ? -1 : stk.top();
ans = max(ans, h[peak] * (i - left - 1));
}
stk.push(i);
}
return ans;
}
全是1的子矩阵的个数
int numSubmat(vector<vector<int>>& mat) {
// 把R行C列的矩阵看作R个以第r行为底的直方图
if (mat.empty()) return 0;
const int R = mat.size(), C = mat[0].size();
int ans = 0;
vector<int> h(C, 0);
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
if (mat[r][c] == 0) h[c] = 0;
else h[c]++;
}
ans += numSubmat(h);
}
return ans;
}
int numSubmat(vector<int>& h) { // 以第r行为底的直方图
// 在直方图中统计全1子矩阵的个数
// 找波峰,对应找下一个更小的数
const int N = h.size();
int ans = 0;
vector<int> sum(N); // sum[i]保存右下角在(r,i)的子矩阵数
stack<int> stk; // 保存下标
for (int i = 0; i < N; i++) {
while (!stk.empty() && h[i] <= h[stk.top()]) {
stk.pop();
}
int lo = stk.empty() ? -1 : stk.top();
// 现在h[lo]<h[i]
// (lo..i]位置的高度>h[i],贡献右下角在(r,i)的子矩阵数(i-lo)*h[i]
// (..lo]位置的高度<=h[i],贡献右下角在(r,i)的子矩阵数已记录在sum[lo]中
sum[i] += (i - lo) * h[i];
if (lo >= 0) sum[i] += sum[lo];
ans += sum[i];
stk.push(i);
}
return ans;
}
扩展:最大子矩阵和
参见:最大子段和
全是1的正方形子矩阵
- https://leetcode.com/problems/maximal-square/
- https://leetcode.com/problems/count-square-submatrices-with-all-ones/
int maximalSquare(vector<vector<char>>& matrix) {
// 设dp[i][j]表示右下角在[i-1,j-1]的最大正方形子矩阵边长。
// 当matrix[i-1][j-1]=='1'时,
// dp[i][j] = 1 /*右下角的'1'*/ + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) /*即左、上、左上矩阵*/
// 初始dp[0][..]=dp[..][0]=0
if (matrix.empty()) return 0;
const int M = matrix.size(), N = matrix[0].size();
vector<vector<int>> dp(M + 1, vector<int>(N + 1, 0));
int maxlen = 0;
for (int i = 1; i <= M; i++) {
for (int j = 1; j <= N; j++) {
if (matrix[i-1][j-1] == '1') {
dp[i][j] = 1 + min({ dp[i-1][j-1], dp[i-1][j], dp[i][j-1] });
maxlen = max(maxlen, dp[i][j]);
}
}
}
return maxlen * maxlen;
}
四边全为1的最大正方形
int largest1BorderedSquare(vector<vector<int>>& grid) {
const int R = grid.size(), C = grid[0].size();
// 辅助矩阵统计grid[i][j]往右、往下连续1的个数
vector<vector<int>> right(R, vector<int>(C, 0));
vector<vector<int>> down(R, vector<int>(C, 0));
for (int r = R - 1; r >= 0; r--) {
for (int c = C - 1; c >= 0; c--) {
if (grid[r][c]) {
right[r][c] = (c + 1 < C) ? right[r][c+1] + 1 : 1;
down[r][c] = (r + 1 < R) ? down[r+1][c] + 1 : 1;
}
}
}
int maxLen = 0;
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
for (int len = min(right[r][c], down[r][c]); len > maxLen; len--) {
// 检查len是否可行
if (right[r+len-1][c] >= len && down[r][c+len-1] >= len) {
maxLen = len;
break;
}
}
}
}
return maxLen * maxLen;
}
边角矩形的数量
Given a grid where each entry is only 0 or 1, find the number of corner rectangles.
A corner rectangle is 4 distinct 1s on the grid that form an axis-aligned rectangle. Note that only the corners need to have the value 1. Also, all four 1s used must be distinct.
Example 1:
Input: grid = [[1, 0, 0, 1, 0], [0, 0, 1, 0, 1], [0, 0, 0, 1, 0], [1, 0, 1, 0, 1]] Output: 1 Explanation: There is only one corner rectangle, with corners grid[1][2], grid[1][4], grid[3][2], grid[3][4].Example 2:
Input: grid = [[1, 1, 1], [1, 1, 1], [1, 1, 1]] Output: 9 Explanation: There are four 2x2 rectangles, four 2x3 and 3x2 rectangles, and one 3x3 rectangle.Example 3:
Input: grid = [[1, 1, 1, 1]] Output: 0 Explanation: Rectangles must have four distinct corners.Note:
- The number of rows and columns of
gridwill each be in the range[1, 200].- Each
grid[i][j]will be either0or1.- The number of
1s in the grid will be at most6000.
int countCornerRectangles(vector<vector<int>>& grid) {
if (grid.empty()) return 0;
const int R = grid.size(), C = grid[0].size();
int ans = 0;
// 遍历两个行
for (int r1 = 0; r1 < R; r1++) {
for (int r2 = r1 + 1; r2 < R; r2++) {
int cnt = 0; // “某列两端同为1”的列数
for (int c = 0; c < C; c++) {
if (grid[r1][c] && grid[r2][c]) cnt++;
}
ans += cnt * (cnt - 1) / 2;
}
}
return ans;
}
-
x去掉最后一位1:
x & (x-1) -
x只取最后一位1:
x & -x
“范围与”的结果是首尾两数二进制的公共前缀
int rangeBitwiseAnd(int m, int n) {
// "范围与"的结果是:首尾两数二进制的公共前缀
// 因为非公共前缀一定是0xxx和1xxx的形式,存在中间数1000将这部分"范围与"清零
while (n > m) {
n &= n - 1; // 去掉最后1位
}
return n;
}
4的幂
bool isPowerOfFour(int num) {
// 二进制仅有一个1,且这个1出现在...010101这些位
return num > 0 && (num & (num - 1)) == 0 && (num & 0x55555555) != 0;
}
数字有效位反转
int findComplement(int num) {
// 先找哪些是有效位
int mask = ~0;
while (mask & num) mask <<= 1;
return ~mask ^ num;
}
位运算实现两数相加
int getSum(int a, int b) {
while (b) {
int carry = (a & b) << 1;
a = a ^ b;
b = carry;
}
return a;
}
也适用于负数,负数是补码表示。
字母变换大小写
回溯法。根据ascii表,letter^=32就变换大小写,32 == 刚好>字母数26的 2的幂。
不用比较符找两数的较大值
不用比较符,表达式ans = cond ? a : b;等价于 ans = cond * a + !cond * b
≥号的表达式cond = (a>=b) = (a-b >= 0) = sign(a-b)。当a,b异号时a-b可能溢出,sign(a-b)不能用;而a,b异号,a>0>b或a<0<b,sign(a-b)==sign(a),于是用sign(a)代替sign(a-b)。因此 cond = (a,b异号) ? sign(a) : sign(a-b),较大的数 = cond * a + !cond * b。
见ctci p476
二进制无连续1s的、[0..num]间的数有多少个
int findIntegers(int num) {
// 设f[n]表示n位二进制中无连续1s的有多少个
// 根据最高位是0还是1,f[n] = f[n-1]/*最高位0*/ + f[n-2]/*最高位1*/
// 初始f[1]=2、f[2]=3,推得f[0]=1
const int N = 32; // 32bit
vector<int> f(N, 0);
f[0] = 1, f[1] = 2;
for (int n = 2; n < N; n++) {
f[n] = f[n-1] + f[n-2];
}
// 求[0,num)间无连续1s的数有多少个
// 从num的最高位i=N-2看起(不含符号位),一位位拼入prefix,prefix = bit[N-2...i]位来自num、后面位全是0的数。
// 累加拼入num的第i位前后,新增区间[lastPrefix, prefix)内无连续1s的个数。比如num=23 (10111) 要统计
// 新增区间[0,10000), [10000,10000), [10000,10100), [10100,10110), [10110,10111)。
// 统计新增区间,看拼入的num第i位:
// 若拼入的位为1,新增区间内第i位为0、bit[i-1..0]位任意,贡献f[i]个无连续1s的数;
// 若拼入的位为0,新增区间为空。
int count = 0;
int prebit = 0; // 初始是符号位
for (int i = N - 2; i >= 0; i--) {
if (num & (1 << i)) { // 第i位为1
count += f[i];
if (prebit) return count; // 遇到连续的1s,提前返回
prebit = 1;
} else {
prebit = 0;
}
}
// 已统计[0,num),再算上num自身
return count + 1;
}
数组中两数的最大XOR值,O(n)解法
int findMaximumXOR(vector<int>& nums) {
// 假设A^B=maxXor,只考虑i位前缀,有prefixA^prefixB=prefixMaxXor。
// 就像两数之和的问题,问是否存在两数a+b=sum,解法是对每个a看对应的sum-a在数集中是否存在。
// 这里为使xor尽量大,特意设置guess=第i位为1的prefixMaxXor,再问是否存在两个i位前缀a^b==guess。
// 解法是对每个i位前缀a,看对应的guess^a在前缀集中是否存在。存在则maxXor的第i位为1。
int mask = 0, maxXor = 0;
for (int i = 31; i >= 0; i--) {
unordered_set<int> st;
mask |= (1 << i);
for (int num : nums) {
st.insert(num & mask);
}
// 是否存在两个i位前缀,使xor结果的第i位为1
int guess = maxXor | (1 << i);
for (int a : st) {
if (st.count(guess ^ a)) { // 存在
maxXor = guess;
break;
}
}
}
return maxXor;
}
"范围与”最接近k的差值
int closestToTarget(vector<int>& arr, int target) {
// func(arr,l,r)做范围与操作
// 用set[r]记录所有arr[..r]的“范围与”结果,
// set[r+1] = { set[r]中各数 & arr[r+1], arr[r+1] }
unordered_set<int> set1;
int ans = INT_MAX;
for (int a : arr) {
unordered_set<int> set2;
set2.insert(a);
for (auto x : set1) {
set2.insert(x & a);
}
for (auto x : set2) {
ans = min(ans, abs(x - target));
}
swap(set1, set2);
}
return ans;
}
也可用数组保存中间结果,像下面
子段的范围或
超时
int subarrayBitwiseORs(vector<int>& A) {
// 用set[r]记录A[..r]子段的ORs结果
// set[r+1]为 {set[r]中各数 | A[r+1], A[r+1]}
unordered_set<int> set1, set2, ans;
for (int a : A) {
set2 = {a};
for (int x : set1) {
set2.insert(x | a);
}
ans.insert(set2.begin(), set2.end());
swap(set1, set2);
}
return ans.size();
}
用数组保存中间结果
int subarrayBitwiseORs(vector<int>& A) {
// 在数组子段B[lo..hi)记录以A[idx]结尾的、A[..idx]所有子段的OR结果
// A[..idx+1]所有子段的OR结果为 {B[lo..hi)中各数 | A[idx+1], A[idx+1]}
// OR值放入B中的顺序是 A[idx]、A[idx-1..idx]、A[idx-2..idx]...,一定单调递增,
// 只需防止到达OR值极限后继续往里塞
vector<int> B;
int lo = 0, hi = 0;
for (int a : A) {
B.push_back(a);
for (int i = lo; i < hi; i++) {
int a2 = B[i] | a;
// 防止到达OR值极限后继续往里塞
if (a2 != B.back()) B.push_back(a2);
}
lo = hi;
hi = B.size();
}
return unordered_set<int>(begin(B), end(B)).size();
}
最短唯一单词缩写
A string such as
"word"contains the following abbreviations:["word", "1ord", "w1rd", "wo1d", "wor1", "2rd", "w2d", "wo2", "1o1d", "1or1", "w1r1", "1o2", "2r1", "3d", "w3", "4"]Given a target string and a set of strings in a dictionary, find an abbreviation of this target string with the smallest possible length such that it does not conflict with abbreviations of the strings in the dictionary.
Each number or letter in the abbreviation is considered length = 1. For example, the abbreviation "a32bc" has length = 4.
Note:
- In the case of multiple answers as shown in the second example below, you may return any one of them.
- Assume length of target string = m, and dictionary size = n. You may assume that m ≤ 21, n ≤ 1000, and log2(n) + m ≤ 20.
Examples:
"apple", ["blade"] -> "a4" (because "5" or "4e" conflicts with "blade") "apple", ["plain", "amber", "blade"] -> "1p3" (other valid answers include "ap3", "a3e", "2p2", "3le", "3l1").
string minAbbreviation(string target, vector<string>& dictionary) {
// 1. 某单词要与target等长,缩略词才可能冲突,所以只考虑dictionary中与target等长的子集。
// 2. target与dict中某单词的相异点diff:相同字母处取0、不同字母处取1,这样得到二进制数diff,
// diff中的位1表示相异点。只要将任一位1处取字母、其他位取数字,得到的缩写就不会冲突。
// 注意,target[i]^word[i]对应二进制diff[N-1-i]
// 3. target的某缩写是否可行?将缩写的字母处取1、数字x处取x个0,得到缩写的二进制表示abbr。
// 当abbr&diff!=0时,缩写中存在相异点,缩写不冲突;当abbr&diff==0时,缩写中不存在相异点,缩写冲突。
// 若abbr与所有diff都不冲突,这个abbr可行。
// 4. 回溯法尝试abbr的取值看是否可行:
// 从单词的当前idx开始,或者省略连续几个字母(abbr对应位取0),或者保留字母(abbr对应位取1)。
const int N = target.size();
set<int> diffs; // 将单词子集转为diffs集合
for (auto &word : dictionary) {
if (word.size() == N) {
diffs.insert(getDiff(target, word));
}
}
int minAbbrLen = N + 1, ansAbbr = -1; // 无效值
search(0, N, 0, 0, diffs, minAbbrLen, ansAbbr);
if (minAbbrLen > N) return "";
return getAbbrStr(ansAbbr, target);
}
int getDiff(const string &target, const string &word) {
// target[i]^word[i]对应二进制diff[N-1-i]
int diff = 0;
for (int i = 0; i < target.size(); i++) {
diff <<= 1;
diff |= target[i] != word[i];
}
return diff;
}
// abbr是target[0..idx)对应的缩写,abbrLen是abbr对应的串长(多位数字算长1)
void search(int idx, const int N, int abbr, int abbrLen,
const set<int> &diffs, int &minAbbrLen, int &ansAbbr) {
if (abbrLen >= minAbbrLen) return; // 剪枝
if (idx == N) {
for (int diff : diffs) {
if ((abbr & diff) == 0) return; // abbr冲突
}
// 至此,abbr可行
if (abbrLen < minAbbrLen) {
minAbbrLen = abbrLen;
ansAbbr = abbr;
}
return;
}
// 刚开头或上次保留了字母,这次忽略[idx..i]间的字母
if (idx == 0 || (abbr & 1)) {
for (int i = N - 1; i >= idx; i--) {
search(i + 1, N, abbr << (i - idx + 1), abbrLen + 1,
diffs, minAbbrLen, ansAbbr);
}
}
// 或者,这次再保留字母
search(idx + 1, N, (abbr << 1) | 1, abbrLen + 1,
diffs, minAbbrLen, ansAbbr);
}
string getAbbrStr(int abbr, const string &target) {
// abbr[i]对应target[N-1-i]
const int N = target.size();
int cnt0s = 0;
string ans;
for (int i = N - 1; i >= 0; i--) {
if (abbr & (1 << i)) {
if (cnt0s > 0) {
ans += to_string(cnt0s);
cnt0s = 0;
}
ans += target[N-1-i];
} else {
cnt0s++;
}
}
if (cnt0s > 0) ans += to_string(cnt0s);
return ans;
}
长10的重复DNA片段
vector<string> findRepeatedDnaSequences(string s) {
// 只有ACGT四种字符,字符编码只要2位,长10的串编码只要20位(掩码:0xfffff)
const int LENGTH = 10;
unordered_map<char, int> coding = {
{'A', 0b00},
{'C', 0b01},
{'G', 0b10},
{'T', 0b11},
};
vector<string> ans;
unordered_map<int, int> cnt; // code=>count
int code = 0;
for (int i = 0; i < s.size(); i++) {
code = ((code << 2) + coding[s[i]]) & 0xfffff;
if (i < LENGTH - 1) continue; // <LENGTH的串编码不要
cnt[code]++;
if (cnt[code] == 2) {
ans.push_back(s.substr(i - LENGTH + 1, LENGTH));
}
}
return ans;
}
比特反转
uint32_t reverseBits(uint32_t n) {
// 分治,所有小组内交换前半段和后半段
// 比如n是uint8_t,初始len=8,mask=11111111
// 所有组内对半分并交换:
// 1. 算出对应所有组内后半段的mask:len >>= 1; mask ^= (mask << len);
// 2. 将所有组内前半段后移并用mask取出、后半段前移并用~mask取出
// 第1轮:mask=00001111,~mask=11110000
// 第2轮:mask=00110011,~mask=11001100
// 第3轮:mask=01010101,~mask=10101010
int len = sizeof(n) * 8;
uint32_t mask = ~0;
while (len > 1) {
len >>= 1;
mask ^= (mask << len); // 算mask是关键
n = ((n >> len) & mask) | ((n << len) & ~mask);
}
return n;
}
为使01数组都为1,连续K位flip的最少次数
int minKBitFlips(vector<int>& A, int K) {
// 若翻转A[i..i+K-1],将i放入双端队列q
// 对A[i]有影响的是有效窗口[i-K+1,i-1]内的翻转次数q.size()
// 若A[i]==0&&q.size()%2==0 或 A[i]==1&&q.size()%2==1,要翻转
const int N = A.size();
int ans = 0;
deque<int> q;
for (int i = 0; i < N; i++) {
if (!q.empty() && q.front() < i - K + 1) {
q.pop_front();
}
if (A[i] == q.size() % 2) { // 要翻转
if (i + K - 1 >= N) return -1;
q.push_back(i);
ans++;
}
}
return ans;
}
用猪试毒药
int poorPigs(int buckets, int minutesToDie, int minutesToTest) {
// 一只猪可不吃食或者a次试毒,有(a+1)种选择,作(a+1)进制的一位。
// b只猪就是共b位。总共可试毒(a+1)^b桶。
int base = minutesToTest / minutesToDie + 1;
int power = 0;
while (pow(base, power) < buckets)
power++;
return power;
}
行列交换以变成黑白相间的棋盘样
int movesToChessboard(vector<vector<int>>& board) {
// 合法棋盘只有两种行,且01相反、01个数相等或差1;
// 列,...
const int N = board.size();
vector<int> rows(N, 0), cols(N, 0);
for (int r = 0; r < N; r++) {
for (int c = 0; c < N; c++) {
if (board[r][c] == 1) {
rows[r] |= 1 << c; // 行r变成二进制数
cols[c] |= 1 << r;
}
}
}
int swap1 = minSwaps(rows);
if (swap1 == -1) return -1;
int swap2 = minSwaps(cols);
if (swap2 == -1) return -1;
return swap1 + swap2;
}
int minSwaps(vector<int> &lines) {
const int N = lines.size();
set<int> st(lines.begin(), lines.end());
if (st.size() != 2) return -1; // 只有两种行
vector<int> l(st.begin(), st.end());
if ((l[0] & l[1]) != 0) return -1; // 且01相反
int ones = count1s(l[0]), zeros = N - ones;
if (abs(ones - zeros) > 1) return -1; // 01个数相等或差1
// 往mask1或mask2靠,最少需要多少swap
const int mask = (1 << N) - 1; // 后N位全是1
const int mask1 = 0x55555555 & mask, mask2 = 0xaaaaaaaa & mask;
int ans = INT_MAX;
if (ones >= zeros) ans = min(ans, count1s(l[0] ^ mask1) / 2); // 往mask1靠
if (ones <= zeros) ans = min(ans, count1s(l[0] ^ mask2) / 2); // 往mask2靠
return ans;
}
int count1s(int n) {
int ans = 0;
while (n) {
n &= n - 1 ;
ans++;
}
return ans;
}
格雷码
vector<int> grayCode(int n) {
vector<int> ans;
for (int i = 0; i < 1 << n; i++) {
ans.push_back(i ^ (i >> 1));
}
return ans;
}
第k位开始的格雷码
vector<int> circularPermutation(int n, int start) {
vector<int> ans;
for (int i = 0; i < 1 << n; i++) {
ans.push_back(start ^ (i ^ (i >> 1)));
}
return ans;
}
最长有效括号对
int longestValidParentheses(string s) {
int ans = 0;
stack<int> stk; // 没匹配掉的压入栈中,保存下标
for (int i = 0; i < s.size(); i++) {
if (s[i] == ')' && !stk.empty() && s[stk.top()] == '(') {
stk.pop();
int lastMismatch = stk.empty() ? -1 : stk.top();
ans = max(ans, i - lastMismatch); // 前开后闭
} else {
stk.push(i);
}
}
return ans;
}
所有有效括号对
vector<string> generateParenthesis(int n) {
vector<string> ans;
search(n, n, "", ans);
return ans;
}
void search(int remainL, int remainR, string paren, vector<string> &ans) {
// 确保 0 <= remainL <= remainR
if (remainL == 0 && remainR == 0) {
ans.push_back(paren);
return;
}
if (remainL > 0) search(remainL - 1, remainR, paren + '(', ans);
if (remainL < remainR) search(remainL, remainR - 1, paren + ')', ans);
}
仅含 ( ) * 的串是否有效
- https://leetcode.com/problems/valid-parenthesis-string
bool checkValidString(string s) {
// 设多出的左括号数leftDiff的范围为[lo, hi]。
// 1. 遇到'(',左括号多1,lo++, hi++;
// 2. 遇到')',左括号少1,lo--, hi--;
// 3. 遇到'*',它可以是'('、空串、')',[lo, hi]内的每个值都可-1、+0、+1,整体扩张lo--, hi++。
// 综上,遇到'('时lo++、其他lo--;遇到')'时hi--,其他hi++。
int lo = 0, hi = 0;
for (char c : s) {
lo += (c == '(') ? 1 : -1;
hi += (c == ')') ? -1 : 1;
// 在扫描过程中,一旦hi<0,leftDiff<0,串无效
if (hi < 0) return false;
// 而lo<0的部分可以直接丢弃,因为有效串的leftDiff>=0
lo = max(lo, 0);
}
// 最终能够leftDiff==0,串才有效
return lo == 0;
}
删除最少的无用括号使剩下的括号串有效,返回所有可能
vector<string> removeInvalidParentheses(string s) {
// 有多少左右括号不匹配待删除,
// rmL和rmR将作为dfs的剪枝条件
int rmL = 0, rmR = 0;
for (char c : s) {
if (c == '(') {
rmL++;
} else if (c == ')') {
if (rmL > 0) rmL--; // 匹配
else rmR++;
}
}
vector<string> ans;
remove(s, 0, 0, rmL, rmR, ans);
return ans;
}
// s[..idx)的左括号比右括号多moreL,s[idx..]有rmL个'('待删除、rmR个')'待删除
void remove(const string &s, int idx, int moreL, int rmL, int rmR, vector<string> &ans) {
if (moreL < 0 || rmL < 0 || rmR < 0) return;
const int N = s.size();
for (int i = idx; i < N && moreL >= 0; i++) {
if (s[i] == '(') {
if (rmL > 0 && (i == idx || s[i] != s[i-1])) { // 连续'('只删第一个
remove(s.substr(0, i) + s.substr(i + 1), i, moreL, rmL - 1, rmR, ans);
}
moreL++;
} else if (s[i] == ')') {
if (rmR > 0 && (i == idx || s[i] != s[i-1])) {
remove(s.substr(0, i) + s.substr(i + 1), i, moreL, rmL, rmR - 1, ans);
}
moreL--;
}
}
if (moreL == 0 && rmL == 0 && rmR == 0) {
ans.push_back(s);
}
}
括号按深度得分
int scoreOfParentheses(string S) {
// 深度depth的"()"贡献2^depth分
int depth = 0, ans = 0;
for (int i = 0; i < S.size(); i++) {
if (S[i] == '(') depth++;
else {
depth--;
if (S[i-1] == '(') ans += 1 << depth;
}
}
return ans;
}
使括号有效的最小添加个数
int minAddToMakeValid(string S) {
int bal = 0, ans = 0;
for (char c : S) {
bal += (c == '(') ? 1 : -1;
if (bal == -1) {
bal++;
ans++;
}
}
ans += bal;
return ans;
}
遍历每个算符,每个算符把表达式分成左右两个子问题。
逻辑表达式加括号的方式数
给定只有0、1、&、|、^ 的布尔表达式,和期望的结果result,求加括号的方式数。如 countEval("1^0|0|1", false) -> 2,countEval("0&0&0&1^1|1", true) -> 10
int countEval(String s, boolean result) {
if (s.length() == 0) return 0;
if (s.length() == 1) return s.equals("1") == result ? 1 : 0;
int ways = 0;
for (int i = 1; i < s.length(); i += 2) { // 遍历每个算符
char c = s.charAt(i);
String left = s.substring(0, i);
String right = s.substring(i + 1, s.length());
int leftTrue = countEval(left, true);
int leftFalse = countEval(left, false);
int rightTrue = countEval(right, true);
int rightFalse = countEval(right, false);
int total = (leftTrue + leftFalse) * (rightTrue + rightFalse);
int totalTrue = 0;
if (c == '&') {
totalTrue = leftTrue * rightTrue;
} else if (c == '|') {
totalTrue = leftTrue * rightTrue + leftFalse * rightTrue + leftTrue * rightFalse;
} else if (c == '^') {
totalTrue = leftTrue * rightFalse + leftFalse * rightTrue;
}
ways += result ? totalTrue : total - totalTrue;
}
return ways;
}
会重复计算子问题,要用备忘录memo缓存计算结果(s+result => count)。
加减乘表达式加括号计算的所有结果
vector<int> diffWaysToCompute(string input) {
vector<int> ans;
for (int i = 0; i < input.size(); i++) {
char c = input[i];
if (c == '+' || c == '-' || c == '*') { // 分治
auto ans1 = diffWaysToCompute(input.substr(0, i));
auto ans2 = diffWaysToCompute(input.substr(i + 1));
for (auto n1 : ans1) {
for (auto n2 : ans2) {
if (c == '+') {
ans.push_back(n1 + n2);
} else if (c == '-') {
ans.push_back(n1 - n2);
} else {
ans.push_back(n1 * n2);
}
}
}
}
}
if (ans.empty()) { // input是纯数字串
ans.push_back(stoi(input));
}
return ans;
}
另参见回文子序列
哪些单词对能拼成回文串
vector<vector<int>> palindromePairs(vector<string>& words) {
// 将w1分成w1[0..cut)、w1[cut..)两段,0<=cut<=len,两种情况:
// 1. w1[0..cut)是回文,w1[cut..)是w2反转,w2+w1是回文
// 2. w1[cut..)是回文,w1[0..cut)是w2反转,w1+w2是回文
// 若w1、w2互为反转,比如abc、cba,
// abc|null + cba,处理单词abc、cut=len,返回{0,1}
// abc + null|cba,处理单词cba、cut=0, 返回{0,1}
// 这就重复了,这时用cut>0或cut<len去掉一种重复。
const int N = words.size();
unordered_map<string, int> mp; // reverse=>idx
for (int i = 0; i < N; i++) {
string reverse(words[i].rbegin(), words[i].rend());
mp[reverse] = i;
}
vector<vector<int>> ans;
for (int i = 0; i < N; i++) {
const string &word = words[i];
const int len = word.size();
for (int cut = 0; cut <= len; cut++) {
// case 1
if (isPalindrome(word, 0, cut - 1)) {
string s2 = word.substr(cut);
if (mp.count(s2) && mp[s2] != i) {
ans.push_back({mp[s2], i});
}
}
// case 2
if (isPalindrome(word, cut, len - 1)) {
string s1 = word.substr(0, cut);
if (mp.count(s1) && mp[s1] != i && cut < len) { // 用cut<len去重
ans.push_back({i, mp[s1]});
}
}
}
}
return ans;
}
bool isPalindrome(const string &s, int lo, int hi) {
while (lo < hi) {
if (s[lo++] != s[hi--]) return false;
}
return true;
}
回文数
bool isPalindrome(int x) {
// 以0结尾的正数没有回文数
if (x < 0 || (x > 0 && x % 10 == 0)) return false;
int rev = 0;
while (x > rev) {
rev = rev * 10 + x % 10;
x /= 10;
}
return rev == x || rev / 10 == x;
}
>=N的最小回文素数
int primePalindrome(int N) {
// 先生成回文,再判断是素数。因为1<=N<=10^8,只需[1..10^5)的数生成回文:
// 正反拼接(偶数位)或 正反拼接且最后一位重合(奇数位)
// 特别地,所有偶位数回文都是11的倍数
// 因为1001%11=(1111-11*10)%11=0、100001%11=(111111-1111*10)%11=0、...
// 所以abccba%11=(a*100001+b*1001*10+11*100)%11=0
// 所以偶数位回文只有11是素数回文,而11只有当8<=N<=11时才返回。
// 接下来只需判断奇数位回文
if (8 <= N && N <= 11) return 11;
for (int i = 1; i < 1e5; i++) {
string s = to_string(i);
s += string(s.rbegin() + 1, s.rend());
int x = stoi(s);
if (x >= N && isPrime(x)) return x;
}
return -1;
}
bool isPrime(int num) {
if (num <= 1) return false;
for (int i = 2; i * i <= num; i++) {
if (num % i == 0) return false;
}
return true;
}
所有回文分割
“分割"类的标准回溯写法
vector<vector<string>> partition(string s) {
vector<vector<string>> ans;
vector<string> parts;
search(s, 0, parts, ans);
return ans;
}
void search(const string &s, int idx, vector<string> &parts, vector<vector<string>> &ans) {
const int N = s.size();
if (idx == N) {
ans.push_back(parts);
return;
}
// 尝试割下s[idx..i]
for (int i = idx; i < N; i++) {
if (!isPalindrome(s, idx, i)) continue;
parts.push_back(s.substr(idx, i - idx + 1));
search(s, i + 1, parts, ans);
parts.pop_back();
}
}
bool isPalindrome(const string &s, int l, int r) {
while (l < r) {
if (s[l++] != s[r--]) return false;
}
return true;
}
最少回文分割数
int minCut(string s) {
// 设dp[i][j]表示s[i..j]是回文串,0<=i<=j<N
// dp[i][j] = s[i]==s[j] && dp[i+1][j-1]
const int N = s.size();
vector<vector<bool>> dp(N, vector<bool>(N, false));
for (int i = N - 1; i >= 0; i--) {
for (int j = i; j < N; j++) {
dp[i][j] = s[i] == s[j];
if (i + 1 <= j - 1) dp[i][j] = dp[i][j] && dp[i+1][j-1];
}
}
// 设cut[i]表示子串s[0..i]的minCut,0<=i<N
// dp[0][i]==true时,cut[i]=0;否则,
// 对于0<=k<i的k若满足dp[k+1][i]==true,cut[i]=min{ cut[k]+1 }
vector<int> cut(N, INT_MAX);
for (int i = 0; i < N; i++) {
if (dp[0][i]) {
cut[i] = 0;
} else {
for (int k = 0; k < i; k++) {
if (dp[k+1][i]) {
cut[i] = min(cut[i], cut[k] + 1);
}
}
}
}
return cut[N-1];
}
回文排列
Given a string
s, return all the palindromic permutations (without duplicates) of it. Return an empty list if no palindromic permutation could be form.For example:
Given
s = "aabb", return["abba", "baab"].Given
s = "abc", return[].Hint:
- If a palindromic permutation exists, we just need to generate the first half of the string.
- To generate all distinct permutations of a (half of) string, use a similar approach from: Permutations II or Next Permutation.
vector<string> generatePalindromes(string s) {
unordered_map<char, int> mp;
int oddCnt = 0;
for (char c : s) {
mp[c]++;
if (mp[c] % 2 == 1) oddCnt++;
else oddCnt--;
}
if (oddCnt > 1) return {};
string odd, half;
for (auto &e : mp) {
if (e.second % 2 == 1) odd = string(1, e.first);
half += string(e.second / 2, e.first);
}
vector<string> ans;
permute(half, 0, odd, ans);
return ans;
}
void permute(string &half, int idx, const string &odd, vector<string> &ans) {
if (idx == half.size()) {
ans.push_back(half + odd + string(half.rbegin(), half.rend()));
return;
}
unordered_set<char> seen; // 防止相同元素再次交换
for (int i = idx; i < half.size(); i++) {
if (seen.count(half[i])) continue;
seen.insert(half[i]);
swap(half[i], half[idx]);
permute(half, idx + 1, odd, ans);
swap(half[i], half[idx]);
}
}
对称数
旋转180度看上去一样的数叫对称数。求在[low, high]范围内对称数的个数
class Solution {
unordered_map<string, string> mp = { {"0","0"}, {"1","1"}, {"6","9"}, {"8","8"}, {"9","6"} };
public:
int strobogrammaticInRange(string low, string high) {
int count = 0;
expand("", low, high, count); // n%2==0
for (string s : {"0", "1", "8"}) { // n%2==1
expand(s, low, high, count);
}
return count;
}
// 扩展num两端,添加数对
void expand(string num, const string &low, const string &high, int &count) {
const int len = num.size(), lowLen = low.size(), highLen = high.size();
if (len > highLen) return; // 排除过长的
if (len >= lowLen && !(len > 1 && num[0] == '0')) { // 长度合适,排除以‘0’开头的
if (lowLen == highLen) {
if (low <= num && num <= high) count++;
} else {
if ((len == lowLen && num >= low) || (len == highLen && num <= high)
|| (lowLen < len && len < highLen)) count++;
}
}
for (auto &e : mp) {
expand(e.first + num + e.second, low, high, count);
}
}
};
串首开始的最长回文串长
构造 S = s + "#" + reverse(s),原题就是求S中的前缀等于后缀的最大长度。正是KMP算法预处理pattern求各前缀的最大边界,只要最后返回b[S.size()]。
[kmp算法](http://www.inf.fh-flensburg.de/lang/algorithmen/pattern/kmpen.htm
把前缀等于后缀的部分叫做边界,0<=len(border)<N,求最大边界。
- 性质1:如果s是x的边界、r是s的边界,r也是x的边界,即边界的边界也是边界。
|-r-| |-r-| |-r-| |-r-|
|----s----|--------|----s----|
|--------------x-------------|
- 性质2:r是x的边界,后缀边界和前缀边界后的字符相同,则边界扩展1。
|--r--|a |--r--|a
|-------------x--------------|a
设b[i]表示前缀x[0..i-1]的最大边界长。初始设b[0]=-1,空串无边界。假设已解决子问题b[i]、b[i-1]等,现在考虑b[i+1]。
设j=b[i],当x[i]==x[j],由性质2,b[i+1]=j+1;否则,由性质1,继续尝试下一边界j=b[j],直到j=-1,这时刚好b[i+1]=j+1。
写法规整后就是维护不变式:i是前缀长、j是边界长
vector<int> buildBorder(const string& pattern) {
const int M = pattern.size();
// 设b[i]表示长为i的前缀pattern[0..i-1]的最大边界长
vector<int> b(M + 1);
int i = 0, j = -1; // 空串,无边界
b[i] = j;
while (i < M) {
while (j >= 0 && pattern[i] != pattern[j]) { // 当前边界不能扩展,性质2
j = b[j]; // 考虑下一小的边界,性质1
}
// 扩展边界
i++, j++;
b[i] = j;
}
return b;
}
int kmp(string pattern, string text) {
const int N = text.size(), M = pattern.size();
vector<int> b = buildBorder(pattern);
int ans = 0;
int t = 0, p = 0;
while (t < N) {
while (p >= 0 && text[t] != pattern[p]) {
p = b[p];
}
t++, p++;
if (p == M) {
ans++; // 匹配成功
p = b[p]; // 继续寻找下一匹配
}
}
return ans;
}
manacher(马拉车)算法,最长回文串长,O(n)
最长递增子序列 LIS
动态规划,O(n^2)
int lengthOfLIS(vector<int>& nums) {
// 设dp[i]表示以nums[i]结尾的nums[0..i]的最长递增子序列长,
// dp[i] = max{1, dp[j]+1 | 0<=j<i且nums[j]<nums[i] }
// 所求为 max{dp[i]}
const int N = nums.size();
vector<int> dp(N, 1);
int ans = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
ans = max(ans, dp[i]);
}
return ans;
}
贪心法,O(nlgn)
int lengthOfLIS(vector<int>& nums) {
vector<int> tails; // 保存各长度LIS的最小尾元素,是个递增数组
for (int num : nums) {
// 在tails[]中找第一个>=num的数
auto it = lower_bound(tails.begin(), tails.end(), num);
if (it != tails.end()) *it = num; // 若找到,则用num替换该项(末尾变小、LIS长度不变)
else tails.push_back(num); // 若找不到,说明所有尾元素都<num,可扩展序列
}
return tails.size();
}
长宽同时递增的最长子序列
- https://leetcode.com/problems/russian-doll-envelopes/
int maxEnvelopes(vector<vector<int>>& envelopes) {
// 长宽同时递增的最长子序列
// 按width排序,然后根据height找最长递增子序列
// width相等时按照height递减排序,能避免选中[3,4],[3,5]这种情况
sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b) {
return a[0] < b[0] || (a[0] == b[0] && a[1] > b[1]);
});
// 根据height找LIS,O(nlgn)
vector<int> tails; // 各长度LIS的最小尾元素
for (auto& e : envelopes) {
int height = e[1];
auto it = lower_bound(tails.begin(), tails.end(), height);
if (it != tails.end()) {
*it = height;
} else {
tails.push_back(height);
}
}
return tails.size();
}
任意两数可整除的最大子集
vector<int> largestDivisibleSubset(vector<int>& nums) {
// 先排序数组。nums[i]能整除某子集的所有值 <=> nums[i]能整除该子集最大值
// 设dp[i]表示以nums[i]结尾的nums[0..i]的最大可整除子集长,
// 若 nums[i] % nums[j] == 0,j < i,则 dp[i] = max(dp[j]+1)
sort(nums.begin(), nums.end());
const int N = nums.size();
vector<int> dp(N, 1);
vector<int> prev(N, -1);
int maxlen = 0, maxidx = -1;
for (int i = 0; i < N; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] % nums[j] == 0) { // 能扩展
if (dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
prev[i] = j;
}
}
}
if (dp[i] > maxlen) {
maxlen = dp[i];
maxidx = i;
}
}
vector<int> ans;
for (int i = maxidx; i != -1; i = prev[i]) {
ans.push_back(nums[i]);
}
reverse(ans.begin(), ans.end());
return ans;
}
最长递增子序列的个数
int findNumberOfLIS(vector<int>& nums) {
if (nums.empty()) return 0;
const int N = nums.size();
vector<int> len(N, 1); // 以nums[i]结尾的递增子序列长度
vector<int> cnt(N, 1); // ... 个数
int longest = INT_MIN, ans = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (len[j] + 1 > len[i]) { // 新的最长
len[i] = len[j] + 1;
cnt[i] = cnt[j];
} else if (len[j] + 1 == len[i]) { // 已知最长
cnt[i] += cnt[j];
}
}
}
if (len[i] > longest) {
longest = len[i];
ans = cnt[i];
} else if (len[i] == longest) {
ans += cnt[i];
}
}
return ans;
}
最长和谐子序列
和谐子序列指子序列的最大最小值相差1,即子序列的值只在范围[x..x+1]内。
只需遍历各个值x,找cnt[x]+cnt[x+1]的最大值。
int findLHS(vector<int>& nums) {
unordered_map<int, int> cnt;
for (int num : nums) {
cnt[num]++;
}
int ans = 0;
for (auto& [x, _] : cnt) {
if (!cnt.count(x+1)) continue;
ans = max(ans, cnt[x] + cnt[x+1]);
}
return ans;
}
最长严格递增子段,可修改一个元素
等差子段的个数
int numberOfArithmeticSlices(vector<int>& A) {
// 设dp[i]表示以A[i]结尾的等差子段数。
// 若A[i]-A[i-1]=A[i-1]-A[i-2]时,dp[i]=dp[i-1]+1;
// 其中dp[i-1]来自扩展的旧子段,1来自新出现的长度==3的子段。
// 若不满足等差,dp[i]=0。
// 初始dp[0]=dp[1]=0,因为长度要>=3。
// i这维只依赖前一项,省掉i这维,满足等差时dp+=1,不满足时dp=0。
int dp = 0, ans = 0;
for (int i = 2; i < A.size(); i++) {
if (A[i] - A[i-1] == A[i-1] - A[i-2]) {
dp += 1;
ans += dp;
} else {
dp = 0;
}
}
return ans;
}
等差子序列的个数
设dp[i]表示以A[i]结尾的等差子序列个数,递推过程太复杂,说明假设缺少信息。
int numberOfArithmeticSlices(vector<int>& A) {
// 设dp[i][d]表示以A[i]结尾、等差为d、长度>=2的等差子序列数。
// 对所有j<i,d=A[i]-A[j],dp[i][d]=sum(dp[j][d]+1)
// 其中dp[j][d]来自扩展的旧序列,1来自新出现的长度==2序列
// 优化:由于d是两数之差,范围无限,dp[i][d]的d这一维要用unordered_map
// 要用dp[j].count(d)测试,防止d值不存在时往map里添0,导致内存溢出
const int N = A.size();
vector<unordered_map<int,int>> dp(N);
int ans = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < i; j++) {
long d = (long)A[i] - A[j];
if (d < INT_MIN || d > INT_MAX) continue; // pass OL
int dp_jd = dp[j].count(d) ? dp[j][d] : 0;
dp[i][d] += dp_jd + 1;
// dp[j][d]是扩展的旧序列,现在长度>=3,统计长度>=3的
ans += dp_jd;
}
}
return ans;
}
最长等差子序列
int longestArithSeqLength(vector<int>& A) {
// 最长等差子序列长
// 设dp[i][d]表示以A[i]结尾、等差为d的子问题解
const int N = A.size();
vector<unordered_map<int,int>> dp(N);
int ans = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < i; j++) {
int d = A[i] - A[j];
dp[i][d] = dp[j].count(d) ? (dp[j][d] + 1) : 2;
ans = max(ans, dp[i][d]);
}
}
return ans;
}
指定等差的最长子序列
int longestSubsequence(vector<int>& arr, int difference) {
// 最长指定差的子序列长
// ~~O(n^2)解法超时:设dp[i]表示以A[i]结尾、等差为difference的子问题解~~
// 设dp[v]表示子序列结尾值为v的子问题解,dp[v] = dp[v-difference]+1
unordered_map<int, int> dp; // 子序列结尾值v=>子序列长
int ans = 0;
for (int v : arr) {
dp[v] = dp.count(v - difference) ? dp[v - difference] + 1 : 1;
ans = max(ans, dp[v]);
}
return ans;
}
最长Fib子序列
int lenLongestFibSubseq(vector<int>& A) {
// 设dp[i][j]表示以A[i]、A[j]结尾的长>=2的最长Fib子序列长
// 将A中值作val=>idx的映射,i,j的前一个索引idx=mp[A[j]-A[i]],
// dp[i][j] = dp[idx][i] + 1
// i从左往右遍历,j从左往右遍历
const int N = A.size();
unordered_map<int, int> mp; // val=>idx
vector<vector<int>> dp(N, vector<int>(N, 0));
int ans = 0;
for (int j = 0; j < N; j++) {
mp[A[j]] = j;
for (int i = 0; i < j; i++) {
int val = A[j] - A[i];
if (val < A[i] && mp.count(val)) {
int idx = mp[val];
dp[i][j] = dp[idx][i] + 1; // dp[i][j]现在>=3
ans = max(ans, dp[i][j]);
} else {
dp[i][j] = 2;
}
}
}
return ans;
}
最长回文串
- https://leetcode.com/problems/longest-palindromic-substring
- https://leetcode.com/problems/palindromic-substrings/
int countSubstrings(string s) {
// 设dp[i][j]表示s[i..j]是否是回文串,0<=i<=j<N
// dp[i][j] = s[i]==s[j] && dp[i+1][j-1]
// dp在i维上只依赖i+1项,可省掉i维,i仍从右往左遍历
// 要让dp[j-1]表示旧状态dp[i+1][j-1],j从右往左遍历
const int N = s.size();
vector<bool> dp(N, false);
int ans = 0;
for (int i = N - 1; i >= 0; i--) {
for (int j = N - 1; j >= i; j--) {
dp[j] = (s[i] == s[j]);
if (i + 1 <= j - 1) dp[j] = dp[j] && dp[j-1];
if (dp[j]) ans++;
}
}
return ans;
}
找回文串还可以从各可能的中心往外扩展。
string longestPalindrome(string s) {
int longest = 0, lo = 0;
for (int i = 0; i < s.size(); i++) {
int len1 = expand(s, i, i);
int len2 = expand(s, i, i + 1);
int len = max(len1, len2);
if (len > longest) {
longest = len;
lo = i - (len - 1) / 2;
}
}
return s.substr(lo, longest);
}
int expand(const string &s, int l, int r) {
while (l >= 0 && r < s.size() && s[l] == s[r]) {
l--;
r++;
}
return r - l - 1; // (l,r)
}
去找S和S'(\(S_{reversed}\))的最长公共子串是错的。比如S=abacdfgdcaba、S'=abacdgfdcaba,其最长公共子串abacd并不是回文。
做最少的切割,使变成一个个回文串
回文子串的判断同上dp
int minCut(string s) {
// 设dp[i][j]表示s[i..j]是回文串,0<=i<=j<N
// dp[i][j] = s[i]==s[j] && dp[i+1][j-1]
const int N = s.size();
vector<vector<bool>> dp(N, vector<bool>(N, false));
for (int i = N - 1; i >= 0; i--) {
for (int j = i; j < N; j++) {
dp[i][j] = s[i] == s[j];
if (i + 1 <= j - 1) dp[i][j] = dp[i][j] && dp[i+1][j-1];
}
}
// 设cut[i]表示子串s[0..i]的最少切割数,0<=i<N
// dp[0][i]==true时,cut[i]=0;否则,对于0<=k<i,
// 若满足dp[k+1][i]==true,cut[i]=min{ cut[k]+1 }
vector<int> cut(N, INT_MAX);
for (int i = 0; i < N; i++) {
if (dp[0][i]) {
cut[i] = 0;
} else {
for (int k = 0; k < i; k++) {
if (dp[k+1][i]) {
cut[i] = min(cut[i], cut[k] + 1);
}
}
}
}
return cut[N-1];
}
最长回文子序列
int longestPalindromeSubseq(string s) {
// 设dp[i][j]表示s[i..j]的最长回文子序列长,0<=i<=j<N
// 若s[i]==s[j],dp[i][j]=dp[i+1][j-1]+2,i<j
// 否则,dp[i][j]=max(dp[i+1][j], dp[i][j-1])
// 初始dp[i][i]=1
const int N = s.size();
vector<vector<int>> dp(N, vector<int>(N, 0));
for (int i = 0; i < N; i++) {
dp[i][i] = 1;
}
for (int len = 2; len <= N; len++) {
for (int i = 0; i + len <= N; i++) {
int j = i + len - 1;
if (s[i] == s[j]) {
dp[i][j] = dp[i+1][j-1] + 2;
} else {
dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
}
}
}
return dp[0][N-1];
}
不同回文子序列的个数
“不同”子序列,按字母划分子问题
class Solution {
const int CHAR_CNT = 4; // 只有abcd四种字母
const int MOD = 1e9 + 7;
public:
int countPalindromicSubsequences(string S) {
// 考虑某字母作最外层的区间,比如"bccb",b作最外层的区间S[firstIdx(b)..lastIdx(b)]。
// 这最外层可贡献回文b(不再考虑内层)、bb(firstIdx(b)!=lastIdx(b))。
// 当最外层为bb时,内层可为空集和非空子问题S[firstIdx(b)+1..lastIdx(b)-1]]。
// 只要最外层不同就一定是不同的回文串,不用管内层是什么,计数可累加。
const int N = S.size();
vector<set<int>> idxSets(CHAR_CNT); // char=>idxSet
for (int i = 0; i < N; i++) {
idxSets[S[i] - 'a'].insert(i);
}
vector<vector<int>> memo(N, vector<int>(N, -1)); // S[lo..hi] => count
return dfs(0, N - 1, idxSets, memo);
}
int dfs(int lo, int hi, vector<set<int>> &idxSets, vector<vector<int>> &memo) {
if (lo > hi) return 0;
if (memo[lo][hi] != -1) return memo[lo][hi];
long ans = 0;
for (int i = 0; i < CHAR_CNT; i++) {
auto itLo = idxSets[i].lower_bound(lo);
if (itLo == idxSets[i].end() || *itLo > hi) continue;
auto itHi = idxSets[i].upper_bound(hi);
itHi--; // *itLo<=hi,<=hi非空,可itHi--
ans += (*itLo == *itHi) ? 1 : 2; // 相等时贡献c,不等时贡献c和cc
ans += dfs(*itLo + 1, *itHi - 1, idxSets, memo); // 剥掉最外层、内层子问题
}
memo[lo][hi] = ans % MOD;
return memo[lo][hi];
}
};
扩展:不同子序列的个数,都是按字母作某种划分
通常设dp[i][j]为子问题s1[i..]、s2[j..]的解。 有时设dp[i][j]为子问题s1[..i)、s2[..j)的解。
最长公共子序列
- https://leetcode.com/problems/delete-operation-for-two-strings/
- https://leetcode.com/problems/longest-common-subsequence/
- https://leetcode.com/problems/shortest-common-supersequence/
int minDistance(string word1, string word2) {
// 设dp[i][j]表示word1[i..]和word2[j..]的最长公共子序列长
// word1[i]==word2[j] => dp[i][j] = 1 + dp[i+1][j+1],
// != => = max(dp[i][j+1], dp[i+1][j])
// 初始dp[n1][..]=dp[..][n2]=0,所求dp[0][0]
const int n1 = word1.size(), n2 = word2.size();
vector<vector<int>> dp(n1 + 1, vector<int>(n2 + 1, 0));
for (int i = n1 - 1; i >= 0; i--) {
for (int j = n2 - 1; j >= 0; j--) {
if (word1[i] == word2[j]) {
dp[i][j] = 1 + dp[i+1][j+1];
} else {
dp[i][j] = max(dp[i][j+1], dp[i+1][j]);
}
}
}
return n1 + n2 - 2 * dp[0][0]; // 需要删除的个数
}
最长公共子段
设dp[i][j]表示以A[i]开头的A[i..]、以B[j]开头的B[j..]的最长公共子段长。 若A[i]==B[j],dp[i][j]=1+dp[i+1][j+1];否则,dp[i][j]=0
最小编辑距离
int minDistance(string word1, string word2) {
// 设dp[i][j]表示s1[i..]和s2[j..]的最小编辑距离
// 若s1[i]==s2[j],dp[i][j]=dp[i+1][j+1];否则,
// dp[i][j] = 1 + min{ dp[i+1][j]/*删除*/, dp[i][j+1]/*添加*/,dp[i+1][j+1]/*替换*/ }
const int M = word1.size(), N = word2.size();
vector<vector<int>> dp(M + 1, vector<int>(N + 1, 0));
for (int i = M - 1; i >= 0; i--) dp[i][N] = M - i;
for (int j = N - 1; j >= 0; j--) dp[M][j] = N - j;
for (int i = M - 1; i >= 0; i--) {
for (int j = N - 1; j >= 0; j--) {
if (word1[i] == word2[j]) {
dp[i][j] = dp[i+1][j+1];
} else {
dp[i][j] = 1 + min({dp[i+1][j], dp[i][j+1], dp[i+1][j+1]});
}
}
}
return dp[0][0];
}
若设dp[i][j]表示串a[0..i)和串b[0..j)的最小编辑距离,其实和前面设法一样,一个设后半段、一个设前半段,写法稍改。
等于某串的子序列个数
int numDistinct(string s, string t) {
// 设dp[i][j]表示s[i..]子序列等于t[j..]的个数 (0<=i<=M, 0<=j<=N)
// 若s[i]==t[j],dp[i][j] = dp[i+1][j+1]/*使用s[i]*/ + dp[i+1][j]/*不用s[i]*/
// 若s[i]!=t[j],dp[i][j] = dp[i+1][j]
// 初值:dp[i][N]=1,dp[M][j<N]=0
// 省略i这一维,i仍从右向左遍历,要让dp[j+1]是旧状态,j从左向右遍历
const int M = s.size(), N = t.size();
vector<int> dp(N + 1, 0); // 对应i==M
dp[N] = 1;
for (int i = M; i >= 0; i--) {
for (int j = 0; j < N; j++) {
if (s[i] == t[j]) {
dp[j] += dp[j+1];
}
}
}
return dp[0];
}
最小ascii删除和
int minimumDeleteSum(string s1, string s2) {
// 设dp[i][j]表示s1[i..]和s2[j..]的最小ascii删除和。
// 若s1[i]==s2[j],dp[i][j] = dp[i+1][j+1]
// 若s1[i]!=s2[j],dp[i][j] = min(s1[i]+dp[i+1][j], s2[j]+dp[i][j+1])
// 初始dp[M][N]=0,所求dp[0][0]
const int M = s1.size(), N = s2.size();
vector<vector<int>> dp(M + 1, vector<int>(N + 1));
dp[M][N] = 0;
for (int i = M - 1; i >= 0; i--) {
dp[i][N] = s1[i] + dp[i+1][N];
}
for (int j = N - 1; j >= 0; j--) {
dp[M][j] = s2[j] + dp[M][j+1];
}
for (int i = M - 1; i >= 0; i--) {
for (int j = N - 1; j >= 0; j--) {
if (s1[i] == s2[j]) {
dp[i][j] = dp[i+1][j+1];
} else {
dp[i][j] = min(s1[i] + dp[i+1][j], s2[j] + dp[i][j+1]);
}
}
}
return dp[0][0];
}
含子序列的最小窗口
Given strings
SandT, find the minimum (contiguous) substringWofS, so thatTis a subsequence ofW.If there is no such window in
Sthat covers all characters inT, return the empty string"". If there are multiple such minimum-length windows, return the one with the left-most starting index.Example 1:
Input: S = "abcdebdde", T = "bde" Output: "bcde" Explanation: "bcde" is the answer because it occurs before "bdde" which has the same length. "deb" is not a smaller window because the elements of T in the window must occur in order.Note:
- All the strings in the input will only contain lowercase letters.
- The length of
Swill be in the range[1, 20000].- The length of
Twill be in the range[1, 100].
string minWindow(string S, string T) {
// 设dp[i][j]表示在S[0..i)中找T[0..j)子序列、找到的最右起始索引。
// 之所以取最右,联想滑动窗口法找最小窗口时lo++。
// 找到时S[dp[i][j]..i)是包含T[0..j)的最小窗口,找不到时dp[i][j]==-1。
// 若S[i-1]==T[j-1],需在S[0..i-1)中找T[0..j-1),dp[i][j]=dp[i-1][j-1]
// 若S[i-1]!=T[j-1],需在S[0..i-1)中找T[0..j),dp[i][j]=dp[i-1][j]
// 所以 dp[i][j] = (S[i-1]==T[j-1]) ? dp[i-1][j-1] : dp[i-1][j]
// 初始时dp[i][0]=i /*T为空串*/, dp[0][j>0]=-1。
const int M = S.size(), N = T.size();
vector<vector<int>> dp(M + 1, vector<int>(N + 1, -1));
for (int i = 0; i <= M; i++) dp[i][0] = i;
int minlen = INT_MAX, lo = -1;
for (int i = 1; i <= M; i++) {
for (int j = 1; j <= N; j++) {
dp[i][j] = (S[i-1] == T[j-1]) ? dp[i-1][j-1] : dp[i-1][j];
}
if (dp[i][N] != -1) { // [dp[i][N]..i)包含T
int len = i - dp[i][N];
if (len < minlen) { // 多个解取最左的,用<号
minlen = len;
lo = dp[i][N];
}
}
}
return (minlen == INT_MAX) ? "" : S.substr(lo, minlen);
}
字符串交错
串s1和串s2交错,能否形成s3。
bool isInterleave(string s1, string s2, string s3) {
// 设dp[i][j]表示s1[0..i)和s2[0..j)能否交错出s3[0..i+j)
// 1) 若s1[i-1]==s3[i+j-1],dp[i][j]=dp[i-1][j];
// 2) 若s2[j-1]==s3[i+j-1],dp[i][j]=dp[i][j-1];
// 两种情况有一种能交错就行,所以,
// dp[i][j] = (s1[i-1]==s3[i+j-1] && dp[i-1][j])
// || (s2[j-1]==s3[i+j-1] && dp[i][j-1])
// 初始dp[0][0]=true
const int M = s1.size(), N = s2.size();
if (s3.size() != M + N) return false;
vector<vector<bool>> dp(M + 1, vector<bool>(N + 1, false));
dp[0][0] = true;
for (int i = 0; i <= M; i++) {
for (int j = 0; j <= N; j++) {
if (i > 0) {
dp[i][j] = dp[i][j] || ((s1[i-1] == s3[i+j-1]) && dp[i-1][j]);
}
if (j > 0) {
dp[i][j] = dp[i][j] || ((s2[j-1] == s3[i+j-1]) && dp[i][j-1]);
}
}
}
return dp[M][N];
}
“不同”子序列,按照字母做某种划分
串的不同子序列数
int distinctSubseqII(string S) {
const int MOD = 1e9 + 7;
vector<int> endsWith(26, 0); // 扫描到S[i]时、以各字母结尾的子序列数
for (char c : S) {
// 原先子序列都扩展个c,有sum(endsWith[])个;还有1个新的字母c序列
// (不用担心旧的字母c序列,它被扩展成cc序列,和新的字母c序列不冲突)
endsWith[c - 'a'] = accumulate(begin(endsWith), end(endsWith), 1L) % MOD;
}
return accumulate(begin(endsWith), end(endsWith), 0L) % MOD;
}
循环字母串中,串的不同子串数
int findSubstringInWraproundString(string p) {
// 找以各个字母结尾的最长子串长
// 比如以d结尾的最长子串bcd长3,它就贡献了d、cd、bcd三个子串
vector<int> endsWith(26);
int len = 0;
for (int i = 0; i < p.size(); i++) {
if (i > 0 && (p[i] == p[i-1] + 1 || p[i] == p[i-1] - 25)) len++;
else len = 1;
int charIdx = p[i] - 'a';
endsWith[charIdx] = max(endsWith[charIdx], len);
}
return accumulate(begin(endsWith), end(endsWith), 0);
}
短串列表中,长串的子序列数
可一个个判断。也可将短串按待匹配字母分桶。
int numMatchingSubseq(string S, vector<string>& words) {
// 将word按待匹配char分桶,charToMatch=>{wordIdx,charIdxInWord}
vector<pair<int, int>> buckets[128];
for (int i = 0; i < words.size(); i++) {
buckets[words[i][0]].push_back({i, 0});
}
for (char c : S) { // 待匹配字母c
auto cBucket = buckets[c];
buckets[c].clear();
for (auto [wordIdx, charIdx] : cBucket) { // 桶中word都匹配掉c
charIdx++;
buckets[words[wordIdx][charIdx]].push_back({wordIdx, charIdx});
}
}
// 全单词匹配的最后分到'\0'桶里
return buckets[0].size();
}
是不是长串的子序列(长串不变,有很多短串要判断)
bool isSubsequence(string s, string t) {
// follow up,预处理长串t,记录各字母出现的位置列表
vector<vector<int>> pos(26);
for (int i = 0; i < t.size(); i++) {
pos[t[i] - 'a'].push_back(i);
}
int lastIdx = -1;
for (char c : s) { // 应在各字母的位置列表穿梭前进
auto &list = pos[c - 'a'];
auto it = upper_bound(list.begin(), list.end(), lastIdx);
if (it == list.end()) return false;
lastIdx = *it;
}
return true;
}
最长摆动子序列
int wiggleMaxLength(vector<int>& nums) {
// 贪心法,摆动发生时增长子序列
if (nums.empty()) return 0;
int ans = 1;
int prevDiff = 0;
for (int i = 1; i < nums.size(); i++) {
int diff = nums[i] - nums[i-1];
if ((diff > 0 && prevDiff <= 0) || (diff < 0 && prevDiff >= 0)) {
ans++;
prevDiff = diff;
}
}
return ans;
}
用动态规划麻烦些。O(n^2)的dp类似最长递增子序列。这里讨论O(n)的dp思路。
用up[i]表示子问题nums[0..i]、且最后上升的、最长摇摆子序列长;down[i]表示...且最后下降的...长。
int wiggleMaxLength(vector<int>& nums) {
if (nums.empty()) return 0;
const int N = nums.size();
vector<int> up(N, 0), down(N, 0);
up[0] = down[0] = 1;
for (int i = 1; i < N; i++) {
if (nums[i] > nums[i-1]) {
// 设down[i-1]对应子序列的最后数是last,last可以是nums[0..i-1]间的数
// 若last <= nums[i-1],nums[i] > nums[i-1] >= last,可扩展down[i-1]
// 若last > nums[i-1],可用nums[i-1]替代last,这样nums[i] > 新last,可扩展down[i-1]
// 所以与last大小无关,都可扩展down[i-1]
up[i] = down[i-1] + 1;
// 对down[i]无影响
down[i] = down[i-1];
} else if (nums[i] < nums[i-1]) {
// 同理
down[i] = up[i-1] + 1;
up[i] = up[i-1];
} else {
// 不能比i-1的情况做得更好
down[i] = down[i-1];
up[i] = up[i-1];
}
}
return max(up[N-1], down[N-1]);
}
空间优化:因为第i项只依赖于第i-1项,可将两数组优化成两变量up和down。
int wiggleMaxLength(vector<int>& nums) {
if (nums.empty()) return 0;
int up = 1, down = 1;
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i-1]) {
up = down + 1;
} else if (nums[i] < nums[i-1]) {
down = up + 1;
}
}
return max(up, down);
}
最长摆动子段
贪心法
int maxTurbulenceSize(vector<int>& A) {
if (A.empty()) return 0;
int len = 1, ans = 1;
int prevDiff = 0;
for (int i = 1; i < A.size(); i++) {
int diff = A[i] - A[i-1];
if ((diff > 0 && prevDiff <= 0) || (diff < 0 && prevDiff >= 0)) {
len++;
} else {
len = diff == 0 ? 1 : 2;
}
ans = max(ans, len);
prevDiff = diff;
}
return ans;
}
动态规划
int maxTurbulenceSize(vector<int>& A) {
if (A.empty()) return 0;
int up = 1, down = 1, ans = 1;
for (int i = 1; i < A.size(); i++) {
if (A[i] > A[i-1]) {
up = down + 1;
down = 1;
} else if (A[i] < A[i-1]) {
down = up + 1;
up = 1;
} else {
up = down = 1;
}
ans = max({ans, up, down});
}
return ans;
}
摆动排序见笔记
背包问题:f[v]记录背包容量剩余v时的最大价值。
提炼01背包和完全背包的子过程,用物品(代价c价值w)更新各容量的f[v]。
zeroOnePack(f, c, w):
for v in V..c // 逆序遍历各容量
f[v] = max( f[v], f[v-c]+w )
completePack(f, c, w):
for v in c..V // 顺序遍历各容量
f[v] = max( f[v], f[v-c]+w )
不管正序还是逆序,右边的f[v]肯定是f[i-1][v]。01背包和完全背包都在考虑放不放第i个物品,01背包的f[v-c]是旧状态f[i-1][v-c],逆序保证v维左边是旧状态。完全背包在考虑第i个物品时不管这个物品放没放入过,所以完全背包的f[v-c]是新状态f[i][v-c],正序保证v维左边是新状态。
初始值:初始的f[]就是在没有放入任何物品时背包的合法状态。如果要求“恰好“装满背包,则f[0]=0其他f[1..V]=-∞;如果没要求恰好装满,则f[0..V]=0。
多重背包:或者某物品多得可当作完全背包问题,或者可把原物品分堆成1,2,4...等重量翻倍的物品 和 剩余重量>0的一个物品,这样就变成多个01背包问题。
multiplePack(f, c, w, m):
if (cm >= V)
completePack(f, c, w)
return
k = 1
while (k < m) // <号保证剩余重量>0
zeroOnePack(f, kc, kw);
m -= k;
k *= 2;
zeroOnePack(f, mc, mw);
二维费用的背包问题:像背包空间v作费用那样,多加一维u作费用,就变成 f[v,u]。对于u同样地,01背包逆序循环,完全背包正序循环。比如限制物品总个数,相当于多了一维件数费用,每个物品的件数费用为1。
分组的背包问题:分为K组,每组最多选一件。设f[k, v]表示前k组物品、容量剩余v时的最大价值,第k组的策略是不选或组中选一件,f[k,v] = max{ f[k-1,v], f[k-1, v-ci] + wi | item i ∈ group k }。代码就是在zeroOnePack()最内层加一层循环在组内选择。
参考:背包问题九讲 https://github.com/tianyicui/pack/blob/master/V2.pdf
整数分割就是完全背包问题,要分割的整数就是要恰好装满的背包容量。
背包问题的目标函数也不一定要求max,可以求min、求和、求积。
整数分割成一些数的和,并使它们的积最大
int integerBreak(int n) {
// 从[1..n-1]中取数,完全背包问题
// n是背包容量,取的数i是物品体积,数i也是物品价值
vector<int> dp(n + 1);
dp[0] = 1;
for (int i = 1; i < n; i++) {
for (int v = i; v <= n; v++) { // 正序遍历各个容量
dp[v] = max(dp[v], i * dp[v - i]);
}
}
return dp[n];
}
整数分割成一些完全平方数的和,并使完全平方数个数最少
int numSquares(int n) {
// 设dp[i]表示和值i的最少完全平方数个数
// dp[i] = min( dp[i-j*j]+1 /*分离出数j*/),1<=j*j<=i
// 初始dp[0]=0
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j * j <= i; j++) {
dp[i] = min(dp[i], dp[i - j*j] + 1);
}
}
return dp[n];
}
整数有几种方式分割成连续数的和
只用一点数学
int consecutiveNumbersSum(int N) {
// 假设N可分成k个连续数之和:N=x+(x+1)+(x+2)+...+(x+k-1)=kx+k(k-1)/2,
// kx=N-k(k-1)/2,(N-k(k-1)/2)%k==0 (1)
// x是>=1的整数,N-k(k-1)/2>=k,k(k+1)<=2N (2)
int ans = 0;
for (int k = 1; k * (k + 1) <= 2 * N; k++) {
if ((N - k * (k - 1) / 2) % k == 0) ans++;
}
return ans;
}
整数分割为k个数,不考虑顺序,最大方案数
int divideNumber(int n, int k) {
// 设dp[i][j]表示最大方案数,0<=i<=n, 0<=j<=k
// 若j个数中至少有一个是1,方案数为 dp[i-1][j-1]
// 若j个数中没有一个数是1,则所有j个数都减去1,方案数相同,为 dp[i-j][j]
// 所以,dp[i][j] = dp[i-1][j-1] + dp[i-j][j]
// 初始 dp[0][0] = 1,空方案
// 若 i > j,dp[i][j] = 0
const int MOD = 1e9 + 7;
vector<vector<int>> dp(n + 1, vector<int>(k + 1, 0));
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) {
if (i >= j) {
dp[i][j] = (dp[i - 1][j - 1] + dp[i - j][j]) % MOD;
}
}
}
return dp[n][k];
}
不同找零方式数
设f(m,n)表示前m种硬币凑n分钱的方式数,
f(m, n) = f(m-1, n) /*不取第m种硬币*/ + f(m, n-v[m]) /*取第m种硬币*/,初始f(0,0)=1、其他为0(无效)
用完全背包的一维数组写法,对某种硬币面值,正序遍历各个“容量”(要凑的钱数),f(n) = f(n) + f(n-v[m])
为什么这题不能像求走台阶的方式数那样 f(n) = f(n-v1) + f(n-v2) + ... + f(n-vm) 呢?因为比如走4级台阶,先走1步再走3步,和先走3步再走1步是不同的。而找4分零钱,先找1分再找3分,和先找3分再找1分是相同的。比如要从{1, 3}两种面值硬币中找4分钱,用走台阶法 f(0)=1、f(1)=1、f(2)=f(1)=1、f(3)=f(2)+f(0)=2、f(4)=f(3)+f(1)=3就是错的,它重复计算了{3, 1}和{1, 3}两种情况。走台阶法对应着"排列",而背包问题对应着"组合"。
最少使用硬币数
设f(m,n)表示前m种硬币凑n分钱的最少使用硬币数,
f(m, n) = min{ f(m-1, n), f(m, n-v[m])+1 },初始f(0,0)=0、其他为∞(无效)
用完全背包问题的一维数组写法,对某种硬币面值,正序遍历各个“容量”(要凑的钱数),f(n) = min{ f(n), f(n-v[m])+1 }
int coinChange(vector<int>& coins, int amount) {
// 完全背包问题:选最少的硬币数使面值总和为amount
const int THE_MAX = amount + 1;
vector<int> dp(amount + 1, THE_MAX);
dp[0] = 0;
for (int c : coins) {
for (int v = c; v <= amount; v++) { // 正序遍历各容量
dp[v] = min(dp[v], dp[v-c] + 1);
}
}
return (dp[amount] != THE_MAX) ? dp[amount] : -1;
}
最多能选几个01串
int findMaxForm(vector<string>& strs, int m, int n) {
// 选子集,01背包问题,两维代价m和n
// 设dp[k][i][j]表示前k个串、剩余i个0和j个1时可得的最大个数
// dp[k][i][j] = max{ dp[k-1][i][j], 1 + dp[k-1][i-(0s)_k][j-(1s)_k] /*选不选第k个串*/ }
// 递推式在k这维上只依赖k-1项,可省掉k这维,k仍从左往右遍历
// dp[i][j] = max{ dp[i][j], 1 + dp[i-(0s)_k][j-(1s)_k] }
// dp[i-(0s)_k][j-(1s)_k]要表示旧状态,i、j从右往左遍历
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (const auto &str: strs) {
int zeros = 0, ones = 0;
for (char c : str) {
if (c == '0') zeros++;
else ones++;
}
// 01背包,逆序遍历容量
for (int i = m; i >= zeros; i--) {
for (int j = n; j >= ones; j--) {
dp[i][j] = max( dp[i][j], 1 + dp[i - zeros][j - ones] );
}
}
}
return dp[m][n];
}
有利可图的方案数
int profitableSchemes(int G, int P, vector<int>& group, vector<int>& profit) {
// 设dp[i][g][p]表示前i个案件、g人至少p利润时的方案数
// dp[i][g][p] = dp[i-1][g][p] + dp[i-1][g-group[i]][max(0, p-profit[i])] )
// 当p-profit[i]为负时要求“至少负利润”,这是都能达到的,等价于“至少0利润”,所以用max(0, p-profit[i])
// 初始dp[0][0][0] = 1
// 省掉i这维,i仍从左往右遍历。01背包问题,两维代价,逆序遍历
const int MOD = 1e9 + 7;
const int N = group.size();
vector<vector<int>> dp(G + 1, vector<int>(P + 1, 0));
dp[0][0] = 1;
for (int i = 0; i < N; i++) {
for (int g = G; g >= group[i]; g--) {
for (int p = P; p >= 0; p--) {
dp[g][p] = (dp[g][p] + dp[g - group[i]][max(0, p - profit[i])]) % MOD;
}
}
}
int ans = 0;
for (int g = 0; g <= G; g++) {
ans = (ans + dp[g][P]) % MOD;
}
return ans;
}
子集和就是01背包问题
正整数数组分成"等和"子集
选一些数,使它们和等于sum/2。01背包问题,和值相当于容量。
递推式先从二维数组写法入手,设dp[i][j]表示[0..i]的子集和等于j是否可行,dp[i][j] = dp[i-1][j]/*不取数i*/ || dp[i-1][j-nums[i]]/*取数i*/,只依赖dp[i-1][],可用一维数组写法。处理每个物品,逆序遍历各容量,d[j]=dp[j]||dp[j-num]
vector<bool> dp(sum + 1, false);
dp[0] = true;
for (int num : nums) {
for (int j = sum; j >= num; j--) {
dp[j] = dp[j] || dp[j - num];
}
}
return dp[sum];
数组分成“等和”的K个子集
分成k个子集,就只能回溯法。背包问题只能选一个子集,也就是分成两个子集。
正整数数组各项加上正负号,使它们和为S的方式数
问题即:分成两个子集,使它们差为S。设加正号的成子集和为A,加负号的成子集和为B,A-B=S, A+B=sum,所以A=(sum+S)/2,B=(sum-S)/2,要满足sum>=S且sum-S是偶数。
问题转变成:选一些数,使它们和等于(sum-S)/2(比(sum+S)/2遍历范围更小)。01背包问题,返回方式数。
递推式先从二维数组写法入手,设dp[i][j]表示[0..i]的子集和等于j的方式数,dp[i][j] = dp[i-1][j]/*不取数i*/ + dp[i-1][j-nums[i]]/*取数i*/。处理每个物品,逆序遍历各容量,d[j]+=dp[j-num]。
主要思想与第k步走+k或-k这题类似
分成"和最接近"的两个子集
int lastStoneWeightII(vector<int>& stones) {
// 分成和最接近的两个子集,求 和<=sum/2的最大子集,01背包
// 设dp[i][v]表示前i个数的子集和等于v是否可行。
// dp[i][v] = dp[i-1][v]/*不取nums[i]*/ || dp[i-1][v-num]/*取nums[i]*/。
// 递推式i这维依赖于i-1项,可省掉i这一维。dp[v] = dp[v] || dp[v-num]。v逆序遍历。
int sum = 0;
for (int w : stones)
sum += w;
vector<bool> dp(sum / 2, false);
dp[0] = true;
for (int w : stones) {
for (int v = sum / 2; v >= w; v--) {
dp[v] = dp[v] || dp[v-w];
}
}
for (int v = sum / 2; v >= 0; v--) {
if (dp[v]) return sum - v - v;
}
return -1; // invalid
}
分成“和最接近”的两个等长子集
从2n个数中选n个数,使它们的和最接近sum/2,不妨取<=sum/2的最大数。
设dp[i][k][v]表示从前i个数中取k个数、子集和等于v是否可行。考虑第i个数,dp[i][k][v]=dp[i-1][k][v]/*不取第i个数*/||dp[i-1][k-1][v-num]/*取第i个数*/。递推式i这维依赖于i-1项,可省掉i这一维。二维费用01背包问题,逆序遍历k和v:dp[k][v]=dp[k][v]||dp[k-1][v-num]
dp[0][0] = true;
for (int num : nums) {
// 01背包,逆序遍历k和v
for (int k = n; k >= 1; k--) {
for (int v = sum/2; v >= num; v--) {
dp[k][v] = dp[k][v] || dp[k-1][v-num];
}
}
}
最终取dp[n][sum/2..0]中第一个为true的。
类似ksum问题,二维费用背包问题
能被3整除的最大子集和
int maxSumDivThree(vector<int>& nums) {
// 设dp[i][j]表示nums[0..i]的被3除余j的最大子集和
// dp[i][j]=max(dp[i-1][j], dp[i-1][(j-nums[i])%3]+nums[i])
// 第i项只依赖于第i-1项,可省掉i这维,i仍从左往右遍历
// dp[j]=max(dp[j], dp[(j-num)%3]+num)=max(dp[j], dp[(j+2*num)%3]+num)
vector<int> dp({0, INT_MIN, INT_MIN});
for (int num : nums) {
vector<int> ndp(3);
for (int j = 0; j < 3; j++) {
ndp[j] = max(dp[j], dp[(j+2*num)%3] + num);
}
swap(dp, ndp);
}
return dp[0];
}
子集排列数
int combinationSum4(vector<int>& nums, int target) {
// 和为i的子集排列数 dp[i]=sum(dp[i-num])
// 子集排列数的写法刚好 将背包问题的外层物品循环改为内层
vector<int> dp(target + 1, 0);
dp[0] = 1;
for (int i = 0; i <= target; i++) {
for (int num : nums) {
if (i - num >= 0) dp[i] += dp[i-num];
}
}
return dp[target];
}
ksum,找k数之和等于t
是二维费用背包问题
int kSum(vector<int> &A, int K, int target) {
// 设dp[i][k][t]表示前i个数中选k个数,它们的和为t的方式数
// dp[i][k][t] = dp[i-1][k][t] /*不选num*/ + dp[i-1][k-1][t-num] /*选num*/
// 二维费用的01背包问题;可省掉i这维,i仍从左往右遍历,k和t逆序遍历
const int N = A.size();
vector<vector<int>> dp(K + 1, vector<int>(target + 1, 0));
dp[0][0] = 1;
for (int num : A) {
for (int k = K; k >= 1; k--) {
for (int t = target; t >= num; t--) {
dp[k][t] += dp[k-1][t-num];
}
}
}
return dp[K][target];
}
分成“均值相等”的两个子集
bool splitArraySameAverage(vector<int>& A) {
// 把A分成B和C,不妨设B是较小的那个(1<=k<=N/2)。B和C的均值相等,等于A的整体均值。
// avgB==avgA, sumB/k==sum/N, sumB==k*sum/N。因为sumB是整数,所以k需满足(k*sum)%N==0
// 转变成子集和问题:从A中找k个数,k满足(k*sum)%N==0,且k个数的子集和为k*sum/N
/*
// 子集和问题是01背包问题
// 设dp[i][k][v]表示能从前i个数中取k个数,使子集和等于v。
// 考虑num,dp[i][k][v] = dp[i-1][k][v] || dp[i-1][k-1][v-num]
// 递推式在i这维只依赖于i-1项,可省掉i这一维。
// 01背包问题,逆序遍历k和v:dp[k][v] = dp[k][v] || dp[k-1][v-num]
const int N = A.size();
int sum = 0;
for (int num : A) sum += num;
vector<vector<bool>> dp(N / 2 + 1, vector<bool>(sum + 1, false));
dp[0][0] = true;
for (int num : A) {
for (int k = N / 2; k >= 1; k--) { // 01背包,逆序遍历k和v
for (int v = sum; v >= num; v--) {
dp[k][v] = dp[k][v] || dp[k-1][v-num];
}
}
}
for (int k = 1; k <= N / 2; k++) {
if (k * sum % N == 0 && dp[k][k * sum / N]) return true;
}
return false;
*/
// 优化:这里num在范围[0,10000],和值v较大,用数组dp[i][k][v]浪费空间,运行超时。
// 改用sums[i][k]表示从前i个数中取k个数、能达到的和值集,初始sums[0][0] = {0}
// sums[i][k] = sums[i-1][k] /*不取A[i]*/ ∪ (sums[i-1][k-1] + A[i]) /*取A[i]*/
// 递推式在i这维只依赖i-1项,可省掉i这维。01背包问题,逆序遍历k
// sums[k] = sums[k] ∪ (sums[k-1] + A[i])
const int N = A.size();
int sum = 0;
for (int num : A) sum += num;
if (!canSplit(sum, N)) return false; // 提早返回
vector<unordered_set<int>> sums(N / 2 + 1);
sums[0] = {0};
for (int num : A) {
for (int k = N / 2; k >= 1; k--) { // 01背包,逆序遍历
for (int s : sums[k-1]) {
sums[k].insert(s + num);
}
}
}
for (int k = 1; k <= N / 2; k++) {
if (k * sum % N == 0 && sums[k].count(k * sum / N)) return true;
}
return false;
}
bool canSplit(int sum, int N) {
for (int k = 1; k <= N / 2; k++) {
if (k * sum % N == 0) return true;
}
return false;
}
初始空集
dp[i]表示子问题A[..i),则元素访问要用A[i-1],i>=1,初始空集在dp[0];
dp[i]表示子问题A[i..],则元素访问仍用A[i],初始空集在超出结尾的dp[N]。
遍历顺序:某一维上看递推式的依赖项来自哪个方向。比如dp[i][j] = dp[i+1][j-1],在i维上从右往左遍历、在j维上从左往右遍历。
- 若某些维度提供了全部初始值,这些维度上的遍历顺序可任意。比如
- student-attendance-record-ii提供了初始切面上a,l维的全部初始值,a,l维的遍历顺序可任意。
- scramble-string提供
n==1切面上i,j维的全部初始值,i,j维的遍历顺序可任意。 - remove-boxes提供了i=j切面上k∈[0,i]范围的全部初始值,k维在[0,i]内的遍历顺序可任意。
维度省略:递推式在某一维上只依赖前一项,可以省掉这一维。比如dp[i][j] = dp[i-1][j],在i维上只依赖dp[i-1][],可以省掉i这维,i这维的遍历顺序保持不变。
-
默认降维要使用临时变量ndp[]。这是为简化判断,等效于不降维。
ndp[]在循环内的初始化要符合递推式的初始化。- 三维降维,一定要用临时变量。这是为简化判断。
-
二维降维若不用临时变量,剩余那维的遍历顺序都是从新状态到旧状态,可记作"旧逆新正"。
- 赋值右边有旧状态(如
dp[i+1][]或dp[i-1][])。比如dp[i][j] = dp[i+1][j-1],省掉i这维,i保持从右往左遍历,j要从右往左遍历("旧逆")。
依赖旧状态,降维后剩余那维的遍历顺序从新状态到旧状态。 列举所有情况: * 若dp[i][j] = dp[i+1][j-1],省掉i这维,i保持从右往左遍历。dp[j] = dp[j-1],要让dp[j-1]是旧状态,j要从右往左遍历; * 若dp[i][j] = dp[i+1][j+1],省掉i这维,i保持从右往左遍历。dp[j] = dp[j+1],要让dp[j+1]是旧状态,j要从左往右遍历; * 若dp[i][j] = dp[i+1][j], 省掉i这维,i保持从右往左遍历。dp[j] = dp[j], 要让dp[j]是旧状态, j可任意顺序遍历; * 若dp[i][j] = dp[i-1][j-1],省掉i这维,i保持从左往右遍历。dp[j] = dp[j-1],要让dp[j-1]是旧状态,j要从右往左遍历; * 若dp[i][j] = dp[i-1][j+1],省掉i这维,i保持从左往右遍历。dp[j] = dp[j+1],要让dp[j+1]是旧状态,j要从左往右遍历; * 若dp[i][j] = dp[i-1][j], 省掉i这维,i保持从左往右遍历。dp[j] = dp[j], 要让dp[j]是旧状态, j可任意顺序遍历。- 赋值右边有新状态(如
dp[i][])。比如完全背包问题dp[i][j] = max( dp[i-1][j], dp[i][j-c] + w ),省掉i这维,dp[j-c]要表示新状态dp[i][j-c],j要从左往右遍历("新正")。 - 若赋值右边同时有新旧状态,而新旧状态对剩余那维要求的遍历顺序不同,只能用临时变量。
- 赋值右边有旧状态(如
自顶向下带memo的递归写法,都可以改成自底向上的dp写法
- 几个参数就是几维dp
- 递归终止条件就是dp的初始赋值
- 递归主体部分要放到循环中,几个参数就是几重循环
- 递归调用子问题就是对其他dp值的使用
K个鸡蛋N层楼、为确定安全楼层(最坏情况下)的最少步骤
设dp[k][i]表示k个鸡蛋i层楼(i>=1)、为确定安全楼层(最坏情况下)的最少步骤。当前鸡蛋在第i层扔,如果碎了,剩下k-1个鸡蛋在下面i-1层找,dp[k][i]=1+dp[k-1][i-1];如果没碎,剩下k个鸡蛋在上面N-i层找(等价于在仅有N-i层的楼中找),dp[k][i]=1+dp[k][N-i]。所以,dp[k][i] = min( dp[k][i], max(1 + dp[k-1][i-1], 1 + dp[k][N-i]) ),max表示取最坏情况,min表示最少步骤。
因为dp[k][i]依赖于dp[k][N-i],N-i可能>i也可能<i,i无论正序循环还是逆序循环都要依赖未知值,自底向上的dp无法计算。
只能带memo自顶向下递归计算。复杂度O(K*N^2)。运行超时。
优化递归里的O(i)循环,改成O(lgi)的二分搜索找i,复杂度O(K*N*lgN)。
上面方法还是太复杂,更好的方法是反过来想:
M个步骤K个鸡蛋、最多能确定多少层楼
int superEggDrop(int K, int N) {
// 设dp[m][k]表示m个步骤k个鸡蛋、最坏情况下、最多检查的楼层数。
// 假设在第i层扔鸡蛋(i>=1):
// 若鸡蛋碎,往下检查要覆盖下面的楼层,dp[m-1][k-1]>=i-1
// 若鸡蛋不碎,往上检查要覆盖上面的楼层,dp[m-1][k]>=N-i
// 两式相加,dp[m-1][k-1] + 1 + dp[m-1][k] >= N,覆盖了所有楼层
// 所以,让 dp[m][k] = dp[m-1][k-1] + 1 + dp[m-1][k] 就能保证找到安全楼层
// 初始dp[m][0]=0, dp[0][k]=0
// 递推式在m维只依赖m-1项,可省掉m这维,m仍从左往右遍历
// 要赋值右边的dp[k-1]表示旧状态,k从右往左遍历
// dp[k] += dp[k-1] + 1
vector<int> dp(K + 1, 0);
int m;
for (m = 0; dp[K] < N; m++) {
for (int k = K; k >= 1; k--) {
dp[k] += dp[k - 1] + 1;
}
}
return m;
}
M=O(lgN),所以复杂度O(K*lgN)。
戳破气球得分
一排气球上写有数字,戳破气球i得分 nums[left] * nums[i] * nums[right],left是左邻、right是右邻,戳破i后left和right相邻。最多能得多少分?
int maxCoins(vector<int>& nums) {
// 对区间nums[i..j]考虑其最后一戳k,在此之前一定戳完了区间nums[i..k-1]和nums[k+1..j],
// 设dp[i][j]表示nums[i..j]戳气球的最大得分,
// dp[i][j] = max{ nums[i-1]*nums[k]*nums[j+1](戳气球k得分)+ dp[i][k-1] + dp[k+1][j] },i<=k<=j
// i维上i依赖于k+1(>i),i逆序遍历;j维上j依赖于k-1(<j),j正序遍历
// 先把nums前后添加1扩展成a,设N是扩展后的长度,则1<=i<=j<=N-2,所求为dp[1][N-2]
const int N = (int)nums.size() + 2;
vector<int> a(N);
a[0] = a[N-1] = 1;
for (int i = 1; i <= N - 2; i++) {
a[i] = nums[i-1];
}
vector<vector<int>> dp(N, vector<int>(N, 0));
for (int i = N - 2; i >= 1; i--) {
for (int j = i; j <= N - 2; j++) {
for (int k = i; k <= j; k++) {
dp[i][j] = max(dp[i][j], a[i-1] * a[k] * a[j+1] + dp[i][k-1] + dp[k+1][j]);
}
}
}
return dp[1][N-2];
}
股票买卖问题
一般化,设dp[i][j][s]表示第i天、至多交易了j次、手上有s=0或1股股票时的最大利润。
-
第i天这一维当然要有,手上的股票数s这一维不能少,若没有交易数限制可省掉j这一维。
-
有交易数限制时,可以买时算新交易改变j、也可卖时算新交易改变j,不妨买时算新交易。
-
有交易费用时,可以买时算费用、也可卖时算费用,不妨买时算费用、跟买股票一起花钱。
-
有交易冷却天数限制时,没法像通常dp[i][][]=dp[i-1][][]...那样省掉i这一维
非dp解:只买卖一次,若在第i天卖,应在[0,i)天最便宜时买
非dp解:可以无限次购买,相隔两数有利可图就购买
dp解同ii题,买时加上费用
int maxProfit(vector<int>& prices, int fee) {
// 设dp[i][s]表示第i天、手上有s=0或1股股票时的最大利润
// dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i] /*卖股票*/)
// dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]-fee /*买股票*/)
// 初始dp[-1][0]=0,dp[-1][1]=INT_MIN
// dp[i][]只依赖dp[i-1][],去掉i这维
vector<int> dp = { 0, INT_MIN };
for (int price : prices) {
int dp0 = dp[0];
dp[0] = max(dp0, dp[1] + price);
dp[1] = max(dp[1], dp0 - price - fee);
}
return dp[0];
}
- https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/
- https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/
int maxProfit(int k, vector<int>& prices) {
const int N = prices.size();
if (k == 0 || N < 2) return 0;
// k足够大时,相当于对交易次数无限制
if (k >= N/2) return maxProfitInfK(prices);
// 设dp[i][j][s]表示第i天、至多交易了j次、手上有s=0或1股股票时的最大利润
// dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i] /*卖股票*/)
// dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i] /*买股票,新交易*/)
// 初始dp[-1][j][0]=0,dp[-1][j][1]=INT_MIN
// dp在i这维上只依赖于i-1项,去掉i这维
// 降维后,要让dp[j-1][]表示旧状态d,j要从右往左遍历
vector<vector<int>> dp(k + 1, vector<int>({ 0, INT_MIN }));
for (int price : prices) {
for (int j = k; j >= 1; j--) {
dp[j][0] = max(dp[j][0], dp[j][1] + price);
dp[j][1] = max(dp[j][1], dp[j-1][0] - price);
}
}
return dp[k][0];
}
int maxProfitInfK(vector<int>& prices) {
// 可以无限次购买,相隔两数有利可图就购买
const int N = prices.size();
int ans = 0;
for (auto i = 0; i < N - 1; i++) {
if (prices[i] < prices[i+1]) {
ans += prices[i+1] - prices[i];
}
}
return ans;
}
int maxProfit(vector<int>& prices) {
// 设dp[i][s]表示第i天、手上有s=0或1股股票时的最大利润
// dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i] /*卖股票*/)
// dp[i][1] = max(dp[i-1][1], dp[i-2][0]-prices[i] /*买股票, cooldown=1天*/)
// 初始dp[-1][0]=0,dp[-1][1]=INT_MIN;dp[-2][0]=0,dp[-2][1]=INT_MIN
// 递推式在i这维上只依赖i-1项、i-2项
vector<int> prev2 = { 0, INT_MIN };
vector<int> prev1 = { 0, INT_MIN };
for (int price : prices) {
vector<int> curr(2);
curr[0] = max(prev1[0], prev1[1] + price);
curr[1] = max(prev1[1], prev2[0] - price);
prev2 = prev1;
prev1 = curr;
}
return prev1[0];
}
多边形的三角化得分
int minScoreTriangulation(vector<int>& A) {
// 题目:分成N-2个三角形,每个得分为三顶点之积,最小化总得分
// 设dp[i][j]表示子多边形A[i..j](有条边连接i~j),
// i、j间有顶点k(i<k<j),将问题分成三部分:三角形ikj、子多边形A[i..k]、子多边形A[k..j]
// dp[i][j] = min( A[i]*A[k]*A[j] + dp[i][k] + dp[k][j] ),i<k<j
// i从右往左遍历,j从左往右遍历
const int N = A.size();
vector<vector<int>> dp(N, vector<int>(N, 0));
for (int i = N - 3; i >= 0; i--) {
for (int j = i + 2; j < N; j++) {
dp[i][j] = INT_MAX;
for (int k = i + 1; k < j; k++) {
dp[i][j] = min( dp[i][j], A[i]*A[k]*A[j] + dp[i][k] + dp[k][j] );
}
}
}
return dp[0][N-1];
}
拿连续同色盒子得分
int removeBoxes(vector<int>& boxes) {
// 题目:有一排着色的盒子,每次可以拿连续同色的k个盒子得k^2分,最多得多少分?
// 设dp[i][j][k]表示子问题boxes[i..j]、前面有连续k个未拿盒子和boxes[i]同色时的最大的分
// 0<=i<=j<N,0<=k<=i
// 分情况:现在拿boxes[i]、还是将来遇到同色时再考虑
// 现在拿,dp[i][j][k] = (k+1)^2 + dp[i+1][j][0];
// 将来遇到同色的boxes[m]时再考虑,dp[i][j][k] = max{ dp[i+1][m-1][0] + dp[m][j][k+1] },
// i<m<=j 且 boxes[m]与boxes[i]同色
// i从右往左遍历,j从左往右遍历
const int N = boxes.size();
vector<vector<vector<int>>> dp(N, vector<vector<int>>(N, vector<int>(N, 0)));
// i == j
for (int i = 0; i < N; i++) {
for (int k = 0; k <= i; k++) {
dp[i][i][k] = (k+1) * (k+1); // 现在拿boxes[i]
}
}
// i < j
for (int i = N - 2; i >= 0; i--) {
for (int j = i + 1; j < N; j++) {
for (int k = 0; k <= i; k++) { // 前面有连续k个未拿盒子与boxes[i]同色
dp[i][j][k] = (k+1) * (k+1) + dp[i+1][j][0]; // 现在拿boxes[i]
for (int m = i + 1; m <= j; m++) {
if (boxes[m] == boxes[i]) { // 将来遇到同色的boxes[m]时再考虑
dp[i][j][k] = max(dp[i][j][k], dp[i+1][m-1][0] + dp[m][j][k+1]);
}
}
}
}
}
return dp[0][N-1][0];
}
长n的所有有效出席记录个数
int checkRecord(int n) {
// 各个条件作dp的一个维度,设dp[i][a][l]表示s[0..i)、*最多*有a个A、末尾*最多*有l个连续L的有效记录数
// 原问题求dp[n][1][2],初始dp[0][][]=1(因为“最多”,所以空串都有效)
// dp[i][a][l] = dp[i-1][a-1][2] //..A,l重置为最大值2
// + dp[i-1][a][l-1] //..L
// + dp[i-1][a][2] //..P
// 递推式在i维上只依赖于i-1项,省掉i这维,i仍从左往右遍历
// 降维使用临时变量:ndp[a][l] = dp[a-1][2] + dp[a][l-1] + dp[a][2]
const int MOD = 1e9 + 7;
array<array<int, 3>, 2> dp = {{
{1,1,1}, {1,1,1},
}}; // i==0
for (int i = 1; i <= n; i++) {
array<array<int, 3>, 2> ndp;
for (int a = 0; a < 2; a++) {
for (int l = 0; l < 3; l++) {
long val = dp[a][2]; // ..P
if (a > 0) val += dp[a-1][2]; // ..A
if (l > 0) val += dp[a][l-1]; // ..L
ndp[a][l] = val % MOD;
}
}
swap(dp, ndp);
}
return dp[1][2];
}
马在K步后留在棋盘上的概率
double knightProbability(int N, int K, int r, int c) {
// 设prob[i][j][k]表示马在第i行j列、已跳k步的概率,
// prob[i][j][k] = 来自8个位置的概率prob[i-dr][j-dc][k-1]的和
// 递推式在k维只依赖k-1项,可省掉k维,k仍从左往右遍历
const vector<array<int, 2>> d = { {-2, 1}, {-1, 2}, {1, 2}, {2, 1},
{2, -1}, {1, -2}, {-1, -2}, {-2, -1} };
vector<vector<double>> prob(N, vector<double>(N, 0));
prob[r][c] = 1;
// 模拟走步更新概率分布
for (int k = 1; k <= K; k++) {
vector<vector<double>> nprob(N, vector<double>(N, 0));
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
for (int m = 0; m < d.size(); m++) {
int pi = i - d[m][0], pj = j - d[m][1];
if (0 <= pi && pi < N && 0 <= pj && pj < N) {
nprob[i][j] += prob[pi][pj] / 8;
}
}
}
}
swap(prob, nprob);
}
double ans = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
ans += prob[i][j];
}
}
return ans;
}
马走日拨号器,按N次有多少种不同号码
int knightDialer(int N) {
const int MOD = 1e9 + 7;
// 各数字可以跳到哪些数字
vector<vector<int>> jump = {
{4, 6}, {6, 8}, {7, 9}, {4, 8}, {0, 3, 9},
{}, {0, 1, 7}, {2, 6}, {1, 3}, {2, 4}
};
// 动态规划,设dp[i][d]表示i个数以数字d结尾的号码数,
// dp[i][dst] = sum(dp[i-1][src])
// i只依赖前一项,省掉i这维,ndp[dst] = sum(dp[src])
vector<long> dp(10, 1);
for (int i = 1; i < N; i++) { // 跳N-1次
vector<long> ndp(10, 0);
for (int d = 0; d < 10; d++) {
for (int nd : jump[d]) {
ndp[nd] += dp[d] % MOD;
}
}
swap(ndp, dp);
}
return accumulate(begin(dp), end(dp), 0L) % MOD;
}
供应A,B两种汤
double soupServings(int N) {
if (N >= 5000) return 1; // tricky:单调递增,N太大时直接返回1
map<array<int, 2>, double> memo; // A,B剩余 => 概率
return prob(N, N, memo);
}
double prob(int a, int b, map<array<int, 2>, double> &memo) {
// 基本的终止情况
if (a <= 0 && b <= 0) return 0.5;
if (a <= 0) return 1;
if (b <= 0) return 0;
if (memo.count({a, b})) return memo[{a, b}];
vector<vector<int>> ops = { {100, 0}, {75, 25}, {50, 50}, {25, 75} };
double ans = 0;
for (auto &op : ops) {
ans += 0.25 * prob(a - op[0], b - op[1], memo);
}
memo[{a, b}] = ans;
return ans;
}
得分在[K,N]间的概率
double new21Game(int N, int K, int W) {
// 题目:<K时不断得[1..W]分,求最后得分在[K..N]间的概率
if (K == 0 || N - K + 1 >= W) return 1;
// 设dp[i]表示得分为i的概率,则dp[i]=sum{dp[i-W..i-1]}/W
// 由于得分>=K就不再得分,dp[K..i-1]=0,dp[i]=sum{dp[i-W..min(K-1,i-1)]}/W
// 维护一个长<=W的窗口,设Wsum=sum{dp[i-W..min(K-1,i-1)]}
vector<double> dp(N + 1, 0);
dp[0] = 1;
double Wsum = dp[0], ans = 0;
for (int i = 1; i <= N; i++) {
dp[i] = Wsum / W;
// 更新Wsum
if (i < K) Wsum += dp[i]; // min(K-1,i-1)=i-1,扩展窗口右边界
else ans += dp[i]; // min(K-1,i-1)=K-1,不用扩展窗口右边界;i>=K,正好可以统计[K..N]间的概率
if (i-W >= 0) Wsum -= dp[i-W]; // 收缩窗口左边界
}
return ans;
}
字符串可二分并交换左右
字符串可不断二分,并随意交换左右子树,构成扰动。判断一个串是否是另一个串的扰动。
bool isScramble(string s1, string s2) {
// 设dp[n][i][j]表示两个等长串s1[i..i+n)和s2[j..j+n)是否scramble
// 每个串可由二叉树分成两部分,遍历左串长1<=k<n的划分
// | k | n-k |、| k | n-k | 或 | k | n-k |、| n-k | k |
// dp[n][i][j] = ( dp[k][i][j] && dp[n-k][i+k][j+k] ) || ( dp[k][i][j+n-k] && dp[n-k][i+k][j] )
// 初始n==1时,dp[1][i][j] = s1[i]==s2[j]
const int N = s1.size();
if (N != s2.size()) return false;
vector<vector<vector<bool>>> dp(N + 1, vector<vector<bool>>(N, vector<bool>(N, false)));
// n==1
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
dp[1][i][j] = s1[i] == s2[j];
}
}
for (int n = 2; n <= N; n++) {
for (int i = 0; i + n <= N; i++) {
for (int j = 0; j + n <= N; j++) {
for (int k = 1; k < n && !dp[n][i][j]; k++) {
dp[n][i][j] = ( dp[k][i][j] && dp[n-k][i+k][j+k] )
|| ( dp[k][i][j+n-k] && dp[n-k][i+k][j] );
}
}
}
}
return dp[N][0][0];
}
刚好k个逆序对的1~n排列个数
int kInversePairs(int n, int k) {
if (k > n * (n - 1) / 2) return 0; // 先确保k<=n*(n-1)/2
const int MOD = 1e9+7;
// 设dp[i][j]表示[1..i]的排列中刚好j个逆序对的排列个数。
// 假设已解决[1..i-1]子问题,考虑新增数i。
// 把i放在最后,不增加逆序对;把i往前移x位,就增加x个逆序对。
// dp[i][j] = sum{ dp[i-1][j-x] },0<=x<=i-1
//
// 初始 dp[0..n][0]=1,dp[0][1..k]=0,特别注意dp[0][0]=1(空集也算一个排列)
// 复杂度O(n^2*k),超时,要优化。
//
// dp[i][j] = dp[i-1][j] + dp[i-1][j-1] + ... + dp[i-1][j-i+1]
// dp[i][j-1] = dp[i-1][j-1] + ... + dp[i-1][j-i+1] + dp[i-1][j-i]
// dp[i][j] - dp[i][j-1] = dp[i-1][j] - dp[i-1][j-i]
// => dp[i][j] = dp[i][j-1] + dp[i-1][j] - dp[i-1][j-i]
vector<vector<int>> dp(n + 1, vector<int>(k + 1, 0));
for (int i = 0; i <= n; i++) {
dp[i][0] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) {
long val = dp[i][j-1] + dp[i-1][j];
if (j - i >= 0) val -= dp[i-1][j-i];
dp[i][j] = (val + MOD) % MOD;
}
}
return dp[n][k];
}
子段覆盖打印机
int strangePrinter(string s) {
// 题目:一次可用一个字符覆盖一段范围,某段文本最少需要几次打印。
// 设dp[i][j]表示s[i..j]的最少打印数,
// 尝试将[i..j]分成两段[i..k]、[k+1..j],i<=k<j,比较两段末尾
// 若s[k]==s[j],j可以在打印k时一起打印,
// dp[i][j] = min{ dp[i][k]+dp[k+1][j-1] }(这里要k+1<=j-1)
// 若所有s[k]和s[j]都不相等,dp[i][j] = dp[i][j-1]+1
// 初始dp[i][i]=1。i从右往左遍历,j从左往右遍历。
if (s.empty()) return 0;
const int N = s.size();
vector<vector<int>> dp(N, vector<int>(N, INT_MAX));
for (int i = 0; i < N; i++) {
dp[i][i] = 1;
}
for (int i = N - 1; i >= 0; i--) {
for (int j = i + 1; j < N; j++) {
dp[i][j] = dp[i][j-1] + 1; // 所有s[k]和s[j]都不相等
for (int k = i; k < j; k++) {
if (s[k] == s[j]) {
int part = k+1 <= j-1 ? dp[k+1][j-1] : 0;
dp[i][j] = min(dp[i][j], dp[i][k] + part);
}
}
}
}
return dp[0][N-1];
}
最短长度编码字符串
Given a **non-empty **string, encode the string such that its encoded length is the shortest.
The encoding rule is:
k[encoded_string], where the encoded_string inside the square brackets is being repeated exactly k times.Note:
- k will be a positive integer and encoded string will not be empty or have extra space.
- You may assume that the input string contains only lowercase English letters. The string's length is at most 160.
- If an encoding process does not make the string shorter, then do not encode it. If there are several solutions, return any of them is fine.
Example 1:
Input: "aaa" Output: "aaa" Explanation: There is no way to encode it such that it is shorter than the input string, so we do not encode it.Example 2:
Input: "aaaaa" Output: "5[a]" Explanation: "5[a]" is shorter than "aaaaa" by 1 character.Example 3:
Input: "aaaaaaaaaa" Output: "10[a]" Explanation: "a9[a]" or "9[a]a" are also valid solutions, both of them have the same length = 5, which is the same as "10[a]".Example 4:
Input: "aabcaabcd" Output: "2[aabc]d" Explanation: "aabc" occurs twice, so one answer can be "2[aabc]d".Example 5:
Input: "abbbabbbcabbbabbbc" Output: "2[2[abbb]c]" Explanation: "abbbabbbc" occurs twice, but "abbbabbbc" can also be encoded to "2[abbb]c", so one answer can be "2[2[abbb]c]".
s=pattern*k <=等价于=> (s+s).find(s,1) < s.size()
设d=(s+s).find(s,1),则pattern=s.substr(0,d),k=s.size()/d。想象将s串平移一小段后仍与原串重叠,平移距离就是pattern长d。
string encode(string s) {
// 用dp[i][L]记录s[i,i+L)的最短编码串
const int N = s.size();
vector<vector<string>> dp(N, vector<string>(N + 1)); // dp[i][0]=""
for (int L = 1; L <= N; L++) {
for (int i = 0; i + L <= N; i++) {
dp[i][L] = collapse(s.substr(i, L), dp[i]); // 尝试pattern*k模式
for (int k = 1; k < L; k++) { // 尝试分两段,第一段长k
if (dp[i][k].size() + dp[i+k][L-k].size() < dp[i][L].size()) {
dp[i][L] = dp[i][k] + dp[i+k][L-k];
}
}
}
}
return dp[0][N];
}
// si是s[i,i+L)子串,要看si有没有更短编码串,dpi是dp[i][<L]
string collapse(const string &si, vector<string> &dpi) {
int d = (si + si).find(si, 1), L = si.size();
if (d < L) { // si=pattern*k
auto encoded = to_string(L / d) + "[" + dpi[d] + "]"; // dp[i][d]是子问题最优解
if (encoded.size() < L) return encoded;
}
return si;
}
数字串,解码成字母串
int numDecodings(string s) {
// 设dp[i]表示s[i..]的解码数,0<=i<=N
// 若isValid(s[i]), dp[i] += dp[i+1];若isValid(s[i..i+1]),dp[i] += dp[i+2]
// 初始dp[N] = 1
const int N = s.size();
vector<int> dp(N + 1, 0);
dp[N] = 1;
for (int i = N - 1; i >= 0; i--) {
if (s[i] != '0')
dp[i] += dp[i+1];
if (i + 1 < N && (s[i] == '1' || s[i] == '2' && s[i+1] <= '6'))
dp[i] += dp[i+2];
}
return dp[0];
}
数字串与表示[1-9]的*,解码成字母串
int numDecodings(string s) {
// 设dp[i]表示s[i..]的解码数
// 若isValid(s[i]), dp[i] += ??dp[i+1];若isValid(s[i..i+1]),dp[i] += ??dp[i+2]
// 若s[i]单独解码:
// s[i]
// * 9*dp[i+1]
// 0 xxx
// [1-9] dp[i+1]
// 若s[i]和s[i+1]一起解码:
// s[i+1] * [0-6] [7-9]
// s[i]
// * 15*dp[i+2] 2*dp[i+2] dp[i+2]
// 1 9*dp[i+2] dp[i+2] dp[i+2]
// 2 6*dp[i+2] dp[i+2] xxx
// 0, 3-9 xxxxxxxxxxxxxxxxxx
// 因为dp[i]只依赖i+1, i+2项,可省掉i这维
// 用curr,next1,next2代表dp[i],dp[i+1],dp[i+2]
const int Mod = 1e9 + 7;
const int N = s.size();
// 用long防止计算过程中溢出!
// 若用int且右端%M,中间变量也可能溢出导致错误。
long next1 = 1, next2 = 1;
for (int i = N; i >= 0; i--) {
long curr = 0;
if (s[i] == '*') {
curr += 9 * next1;
if (i + 1 < N) {
if (s[i+1] == '*') curr += 15 * next2;
else if (s[i+1] <= '6') curr += 2 * next2;
else curr += next2;
}
} else {
if (s[i] != '0') curr += next1;
if (i + 1 < N) {
if (s[i] == '1') {
if (s[i+1] == '*') curr += 9 * next2;
else curr += next2;
} else if (s[i] == '2') {
if (s[i+1] == '*') curr += 6 * next2;
else if (s[i+1] <= '6') curr += next2;
}
}
}
next2 = next1;
next1 = curr % Mod;
}
return (int)next1;
}
字母转盘
int findRotateSteps(string ring, string key) {
// 设dp[r][k]表示从ring[r]对齐12点开始,输入所有key[k..]的字符,最少需要旋转多少次。
// 考虑输入key[k]最少需要旋转多少次。遍历ring中字符,当ring[i]==key[k]时,把ring[i]转到对齐12点,
// 需要旋转 diff = abs(i-r), step = min(diff, R-diff) 次
// dp[r][k] = min{ step + dp[i][k+1] | for all ring[i]==key[k] }
// 初始dp[][K]=0,表示key输入完成的情况
const int R = ring.size(), K = key.size();
vector<vector<int>> dp(R, vector<int>(K + 1, INT_MAX));
for (int r = 0; r < R; r++) dp[r][K] = 0;
// 递推式在k这维只依赖于k+1项,要从右往左遍历k
// 由已知dp[][K]递推,要先算k这维
for (int k = K - 1; k >= 0; k--) {
for (int r = 0; r < R; r++) {
for (int i = 0; i < R; i++) {
if (ring[i] == key[k]) {
int diff = abs(i - r);
int step = min(diff, R - diff);
dp[r][k] = min(dp[r][k], step + dp[i][k+1]);
}
}
}
}
return dp[0][0] + K;
}
分割就是从一端尝试切下一段
将字符串分割成单词表中的单词
bool wordBreak(string s, vector<string>& wordDict) {
// 设dp[i]表示s[i..]可分割成一个或多个单词,
// dp[i] = any{ dp[j] && isWord(s[i..j-1]) },0<=i<j<=N
// 初始dp[N]=true
unordered_set<string> dict;
for (auto &s : wordDict)
dict.insert(s);
const int N = s.size();
vector<bool> dp(N + 1, false);
dp[N] = true;
for (int i = N - 1; i >= 0; i--) {
for (int j = i + 1; j <= N && !dp[i]; j++) {
dp[i] = dp[j] && dict.count(s.substr(i, j - i));
}
}
return dp[0];
}
类似:单词列表中有哪些单词能由多个短单词拼成
变成找哪些长单词能分割成多个短单词。
抢劫房子
设dp[i]表示抢了nums[0..i]后的最大值,根据抢没抢nums[i],dp[i] = max( dp[i-1], nums[i]+dp[i-2] ),初始dp[-2]=dp[-1]=0
int rob(vector<int>& nums) {
if (nums.empty()) return 0;
const int N = nums.size();
if (N == 1) return nums[0];
// 相邻房子不能同时抢,从某对相邻房子中间切开,就能得两个无循环的子问题。
// 比如从首尾这对相邻房子处切开:rob(nums,1,n-1)和rob(nums,0,n-2)
return max(robSub(nums, 1, N - 1), robSub(nums, 0, N - 2));
}
// rob nums[from,to]
int robSub(const vector<int> &nums, int from, int to) {
// 设dp[i]表示抢了nums[from..i]后的最大值,from<=i<=to
// dp[i] = max( dp[i-1], nums[i]+dp[i-2] ),初始dp[-2]=dp[-1]=0
// 递推式只依赖于前两项,记前两项为prev2和prev1
int prev2 = 0, prev1 = 0;
for (int i = from; i <= to; i++) {
int curr = max(prev1, nums[i] + prev2);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
int rob(TreeNode* root) {
// 问题只有两种状态:抢没抢root,子问题也需返回这两种状态
auto ans = robSub(root);
return max(ans[0], ans[1]);
}
// 设子问题返回没抢ans[0]和抢ans[1]两种情况下各自的最大值
vector<int> robSub(TreeNode *root) {
if (!root) return { 0, 0 };
auto left = robSub(root->left);
auto right = robSub(root->right);
int noRobRoot = max(left[0], left[1]) + max(right[0], right[1]);
int robRoot = root->val + left[0] + right[0];
return { noRobRoot, robRoot };
}
删除相邻数字的最大得分,相邻数字类似于相邻房子,变成房子抢劫问题
int deleteAndEarn(vector<int>& nums) {
// 取数num后删除num-1和num+1,
// 类似房子抢劫问题,连续值类似于相邻房子
unordered_map<int, int> count;
for (int num : nums) {
count[num]++;
}
const int N = 1e5; // nums[i]在范围[1..1e5]
// 设dp[i]表示值范围[1..i]的最大得分,1<=i<=N
// dp[i] = max(dp[i-2] + i*count[i] /*取i*/, dp[i-1] /*不取i*/)
// dp[i]只依赖前两项,记前两项为prev2、prev1
int prev2 = 0, prev1 = 0;
for (int i = 1; i <= N; i++) {
int curr = max(prev2 + i * count[i], prev1);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
相邻房子不能同色,共有3种颜色
There are a row of n houses, each house can be painted with one of the three colors: red, blue or green. The cost of painting each house with a certain color is different. You have to paint all the houses such that no two adjacent houses have the same color.
The cost of painting each house with a certain color is represented by a
nx3cost matrix. For example,costs[0][0]is the cost of painting house 0 with color red;costs[1][2]is the cost of painting house 1 with color green, and so on... Find the minimum cost to paint all houses.Note:
All costs are positive integers.
int minCost(vector<vector<int>>& costs) {
// 设dp[i][c]表示前[0..i]房子、第i房颜色为c时的最小代价
// 有dp[i][c]=min(dp[i-1][not_c])+costs[i][c]
// 初始dp[0][c]=costs[0][c]
// 递推式在i维上只依赖i-1项,省掉i这维,i仍从左往右遍历
// c这维的依赖方向不确定,要用临时变量ndp[]
// ndp[c]=min(dp[not_c])+costs[i][c]
if (costs.empty()) return 0;
const int N = costs.size(), C = costs[0].size();
vector<int> dp = costs[0];
for (int i = 1; i < N; i++) {
vector<int> ndp(C);
for (int c = 0; c < C; c++) {
ndp[c] = min(dp[(c+1) % C], dp[(c+2) % C]) + costs[i][c];
}
swap(dp, ndp);
}
return min({dp[0], dp[1],dp[2]});
}
相邻房子不能同色,共有k种颜色
- https://leetcode.com/problems/paint-house-ii/
- https://leetcode.com/problems/minimum-falling-path-sum-ii/
There are a row of n houses, each house can be painted with one of the k colors. The cost of painting each house with a certain color is different. You have to paint all the houses such that no two adjacent houses have the same color.
The cost of painting each house with a certain color is represented by a
nxkcost matrix. For example,costs[0][0]is the cost of painting house 0 with color 0;costs[1][2]is the cost of painting house 1 with color 2, and so on... Find the minimum cost to paint all houses.Note:
All costs are positive integers.Follow up:
Could you solve it in O(nk) runtime?
同上题。可优化成O(nk)解法。
int minCostII(vector<vector<int>>& costs) {
// 设dp[i][c]表示前[0..i]房子、第i房颜色为c时的minCost
// dp[i][c] = min{ dp[i-1][not_c] } + costs[i][c],初始dp[0][c] = costs[0][c]
// 而求min{ dp[i-1][not_c] },只要维护dp[i-1][]的最小min1、取min1时的颜色min1c、以及第二小min2
// 这样 min{ dp[i-1][not_c] } = (c != min1c ? min1 : min2)
// dp[i][c] = (c != min1c ? min1 : min2) + costs[i][c]
// 所求为dp[N-1][]的最小min1
if (costs.empty()) return 0;
const int N = costs.size(), K = costs[0].size();
int min1 = 0, min1c = -1, min2 = 0;
for (int i = 0; i < N; i++) {
int tmp_min1 = INT_MAX, tmp_min1c = -1, tmp_min2 = INT_MAX; // dp[i][]对应的一组值
for (int c = 0; c < K; c++) {
int cost = (c != min1c ? min1 : min2) + costs[i][c];
if (cost < tmp_min1) {
tmp_min2 = tmp_min1;
tmp_min1 = cost;
tmp_min1c = c;
} else if (cost < tmp_min2) {
tmp_min2 = cost;
}
}
min1 = tmp_min1, min1c = tmp_min1c, min2 = tmp_min2;
}
return min1;
}
相邻3根栏杆不能同色
There is a fence with n posts, each post can be painted with one of the k colors.
You have to paint all the posts such that no 3 adjacent fence posts have the same color.
Return the total number of ways you can paint the fence.
Note:
n and k are non-negative integers.
int numWays(int n, int k) {
// 题目:不能有连续三根杆同色
// 漆第i新杆时,根据前两根杆同色或不同色分情况讨论。
// 设前两根同色的子问题的上色方式数为dp[i][0]
// 不同色 dp[i][1]
// 若新杆与前一根同色,则前两根不能同色,dp[i][0] = dp[i-1][1]
// 新杆与前一根不同色,则前两根可同色可不同色,新杆有k-1种选择
// dp[i][1] = (dp[i-1][0] + dp[i-1][1]) * (k-1)
// 递推式在i维上只依赖i-1项,省掉i这维
// ndp[0] = dp[1]
// ndp[1] = (dp[0] + dp[1]) * (k-1)
if (n == 0) return 0;
if (n == 1) return k;
vector<int> dp = { k, k * (k - 1) }; // n==2的情况
for (int i = 3; i <= n; i++) {
dp = { dp[1], (dp[0] + dp[1]) * (k - 1)};
}
return dp[0] + dp[1];
}
两等长序列的对应数可交换,使两序列都严格递增的最小交换数
int minSwap(vector<int>& A, vector<int>& B) {
// 设dp[i][0]表示A[..i]、B[..i]不交换A[i]、B[i]时的minSwap,
// dp[i][1] 交换
// 若A[i-1]<A[i] && B[i-1]<B[i],
// 交换第i-1项、也交换第i项:dp[i][1] = dp[i-1][1]+1
// 不交换第i-1项、也不交换第i项:dp[i][0] = dp[i-1][0]
// 若A[i-1]<B[i] && B[i-1]<A[i],
// 交换第i-i项、不交换第i项:dp[i][0] = dp[i-1][1]
// 不交换第i-1项、交换第i项:dp[i][1] = dp[i-1][0]+1
// 上述两种情况可同时存在。
// 初始dp[0][0]=0, dp[0][1]=1
// 递推式i这维只依赖i-1项,省掉i这维,i仍从左往右遍历
const int N = A.size();
vector<int> dp = {0, 1};
for (int i = 1; i < N; i++) {
vector<int> ndp = {INT_MAX, INT_MAX};
if (A[i-1] < A[i] && B[i-1] < B[i]) {
ndp[1] = min(ndp[1], dp[1] + 1);
ndp[0] = min(ndp[0], dp[0]);
}
if (A[i-1] < B[i] && B[i-1] < A[i]) {
ndp[0] = min(ndp[0], dp[1]);
ndp[1] = min(ndp[1], dp[0] + 1);
}
swap(ndp, dp);
}
return min(dp[0], dp[1]);
}
符合DI序列的排列数
int numPermsDISequence(string S) {
// 设dp[i][j]表示[0..i]的排列、结尾是j时的DI排列数
// dp[i][j]与子问题dp[i-1][]有什么关系?
// 考虑某排列(如201),它跟比它长1的、分别以0,1,2,3结尾的排列(3120,3021,3012,2013)可以建立一一映射,比如:
// 1) 在201后添上>结尾1的数,比如添上2(=>2012),然后把原排列中>=新添数的都加上1(=>3012),
// 这就构成结尾的*I排列*;过程反过来,3012去掉结尾2(=>301),然后把剩下排列中>=去掉数的都减去1(=>201)。
// 若dp[i][j]的结尾是I排列(如3012=>201),dp[i][j]=sum(dp[i-1][<j])
// 2) 在201后添上<=结尾1的数,比如添上1(=>2011),然后把原排列中>=新添数的都加上1(=>3021),
// 这就构成结尾的*D排列*;过程反过来,3021去掉结尾1(=>302),然后把剩下排列中>=去掉数的都减去1(=>201)。
// 若dp[i][j]的结尾是D排列(如3021=>201),dp[i][j]=sum(dp[i-1][>=j])
// 初始dp[0][0]=1。在i维上只依赖i-1项,可省掉i这维,i仍从左往右遍历。
// 第j维由dp[i-1][<j]、dp[i-1][>=j]决定的遍历顺序冲突,要用临时变量ndp[j]。
const int MOD = 1e9 + 7;
const int N = S.size(); // [0..N]的排列
vector<int> dp(N + 1, 0);
dp[0] = 1; // i==0
for (int i = 1; i <= N; i++) {
vector<int> ndp(N + 1, 0);
for (int j = 0; j <= i; j++) {
if (S[i-1] == 'I') {
for (int k = 0; k < j; k++) {
ndp[j] = (ndp[j] + dp[k]) % MOD;
}
} else {
for (int k = j; k < i; k++) {
ndp[j] = (ndp[j] + dp[k]) % MOD;
}
}
}
swap(dp, ndp);
}
int ans = 0;
for (int j = 0; j <= N; j++) {
ans = (ans + dp[j]) % MOD;
}
return ans;
}
联系:符合DI序列的排列 - 贪心法
瓷砖铺满2xN格子的方式数
用一字型两格或L字型三格的瓷砖铺满2xN格子的方式数。
int numTilings(int N) {
// 两个相互依赖的递推式:设d[i]表示铺满2*i格子的方式数,t[i]表示缺右上角或右下角铺满2*i-1格子的方式数
// d[i] = d[i-1] /* 一竖 */ + d[i-2] /* 两横 */ + 2 * t[i-1] /*两种不同朝向的L型*/
// t[i] = t[i-1] /* 一横 */ + d[i-2] /* 一个L型 */
// 初始 d[1]=1, d[2]=2, t[1]=0, t[2]=1(虽然有缺右上角或右下角两种情况,但对确定的缺角情况只有一种L型)
const int MOD = 1e9 + 7;
vector<int> d(N + 1, 0), t(N + 1, 0);
d[1] = 1, d[2] = 2, t[1] = 0, t[2] = 1;
for (int i = 3; i <= N; i++) {
d[i] = (d[i-1] + d[i-2] + 2l * t[i-1]) % MOD;
t[i] = (t[i-1] + d[i-2]) % MOD;
}
return d[N];
}
摘草莓
不可以分两遍走,各自贪心找摘草莓的最大值。因为第一遍摘掉草莓改变了网格状态,分两遍走没法得到全局最优。比如下面,分两遍贪心会剩下一个未摘。
11100
00101
10100
00100
00111
若不改变网格状态,来回两趟又会重复统计某一格。为避免重复统计,要识别出两条路径经过同一格的情况,技巧是让两条路径同时走。
int cherryPickup(vector<vector<int>>& grid) {
// (0,0)=>(N-1,N-1)的两条路径同时走k步,满足条件:k==r1+c1==r2+c2
// 设dp[k][r1][r2]表示从(0,0)走到(r1,c1)、(r2,c2)可摘草莓的最大数。
// 两条路径同时走一步有四种情况:c1、c2走一步,c1、r2走一步、r1、c2走一步、r1、r2走一步
// dp[k][r1][r2] = grid[r1][c1] + grid[r2][c2] // r1==r2时不重复统计这项
// + max{ dp[k-1][r1][r2], dp[k-1][r1][r2-1], dp[k-1][r1-1][r2], dp[k-1][r1-1][r2-1] }
// 0<=k<=2*(N-1);0<=r1<=N-1、0<=c1<=N-1;r2取值范围类似r1
// 递推式在k这维只依赖k-1项,省掉k这维,k仍从左往右遍历
const int N = grid.size();
vector<vector<int>> dp(N, vector<int>(N, -1));
dp[0][0] = grid[0][0]; // 已知grid[0][0]!=-1
for (int k = 1; k <= 2*(N-1); k++) {
vector<vector<int>> ndp(N, vector<int>(N, -1)); // 用-1表示路不通
for (int r1 = 0; r1 < N; r1++) {
for (int r2 = 0; r2 < N; r2++) {
int c1 = k - r1, c2 = k - r2;
if (c1 < 0 || c1 >= N || c2 < 0 || c2 >= N
|| grid[r1][c1] < 0 || grid[r2][c2] < 0) continue;
int theMax = dp[r1][r2];
if (r2 > 0) theMax = max(theMax, dp[r1][r2-1]);
if (r1 > 0) theMax = max(theMax, dp[r1-1][r2]);
if (r1 > 0 && r2 > 0) theMax = max(theMax, dp[r1-1][r2-1]);
if (theMax < 0) continue; // 路不通
int pick = r1 == r2 ? grid[r1][c1] : grid[r1][c1] + grid[r2][c2];
ndp[r1][r2] = theMax + pick;
}
}
swap(dp, ndp);
}
return max(0, dp[N-1][N-1]);
}
或者,自顶向下写法
int cherryPickup(vector<vector<int>>& grid) {
// (0,0)=>(N-1,N-1)的两条路径同时走k步,满足条件:k==r1+c1==r2+c2
// 设dp[k][r1][r2]表示从(0,0)走到(r1,c1)、(r2,c2)可摘草莓的最大数。
// 两条路径同时走一步有四种情况:c1、c2走一步,c1、r2走一步、r1、c2走一步、r1、r2走一步
// dp[k][r1][r2] = grid[r1][c1] + grid[r2][c2] /*r1==r2时不重复统计这项*/
// + max{ dp[k-1][r1][r2], dp[k-1][r1][r2-1], dp[k-1][r1-1][r2], dp[k-1][r1-1][r2-1] }
// 0<=k<=2*(N-1);0<=r1<=N-1、0<=c1<=N-1;r2取值范围类似r1
const int N = grid.size();
vector<vector<vector<int>>> memo(2 * (N - 1) + 1, vector<vector<int>>(N, vector<int>(N, INT_MIN)));
function<int(int,int,int)> dp = [&](int k, int r1, int r2) {
if (k == 0 && r1 == 0 && r2 == 0) return grid[0][0];
int c1 = k - r1, c2 = k - r2;
if (r1 < 0 || c1 < 0 || r2 < 0 || c2 < 0
|| grid[r1][c1] < 0 || grid[r2][c2] < 0) return -1;
if (memo[k][r1][r2] != INT_MIN) return memo[k][r1][r2];
int ans = -1;
ans = max(ans, dp(k - 1, r1, r2));
ans = max(ans, dp(k - 1, r1, r2 - 1));
ans = max(ans, dp(k - 1, r1 - 1, r2));
ans = max(ans, dp(k - 1, r1 - 1, r2 - 1));
if (ans >= 0) {
ans += r1 == r2 ? grid[r1][c1] : grid[r1][c1] + grid[r2][c2];
}
return memo[k][r1][r2] = ans;
};
int ans = dp(2 * (N - 1), N - 1, N - 1);
return ans == -1 ? 0 : ans;
}
最大化休假日
LeetCode wants to give one of its best employees the option to travel among N cities to collect algorithm problems. But all work and no play makes Jack a dull boy, you could take vacations in some particular cities and weeks. Your job is to schedule the traveling to maximize the number of vacation days you could take, but there are certain rules and restrictions you need to follow.
Rules and restrictions:
- You can only travel among N cities, represented by indexes from 0 to N-1. Initially, you are in the city indexed 0 on Monday.
- The cities are connected by flights. The flights are represented as a N*N matrix (not necessary symmetrical), called flights representing the airline status from the city i to the city j. If there is no flight from the city i to the city j, flights[i][j] = 0; Otherwise, flights[i][j] = 1. Also, flights[i][i] = 0 for all i.
- You totally have K weeks (each week has 7 days) to travel. You can only take flights at most once per day and can only take flights on each week's Monday morning. Since flight time is so short, we don't consider the impact of flight time.
- For each city, you can only have restricted vacation days in different weeks, given an N*K matrix called days representing this relationship. For the value of days[i][j], it represents the maximum days you could take vacation in the city i in the week j.
You're given the flights matrix and days matrix, and you need to output the maximum vacation days you could take during K weeks.
Example 1:
Input:flights = [[0,1,1],[1,0,1],[1,1,0]], days = [[1,3,1],[6,0,3],[3,3,3]] Output: 12 Explanation: Ans = 6 + 3 + 3 = 12. One of the best strategies is: 1st week : fly from city 0 to city 1 on Monday, and play 6 days and work 1 day. (Although you start at city 0, we could also fly to and start at other cities since it is Monday.) 2nd week : fly from city 1 to city 2 on Monday, and play 3 days and work 4 days. 3rd week : stay at city 2, and play 3 days and work 4 days.Example 2:
Input:flights = [[0,0,0],[0,0,0],[0,0,0]], days = [[1,1,1],[7,7,7],[7,7,7]] Output: 3 Explanation: Ans = 1 + 1 + 1 = 3. Since there is no flights enable you to move to another city, you have to stay at city 0 for the whole 3 weeks. For each week, you only have one day to play and six days to work. So the maximum number of vacation days is 3.Example 3:
Input:flights = [[0,1,1],[1,0,1],[1,1,0]], days = [[7,0,0],[0,7,0],[0,0,7]] Output: 21 Explanation: Ans = 7 + 7 + 7 = 21 One of the best strategies is: 1st week : stay at city 0, and play 7 days. 2nd week : fly from city 0 to city 1 on Monday, and play 7 days. 3rd week : fly from city 1 to city 2 on Monday, and play 7 days.Note:
- N and K are positive integers, which are in the range of [1, 100].
- In the matrix flights, all the values are integers in the range of [0, 1].
- In the matrix days, all the values are integers in the range [0, 7].
- You could stay at a city beyond the number of vacation days, but you should work on the extra days, which won't be counted as vacation days.
- If you fly from the city A to the city B and take the vacation on that day, the deduction towards vacation days will count towards the vacation days of city B in that week.
- We don't consider the impact of flight hours towards the calculation of vacation days.
int maxVacationDays(vector<vector<int>>& flights, vector<vector<int>>& days) {
// flights[][]是城市间的邻接矩阵,days[][]查某城市哪一周能玩几天
// 设dp[i][k]表示当前在城市i、第k周刚开始、从今往后的最大休假日,
// 这周休假的城市j可能是i或者flights[i]可达的城市,
// dp[i][k] = max{ days[j][k] + dp[j][k+1] }。初始dp[][K]=0。
// 递推式k这维只依赖于k+1项,省掉k这维,k仍从右往左遍历
if (flights.empty() || days.empty()) return 0;
const int N = flights.size();
const int K = days[0].size();
vector<int> dp(N, 0);
for (int k = K - 1; k >= 0; k--) {
vector<int> ndp(N, 0);
for (int i = 0; i < N; i++) {
ndp[i] = days[i][k] + dp[i]; // 呆城市i
for (int j = 0; j < N; j++) { // 飞城市j
if (flights[i][j] == 0) continue;
ndp[i] = max(ndp[i], days[j][k] + dp[j]);
}
}
swap(dp, ndp);
}
return dp[0];
}
博弈题用自顶向下带memo的递归写法,比自底向上的dp写法更清晰
能赢吗
bool canIWin(int maxInt, int desired) {
if (maxInt >= desired) return true;
if (maxInt * (maxInt + 1) / 2 < desired) return false;
// 因为1<=maxInt<=20,memo需要1<<20大小
// memo[i]:0 未计算、1 win、-1 lose
vector<int> memo(1 << 20);
// 把哪些数用过编码到maskUsed
function<bool(int,int)> dp = [&](int desired, unsigned maskUsed) {
if (desired <= 0) return false; // 对方已拿到desired
if (memo[maskUsed] != 0) return memo[maskUsed] == 1;
for (int i = 1; i <= maxInt; i++) {
unsigned maskCur = 1 << (i - 1);
if ((maskUsed & maskCur) == 0) { // 当前数还未用过
if (!dp(desired - i, maskUsed | maskCur)) {
memo[maskUsed] = 1;
return true;
}
}
}
memo[maskUsed] = -1;
return false;
};
return dp(desired, 0);
}
猜数字游戏,需要多少钱才能保证赢
int getMoneyAmount(int n) {
// 设dp[i][j]表示猜[i..j]数字的子问题保证赢需要多少钱,1<=i<=j<=n
// dp[i][j] = min{ k + max(dp[i][k-1], dp[k+1][j]) },i<=k<=j
// 其中k+max(dp[i][k-1],dp[k+1][j])表示猜k保证赢需要多少钱,
// dp[i][j] = min{...}表示所有保证赢的情况里最少需要多少钱。
// 初始dp[i][i]=0
vector<vector<int>> memo(n + 1, vector<int>(n + 1, INT_MAX));
function<int(int,int)> dp = [&](int lo, int hi) {
if (lo >= hi) return 0;
if (memo[lo][hi] != INT_MAX) return memo[lo][hi];
for (int k = lo; k <= hi; k++) {
int money = k + max(dp(lo, k - 1), dp(k + 1, hi));
memo[lo][hi] = min(memo[lo][hi], money);
}
return memo[lo][hi];
};
return dp(1, n);
}
只能从数组首尾取数,取得数的和较大的赢
站在playe1的角度,自己得分越大越好、对手得分越小越好,这里让对手得分为负,minmax简化成max算法。
bool PredictTheWinner(vector<int>& nums) {
// 设dp[i][j]表示当前玩家从nums[i..j]局面能得的最高分,0<=i<=j<N
// dp[i][j] = max(nums[i]-dp[i+1][j], nums[j]-dp[i][j-1])
// 初始dp[i][i]=nums[i]
// 省掉i这维,用临时变量ndp,i从右往左遍历,j从左往右遍历
// ndp[j] = max(nums[i]-dp[j], nums[j]-ndp[j-1])
const int N = nums.size();
vector<int> dp(N);
for (int i = N - 1; i >= 0; i--) {
vector<int> ndp(N);
ndp[i] = nums[i];
for (int j = i + 1; j < N; j++) {
ndp[j] = max(nums[i] - dp[j], nums[j] - ndp[j-1]);
}
swap(dp, ndp);
}
return dp[N-1] >= 0;
}
取石头游戏
int stoneGameII(vector<int>& piles) {
// 设dp[i][m]表示从piles[i..]、参数M=m时的得分(能拿的最大石头数)
// 拿掉前x个后,对手得分dp[i+x][max(m,x)],最小化对手得分即最大化自己得分,
// dp[i][m]=max{ sufsum[i] - dp[i+x][max(m,x)] },1<=x<=2m
// 初始dp[N][]=0,dp[i][N]=sufsum[i]
const int N = piles.size();
vector<int> sufsum(N + 1, 0);
for (int i = N - 1; i >= 0; i--) {
sufsum[i] = sufsum[i+1] + piles[i];
}
vector<vector<int>> memo(N + 1, vector<int>(N + 1, -1));
function<int(int,int)> dp = [&](int i, int m) {
if (i == N) return 0;
if (m == N) return sufsum[i];
if (memo[i][m] != -1) return memo[i][m];
for (int x = 1; x <= 2 * m && i + x <= N; x++) {
int sub = dp(i + x, max(m, x));
memo[i][m] = max(memo[i][m], sufsum[i] - sub);
}
return memo[i][m];
};
return dp(0, 1);
}
bool winnerSquareGame(int n) {
// 每次移出平方数个石头
// 设dp[i]表示i个石头时能否赢,dp[i]=any{ !dp[i-squreNum] }
vector<bool> dp(n + 1); // dp[0]==false
for (int i = 1; i <= n; i++) {
for (int k = 1; k * k <= i; k++) {
if (!dp[i - k * k]) {
dp[i] = true;
break;
}
}
}
return dp[n];
}
int stoneGameV(vector<int>& stones) {
// 设dp[i][j]表示stones[i..j]的最大得分
// dp[i][j] = max{ left+dp[i..k] 若left=sum[i..k]更小,
// right+dp[k+1..j] 若right=sum[k+1..j]更小) }
// k在[i,j-1]。初始dp[i][i]=0
const int N = stones.size();
vector<int> presum(N + 1);
for (int i = 0; i < N; i++) {
presum[i+1] = presum[i] + stones[i];
}
vector<vector<int>> memo(N + 1, vector<int>(N + 1, -1));
function<int(int, int)> dp = [&](int lo, int hi) {
if (lo == hi) return 0;
if (memo[lo][hi] != -1) return memo[lo][hi];
int ans = 0;
for (int k = lo; k < hi; k++) {
int left = presum[k+1] - presum[lo];
int right = presum[hi+1] - presum[k+1];
if (left <= right) {
ans = max(ans, left + dp(lo, k));
}
if (left >= right) {
ans = max(ans, right + dp(k+1, hi));
}
}
return memo[lo][hi] = ans;
};
return dp(0, N-1);
}
每K堆石头合并成一堆
int mergeStones(vector<int>& stones, int K) {
const int INF = 1e7;
const int N = stones.size();
// 就像汽水瓶换汽水的问题
if ((N - 1) % (K - 1) != 0) return -1;
// 设dp[i][j][m]表示把stones[i..j]合成m堆
// dp[i][j][m] = min{ dp[i][mid][1] + dp[mid+1][j][m-1] }, mid在[i..j-m+1]
// 初始dp[i][i][1]=0,dp[i][i][>1]=INF
// 已知可合并,dp[i][j][1]=dp[i][j][K]+sum[i..j]
vector<int> presum(N + 1);
for (int i = 0; i < N; i++) {
presum[i+1] = presum[i] + stones[i];
} // sum[i..j]=presum[j+1]-presum[i]
vector<vector<vector<int>>> memo(N + 1, vector<vector<int>>(N + 1, vector<int>(N + 1, -1)));
function<int(int, int, int)> dp = [&](int lo, int hi, int m) {
if (lo == hi) return m == 1 ? 0 : INF;
if (m == 1) return dp(lo, hi, K) + presum[hi+1] - presum[lo];
if (memo[lo][hi][m] != -1) return memo[lo][hi][m];
int ans = INF;
for (int mid = lo; mid <= hi - m + 1; mid++) {
ans = min(ans, dp(lo, mid, 1) + dp(mid + 1, hi, m - 1));
}
return memo[lo][hi][m] = ans;
};
return dp(0, N - 1, 1);
}
硬币路径
Given an array
A(index starts at1) consisting of N integers: A1, A2, ..., AN and an integerB. The integerBdenotes that from any place (suppose the index isi) in the arrayA, you can jump to any one of the place in the arrayAindexedi+1,i+2, …,i+Bif this place can be jumped to. Also, if you step on the indexi, you have to pay Ai coins. If Ai is -1, it means you can’t jump to the place indexediin the array.Now, you start from the place indexed
1in the arrayA, and your aim is to reach the place indexedNusing the minimum coins. You need to return the path of indexes (starting from 1 to N) in the array you should take to get to the place indexedNusing minimum coins.If there are multiple paths with the same cost, return the lexicographically smallest such path.
If it's not possible to reach the place indexed N then you need to return an empty array.
Example 1:
Input: [1,2,4,-1,2], 2 Output: [1,3,5]Example 2:
Input: [1,2,4,-1,2], 1 Output: []Note:
- Path Pa1, Pa2, ..., Pan is lexicographically smaller than Pb1, Pb2, ..., Pbm, if and only if at the first
iwhere Pai and Pbi differ, Pai < Pbi; when no suchiexists, thenn<m.- A1 >= 0. A2, ..., AN (if exist) will in the range of [-1, 100].
- Length of A is in the range of [1, 1000].
- B is in the range of [1, 100].
vector<int> cheapestJump(vector<int>& A, int B) {
// "两条路径代价相同时,取词典序小的那个"
// 设dp[i]表示从A[i..]起跳的最小代价,在dp[i]按序尝试i+1,i+2,...,i+B,先找到的最小代价字典序就小。
// dp[i] = min{ A[i] + dp[i+b] },1<=b<=B;初始dp[N-1] = 0
const int N = A.size();
vector<int> dp(N, INT_MAX);
dp[N-1] = 0;
vector<int> next(N, -1); // 记录路径
for (int i = N - 2; i >= 0; i--) {
if (A[i] == -1) continue;
int maxJ = min(i + B, N - 1);
for (int j = i + 1; j <= maxJ; j++) {
if (A[j] == -1) continue;
int cost = A[i] + dp[j];
if (cost < dp[i]) {
dp[i] = cost;
next[i] = j;
}
}
}
vector<int> ans;
// for (p = head; p != NULL; p = p->next)
for (int i = 0; i != -1; i = next[i]) {
ans.push_back(i + 1); // 1-indexed
if (i == N - 1) return ans;
}
return {};
}
分组平均数的最大和
double largestSumOfAverages(vector<int>& A, int K) {
// 设dp[i][k]表示对A[0..i]最多分k段时的最大得分
// dp[i][k] = max{ avg(0..i)/*不分段*/, dp[j][k-1] + avg(j+1..i) },0<=j<i
// avg(j+1..i)用累加数组计算:(sum[i]-sum[j])/(i-j),sum[i]表示A[0..i]的和
// 初始dp[i][1]=avg(0..i)
// 递推式k这维只依赖k-1项,省掉k这维,k仍从左往右遍历;
// dp[j]要表示旧状态dp[j][k-1],i从右往左遍历
const int N = A.size();
vector<double> sum(N, 0);
int runningSum = 0;
for (int i = 0; i < N; i++) {
runningSum += A[i];
sum[i] = runningSum;
}
vector<double> dp(N, 0);
for (int i = 0; i < N; i++) {
dp[i] = sum[i] / (i + 1); // 不分段k=1
}
for (int k = 2; k <= K; k++) {
for (int i = N - 1; i >= 0; i--) {
for (int j = 0; j < i; j++) {
dp[i] = max(dp[i], dp[j] + (sum[i] - sum[j]) / (i - j));
}
}
}
return dp[N-1];
}
要输入n个字符,求最少击键次数
int minSteps(int n) {
// 一组操作是C后带若干个P,CP、CPP、...,最终n=len(CP)*len(CPP)*...
// 这题就是把n因子分解,求所有>=2因子的和
int ans = 0, factor = 2;
while (n > 1) {
while (n % factor == 0) {
ans += factor;
n /= factor;
}
factor++;
}
return ans;
}
击键n次,最多能输入多少字符
Imagine you have a special keyboard with the following keys:
Key 1: (A): Print one 'A' on screen.
Key 2: (Ctrl-A): Select the whole screen.
Key 3: (Ctrl-C): Copy selection to buffer.
Key 4: (Ctrl-V): Print buffer on screen appending it after what has already been printed.Now, you can only press the keyboard for N times (with the above four keys), find out the maximum numbers of 'A' you can print on screen.
Example 1:
Input: N = 3 Output: 3 Explanation: We can at most get 3 A's on screen by pressing following key sequence: A, A, AExample 2:
Input: N = 7 Output: 9 Explanation: We can at most get 9 A's on screen by pressing following key sequence: A, A, A, Ctrl A, Ctrl C, Ctrl V, Ctrl VNote:
- 1 <= N <= 50
- Answers will be in the range of 32-bit signed integer.
int maxA(int N) {
// dp[i]表示按i次键盘时A的最大个数
// 一种是按1次A,dp[i]=dp[i-1]+1
// 一种是成组按3<=k<=i次,ctrlA+ctrlC+(k-2)ctrlV,字符数 *= (k-1)
// dp[i]=max{ dp[i-k] * (k-1) }
vector<int> dp(N + 1, 0);
for (int i = 1; i <= N; i++) {
dp[i] = dp[i-1] + 1;
for (int k = 3; k <= i; k++) {
dp[i] = max(dp[i], dp[i-k] * (k-1));
}
}
return dp[N];
}
赛车
int racecar(int target) {
// 走n步A前进 dist(n) = 2^0+2^1+...+2^(n-1) = 2^n-1
// 设dp[i]表示前进距离i需要的最少指令数,
// 为走到位置i,先尽量接近i:dist(n) <= i < dist(n+1)
// 1) 走n步A,到达或快达i。若是快达i,可回退m步A(0<=m<n),再掉头前进,
// 需要 n + 1(掉头)+ m(回退m步)+ 1(掉头)步 + dp[剩余距离+回退距离]
// 2) 走n+1步A,超过i,可掉头前进,
// 需要 n + 1 + 1(掉头)步 + dp[超出距离]
vector<int> dp(target + 1, 0);
for (int i = 1; i <= target; i++) {
int n = log2(i + 1); // dist(n)=2^n-1 <= i
if (dist(n) == i) { // 到达i
dp[i] = n;
continue;
}
int ans = INT_MAX;
// 快达i
for (int m = 0; m < n; m++) {
ans = min(ans, n + 1 + m + 1 + dp[i - dist(n) + dist(m)]);
}
// 超过i
ans = min(ans, n + 1 + 1 + dp[dist(n+1) - i]);
dp[i] = ans;
}
return dp[target];
}
int dist(int n) {
return (1 << n) - 1;
}
长宽高都更小时箱子才能往上堆,最高能堆多高
先把箱子按某一维(如height)从大到小排,子问题boxes[idx, ...]考虑堆不堆第idx箱子。
// 子问题boxes[idx,...]可堆可不堆第idx箱子,memo[idx]记录以第idx箱子为底最高能堆多高
int createStack(ArrayList<Box> boxes, int idx, Box prev, int[] memo) {
if (idx >= boxes.size()) return 0;
Box curr = boxes.get(idx);
int heightWithCurr = 0;
if (prev == null || curr.canAbove(prev)) { // 堆第idx箱子
if (memo[idx] == 0) memo[idx] = curr.height + createStack(boxes, idx + 1, curr, memo);
heightWithCurr = memo[idx];
}
// 不堆第idx箱子
int heightWithoutCurr = createStack(boxes, idx + 1, prev, memo);
return Math.max(heightWithCurr, heightWithoutCurr);
}
旋转乘积的最大值
int maxRotateFunction(vector<int>& A) {
// F(k)乘积中右旋A,相当于左旋[0 1 ... n-1]
// F(k) = [ k k+1 ... n-1 0 1 ... k-1] * A
// F(k-1) = [k-1 k ... n-2 n-1 0 ... k-2] * A
// F(k) - F(k-1) = [1 1 ... 1 1-n 1 .. 1] * A = sum(A) - n*A[n-k]
// F(k) = F(k-1) + sum(A) - n*A[n-k]
//
// F(k)只依赖于k-1项,降维,F += sum(A) - n*A[n-k]
// 初始F(0)=sum(i*A[i])
const long n = A.size();
long sum = 0, F = 0;
for (int i = 0; i < n; i++) {
sum += A[i];
F += i * A[i];
}
long ans = F;
for (int k = 1; k < n; k++) {
F += sum - n * A[n-k];
ans = max(ans, F);
}
return ans;
}
错排的个数
元素不能在自身位置,这叫做"错排"。
int findDerangement(int n) {
// 题目:元素k不能在位置k,有多少种排列?
// https://en.wikipedia.org/wiki/Derangement#Counting_derangements
// 设dp[i]表示[1..i]的错排数。元素1可以换到位置[2..i],有i-1种选择,比如说换到位置k。
// 考虑元素k换不换到位置1:如果换到位置1,元素1和k的位置确定,剩下i-2个元素的子问题dp[i-2];
// 如果不换到位置1,那么考虑"去掉被占位置k外的i-1个位置",元素k在这子问题中成了新的“元素1”;
// 元素k有个禁止位置为位置1,其他元素也有个禁止位置为自身位置,这就是i-1个元素的子问题dp[i-1]。
// 所以 dp[i] = (i-1) * (dp[i-2] + dp[i-1])
// 初始dp[1]=0,dp[0]=1(由dp[2]=1推得)
// dp[i]只依赖于前两项,降维,用prev2表示dp[i-2],prev1表示dp[i-1]
const int MOD = 1e9 + 7;
int prev2 = 1, prev1 = 0;
for (int i = 2; i <= n; i++) {
int curr = ((i-1L) * (prev2 + prev1)) % MOD;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
色子模拟
不能有连续rollMax[x]个数字x
int dieSimulator(int n, vector<int>& rollMax) {
// 设dp[i][j]表示取i次数、第i次取数为j的不同序列数,
// 要求不能有超过r=rollMax[j]个j,
// dp[i][j] = dp[i-1][not_j] /*连续1个j*/ + dp[i-2][not_j] /*连续2个j*/ + ... + dp[i-r][not_j] /*连续r个j*/,
// = sum{ dp[i-k][not_j] }, k在[1..rollMax[j]]
// 令sum[i]表示取i次数的不同序列数,则sum[i]=sum{ dp[i][j], for all j },sum{dp[i][not_j]}=sum[i]-dp[i][j],
// 所以dp[i][j] = sum{ sum[i-k]-dp[i-k][j] }
const int MOD = 1e9 + 7;
const int R = rollMax.size();
vector<vector<int>> dp(n + 1, vector<int>(R, 0));
vector<long> sum(n + 1, 0);
sum[0] = 1; // 这个是关键,空集算1个序列
for (int i = 1; i <= n; i++) {
for (int j = 0; j < R; j++) {
for (int k = 1; k <= rollMax[j] && i - k >= 0; k++) {
dp[i][j] = (dp[i][j] + sum[i-k] - dp[i-k][j] + MOD) % MOD;
}
sum[i] = (sum[i] + dp[i][j]) % MOD;
}
}
return sum[n];
}
填充书架
int minHeightShelves(vector<vector<int>>& books, int shelf_width) {
// 设dp[i]表示books[0..i)子问题的最小高度,
// books[i-1]可以和books[j..i-1)放在一层,即books[j..i-1]在一层,
// j<i 且 sum(books[j..i-1][0])<=shelf_width
// dp[i] = min{ dp[j] + max{ books[j..i-1][1] }}
const int N = books.size();
vector<int> dp(N + 1, 1e9);
dp[0] = 0;
for (int i = 1; i <= N; i++) {
int width = 0, height = 0;
for (int j = i - 1; j >= 0 && width + books[j][0] <= shelf_width; j--) {
width += books[j][0];
height = max(height, books[j][1]);
dp[i] = min(dp[i], dp[j] + height);
}
}
return dp[N];
}
区间增量更新
vector<int> getModifiedArray(int length, vector<vector<int>>& updates) {
// 将区间[l,r]的值增加inc,只在区间边界记录变化量:arr[l]+=inc; arr[r+1]-=inc;
// 然后idx处的值就是前缀和sum(arr[0..idx])
vector<int> ans(length, 0);
for (auto &update : updates) {
int l = update[0], r = update[1], inc = update[2];
ans[l] += inc;
if (r + 1 < length) ans[r+1] -= inc;
}
// 将ans数组从增量变成前缀和
for (int i = 1; i < length; i++) {
ans[i] += ans[i-1];
}
return ans;
}
另:如果要支持前缀和的快速更新和查询,可以进一步用线段树或二叉索引树。
各区间的右侧下一个区间
vector<int> findRightInterval(vector<vector<int>>& intervals) {
map<int, int> mp; // start=>idx
for (int i = 0; i < intervals.size(); i++) {
mp[intervals[i][0]] = i;
}
vector<int> ans;
// 每个区间找>=end的下一起点:mp.lower_bound(end)
for (auto &interval : intervals) {
auto it = mp.lower_bound(interval[1]);
ans.push_back(it != mp.end() ? it->second : -1);
}
return ans;
}
区间问题大都是贪心法
最多的不重叠区间数
最早结束时间优先,给其他区间腾出空间。用end记录已选区间的最右端,比较当前区间起点和end,不重叠则可选当前区间。
- https://leetcode.com/problems/non-overlapping-intervals/
- https://leetcode.com/problems/maximum-length-of-pair-chain/
- https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons/
int findMinArrowShots(vector<vector<int>>& points) {
// 找最多的不重叠区间数
sort(points.begin(), points.end(), [](const vector<int> &a, const vector<int> &b) {
return a[1] < b[1];
});
int ans = 0;
long end = LONG_MIN;
for (auto &p : points) {
if (p[0] > end) {
end = p[1];
ans++;
}
}
return ans;
}
带权重的区间,总权重最大的不重叠区间
要用动态规划+二分搜索
int jobScheduling(vector<int>& startTime, vector<int>& endTime, vector<int>& profit) {
// 先按endTime排序
const int N = startTime.size();
vector<array<int, 3>> jobs;
for (int i = 0; i < N; i++) {
jobs.push_back({ endTime[i], startTime[i], profit[i] });
}
sort(begin(jobs), end(jobs));
// 设dp[endTime]表示到某endTime的maxProfit
map<int, int> dp; // endTime=>maxProfit
dp[0] = 0; // 左哨兵
for (auto& [e, s, p] : jobs) {
// 对每个job找它前面与它相容的最后一个区间
// 相容,即toFind.endTime<=cur.startTime
int preMaxP = prev(dp.upper_bound(s))->second;
int curMaxP = preMaxP + p;
// 利润比已知更大,选择当前区间
if (curMaxP > dp.rbegin()->second) dp[e] = curMaxP;
}
return dp.rbegin()->second;
}
尽量多地上课程
课程(t,d)有持续时间t、最晚结束时间d。列举两个课程(a,x),(b,y)比较的所有情况(a+b<=x、x<a+b<=y、y<a+b;a<=b、a>b;假设x<y)可知:最晚结束时间优先总没错。这结论跟“最多的不重叠区间数”一样,尽管这里是最晚结束时间,那里是实际结束时间。
实际结束时间和最晚结束时间的区别在于,若当前课程跟前面区间重叠,实际结束时间的情况下当前课程不能选,最晚结束时间的情况下:当前课程可以替换掉已选课程中持续时间t最大、且t比当前课程更大的某课程,以腾出更多时间。
int scheduleCourse(vector<vector<int>>& courses) {
sort(courses.begin(), courses.end(), [](const auto &a, const auto &b) {
return a[1] < b[1];
});
priority_queue<int> taken; // 已选课程的持续时间t
int end = 0; // 已选课程的实际结束时间
for (auto &c : courses) {
if (end + c[0] <= c[1]) { // 选课程c
taken.push(c[0]);
end += c[0];
} else if (!taken.empty()) { // 替换掉持续时间t最大且更大的已选课程
int t = taken.top();
if (t > c[0]) {
taken.pop();
taken.push(c[0]);
end += c[0] - t;
}
}
}
return taken.size();
}
为使集合S与每个区间交集>=2,集合S最少要有几个数
int intersectionSizeTwo(vector<vector<int>>& intervals) {
// 区间按right排序,right相同时优先考虑范围小的
sort(intervals.begin(), intervals.end(), [](vector<int> &a, vector<int> &b) {
if (a[1] == b[1]) return a[0] > b[0];
return a[1] < b[1];
});
int ans = 0;
// last1,last2是集合S[..last2,last1]的最后两个数
int last1 = INT_MIN, last2 = INT_MIN;
for (auto &interval : intervals) {
if (last1 < interval[0]) {
// S与当前区间不相交,要在S中加入当前区间的最后两个数
ans += 2;
last1 = interval[1];
last2 = last1 - 1;
} else if (last2 < interval[0]) {
// S与当前区间有一个数相交,要在S中加入当前区间的最后一个数
ans += 1;
last2 = last1;
last1 = interval[1];
}
}
return ans;
}
最多的重叠区间层数
边界计数法(扫描线算法):用有序map模拟时间线,起点处事件数+1、终点处事件数-1,想象有根垂直时间线扫过起点和终点,遍历map并将这些增量累加,就是过程中的事件数。
class MyCalendarTwo {
map<int, int> timeline;
public:
MyCalendarTwo() {
}
bool book(int start, int end) {
timeline[start]++;
timeline[end]--;
int ongoing = 0;
for (auto &e : timeline) {
ongoing += e.second;
if (ongoing >= 3) {
timeline[start]--;
timeline[end]++;
return false;
}
}
return true;
}
};
最少会议室
Given an array of meeting time intervals consisting of start and end times
[[s1,e1],[s2,e2],...](si < ei), determine if a person could attend all meetings.For example,
Given[[0, 30],[5, 10],[15, 20]],
returnfalse.
bool canAttendMeetings(vector<Interval>& intervals) {
// 是否所有区间都不重叠?
sort(intervals.begin(), intervals.end(), [](const Interval &a, const Interval &b) {
return a.end < b.end;
});
int end = INT_MIN;
for (auto &interval : intervals) {
if (interval.start < end) return false;
else end = interval.end;
}
return true;
}
Given an array of meeting time intervals consisting of start and end times
[[s1,e1],[s2,e2],...](si < ei), find the minimum number of conference rooms required.For example,
Given[[0, 30],[5, 10],[15, 20]],
return2.
int minMeetingRooms(vector<Interval>& intervals) {
// 边界计数法
map<int, int> mp;
for (auto & interval : intervals) {
mp[interval.start]++;
mp[interval.end]--;
}
// 遍历mp,累加进行中的事件数
int rooms = 0, ans = 0;
for (auto &e : mp) {
rooms += e.second;
ans = max(ans, rooms);
}
return ans;
}
公共空闲时间
We are given a list
scheduleof employees, which represents the working time for each employee.Each employee has a list of non-overlapping
Intervals, and these intervals are in sorted order.Return the list of finite intervals representing common, positive-length free time for all employees, also in sorted order.
Example 1:
Input: schedule = [[[1,2],[5,6]],[[1,3]],[[4,10]]] Output: [[3,4]] Explanation: There are a total of three employees, and all common free time intervals would be [-inf, 1], [3, 4], [10, inf]. We discard any intervals that contain inf as they aren't finite.Example 2:
Input: schedule = [[[1,3],[6,7]],[[2,4]],[[2,5],[9,12]]] Output: [[5,6],[7,9]](Even though we are representing
Intervalsin the form[x, y], the objects inside areIntervals, not lists or arrays. For example,schedule[0][0].start = 1, schedule[0][0].end = 2, andschedule[0][0][0]is not defined.)Also, we wouldn't include intervals like [5, 5] in our answer, as they have zero length.
Note:
scheduleandschedule[i]are lists with lengths in range[1, 50].0 <= schedule[i].start < schedule[i].end <= 10^8.
vector<Interval> employeeFreeTime(vector<vector<Interval>>& schedule) {
// 边界计数法
map<int, int> timeline;
for (auto &employee : schedule) {
for (auto &interval : employee) {
timeline[interval.start]++;
timeline[interval.end]--;
}
}
vector<Interval> ans;
int event = 0, start = -1;
for (auto &e : timeline) {
event += e.second;
if (event <= 0) { // 空闲时间
if (start == -1) {
start = e.first;
}
} else {
if (start != -1) {
ans.push_back({ start, e.first });
start = -1;
}
}
}
return ans;
}
轮廓线问题
vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
// 扫描线算法,垂直线向右扫,遇到左端点把高度Hi加入集合,遇到右端点把Hi移出集合。
// 若集合中最大高度变化(currHi!=prevHi),就得所需的跃变点。
// 当x相同时,为避免currHi变化输出多余跃变点,左端点再按h从大到小排,右端点再按h从小到大排。
// 不妨将左端点的高度取负值,然后统一排序。这样既区分了左右端点,又满足排序要求。
vector<array<int,2>> points; // (x,h)
for (auto &b : buildings) {
points.push_back({b[0], -b[2]});
points.push_back({b[1], b[2]});
}
sort(points.begin(), points.end());
vector<vector<int>> ans;
multiset<int> st;
int prevHi = 0;
for (auto& [x, h] : points) { // 遍历points相当于垂直线向右扫
if (h < 0) st.insert(-h); // 左端点
else st.erase(st.find(h)); // 右端点
int currHi = st.empty() ? 0 : *st.rbegin();
if (currHi != prevHi) {
ans.push_back({ x, currHi });
prevHi = currHi;
}
}
return ans;
}
所有矩形的覆盖面积
int rectangleArea(vector<vector<int>>& rectangles) {
// 扫描线算法:
// 水平扫描线向上扫,每遇到y边界都累加一横块面积。
// 面积高为 (y边界-前一y边界),面积长为 旧区间集合的重叠长。
// 要水平线向上扫,所以先按y坐标排序
const int LOWER = 0, UPPER = 1;
vector<vector<int>> events;
for (auto &rect : rectangles) {
events.push_back({rect[1], LOWER, rect[0], rect[2]});
events.push_back({rect[3], UPPER, rect[0], rect[2]});
}
sort(events.begin(), events.end());
// 遇到下边界把横坐标区间[x1,x2]加入集合,
// 遇到上边界把[x1,x2]移出集合。
const int MOD = 1e9 + 7;
multiset<vector<int>> st; // st{ [x1,x2] }
int preY = INT_MIN;
int ans = 0;
for (auto &e : events) {
int y = e[0], type = e[1], x1 = e[2], x2 = e[3];
if (preY != INT_MIN) {
int height = y - preY;
int width = getWidth(st);
ans = (ans + (long)width * height) % MOD;
}
if (type == LOWER) st.insert({x1, x2});
else st.erase(st.find({x1, x2}));
preY = y;
}
return ans;
}
// 区间集合的重叠长
int getWidth(multiset<vector<int>> &st) {
int ans = 0;
int curr = INT_MIN;
for (auto &x : st) {
curr = max(curr, x[0]);
ans += max(x[1] - curr, 0);
curr = max(curr, x[1]);
}
return ans;
}
最多可参与的活动数
- https://leetcode.com/problems/maximum-number-of-events-that-can-be-attended/
int maxEvents(vector<vector<int>>& events) {
const int N = events.size();
// 事件按开始时间升序
sort(events.begin(), events.end());
// 尽量选结束时间早的,"进行中事件"的结束时间升序
priority_queue<int, vector<int>, greater<>> pq;
int maxDay = 0;
for (auto& e : events) {
maxDay = max(maxDay, e[1]);
}
int ans = 0;
for (int day = 1, i = 0; day <= maxDay; day++) { // 扫描线
// 当天或之前开始的事件,结束时间入堆
while (i < N && events[i][0] <= day) {
pq.push(events[i][1]);
i++;
}
// 移除已结束的事件
while (!pq.empty() && pq.top() < day) {
pq.pop();
}
// 取一个事件
if (!pq.empty()) {
pq.pop();
ans++;
}
}
return ans;
}
需要最少多少区间来覆盖某范围
- https://leetcode.com/problems/minimum-number-of-taps-to-open-to-water-a-garden/
- https://leetcode.com/problems/video-stitching/
int videoStitching(vector<vector<int>>& clips, int T) {
const int N = clips.size();
sort(begin(clips), end(clips));
int i = 0, ans = 0;
int sofar = 0; // 当前已覆盖[..sofar]区间
int frontier = 0; // 尝试扩展当前区间
while (sofar < T) {
while (i < N && clips[i][0] <= sofar) {
frontier = max(frontier, clips[i][1]);
i++;
}
if (frontier == sofar) return -1;
sofar = frontier;
ans++;
}
return ans;
}
或者
int videoStitching(vector<vector<int>>& clips, int T) {
// 实际是bfs分层遍历
const int N = clips.size();
sort(begin(clips), end(clips));
int ans = 0;
int frontier = 0;
// 若还有扩展可能,类似 while (!q.empty())
for (int i = 0; i < N && clips[i][0] <= frontier; ) {
ans++;
// 扩展一层,类似 for (int sz=q.size(); sz > 0; sz--)
for (int sofar = frontier; i < N && clips[i][0] <= sofar; i++) {
frontier = max(frontier, clips[i][1]);
if (frontier >= T) return ans;
}
}
return -1;
}
最少跳跃
int jump(vector<int>& nums) {
// 点i覆盖[i,i+nums[i]],需要到覆盖[0,N-1]
const int N = nums.size();
if (N <= 1) return 0;
// 实际是bfs分层遍历
int ans = 0;
for (int i = 0, hi = 0; i <= hi;) {
ans++; // 在nums[i]处跳
int hi0 = hi;
for (; i <= hi0; i++) {
hi = max(hi, i + nums[i]);
if (hi >= N - 1) return ans;
}
}
return -1;
}
若只问能否跳跃到达,不用分层遍历
bool canJump(vector<int>& nums) {
const int N = nums.size();
for (int i = 0, hi = 0; i <= hi; i++) {
hi = max(hi, i + nums[i]);
if (hi >= N - 1) return true;
}
return false;
}
要维护区间集合,区间按起点排
插入新区间
vector<vector<int>> insert(vector<vector<int>>& intervals, vector<int>& newInterval) {
// 区间互不重叠、已按起点排序
vector<vector<int>> before, after; // 前后的不重叠区间
int left = newInterval[0], right = newInterval[1];
for (auto& interval : intervals) {
if (interval[1] < left) before.push_back(interval);
else if (interval[0] > right) after.push_back(interval);
else {
left = min(left, interval[0]);
right = max(right, interval[1]);
}
}
before.push_back({left, right});
before.insert(end(before), begin(after), end(after));
return before;
}
合并区间列表
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.empty()) return {};
sort(begin(intervals), end(intervals));
vector<vector<int>> ans;
for (auto &interval : intervals) {
if (ans.empty() || interval[0] > ans.back()[1]) {
ans.push_back(interval);
} else {
ans.back()[1] = max(ans.back()[1], interval[1]);
}
}
return ans;
}
区间数组合并后的总长
int getWidth(multiset<vector<int>> &st) {
int ans = 0;
int curr = INT_MIN;
// curr往右扩展,只累加变长的部分
for (auto &x : st) {
curr = max(curr, x[0]);
ans += max(x[1] - curr, 0);
curr = max(curr, x[1]);
}
return ans;
}
或者同上题写法
维护区间集合
- https://leetcode.com/problems/data-stream-as-disjoint-intervals/
- https://leetcode.com/problems/range-module
class RangeModule {
map<int, int> _ranges; // left=>right, [left,right)
using RI = map<int, int>::iterator;
array<RI, 2> getOverlapRanges(int left, int right) {
// 在端点重合处,认为重叠
// 左边找第一个相交的区间
auto l = _ranges.upper_bound(left); // toFind.left>left
if (l != begin(_ranges) && prev(l)->second >= left) // toFind.left<=left&&toFind.right>=left
l = prev(l);
// 右边找第一个不相交的区间
auto r = _ranges.upper_bound(right); // toFind.left>right
return {l, r};
}
public:
RangeModule() {
}
void addRange(int left, int right) {
auto [l, r] = getOverlapRanges(left, right);
if (l != r) {
left = min(left, l->first);
right = max(right, prev(r)->second);
_ranges.erase(l, r);
}
_ranges[left] = right;
}
bool queryRange(int left, int right) {
auto [l, r] = getOverlapRanges(left, right);
return l != r && l->first <= left && right <= l->second;
}
void removeRange(int left, int right) {
auto [l, r] = getOverlapRanges(left, right);
if (l != r) {
int lower = min(left, l->first);
int upper = max(right, prev(r)->second);
_ranges.erase(l, r);
if (lower < left) _ranges[lower] = left;
if (right < upper) _ranges[right] = upper;
}
}
};
俄罗斯方块落下后的最大高度
class Solution {
// left=>[right,height], [left,right)
map<int, array<int, 2>> _ranges;
using RI = map<int, array<int, 2>>::iterator;
array<RI, 2> getOverlapRanges(int left, int right) {
// 在端点重合处,不认为重叠
auto l = _ranges.upper_bound(left); // toFind.left>left
if (l != begin(_ranges) && prev(l)->second[0] > left) // toFind.left<=left&&toFind.right>left
l = prev(l);
auto r = _ranges.lower_bound(right); // toFind.left>=right
return {l, r};
}
public:
vector<int> fallingSquares(vector<vector<int>>& positions) {
vector<int> ans;
int highest = 0;
for (auto &pos : positions) {
int left = pos[0], right = pos[0] + pos[1], height = pos[1];
auto [l, r] = getOverlapRanges(left, right);
if (l != r) {
// 重叠区间的最大高度是新块儿的放置高度
int baseH = 0;
for (auto i = l; i != r; i++) {
baseH = max(baseH, i->second[1]);
}
height += baseH;
// 删除重叠区间
int lower = min(left, l->first), lH = l->second[1];
int upper = max(right, prev(r)->second[0]), rH = prev(r)->second[1];
_ranges.erase(l, r);
if (lower < left) _ranges[lower] = {left, lH};
if (right < upper) _ranges[right] = {upper, rH};
}
_ranges[left] = {right, height};
highest = max(highest, height);
ans.push_back(highest);
}
return ans;
}
};
贪心法:优先做某事就能解决问题
最少加油站
int minRefuelStops(int target, int startFuel, vector<vector<int>>& stations) {
// 车油足够时不加油,只把加油站油量记入最大堆;车油不够时,从堆中取最大值加油。
const int N = stations.size();
int maxDist = startFuel; // 能行驶的最大距离
priority_queue<int> pq;
int ans = 0, idx = 0;
while (true) {
while (idx < N && stations[idx][0] <= maxDist) {
pq.push(stations[idx][1]);
idx++;
}
if (maxDist >= target) return ans;
if (pq.empty()) return -1; // 无油可加
int fuel = pq.top(); pq.pop();
maxDist += fuel;
ans++;
}
}
加油能否跑完一圈
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
// 只要 sum(gas[i]-cost[i]) >=0 就能绕圈
// 因为若无法绕圈,存在某站的消耗 cost[i]-gas[i] > 其他站的总积累 sum(gas[j]-cost[j]),
// 所有站的总积累 sum(gas[i]-cost[i]) < 0 ==取逆否命题=> sum(gas[i]-cost[i]) >=0 就能绕圈
const int N = gas.size();
int gasSum = 0;
int start = 0, gasFromStart = 0;
for (int i = 0; i < N; i++) {
int gasI = gas[i] - cost[i];
gasSum += gasI, gasFromStart += gasI;
if (gasFromStart < 0) { // i站及前面站不能作为起点,下一站作起点候选
start = i + 1;
gasFromStart = 0;
}
}
if (gasSum < 0) return -1;
return start;
}
相同字母至少距离k
vector<int> rearrangeBarcodes(vector<int>& barcodes) {
const int N = barcodes.size();
unordered_map<int, int> cnt;
for (int code : barcodes) {
cnt[code]++;
}
auto cmp = [&cnt](int a, int b) { return cnt[a] < cnt[b]; };
priority_queue<int, vector<int>, decltype(cmp)> pq(cmp);
for (auto &e : cnt) pq.push(e.first);
vector<int> ans;
queue<int> freezed;
while (!pq.empty()) {
int code = pq.top(); pq.pop();
ans.push_back(code);
cnt[code]--;
freezed.push(code);
if (freezed.size() >= 2) { // 相同项至少距离2
int released = freezed.front(); freezed.pop();
if (cnt[released] > 0) pq.push(released);
}
}
return ans;
}
Given a non-empty string **str **and an integer k, rearrange the string such that the same characters are at least distance **k **from each other.
All input strings are given in lowercase letters. If it is not possible to rearrange the string, return an empty string
"".Example 1:
str = "aabbcc", k = 3 Result: "abcabc" The same letters are at least distance 3 from each other.Example 2:
str = "aaabc", k = 3 Answer: "" It is not possible to rearrange the string.Example 3:
str = "aaadbbcc", k = 2 Answer: "abacabcd" Another possible answer is: "abcabcda" The same letters are at least distance 2 from each other.
class Solution {
public:
using Pair = pair<char, int>;
string rearrangeString(string s, int k) {
// 剩余最多的字母优先
unordered_map<char, int> cnt;
for (char c : s) cnt[c]++;
auto cmp = [](const Pair &a, const Pair &b) {
return a.second < b.second; // 最大堆
};
priority_queue<Pair, vector<Pair>, decltype(cmp)> pq(cmp);
for (auto &e : cnt) pq.push(e);
string ans;
queue<Pair> freezed;
while (!pq.empty()) {
auto top = pq.top(); pq.pop();
ans += top.first;
top.second--;
// 每个字母输出后都进freezed队列,包括{c,0}
freezed.push(top);
if (freezed.size() >= k) { // 队头解冻回堆
auto released = freezed.front(); freezed.pop();
if (released.second > 0) pq.push(released);
}
}
return ans.size() == s.size() ? ans : "";
}
};
避免连续相同字母的最小删除代价
int minCost(string s, vector<int>& cost) {
// 连续相同字符只剩cost最大的不删除
const int N = s.size();
int ans = 0, maxCost = 0;
for (int i = 0; i < N; i++) {
if (i > 0 && s[i] != s[i-1]) {
ans -= maxCost;
maxCost = 0;
}
maxCost = max(maxCost, cost[i]);
ans += cost[i];
}
ans -= maxCost;
return ans;
}
同一字母分在同一子段,最多能分多少段
每个字母都向右扩展当前区间右端点,右端点不再扩展时子段结束、区间数++
vector<int> partitionLabels(string S) {
unordered_map<char, int> lastIdx;
for (int i = 0; i < S.size(); i++) {
lastIdx[S[i]] = i;
}
vector<int> ans;
// 每个字母都向右扩展当前区间右端点
int left = 0, right = -1;
for (int i = 0; i < S.size(); i++) {
right = max(right, lastIdx[S[i]]);
if (i == right) { // 右端点不再扩展时子段结束
ans.push_back(right - left + 1);
left = right + 1;
}
}
return ans;
}
将有序数组分成长>=3的一些连续数子序列
bool isPossible(vector<int>& nums) {
unordered_map<int, int> count;
for (int x : nums) count[x]++;
// 数x优先扩展旧序列,即使旧序列太长也可拆分成多个新序列
unordered_map<int, int> need; // 扩展旧序列需要need[x]个x
for (int x : nums) {
if (count[x] == 0) continue;
if (need[x] > 0) { // 优先扩展旧序列
count[x]--;
need[x]--;
need[x+1]++;
} else if (count[x+1] > 0 && count[x+2] > 0) { // x可作长>=3新序列的头
count[x]--;
count[x+1]--;
count[x+2]--;
need[x+3]++;
} else {
return false;
}
}
return true;
}
子段最大值在[L,R]间的子段个数
int numSubarrayBoundedMax(vector<int>& A, int L, int R) {
// 题目:求满足 L<= 子段最大值 <=R 的子段数
// 从左往右,统计以A[i]结尾的有效子段数
// 例如 A=[0,1,2,-1], L=2, R=3
// i=0时,无;i=1时,无;i=2时,三个以2结尾的有效子段:[0,1,2], [1,2], [2];
// i=3时,和i=2的情况相同,三个以-1结尾的有效子段:[0,1,2,-1], [1,2,-1], [2,-1]
//
// 设[lo..hi]是有效子段起始的范围,ans += hi-lo+1
// 若L<=A[i]<=R,hi=i;
// 若A[i]>R,lo=i+1,hi=i;
// 若A[i]<L,lo和hi不变
int ans = 0;
int lo = 0, hi = -1;
for (int i = 0; i < A.size(); i++) {
if (L <= A[i] && A[i] <= R) {
hi = i;
} else if (A[i] > R) {
lo = i + 1, hi = i;
}
ans += hi - lo + 1; // A[lo..i], A[lo+1..i], ..., A[hi..i]
}
return ans;
}
数字递增的<=N的数
int monotoneIncreasingDigits(int N) {
// 将N的串表示从右往左不断将逆序波峰值减1,
// 然后将最左逆序波峰后面的所有数字都变为9
string s = to_string(N);
int pos = s.size();
for (int i = s.size() - 2; i >= 0; i--) {
if (s[i] > s[i+1]) {
s[i]--;
pos = i;
}
}
for (int i = pos + 1; i < s.size(); i++) {
s[i] = '9';
}
return stoi(s);
}
找DI序列对应的最小排列
By now, you are given a secret signature consisting of character 'D' and 'I'. 'D' represents a decreasing relationship between two numbers, 'I' represents an increasing relationship between two numbers. And our secret signature was constructed by a special integer array, which contains uniquely all the different number from 1 to n (n is the length of the secret signature plus 1). For example, the secret signature "DI" can be constructed by array [2,1,3] or [3,1,2], but won't be constructed by array [3,2,4] or [2,1,3,4], which are both illegal constructing special string that can't represent the "DI" secret signature.
On the other hand, now your job is to find the lexicographically smallest permutation of [1, 2, ... n] could refer to the given secret signature in the input.
Example 1:
Input: "I" Output: [1,2] Explanation: [1,2] is the only legal initial spectial string can construct secret signature "I", where the number 1 and 2 construct an increasing relationship.Example 2:
Input: "DI" Output: [2,1,3] Explanation: Both [2,1,3] and [3,1,2] can construct the secret signature "DI", but since we want to find the one with the smallest lexicographical permutation, you need to output [2,1,3]Note:
- The input string will only contain the character 'D' and 'I'.
- The length of input string is a positive integer and will not exceed 10,000
vector<int> findPermutation(string s) {
// 将连续D对应的子段翻转
const int N = s.size();
vector<int> ans;
for (int i = 0; i <= N; i++)
ans.push_back(i + 1);
for (int i = 0; i < N; i++) {
if (s[i] == 'D') {
int start = i;
while (i < N && s[i] == 'D')
i++;
reverse(ans, start, i);
}
}
return ans;
}
void reverse(vector<int> &v, int left, int right) {
while (left < right) {
swap(v[left++], v[right--]);
}
}
找DI序列对应的任意排列
vector<int> diStringMatch(string S) {
// 遇'I'输出最小值lo(下一值肯定更大),
// 遇'D'输出最大值hi(下一值肯定更小)。
vector<int> ans;
int lo = 0, hi = S.size();
for (char c : S) {
ans.push_back(c == 'I' ? lo++ : hi--);
}
ans.push_back(lo);
return ans;
}
联系:符合DI序列的排列数 - 动态规划
田忌赛马
vector<int> advantageCount(vector<int>& A, vector<int>& B) {
// 选A中比b稍大的,若没有则选A中最小的
multiset<int> ms(A.begin(), A.end());
vector<int> ans;
for (int b : B) {
auto it = ms.upper_bound(b);
if (it == ms.end()) it = ms.begin();
ans.push_back(*it);
ms.erase(it);
}
return ans;
}
罗马数字转数字
int romanToInt(string s) {
unordered_map<char, int> mp = {
{'I', 1},
{'V', 5},
{'X', 10},
{'L', 50},
{'C', 100},
{'D', 500},
{'M', 1000},
};
// 从左到右累加当前值,如果当前字符比上一个大,补减上一个值
int ans = 0;
for (int i = 0; i < s.size(); i++) {
ans += mp[s[i]];
if (i > 0 && mp[s[i]] > mp[s[i-1]]) {
ans -= 2 * mp[s[i-1]];
}
}
return ans;
}
数字转罗马数字
string intToRoman(int num) {
const vector<int> radix = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
const vector<string> symbol = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
// 贪心法
ostringstream oss;
int i = 0;
while (num) {
int cnt = num / radix[i];
num %= radix[i];
while (cnt) {
oss << symbol[i];
cnt--;
}
i++;
}
return oss.str();
}
分数转成循环小数
string fractionToDecimal(int numerator, int denominator) {
if (denominator == 0) return "NAN";
if (numerator == 0) return "0";
string ans;
bool neg = (numerator ^ denominator) < 0;
if (neg) ans += "-";
long n = labs(numerator), d = labs(denominator);
ans += to_string(n / d);
long rmd = n % d;
if (rmd > 0) ans += ".";
// 为插入循环节的左括号,记录 余数=>对应左括号应在ans串的位置
unordered_map<long, int> mp;
while (rmd) {
if (mp.count(rmd)) { // 找到循环节
ans.insert(mp[rmd], "(");
ans += ")";
return ans;
}
mp[rmd] = ans.size();
rmd *= 10;
ans += to_string(rmd / d);
rmd %= d;
}
return ans;
}
青蛙过河
bool canCross(vector<int>& stones) {
// 题目:河宽被分成一格格,已知某些格有石头。青蛙往前跳,初始跳1步,
// 若上次跳k步、这次跳k-1、k、k+1步。问青蛙能否过河?
// 写法类似bfs,石头当作顶点,jump[i]当作顶点i的出边
unordered_map<int, set<int>> jump; // 某石头=>set{可以跳几步}
jump[0].insert(1);
set<int> st; // 快速判断某位置是否有石头
for (int pos : stones)
st.insert(pos);
for (int pos : stones) { // 遍历有石头的位置
for (int k : jump[pos]) {
int next = pos + k;
if (!st.count(next)) continue;
if (next == stones.back()) return true;
if (k - 1 > 0) jump[next].insert(k - 1);
jump[next].insert(k);
jump[next].insert(k + 1);
}
}
return false;
}
一船每次两人,最少过河时间
按过河时间排序,<=3人的情况单独讨论。>=4人时假设最快a、b,最慢c、d,贪心法目标是先送最慢的两人过去,两种方案:
- a送c、d过去:a送d + a回来 + a送c + a回来,d+a+c+a
- c、d一起过去:ab先过去 + a回来 + cd过去 + b回来,b+a+d+b
那种更快?(1)-(2)=a+c-2b
寻找名人
Suppose you are at a party with
npeople (labeled from0ton - 1) and among them, there may exist one celebrity. The definition of a celebrity is that all the othern - 1people know him/her but he/she does not know any of them.Now you want to find out who the celebrity is or verify that there is not one. The only thing you are allowed to do is to ask questions like: "Hi, A. Do you know B?" to get information of whether A knows B. You need to find out the celebrity (or verify there is not one) by asking as few questions as possible (in the asymptotic sense).
You are given a helper function
bool knows(a, b)which tells you whether A knows B. Implement a functionint findCelebrity(n), your function should minimize the number of calls toknows.Note: There will be exactly one celebrity if he/she is in the party. Return the celebrity's label if there is a celebrity in the party. If there is no celebrity, return
-1.
int findCelebrity(int n) {
// 名人:其他人知道他、他不知道其他人
int cand = 0;
for (int i = 1; i < n; i++) {
if (knows(cand, i)) { // cand知道i,cand不是名人
cand = i;
}
}
// [0..cand)知道其他人,[0..cand)不是名人;
// [cand+1..n)不被cand知道,[cand+1..n)不是名人;
// cand是唯一候选。下面验证cand是不是真名人。
// 已知"cand不知道[cand+1..n)"(不用再验证)
for (int i = 0; i < cand; i++) {
if (!knows(i, cand) || knows(cand, i)) return -1;
}
for (int i = cand + 1; i < n; i++) {
if (!knows(i, cand)) return -1;
}
return cand;
}
屏幕上能输出句子几遍
Given a
rows x colsscreen and a sentence represented by a list of words, find how many times the given sentence can be fitted on the screen.Note:
- A word cannot be split into two lines.
- The order of words in the sentence must remain unchanged.
- Two consecutive words in a line must be separated by a single space.
- Total words in the sentence won't exceed 100.
- Length of each word won't exceed 10.
- 1 ≤ rows, cols ≤ 20,000.
Example 1:
Input: rows = 2, cols = 8, sentence = ["hello", "world"] Output: 1 Explanation: hello--- world--- The character '-' signifies an empty space on the screen.Example 2:
Input: rows = 3, cols = 6, sentence = ["a", "bcd", "e"] Output: 2 Explanation: a-bcd- e-a--- bcd-e- The character '-' signifies an empty space on the screen.Example 3:
Input: rows = 4, cols = 5, sentence = ["I", "had", "apple", "pie"] Output: 1 Explanation: I-had apple pie-I had-- The character '-' signifies an empty space on the screen.
int wordsTyping(vector<string>& sentence, int rows, int cols) {
// 把word+" "+word+" "+...无限扩展
string s;
for (auto &word : sentence)
s += word + " ";
int len = s.size();
int cnt = 0; // 总共输入了多少字符
for (int i = 0; i < rows; i++) {
cnt += cols; // 尝试键入一满行
while (cnt > 0 && s[cnt % len] != ' ') // 若对着单词中间则回退
cnt--;
cnt++; // 跳过空格
}
return cnt / len;
}
LR串邻位交换
bool canTransform(string start, string end) {
// 把L看作面朝左的人、R看作面朝右的人、X看作空位,那么L朝左走、R朝右走、且不能越过别人
// 比较start串和end串除了X外的LR序列是否相同,
// 且end的相应L要在start的L的左边(朝左走)、end的相应R要在start的R的右边(朝右走)
const int M = start.size(), N = end.size();
int i = 0, j = 0;
while (true) {
while (i < M && start[i] == 'X') i++;
while (j < N && end[j] == 'X') j++;
if (i == M && j == N) return true;
if (i == M || j == N) return false;
if (start[i] != end[j]) return false;
if (start[i] == 'L' && j > i) return false;
if (start[i] == 'R' && j < i) return false;
i++;
j++;
}
}
倒水往低流
vector<int> pourWater(vector<int>& heights, int V, int K) {
// 模拟倒水,先尽量往左边低地流、再尽量往右边低地流
const int N = heights.size();
while (V--) {
int low = K;
for (int i = K; i > 0 && heights[i-1] <= heights[i]; i--) { // <=的地儿都要查
if (heights[i-1] < heights[i]) low = i-1;
}
if (low == K) { // 没往左边流,再尝试往右流
for (int i = K; i < N - 1 && heights[i+1] <= heights[i]; i++) {
if (heights[i+1] < heights[i]) low = i+1;
}
}
heights[low]++;
}
return heights;
}
强密码检查器
对删除操作的解释,摘录如下
We know that replace is the most effective way to change the password. But
If len>20, we at lease need to delete length-20 characters. The deletion can work as follow:
Assume the length of repeating characters is n, there are 3 cases for repeating characters:n%3==0, n%3==1, n%3==2.
- n%3==0 if n=3,6,9… If String s=“aaa”,delete the last one and s=“aa”. If String s=“aaaaaa”, replace one character and delete the last one works and s="aabaa ". If String s=“aaaaaaaaa”, replace two character and delete the last one works and s="aabaacaa ".
- n%3==1 if n=4,7,10… If String s=“aaaa”,delete the last 2 characters and s=“aa”. If String s=“aaaaaaa”, replace the one character and delete the last two works and s="aabaa ".
- n%3==2 if n=5,8,11… If String s=“aaaaa”,delete the last 3 characters and s=“aa”. If String s=“aaaaaaaa”, replace the one character and delete the last three works and s="aabaa ".
Always delete the last few characters and use the replace most of times. One deletion works for one n%3==0, two deletion works for one n%3==1, and three deletion works for one n%3==2. The deletion first used for n%3==0 cases then n%3==1 and finally n%3==2.
int strongPasswordChecker(string s) {
const int N = s.size();
int needLower = 1, needUpper = 1, needDigit = 1;
int modifyOp = 0;
vector<int> type(3, 0);
// 长len>=3的重复子串,按照len%3分三类,用type[len%3]计数
// 假设已进行过修改操作,比如aaaaaa已修改成aa#aa#
// len%3==0这类子串,末尾可用1个删除省掉1个修改,比如 aa#aa# => aa#aa
// len%3==1这类子串,末尾可用2个删除省掉1个修改,比如 aa#aa#a => aa#aa
// len%3==2这类子串,末尾可用3个删除省掉1个修改,比如 aa#aa#aa => aa#aa
for (int i = 0, j; i < N; i = j) {
if(islower(s[i])) needLower = 0;
else if(isupper(s[i])) needUpper = 0;
else if(isdigit(s[i])) needDigit = 0;
j = i + 1;
while (j < N && s[j] == s[i]) j++;
int len = j - i;
if (len >= 3) {
modifyOp += len / 3; // 每3个字母修改第3个
type[len % 3]++;
}
}
int needLUD = needLower + needUpper + needDigit;
if (N < 6) return max(6 - N, needLUD); // 只需插入操作
if (N <= 20) return max(modifyOp, needLUD); // 只需修改操作
// N > 20
const int needDelete = N - 20; // 需要的删除操作数
// 用删除操作取代部分修改操作,可以省掉多少修改操作?
int deleteOp = needDelete;
if (deleteOp <= type[0]) {
modifyOp -= deleteOp;
} else {
modifyOp -= type[0];
deleteOp -= type[0];
// len%3==1这类子串要省掉type[1]个修改,需要2*type[1]个删除
if (deleteOp <= 2 * type[1]) {
modifyOp -= deleteOp / 2;
} else {
modifyOp -= type[1];
deleteOp -= 2 * type[1];
// 剩下的deleteOp都作用在len%3==2这类子串上
modifyOp -= deleteOp / 3;
}
}
// modifyOp可以同时解决needLUD问题,所以用 max(modifyOp, needLUD)
return needDelete + max(modifyOp, needLUD);
}
回溯法可用dfs、bfs等方法搜索解空间,剪掉不可能有解的分支。
用dfs配合外部引用参数时,在递归之前设置值(比如设置dfs的visited[]数组)、在递归之后复原值。虽然不像dp那样有重叠子问题,也要看用哪些参数能定义子问题,dfs的终止条件就是最简单子问题的解。
数组有重复元素,不论组合或排列都应去重
不管是求组合还是排列,数组有重复元素时,要确保相同元素只选第一个。
- 可以排序数组,再看相邻元素,
if (i > idx && nums[i] == nums[i-1]) continue; - 如果不能排序,使用
unordered_set<int> seen;
求所有可能组合:元素选一次还是多次?
- search(idx)从idx开始遍历
- 元素只选一次,递归时i+1;元素可选多次,递归时还是i。
/* 模板 */
void search(..., int idx, vector<int> &comb, vector<vector<int>> &ans) {
// 前面是终止条件,对应子问题[idx,...];
if (idx == nums.size()) {
ans.push_back(comb);
return;
}
// 有重复元素,相同元素只选第一个;数组已排序
for (int i = idx; i < nums.size(); i++) {
if (i > idx && nums[i] == nums[i-1]) continue;
comb.push_back(nums[i]);
search(...); // 元素只选一次,递归时i+1;元素可选多次,递归时还是i
comb.pop_back();
}
}
// 有重复元素,相同元素只选第一个;数组不能排序
unordered_set<int> seen;
for (int i = idx; i < nums.size(); i++) {
if (seen.count(nums[i])) continue;
seen.insert(nums[i]);
...
}
题目:
- https://leetcode.com/problems/combination-sum 元素可选多次
- https://leetcode.com/problems/combination-sum-ii 元素只选一次、元素有重复
- https://leetcode.com/problems/combination-sum-iii 元素只选一次
- https://leetcode.com/problems/combinations/ 元素只选一次
特别地,要输出全组合,元素只选一次,有两种写法
// 记这种写法,只需改成前面不终止
void search(const vector<int> &nums, int idx, vector<int> &subset, vector<vector<int>> &ans) {
// 前面不终止,对应子问题[...,idx)
ans.push_back(subset);
for (int i = idx; i < nums.size(); i++) {
subset.push_back(nums[i]);
search(nums, i + 1, subset, ans);
subset.pop_back();
}
}
// 这种写法不用记,考虑位置idx的各种可能
void subsets(const vector<int> &nums, int idx, vector<int> &subset, vector<vector<int>> &ans) {
if (idx == nums.size()) {
ans.push_back(subset);
return;
}
// 不选
subsets(nums, idx + 1, subset, ans);
// 选
subset.push_back(nums[idx]);
subsets(nums, idx + 1, subset, ans);
subset.pop_back();
}
- https://leetcode.com/problems/subsets/ 元素只选一次。这题也可对应[0, 2^n)间的二进制遍历,二进制中1选0不选
- https://leetcode.com/problems/subsets-ii/ 元素只选一次、元素有重复
- https://leetcode.com/problems/increasing-subsequences 元素只选一次、元素有重复
vector<vector<int>> findSubsequences(vector<int>& nums) {
vector<vector<int>> ans;
vector<int> seq;
search(nums, 0, seq, ans);
return ans;
}
void search(vector<int>& nums, int idx, vector<int> &seq, vector<vector<int>> &ans) {
if (seq.size() >= 2) ans.push_back(seq);
unordered_set<int> selected;
for (int i = idx; i < nums.size(); i++) {
if (selected.count(nums[i])) continue; // 确保相同元素只选第一个
selected.insert(nums[i]);
if (!seq.empty() && nums[i] < seq.back()) continue; // 确保递增
seq.push_back(nums[i]);
search(nums, i + 1, seq, ans);
seq.pop_back();
}
}
求所有可能排列:元素只选一次
- 元素只选一次,search()从idx开始遍历,考虑位置idx的各种可能(nums[idx]和nums[i>=idx]交换)、递归时idx+1;
元素可选多次,search()都从0开始遍历、不用idx参数
题目:
- https://leetcode.com/problems/permutations/ 元素只选一次
- https://leetcode.com/problems/permutations-ii 元素只选一次、元素有重复
- https://leetcode.com/problems/letter-case-permutation/ 元素只选一次
void search(vector<int> &nums, int idx, vector<vector<int>> &ans) {
const int N = nums.size();
if (idx == N) {
ans.push_back(nums);
return;
}
unordered_set<int> seen; // 排列去重,略过相同元素
for (int i = idx; i < N; i++) {
if (seen.count(nums[i])) continue;
seen.insert(nums[i]);
swap(nums[idx], nums[i]);
search(nums, idx + 1, ans); // 这里是idx相关
swap(nums[idx], nums[i]);
}
}
或者,使用组合的写法,多用一个visited[]数组
void search(vector<int> &nums, vector<bool> &visited,
vector<int> &seq, vector<vector<int>> &ans) {
const int N = nums.size();
if (seq.size() == N) {
ans.push_back(seq);
return;
}
unordered_set<int> seen; // 排列去重,略过相同元素
for (int i = 0; i < N; i++) {
if (visited[i] || seen.count(nums[i])) continue;
visited[i] = true;
seen.insert(nums[i]);
seq.push_back(nums[i]);
search(nums, visited, seq, ans);
seq.pop_back();
visited[i] = false;
}
}
联系:生成下一排列
数组分成k个和相等的子集
- https://leetcode.com/problems/partition-to-k-equal-sum-subsets
- https://leetcode.com/problems/matchsticks-to-square
bool canPartitionKSubsets(vector<int>& nums, int k) {
if (nums.size() < k) return false;
int sum = 0;
for (int num : nums)
sum += num;
if (sum % k != 0) return false;
vector<bool> visited(nums.size(), false);
return search(nums, 0, visited, k, 0, sum / k);
}
// 从nums[idx..]取数,还要分割k个子集,当前子集已累积和subSum
bool search(vector<int> &nums, int idx, vector<bool> &visited, int k, int subSum, int target) {
if (k == 0) return true;
if (subSum == target) return search(nums, 0, visited, k - 1, 0, target);
for (int i = idx; i < nums.size(); i++) {
if (visited[i] || subSum + nums[i] > target) continue;
visited[i] = true;
if (search(nums, i + 1, visited, k, subSum + nums[i], target)) return true;
visited[i] = false;
}
return false;
}
数字间加上加减乘,表达式结果等于某个数的所有可能
vector<string> addOperators(string num, int target) {
vector<string> ans;
search(num, target, 0, "", 0, 0, ans);
return ans;
}
void search(string num, int target, int idx, string lastExprStr,
long lastExprVal, long lastNum, vector<string> &ans) {
if (idx == num.size()) {
if (lastExprVal == target) ans.push_back(lastExprStr);
return;
}
// [idx..i]是currStr, [i+1..]进递归
for (int i = idx; i < num.size(); i++) {
if (i > idx && num[idx] == '0') continue; // 多位数不能以"0"开头
auto currStr = num.substr(idx, i - idx + 1);
long currNum = stol(currStr);
if (idx == 0) {
search(num, target, i + 1, currStr, currNum, currNum, ans);
} else {
search(num, target, i + 1, lastExprStr + "+" + currStr,
lastExprVal + currNum, currNum, ans);
search(num, target, i + 1, lastExprStr + "-" + currStr,
lastExprVal - currNum, -currNum, ans);
// 比如 lastExprVal=1+2+3,现在遇到*4,要lastExprVal-3+3*4
// 若再遇到*5,要lastExprVal-(3*4)+(3*4)*5
search(num, target, i + 1, lastExprStr + "*" + currStr,
lastExprVal - lastNum + lastNum * currNum, lastNum * currNum, ans);
}
}
}
联系:表达式加括号的方式数只需要分治
24点游戏
穷举:从nums[]中任取两个数,遍历四种运算符,对每种运算符生成 arr[其余操作数, 此次运算结果],在arr上递归。
bool judgePoint24(vector<int>& nums) {
vector<double> v(nums.begin(), nums.end());
return solve(v);
}
bool solve(vector<double>& nums) {
const vector<char> ops = {'+', '-', '*', '/'};
const int N = nums.size();
const double EPSILON = 1e-6;
if (N == 1) return abs(nums[0] - 24) < EPSILON;
// 任取两个数
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (i == j) continue;
// 先把剩下的数放入新数组
vector<double> arr;
for (int k = 0; k < N; k++) {
if (k != i && k != j) {
arr.push_back(nums[k]);
}
}
for (char op : ops) {
if ((op == '+' || op == '*') && i > j) continue; // +*满足交换律,减少重复计算
if (op == '/' && abs(nums[j]) < EPSILON) continue;
double ans;
if (op == '+') ans = nums[i] + nums[j];
else if (op == '-') ans = nums[i] - nums[j];
else if (op == '*') ans = nums[i] * nums[j];
else if (op == '/') ans = nums[i] / nums[j];
arr.push_back(ans);
if (solve(arr)) return true;
arr.pop_back();
}
}
}
return false;
}
横竖相同的单词方阵
Given a set of words (without duplicates), find all word squares you can build from them.
A sequence of words forms a valid word square if the kth row and column read the exact same string, where 0 ≤ k < max(numRows, numColumns).
For example, the word sequence
["ball","area","lead","lady"]forms a word square because each word reads the same both horizontally and vertically.b a l l a r e a l e a d l a d yNote:
- There are at least 1 and at most 1000 words.
- All words will have the exact same length.
- Word length is at least 1 and at most 5.
- Each word contains only lowercase English alphabet
a-z.Example 1:
Input: ["area","lead","wall","lady","ball"] Output: [ [ "wall", "area", "lead", "lady" ], [ "ball", "area", "lead", "lady" ] ] Explanation: The output consists of two word squares. The order of output does not matter (just the order of words in each word square matters).Example 2:
Input: ["abat","baba","atan","atal"] Output: [ [ "baba", "abat", "baba", "atan" ], [ "baba", "abat", "baba", "atal" ] ] Explanation: The output consists of two word squares. The order of output does not matter (just the order of words in each word square matters).
vector<vector<string>> wordSquares(vector<string>& words) {
// b a l l
// a r e a
// l e a d
// l a d y
// 第0行选定后,第1行要以a开头(ball[1]);
// 第1行选定后,第2行要以le开头(ball[2]、area[2]);
// 第2行选定后,第3行要以lad开头(ball[3]、area[3]、lead[3]);
// ... 第k行要以串rows[0..k-1][k]开头
// 需要根据前缀找单词的功能,可以用prefix=>wordIdx[]的哈希表,或者tire节点中存wordIdx[]
if (words.empty()) return {};
unordered_map<string, vector<int>> mp; // prefix=>wordIdx[]
for (int i = 0; i < words.size(); i++) {
for (int j = 0; j < words[i].size(); j++) {
auto prefix = words[i].substr(0, j + 1);
mp[prefix].push_back(i);
}
}
// 回溯法
vector<vector<string>> ans;
vector<string> rows;
for (auto &word : words) {
rows.push_back(word);
search(rows, words, mp, ans);
rows.pop_back();
}
return ans;
}
void search(vector<string> &rows, vector<string> &words,
unordered_map<string, vector<int>> &mp, vector<vector<string>> &ans) {
if (rows.size() == words[0].size()) { // 有len(word)行
ans.push_back(rows);
return;
}
int k = rows.size(); // 第k行要以rows[0..k-1][k]开头
string prefix;
for (int i = 0; i < k; i++) {
prefix += rows[i][k];
}
for (int idx : mp[prefix]) { // 前缀为prefix的那些单词,可作为rows[]新一行的候选
rows.push_back(words[idx]);
search(rows, words, mp, ans);
rows.pop_back();
}
}
字母模式匹配单词
Given a
patternand a stringstr, find ifstrfollows the same pattern.Here follow means a full match, such that there is a bijection between a letter in
patternand a non-empty substring instr.Examples:
- pattern =
"abab", str ="redblueredblue"should return true.- pattern =
"aaaa", str ="asdasdasdasd"should return true.- pattern =
"aabb", str ="xyzabcxzyabc"should return false.Notes:
You may assume bothpatternandstrcontains only lowercase letters.
bool wordPatternMatch(string pattern, string str) {
unordered_map<char, string> mp; // c => mappingStr
unordered_set<string> st; // 防止mappingStr多次匹配
return match(pattern, 0, str, 0, mp, st);
}
bool match(const string &pattern, int pi, const string &str, int si,
unordered_map<char, string> &mp, unordered_set<string> &st) {
const int M = pattern.size(), N = str.size();
if (pi == M && si == N) return true; // 两个都匹配完
if (pi == M || si == N) return false; // 只一个匹配完
char c = pattern[pi];
if (mp.count(c)) {
auto mappingStr = mp[c];
if (!startsWith(mappingStr, str, si)) return false;
return match(pattern, pi + 1, str, si + mappingStr.size(), mp, st);
}
// 回溯法,尝试给c匹配str[si..i]
for (int i = si; i < str.size(); i++) {
auto mappingStr = str.substr(si, i - si + 1);
if (st.count(mappingStr)) continue;
mp[c] = mappingStr;
st.insert(mappingStr);
if (match(pattern, pi + 1, str, i + 1, mp, st)) return true;
mp.erase(c);
st.erase(mappingStr);
}
return false;
}
// prefix和str[idx..]是否相等
bool startsWith(const string &prefix, const string &str, int idx) {
if (idx + prefix.size() > str.size()) return false;
for (int i = 0; i < prefix.size(); i++) {
if (prefix[i] != str[idx + i]) return false;
}
return true;
}
数字串分割成fibonacci数列
只要前两个确定,整个分割也都确定了
bool isAdditiveNumber(string num) {
// 选头两个数字长为i和j
const int N = num.size();
for (int i = 1; i <= N / 2; i++) {
if (i > 1 && num[0] == '0') break;
for (int j = 1; j <= (N - i) / 2; j++) {
if (j > 1 && num[i] == '0') break;
if (isValid(num, i + j, num.substr(0, i), num.substr(i, j))) return true;
}
}
return false;
}
// num[idx..]开头是不是等于n1+n2
bool isValid(const string &num, int idx, const string &n1, const string &n2) {
const int N = num.size();
if (idx == N) return true;
auto sum = add(n1, n2);
int len = sum.size();
if (idx + len > N || num.substr(idx, len) != sum) return false;
return isValid(num, idx + len, n2, sum);
}
// 数字串加法可解决超大数溢出问题
string add(const string &a, const string &b) {
// return to_string(stoll(a) + stoll(b));
string ans;
int carry = 0;
int i = (int)a.size() - 1, j = (int)b.size() - 1;
while (i >= 0 || j >= 0 || carry > 0) {
if (i >= 0) carry += a[i--] - '0';
if (j >= 0) carry += b[j--] - '0';
ans.push_back(carry % 10 + '0');
carry /= 10;
}
reverse(ans.begin(), ans.end());
return ans;
}
返回所有分割,回溯法
vector<int> splitIntoFibonacci(string S) {
vector<int> ans;
search(S, 0, ans);
return ans;
}
bool search(const string &s, int idx, vector<int> &ans) {
const int N = s.size(), M = ans.size();
if (idx == N && M >= 3) return true;
// s[idx..i]分割出数num
for (int i = idx; i < N; i++) {
if (i > idx && s[idx] == '0') continue; // 多位数的首位不能为0
int len = i - idx + 1;
if (len > 10) continue; // < 2^31, 最多10位数
long num = stol(s.substr(idx, len));
if (num > INT_MAX) continue; // 太大溢出
if (M >= 2 && ans[M-2] + ans[M-1] != num) continue; // 不满足Fibonacci性质
ans.push_back(num);
if (search(s, i + 1, ans)) return true;
ans.pop_back();
}
return false;
}
因子乘积的组合
Numbers can be regarded as product of its factors. For example,
8 = 2 x 2 x 2; = 2 x 4.Write a function that takes an integer n and return all possible combinations of its factors.
Note:
- Each combination's factors must be sorted ascending, for example: The factors of 2 and 6 is
[2, 6], not[6, 2].- You may assume that n is always positive.
- Factors should be greater than 1 and less than n.
Examples:
input:1
output:[]input:
37
output:[]input:
12
output:[ [2, 6], [2, 2, 3], [3, 4] ]input:
32
output:[ [2, 16], [2, 2, 8], [2, 2, 2, 4], [2, 2, 2, 2, 2], [2, 4, 4], [4, 8] ]
vector<vector<int>> getFactors(int n) {
vector<vector<int>> ans;
vector<int> comb;
search(n, 2, comb, ans);
return ans;
}
void search(int n, int startNum, vector<int> &comb, vector<vector<int>> &ans) {
if (n == 1) {
ans.push_back(comb);
return;
}
for (int i = startNum; i * i <= n; i++) {
if (n % i == 0) {
comb.push_back(i);
// n/i能拆分成更多小因子乘积
search(n / i, i, comb, ans);
// 或n/i不再拆分了
comb.push_back(n / i);
ans.push_back(comb);
comb.pop_back();
comb.pop_back();
}
}
}
生成单词缩写
Write a function to generate the generalized abbreviations of a word.
Example:
Given word =
"word", return the following list (order does not matter):["word", "1ord", "w1rd", "wo1d", "wor1", "2rd", "w2d", "wo2", "1o1d", "1or1", "w1r1", "1o2", "2r1", "3d", "w3", "4"]
方法一:回溯,排列的写法(考虑位置idx的各种可能)
vector<string> generateAbbreviations(string word) {
vector<string> ans;
dfs(word, 0, 0, "", ans);
return ans;
}
// count表示在word[idx..]之前有几个被省略还未输出
void search(const string &word, int idx, int count, string abbr, vector<string> &ans) {
if (idx == word.size()) {
if (count > 0) abbr += to_string(count);
ans.push_back(abbr);
return;
}
// 对每个字母,或者:省略它、并增加计数
search(word, idx + 1, count + 1, abbr, ans);
// 或者:计数>0时输出计数、再输出字母
abbr += (count > 0 ? to_string(count) : "") + word[idx];
search(word, idx + 1, 0, abbr, ans);
}
方法二:根据二进制01来选不选相应字母
联想:最短唯一单词缩写
开机解锁密码数
int numberOfPatterns(int m, int n) {
// 判断有效move的关键:3x3棋盘很小,去下一个数字需要跳过某数字的情况可以都记在表中
vector<vector<int>> jump(10, vector<int>(10, -1));
jump[1][3] = jump[3][1] = 2;
jump[1][7] = jump[7][1] = 4;
jump[3][9] = jump[9][3] = 6;
jump[7][9] = jump[9][7] = 8;
jump[1][9] = jump[9][1] = jump[2][8] = jump[8][2]
= jump[3][7] = jump[7][3] = jump[4][6] = jump[6][4] = 5;
// 回溯法
int ans = 0;
vector<bool> visited(10, false);
ans += countPatterns(1, 1, m, n, jump, visited) * 4; // 1、3、7、9位置对称
ans += countPatterns(2, 1, m, n, jump, visited) * 4; // 2、4、6、8位置对称
ans += countPatterns(5, 1, m, n, jump, visited);
return ans;
}
// num是当前选择的数字,pattLen是当前pattern长
int countPatterns(int num, int pattLen, const int m, const int n,
const vector<vector<int>> &jump, vector<bool> &visited) {
int ans = 0;
dfs(num, pattLen, m, n, jump, visited, ans);
return ans;
}
void dfs(int num, int pattLen, const int m, const int n,
const vector<vector<int>> &jump, vector<bool> &visited, int &pattCnt) {
if (pattLen > n) return;
if (pattLen >= m) pattCnt++; // 有效pattern:m<=pattLen<=n
visited[num] = true;
for (int next = 1; next <= 9; next++) {
if (visited[next]) continue;
int jumpNum = jump[num][next];
// 有效move:不需要跳过某数字,或被跳过的数字已访问
if (jumpNum == -1 || visited[jumpNum]) {
dfs(next, pattLen + 1, m, n, jump, visited, pattCnt);
}
}
visited[num] = false;
}
dfs用栈、bfs用队列、dijkstra用优先队列,都是图遍历,都要有个visited[]数组。dijkstra常用dist[]替代visited[]功能。
-
bfs在入队前设置
visited[],这样当两个节点指向同一个节点时,可防止那个节点重复入队。若在出队后设置visited[],会重复入队,但只要一出队就作visited[x]判重,也没问题。 -
dfs一样在入栈前设置
visited[]可防止重复入栈。若在出栈后设置visited[],只要一出栈就作visited[x]判重,也没问题。 -
dfs的递归写法是,在递归前判重,在函数开头只设置
visited[x],这对应于入栈前设置visited[]的情况。或者在函数开头就判重退出,并设置visited[x],然后不用判断直接递归,这对应于出栈后设置visited[]的情况。 -
dijkstra因为距离值会更新,要允许节点重复入队,所以在出队后设置
visited[]。可用dist[]数组代替visited[]功能,当newdist<dist[v]时相当于在说v未访问,插入(newdist,v)元组。pq中已有v点会重复插入较小距离,这没关系,因为原先的较大值在后面运行时不再满足newdist<dist[v],从而跳过。
树是一种特殊的图,dfs(前序、中序、后序遍历)、bfs(按层遍历)都不用visited[]数组。该图没有dijkstra遍历。
最短路径
Dijkstra算法,无负边,单源最短路径:设d[u]表示顶点u到源点的最短路径长,用优先队列的图遍历。
Bellman-Ford算法,有负边,单源最短路径:对所有边做V-1遍松弛,因为V个顶点的图中最短路径最多V-1条边。之后再对所有边做V-1遍松弛,这时若发现还有边可松弛,说明有负环。
// 对所有边做V-1遍松弛
for (i = 0; i < V - 1; i++) {
for [u,v] int graph.edges {
if (dist[u] + cost(u,v) < dist[v])
dist[v] = dist[u] + cost(u,v);
}
}
// 查负环,再对所有边做V-1遍松弛
for (i = 0; i < V - 1; i++) {
for [u,v] int graph.edges {
if (dist[u] + cost(u,v) < dist[v])
dist[v] = -INF;
}
}
Dijkstra算法和Bellman-Ford算法见单源最短路径
A*算法,就是一致代价搜索(Uniform Cost Search,UCS)+ 乐观估计。UCS类似dijkstra算法;乐观估计就是放松问题限制再估计,估计距离差h(u)-h(v) <= 真实代价cost(u,v)。写法上,除了用d[u]记顶点u到源点的最短距离,用h(u)乐观估计顶点u到汇点的最短距离,优先队列Q选f(u)=d[u]+h(u)最小的顶点来扩展。
dijkstra要求边值非负,即对u->v要求d[u]<=d[v],作队列Q键的d值递增;A*对u->v要求h(u)<=w(u,v)+h(v),这样f(u)=d[u]+h(u)<=d[u]+w(u,v)+h(v)=d[v]+h(v)=f(v),作队列Q键的f值也递增。
A*搜索的有效性证明见 Stanford CS221 2019。启发式只要满足乐观估计,即对u→v有h(u)-h(v)≤cost(u,v),估计距离差≤实际代价,A*就有效。怎么乐观估计?放松问题限制。h(x)是relaxed问题\(Cost_{relaxed}(s,a)≤Cost(s,a)\)的解。
Floyd-Warshall算法,所有点对的最短路径:设\(d_{ij}^{(k)}\)表示从顶点i到顶点j、中间顶点在集合{1, 2, ..., k}的最短路径长。根据中间顶点经不经过顶点k分为:不经过,\(d_{ij}^{(k)}=d_{ij}^{(k-1)}\);经过,\(d_{ij}^{(k)}=d_{ik}^{(k-1)}+d_{kj}^{(k-1)}\)。所以,\(d_{ij}^{(k)}=min(d_{ij}^{(k-1)}, d_{ik}^{(k-1)}+d_{kj}^{(k-1)})\),k=1..n。初始时,\(d_{ij}^{(0)}=w_{ij}\)是无中间顶点的情况。见ita p387。
写dp时省掉k这维,k遍历所有顶点。初始dp[i][j]=dist[i][j],dp[i][j]=min(dp[i][j], dp[i][k] + dp[k][j])。
// 三重循环做松弛
for (int k = 0; k < N; k++) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (dp[i][k] + dp[k][j] < dp[i][j])
dp[i][j] = dp[i][k] + dp[k][j];
}
}
}
// 查负环,再三重循环做松弛
for (int k = 0; k < N; k++) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (dp[i][k] + dp[k][j] < dp[i][j])
dp[i][j] = -INF;
}
}
}
Floyd-Warshall算法见所有点对的最短路径
Dijkstra算法,用优先队列的图遍历
注: 比较函数若使用c++中的引用语法[&dist](){...},优先队列中可不用存节点距离d。这里用各语言通用的写法。
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
// dijkstra算法
unordered_map<int, unordered_map<int, int>> adj;
for (auto &e : times) {
adj[e[0]][e[1]] = e[2];
}
vector<int> dist(N + 1, INT_MAX); // 节点1..N
dist[K] = 0;
using arr2 = array<int, 2>; // [dist, node]
auto cmp = [](arr2 &a, arr2 &b) {
return a[0] > b[0]; // 最小堆
};
priority_queue<arr2, vector<arr2>, decltype(cmp)> pq(cmp);
pq.push({dist[K], K});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
for (auto& [v, cost] : adj[u]) { // 遍历u的所有邻接点
int newdist = d + cost;
if (newdist < dist[v]) {
dist[v] = newdist;
pq.push({newdist, v});
}
}
}
int ans = INT_MIN;
for (int i = 1; i <= N; i++) {
ans = max(ans, dist[i]);
}
return (ans != INT_MAX) ? ans : -1;
}
Bellman-Ford算法
对所有边做V-1遍松弛(V个顶点的最短路径最多有V-1条边)。根据上一遍的距离值prev[]更新这一遍的距离值dist[]。
- https://leetcode.com/problems/network-delay-time/>]
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
// Bellman-Ford算法,对所有边做N-1次松弛
const int INF = 1e7;
vector<int> dist(N + 1, INF);
dist[K] = 0;
for (int i = 1; i < N; i++) {
auto prev = dist;
for (auto &e : times) {
int u = e[0], v = e[1], cost = e[2];
dist[v] = min(dist[v], prev[u] + cost);
}
}
int ans = INT_MIN;
for (int i = 1; i <= N; i++) {
ans = max(ans, dist[i]);
}
return (ans != INF) ? ans : -1;
}
- https://leetcode.com/problems/cheapest-flights-within-k-stops/
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
// 从src到dst最多K个中间站,共V=K+2个节点
// Bellman-Ford算法,对所有边做V-1=K+1次松弛
const int INF = 1e7;
vector<int> dist(n, INF); // src到各节点的距离
dist[src] = 0;
for (int i = 0; i <= K; i++) {
auto prev = dist;
for (auto &e : flights) {
int u = e[0], v = e[1], cost = e[2];
dist[v] = min(dist[v], prev[u] + cost);
}
}
return (dist[dst] != INF) ? dist[dst] : -1;
}
BFS
- https://leetcode.com/problems/shortest-path-in-binary-matrix/
int shortestPathBinaryMatrix(vector<vector<int>>& grid) {
// 从左上角到右下角的最短路径,bfs
if (grid[0][0] == 1) return -1;
const int R = grid.size(), C = grid[0].size();
vector<vector<bool>> visited(R, vector<bool>(C, false));
using arr3 = array<int, 3>; // [dist, i, j]
queue<arr3> q;
q.push({1, 0, 0});
visited[0][0] = true;
while (!q.empty()) {
auto [d, r, c] = q.front(); q.pop();
if (r == R - 1 && c == C - 1) return d;
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
if (dr == 0 && dc == 0) continue;
// 遍历8个方向
int nr = r + dr, nc = c + dc;
if (nr < 0 || nr >= R || nc < 0 || nc >= C
|| grid[nr][nc] || visited[nr][nc]) continue;
q.push({d + 1, nr, nc});
visited[nr][nc] = true;
}
}
}
return -1;
}
离某值节点最近的叶节点
Given a binary tree where every node has a unique value, and a target key
k, find the value of the nearest leaf node to targetkin the tree.Here, nearest to a leaf means the least number of edges travelled on the binary tree to reach any leaf of the tree. Also, a node is called a leaf if it has no children.
In the following examples, the input tree is represented in flattened form row by row. The actual
roottree given will be a TreeNode object.Example 1:
Input: root = [1, 3, 2], k = 1 Diagram of binary tree: 1 / \ 3 2 Output: 2 (or 3) Explanation: Either 2 or 3 is the nearest leaf node to the target of 1.Example 2:
Input: root = [1], k = 1 Output: 1 Explanation: The nearest leaf node is the root node itself.Example 3:
Input: root = [1,2,3,4,null,null,null,5,null,6], k = 2 Diagram of binary tree: 1 / \ 2 3 / 4 / 5 / 6 Output: 3 Explanation: The leaf node with value 3 (and not the leaf node with value 6) is nearest to the node with value 2.Note:
rootrepresents a binary tree with at least1node and at most1000nodes.- Every node has a unique
node.valin range[1, 1000].- There exists some node in the given binary tree for which
node.val == k.
int findClosestLeaf(TreeNode* root, int k) {
// 题目:树中有一个节点值为k,找出离这个节点最近的叶节点值
// 可沿边上下运动,其实就是把这树当作无向图,用bfs找最短路径
// 先dfs创建无向图,同时找出k节点(用后序遍历)
unordered_map<TreeNode *, vector<TreeNode *>> adj;
auto theKNode = dfs(root, k, adj);
queue<TreeNode *> q;
q.push(theKNode);
unordered_set<TreeNode *> visited;
visited.insert(theKNode);
while (!q.empty()) {
auto u = q.front(); q.pop();
if (!u->left && !u->right) return u->val;
for (auto v : adj[u]) {
if (visited.count(v)) continue;
q.push(v);
visited.insert(v);
}
}
return INT_MIN;
}
TreeNode* dfs(TreeNode *root, int k, unordered_map<TreeNode *, vector<TreeNode *>> &adj) {
if (!root) return NULL;
if (root->left) {
adj[root].push_back(root->left);
adj[root->left].push_back(root);
}
if (root->right) {
adj[root].push_back(root->right);
adj[root->right].push_back(root);
}
auto left = dfs(root->left, k, adj);
auto right = dfs(root->right, k, adj);
if (left) return left; // 在左子树中找到k节点
if (right) return right;
if (root->val == k) return root;
return NULL;
}
Floyd-Warshall算法,三重循环松弛
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
// Floyd-Warshall算法,所有点对的最短路径
const int INF = 1e7;
vector<vector<int>> dist(N + 1, vector<int>(N + 1, INF));
for (int i = 1; i <= N; i++) {
dist[i][i] = 0;
}
for (auto &e : times) {
dist[e[0]][e[1]] = e[2];
}
// 三重循环松弛
for (int k = 1; k <= N; k++) {
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
int ans = INT_MIN;
for (int i = 1; i <= N; i++) {
ans = max(ans, dist[K][i]);
}
return (ans != INF) ? ans : -1;
}
哈密顿路径,通过每个节点一次且仅且一次。
哈密顿回路(旅行商问题),通过每个节点一次且仅且一次,最终回到起点。
访问所有节点的最短路径
NP,无多项式解。编码bfs的”状态节点“,图遍历。
int shortestPathLength(vector<vector<int>>& graph) {
// 编码所有节点的访问状态到二进制covered,当前节点为cur
// bfs的"状态"为 (covered,cur)
const int N = graph.size();
const int VISIT_ALL = (1 << N) - 1;
vector<vector<bool>> visited(1 << N, vector<bool>(N, false));
queue<array<int, 2>> q;
for (int i = 0; i < N; i++) {
q.push({1 << i, i});
}
int dist = 0;
while (!q.empty()) {
for (int sz = q.size(); sz > 0; sz--) {
auto [covered, cur] = q.front(); q.pop();
if (visited[covered][cur]) continue;
visited[covered][cur] = true;
if (covered == VISIT_ALL) return dist;
for (auto next : graph[cur]) {
q.push({covered | (1 << next), next});
}
}
dist++;
}
return -1;
}
或者dp解法
class Solution {
const int INF = 1e7;
public:
int shortestPathLength(vector<vector<int>>& graph) {
const int N = graph.size();
vector<vector<int>> dist(N, vector<int>(N, INF));
for (int i = 0; i < N; i++) {
for (int j : graph[i]) {
dist[i][j] = 1;
}
}
floyd(dist);
return shortestHamilton(dist);
}
void floyd(vector<vector<int>> &dist) {
// 求所有点对最短距离
const int N = dist.size();
for (int k = 0; k < N; k++) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
int shortestHamilton(vector<vector<int>> &dist) {
// dp求哈密顿路径
// 设dp[group][dst]表示经过集合group中的所有节点、
// 并最终停在group中dst节点的最短路径,
// 在group中选一节点u、在group外选一节点v,u松弛v
// dp[group+{v}][v] = min( dp[group][u] + dist[u][v] )
// 其中dist[u][v]表示u->v的最短路径,可用Floyd-Warshall算法三重循环松弛得到
const int N = dist.size();
vector<vector<int>> dp(1<<N, vector<int>(N, INF));
for (int i = 0; i < N; i++) {
dp[1<<i][i] = 0;
}
for (int group = 1; group < (1<<N); group++) {
// group中选一点u,group外选一点v,u松弛v
for (int u = 0; u < N; u++) {
int umask = 1<<u;
if (!(group & umask)) continue;
for (int v = 0; v < N; v++) {
int vmask = 1<<v;
if (group & vmask) continue;
dp[group|vmask][v] = min(dp[group|vmask][v],
dp[group][u] + dist[u][v]);
}
}
}
// 从所有点出发的最小值
int ans = INF;
for (int i = 0; i < N; i++) {
ans = min(ans, dp[(1<<N)-1][i]);
}
return ans;
}
};
也有这么写dp的
int shortestHamilton(vector<vector<int>> &dist) {
// dp求哈密顿路径
// 设dp[group][dst]表示经过集合group中的所有节点、
// 并最终停在group中dst节点的最短路径,
// 在group中选一节点u、在group-{u}中选一点k,k松弛u
// dp[group][u] = min( dp[group-{u}][k] + dist[k][u] )
// 其中dist[u][v]表示u->v的最短路径,可用Floyd-Warshall算法三重循环松弛得到
const int N = dist.size();
vector<vector<int>> dp(1<<N, vector<int>(N, INF));
for (int i = 0; i < N; i++) {
dp[1<<i][i] = 0;
}
for (int group = 1; group < (1<<N); group++) {
// group中选一点u、group-{u}中选一点k,k松弛u
for (int u = 0; u < N; u++) {
int umask = 1<<u;
if (!(group & umask)) continue;
for (int k = 0; k < N; k++) {
int kmask = 1<<k;
if (!(group ^ umask) & kmask) continue;
dp[group][u] = min(dp[group][u],
dp[group^umask][k] + dist[k][u]);
}
}
}
// 从所有点出发的最小值
int ans = INF;
for (int i = 0; i < N; i++) {
ans = min(ans, dp[(1<<N)-1][i]);
}
return ans;
}
旅行商问题
初始化dp[1][0]=0(假设起点为0),先求最短哈密顿路径,最后
// 哈密顿环从顶点0出发、回到顶点0
int ans = INF;
for (int i = 0; i < N; i++) {
ans = min(ans, dp[(1<<N)-1][i] + dist[i][0]);
}
最小生成树MST
- https://leetcode.com/problems/min-cost-to-connect-all-points/
kruskal加边法
并查集+优先队列。各个顶点先自成集合,按边权重升序遍历边。若边的两个顶点属于不同集合,则选择该边,并将两集合合并。
int minCostConnectPoints(vector<vector<int>>& points) {
// 最小生成树的Kruskal算法,并查集+优先队列
// 完全连接E=O(N^2),所以O(ElgE)=O(N^2*2N)=O(N^3)
const int N = points.size();
vector<int> uf(N);
iota(begin(uf), end(uf), 0);
using arr3 = array<int, 3>;
auto cmp = [](arr3 &a, arr3 &b) {
return a[0] > b[0]; // 最小堆
};
priority_queue<arr3, vector<arr3>, decltype(cmp)> pq(cmp);
for (int i = 0; i < N; i++) {
for (int j = i + 1; j < N; j++) {
int cost = distance(i, j, points);
pq.push({cost, i, j});
}
}
int ans = 0, cnt = 0;
while (!pq.empty() && cnt < N - 1) { // MST有N-1条边
const auto [cost, x, y] = pq.top(); pq.pop();
// unite
int px = find(x, uf), py = find(y, uf);
if (px != py) {
uf[py] = px;
ans += cost;
cnt++;
}
}
return ans;
}
int find(int x, vector<int> &uf) {
if (uf[x] != x) uf[x] = find(uf[x], uf);
return uf[x];
}
int distance(int u, int v, vector<vector<int>> &points) {
auto &pu = points[u], &pv = points[v];
return abs(pu[0] - pv[0]) + abs(pu[1] - pv[1]);
}
prim加点法
当把顶点u加入MST时,松弛所有未访问顶点v,d[v]=min(d[v],cost(u,v)),d[v]表示顶点v到最小生成树MST的距离。
而dijkstra算法用d[v]=min(d[v],dist[u]+cost(u,v))来松弛,d[v]表示顶点v到源点的距离,区别只有这一点。
int minCostConnectPoints(vector<vector<int>>& points) {
// 最小生成树的Prim算法,图遍历+优先队列
// O(N^2)
const int N = points.size();
vector<int> dist(N, INT_MAX); // 各节点到最小生成树的距离
dist[0] = 0;
using arr2 = array<int, 2>; // [dist, idx]
auto cmp = [](arr2 &a, arr2 &b) {
return a[0] > b[0]; // 最小堆
};
priority_queue<arr2, vector<arr2>, decltype(cmp)> pq(cmp);
pq.push({dist[0], 0});
int ans = 0, cnt = 0;
vector<int> visited(N, false);
while (!pq.empty() && cnt < N) { // MST有N个顶点
auto [d, u] = pq.top(); pq.pop();
if (visited[u]) continue;
visited[u] = true;
ans += d;
cnt++;
for (int v = 0; v < N; v++) {
if (visited[v]) continue;
int newdist = distance(u, v, points);
if (newdist < dist[v]) {
dist[v] = newdist;
pq.push({newdist, v});
}
}
}
return ans;
}
int distance(int u, int v, vector<vector<int>> &points) {
auto &pu = points[u], &pv = points[v];
return abs(pu[0] - pv[0]) + abs(pu[1] - pv[1]);
}
拓扑排序
方法一:bfs不断删除入度为0的点。见ctci p632
入度为0的节点先入队,然后出队时把它所有邻居的入度减1,并把新入度为0的节点入队。这就是bfs,因已按特定规则入队,可省略visited[]数组。
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
// 拓扑排序:不断删除入度为0的点,其实就是bfs
vector<vector<int>> graph(numCourses);
vector<int> indegree(numCourses, 0);
for (auto &edge : prerequisites) {
graph[edge[1]].push_back(edge[0]);
indegree[edge[0]]++;
}
queue<int> q;
for (int i = 0; i < numCourses; i++) {
if (indegree[i] == 0) q.push(i);
}
int count = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
count++;
for (int to : graph[u]) {
if (--indegree[to] == 0) q.push(to);
}
}
return count == numCourses;
}
方法二:点的后序编号=>逆拓扑排序,即在访问完所有邻接点后、当前点进输出栈。可以从任意节点开始后序dfs,只要最外层循环把所有节点都跑一遍。
边的后序编号<=>逆欧拉路径。类似:点的后序编号=>逆拓扑排序。
把边加入visited[]数组<=>从图中删除边,这就有Hierholzer算法:删除出边再dfs递归,无边可删时把当前点输出到栈。
重构行程
Hierholzer算法
vector<string> findItinerary(vector<vector<string>>& tickets) {
// 已知欧拉路径存在且从JFK开始,一张机票一条边,求遍历所有边的欧拉路径
// Hierholzer算法:删除出边再dfs递归,无边可删时把当前点输出到栈。
unordered_map<string, multiset<string>> adj;
for (auto &ticket : tickets)
adj[ticket[0]].insert(ticket[1]);
vector<string> ans;
dfs("JFK", adj, ans);
reverse(ans.begin(), ans.end()); // 逆欧拉路径
return ans;
}
void dfs(const string &from, unordered_map<string, multiset<string>> &adj, vector<string> &ans) {
auto &tos = adj[from];
while (!tos.empty()) {
auto next = *tos.begin();
tos.erase(tos.begin());
dfs(next, adj, ans);
}
ans.push_back(from); // ans当栈用
}
开组合锁
正常使用visited[]数组
string crackSafe(int n, int k) {
// 要求能覆盖全部密码组合,就是要找欧拉路径
// 每个节点有k条出边和k条入边,入度==出度,一定有欧拉回路
// n-1位数作为节点,边是 (后n-1位数)+any['0'..'k'),下一节点是边的后n-1位数
// 边的后序遍历编号等于逆欧拉回路
unordered_set<string> visited;
string str(n, '0');
visited.insert(str);
string ans;
function<void(const string &)> dfs = [&](const string &prefix) {
for (int i = 0; i < k; i++) { // 遍历所有未访问的边
string x = to_string(i);
auto next = prefix + x;
if (visited.count(next)) continue;
visited.insert(next);
dfs(next.substr(1));
ans += x; // 后序遍历
}
};
dfs(str.substr(1));
ans += str; // 后序遍历
reverse(begin(ans), end(ans)); // 逆欧拉路径
return ans;
}
tarjan算法找桥
vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
// 不在环上就是关键连接(桥)
vector<vector<int>> graph(n);
for (const auto& edge : connections) {
graph[edge[0]].push_back(edge[1]);
graph[edge[1]].push_back(edge[0]);
}
int time = -1; // 访问顺序计时
vector<int> timestamp(n, -1); // 最初访问时给节点编号
vector<int> lowlink(n, -1); // 能访问到最早的非父节点
vector<vector<int>> ans;
function<void(int,int)> dfs = [&](int u, int parent) {
timestamp[u] = lowlink[u] = ++time;
for (int v : graph[u]) {
if (v == parent) continue;
if (timestamp[v] == -1) { // 子节点未访问
dfs(v, u);
lowlink[u] = min(lowlink[u], lowlink[v]);
// bridge:子节点能访问到的最早非父节点 > 父节点
if (lowlink[v] > timestamp[u]) ans.push_back({u, v});
} else { // 子节点其实是递归树中的祖先节点
lowlink[u] = min(lowlink[u], timestamp[v]);
}
}
};
for (int i = 0; i < n; i++) {
if (timestamp[i] == -1)
dfs(i, -1);
}
return ans;
}
图着色就是简单的dfs
dfs是否有环
直接用visited[]数组
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
// 每个节点用dfs查环
vector<vector<int>> graph(numCourses);
for (const auto &edge : prerequisites) {
graph[edge[1]].push_back(edge[0]);
}
vector<bool> visited(numCourses);
for (int i = 0; i < numCourses; i++) {
if (hasCycle(i, graph, visited)) return false;
}
return true;
}
bool hasCycle(int u, const vector<vector<int>> &graph, vector<bool> &visited) {
if (visited[u]) return true;
visited[u] = true;
for (int v : graph[u]) {
if (hasCycle(v, graph, visited)) return true;
}
visited[u] = false;
return false;
}
或者,用color[]数组表示各节点颜色(状态):{ UNVISITED, VISITING, VISITED }。如果已着色(!=UNVISITED),在dfs开头就能判断是否有环(==VISITING)。访问邻接点前把当前节点设为VISITING,访问后把当前节点设为VISITED。
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
// 每个节点用dfs查环,即dfs时遇到标记为VISITING的节点
// 0: UNVISITED, 1: VISITING, 2: VISITED
vector<vector<int>> graph(numCourses);
for (const auto &edge : prerequisites) {
graph[edge[1]].push_back(edge[0]);
}
vector<int> color(numCourses, 0);
for (int i = 0; i < numCourses; i++) {
if (hasCycle(i, graph, color)) return false;
}
return true;
}
bool hasCycle(int u, const vector<vector<int>> &graph, vector<int> &color) {
if (color[u] != 0) return color[u] == 1;
color[u] = 1;
for (int v : graph[u]) {
if (hasCycle(v, graph, color)) return true;
}
color[u] = 2;
return false;
}
是否二分图
dfs或bfs遍历着色。当前节点若已着色,看它是不是希望的颜色就能判断是否二分。若未着色,着色后再看邻接点。
- https://leetcode.com/problems/possible-bipartition/
- https://leetcode.com/problems/is-graph-bipartite/
bool isBipartite(vector<vector<int>>& graph) {
const int N = graph.size();
vector<int> color(N, -1);
for (int i = 0; i < N; i++) {
if (color[i] == -1 && !canColor(i, 0, graph, color)) return false;
}
return true;
}
bool canColor(int node, int c, vector<vector<int>> &graph, vector<int> &color) {
if (color[node] != -1) return color[node] == c;
color[node] = c;
for (int neighbor : graph[node]) {
if (!canColor(neighbor, 1 - c, graph, color)) return false;
}
return true;
}
m着色:能否用最多m种颜色着色
// 回溯法:对[k,..]顶点完成m着色,0<=k<V
bool graphColoring(int k, int m, int color[], bool G[V][V]) {
if (k == V) return true;
for (int c = 0; c < m; c++) {
if (canColor(k, c, color, G)) {
color[k] = c;
if (graphColoring(k + 1, m, color, G)) return true;
color[k] = -1;
}
}
return false;
}
// 能否对顶点k着c色,要看k的邻接点有没有已着c色的
bool canColor(int k, int c, int color[], bool G[V][V]) {
for (int i = 0; i < V; i++) {
if (G[k][i] && color[i] == c) { // 看k的邻居节点i
return false;
}
}
return true;
}
A循环移位可得B <=等价于=> B是AA的子串
1. A循环移位得到的串都是串AA的子串,故B是AA的子串。
2. B是AA的子串,假设在A|A拼接处B被分成B1|B2,A的后半段是B1、前半段是B2,A=B2|B1,A循环移位可得B。
s=pattern*k <=等价于=> (s+s).find(s,1) < s.size()
设d=(s+s).find(s,1),则pattern=s.substr(0,d),k=s.size()/d。
想象将s串平移一小段后仍与原串重叠,平移距离就是pattern长d。
直观上,记两串s1|s2、平移距离d(对应模式子串P)、平移后两串s1'|s2':
1. s1'末尾的P==s2开头的P、故s1末尾的P==s1开头的P
2. s1'开头的P==s1第二个P、故s1开头的P==s1第二个P
3. s1'第二个P==s1第三个P、故s1第二个P==s1第三个P,以此类推...
联系:最短长度编码字符串
从目录结构串中找最长绝对路径
int lengthLongestPath(string input) {
istringstream iss(input);
string line;
stack<int> stk;
int ans = 0;
while (getline(iss, line)) { // 遍历\n每行路径,并将绝对路径长压栈
int indent = 0;
while (line[indent] == '\t') indent++;
while (stk.size() > indent) stk.pop(); // 父目录数等于缩进数
int filelen = line.size() - indent;
int pathlen = (stk.empty() ? 0 : stk.top() + 1) + filelen; // 1是分隔符长
stk.push(pathlen);
if (line.find(".") != string::npos) ans = max(ans, pathlen);
}
return ans;
}
括号的解析
递归法,假设parse(const string &s, int &idx)函数能解析一层')',内部的递归调用解析掉内层括号
while (idx < N && s[idx] != ')') {
if (s[idx] == '(') {
idx++; // (
auto paren = parse(formula, idx); // 递归解析内层
idx++; // )
...
} else {
...
}
}
或者用遇'('++、遇')'--的计数来确认合法括号的结束位置(当count==0时)
化学式中各原子数
string countOfAtoms(string formula) {
int idx = 0;
auto count = parse(formula, idx);
ostringstream oss;
for (auto &e : count) {
oss << e.first;
if (e.second > 1) oss << e.second;
}
return oss.str();
}
// 解析一层括号
map<string, int> parse(const string &formula, int &idx) {
const int N = formula.size();
map<string, int> count;
while (idx < N && formula[idx] != ')') {
if (formula[idx] == '(') {
idx++; // (
auto paren = parse(formula, idx);
idx++; // )
int num = parseNum(formula, idx);
for (auto &e : paren) {
count[e.first] += e.second * num;
}
} else {
auto name = parseName(formula, idx);
int num = parseNum(formula, idx);
count[name] += num;
}
}
return count;
}
int parseNum(const string &formula, int &idx) {
int start = idx;
while (idx < formula.size() && isdigit(formula[idx])) idx++;
if (idx == start) return 1;
return stoi(formula.substr(start, idx - start));
}
string parseName(const string &formula, int &idx) {
int start = idx;
idx++; // upper case
while (idx < formula.size() && islower(formula[idx])) idx++;
return formula.substr(start, idx - start);
}
解析k[encoded]串
string decodeString(string s) {
int idx = 0;
return decodeString(s, idx);
}
// 能decode掉最外一层[xxx]
string decodeString(string &s, int &idx) {
string ans;
while (idx < s.size() && s[idx] != ']') {
if (isalpha(s[idx])) {
ans += s[idx];
idx++;
} else { // 数字开头
int num = 0;
while (idx < s.size() && isdigit(s[idx])) {
num = num * 10 + s[idx] - '0';
idx++;
}
idx++; // [
string sub = decodeString(s, idx);
idx++; // ]
while (num--) ans += sub;
}
}
return ans;
}
加减乘除计算
- https://leetcode.com/problems/basic-calculator/ 加减、括号
- https://leetcode.com/problems/basic-calculator-ii/ 加减、乘除
- https://leetcode.com/problems/basic-calculator-iii/ 加减、乘除、括号
i和ii是iii的特例,iii的栈解法如下
int calculate(string &s) {
// 中缀转后缀,用nums栈和ops栈
// 遇到下一个优先级<=栈顶的op时、先弹出计算再把op入栈。
// 左括号'('先入栈,等遇到右括号')'时不断弹出计算直到'(';
// 左括号只能遇右括号时弹出,将'('的优先级设为最小,以防止被其他op计算误弹出。
unordered_map<char, int> priority = {
{'*', 2}, {'/', 2},
{'+', 1}, {'-', 1},
{'(', 0},
};
stack<int> nums;
stack<char> ops;
for (int i = 0; i < s.size(); i++) {
char c = s[i];
if (c == ' ') continue;
if (isdigit(c)) {
// 因为最后会i++,这里解析完num后i指针必须停在最后一位数字上
int num = c - '0';
while (i + 1 < s.size() && isdigit(s[i+1]))
num = num * 10 + s[++i] - '0';
nums.push(num);
} else if (c == '(') {
ops.push(c);
} else if (c == ')') {
while (ops.top() != '(')
calc(nums, ops);
ops.pop(); // 弹出'('
} else { // +-*/
while (!ops.empty() && priority[c] <= priority[ops.top()])
calc(nums, ops);
ops.push(c);
}
}
while (!ops.empty())
calc(nums, ops);
return nums.top();
}
void calc(stack<int> &nums, stack<char> &ops) {
if (ops.empty() || nums.size() < 2) return;
char op = ops.top(); ops.pop();
int val2 = nums.top(); nums.pop();
int val1 = nums.top(); nums.pop();
if (op == '*') {
nums.push(val1 * val2);
} else if (op == '/') {
nums.push(val1 / val2);
} else if (op == '+') {
nums.push(val1 + val2);
} else if (op == '-') {
nums.push(val1 - val2);
}
}
三元表达式解析
Given a string representing arbitrarily nested ternary expressions, calculate the result of the expression. You can always assume that the given expression is valid and only consists of digits
0-9,?,:,TandF(TandFrepresent True and False respectively).Note:
- The length of the given string is ≤ 10000.
- Each number will contain only one digit.
- The conditional expressions group right-to-left (as usual in most languages).
- The condition will always be either
TorF. That is, the condition will never be a digit.- The result of the expression will always evaluate to either a digit
0-9,TorF.Example 1:
Input: "T?2:3" Output: "2" Explanation: If true, then result is 2; otherwise result is 3.Example 2:
Input: "F?1:T?4:5" Output: "4" Explanation: The conditional expressions group right-to-left. Using parenthesis, it is read/evaluated as: "(F ? 1 : (T ? 4 : 5))" "(F ? 1 : (T ? 4 : 5))" -> "(F ? 1 : 4)" or -> "(T ? 4 : 5)" -> "4" -> "4"Example 3:
Input: "T?T?F:5:3" Output: "F" Explanation: The conditional expressions group right-to-left. Using parenthesis, it is read/evaluated as: "(T ? (T ? F : 5) : 3)" "(T ? (T ? F : 5) : 3)" -> "(T ? F : 3)" or -> "(T ? F : 5)" -> "F" -> "F"
string parseTernary(string expression) {
// 三元运算是右结合,所以先处理最右的'?',从右往左扫
stack<char> stk;
for (int i = (int)expression.size() - 1; i >= 0; i--) {
char c = expression[i];
if (c == '?') {
auto expr1 = stk.top(); stk.pop();
stk.pop(); // 略过:
auto expr2 = stk.top(); stk.pop();
stk.push(expression[i-1] == 'T' ? expr1 : expr2);
i--; // 略过?前的T或F
} else {
stk.push(c);
}
}
return string(1, stk.top());
}
通配符匹配
支持:?匹配单个字母 和 *匹配 0 或多个字母
bool isMatch(string s, string p) {
// 设dp[i][j]表示s[i..]和p[j..]匹配,则
// 当s[i]==p[j] || p[j]=='?'时,dp[i][j] = dp[i+1][j+1]
// 当p[j]=='*'时,dp[i][j] = dp[i][j+1] /*匹配0个*/ || dp[i+1][j] /*匹配1个或多个*/
// 否则,dp[i][j]=false
// 初始化,s匹配完、p也匹配完,dp[M][N]=true;或者,s匹配完、p剩下的全是*,dp[M][,..]=true
const int M = s.size(), N = p.size();
vector<vector<bool>> dp(M + 1, vector<bool>(N + 1, false));
dp[M][N] = true;
for (int j = N - 1; j >= 0 && p[j] == '*'; j--) {
dp[M][j] = true;
}
for (int i = M - 1; i >= 0; i--) {
for (int j = N - 1; j >= 0; j--) {
if (s[i] == p[j] || p[j] == '?') dp[i][j] = dp[i+1][j+1];
else if (p[j] == '*') dp[i][j] = dp[i][j+1] || dp[i+1][j];
else dp[i][j] = false;
}
}
return dp[0][0];
}
正则表达式匹配
支持:. 匹配任意字母 和 x*匹配 0 或多个 x
bool isMatch(string s, string p) {
// 设dp[i][j]表示s[i..]和p[j..]匹配,0<=i<=M,0<=j<=N,则
// matchOne = i<M && (s[i]==p[j] || p[j]=='.')
// 当p[j+1]=='*'
// dp[i][j] = dp[i][j+2] // 匹配0次
// dp[i][j] ||= matchOne && dp[i+1][j] // 递归匹配多次
// 当p[j+1]!='*'
// dp[i][j] = matchOne && dp[i+1][j+1]
// 初始dp[M][N]=true, dp[<M][N]=false
const int M = s.size(), N = p.size();
vector<vector<bool>> dp(M + 1, vector<bool>(N + 1, false));
dp[M][N] = true; // dp[][N]的其他情况为false
for (int i = M; i >= 0; i--) {
for (int j = N - 1; j >= 0; j--) {
bool matchOne = i < M && (s[i] == p[j] || p[j] == '.');
if (j + 1 < N && p[j+1] == '*') { // look ahead
dp[i][j] = dp[i][j+2] || (matchOne && dp[i+1][j]);
} else {
dp[i][j] = matchOne && dp[i+1][j+1];
}
}
}
return dp[0][0];
}
// 递归
bool isMatch(string s, string p) {
return match(s, 0, p, 0);
}
bool match(const string &s, int si, string &p, int pi) {
const int M = s.size(), N = p.size();
if (pi == N) return si == M;
bool matchOne = si < M && (s[si] == p[pi] || p[pi] == '.');
if (pi + 1 < N && p[pi+1] == '*') {
return match(s, si, p, pi + 2) || (matchOne && match(s, si + 1, p, pi));
} else {
return matchOne && match(s, si + 1, p, pi + 1);
}
}
模式匹配
- https://leetcode.com/problems/word-pattern/
- https://leetcode.com/problems/find-and-replace-pattern/
bool wordPattern(string pattern, string str) {
// 把一一对应的pattern中字母和str中单词同时映射到[1..N]
unordered_map<char, int> p2i;
unordered_map<string, int> w2i;
string w;
istringstream iss(str);
int i = 0, N = pattern.size();
while (iss >> w) {
if (i == N) return false; // str中单词更多
char p = pattern[i];
if (p2i[p] != w2i[w]) return false;
p2i[p] = w2i[w] = ++i;
}
return i == N;
}
用rand5()生成rand7()随机数
rejection sampling:用两个rand5()调用5*rand5()+rand5()生成[0..24]的平均分布,使用其中的[0..20],抛掉多余的范围
int rand7() {
while (true) {
int num = 5 * rand5() + rand5();
if (num < 21) return num % 7;
}
}
n个数中选k个
记录还要选几个select、还剩几个可选remaining,每个数以p = 还要选几个/还剩几个可选的概率选中
vector<int> ans;
int select = k, remaining = n;
for (int i = 0; i < n; i++) {
if (rand() % remaining < select) {
ans.push_back(i);
select--;
}
remaining--;
}
return ans;
从未知流中选k个值
水库抽样:先选中前k个数,然后生成[0,当前总数count)范围内的随机数r,当r<k时将ans[r]换成当前数,即当前数以p = 要选的个数/当前总数的概率选中
vector<int> ans;
int count = 0;
for (int num : inputstream) {
count++;
if (count <= k) {
ans.push_back(num);
} else {
int r = rand() % count;
if (r < k) ans[r] = num;
}
}
return ans;
从未知流中选1个值
水库抽样的特例,以 1/当前总数 的概率替换旧选
class Solution {
vector<int> nums;
public:
Solution(vector<int> nums) : nums(nums) {
srand(time(NULL));
}
int pick(int target) {
int ans = -1;
int count = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] != target) continue;
count++;
// 以1/count概率替换旧选
if (rand() % count == 0) ans = i;
}
return ans;
}
};
带权重抽样
- https://leetcode.com/problems/random-pick-with-weight/
- https://leetcode.com/problems/random-point-in-non-overlapping-rectangles/
class Solution {
vector<int> wsum;
public:
Solution(vector<int> w) {
partial_sum(begin(w), end(w), back_inserter(wsum));
srand(time(NULL));
}
int pickIndex() {
// 按概率选中某项 <=> 从左往右按累计概率选中某项(从左往右保证了排除掉前面的累计概率,只剩当前项的概率)
int rnd = rand() % wsum.back();
// 要rnd<wsum[i],在wsum中找第一个>rnd的位置
return upper_bound(begin(wsum), end(wsum), rnd) - begin(wsum);
}
};
下标重映射
数组中有些元素不能选
class Solution {
unordered_map<int, int> mapping; // 下标重映射
int M;
public:
Solution(int N, vector<int> blacklist) {
unordered_set<int> st;
for (int b : blacklist) {
st.insert(b);
}
M = N - st.size();
// N个数中只能选M个,将idx<M的不可选的数 =映射到=> idx>=M的可选的数
for (int b : blacklist) {
if (b < M) {
while (st.count(N - 1)) N--;
mapping[b] = N - 1;
N--;
}
}
srand(time(NULL));
}
int pick() {
int idx = rand() % M;
return mapping.count(idx) ? mapping[idx] : idx;
}
};
已选中的元素不能再选
class Solution {
// 把2D矩阵当作1D数组处理
// Fisher-Yates洗牌:生成[0,n-1]随机数r,交换r和n-1,n--
unordered_map<int, int> mapping; // 记录r和n-1的交换操作
int rows, cols, n;
public:
Solution(int n_rows, int n_cols) {
rows = n_rows;
cols = n_cols;
n = rows * cols;
srand(time(NULL));
}
vector<int> flip() {
int r = rand() % n;
// 交换r和n-1索引在mapping中的值
int tmp = getMapping(r);
mapping[r] = getMapping(n-1);
mapping[n-1] = tmp;
n--;
return { tmp / cols, tmp % cols };
}
int getMapping(int idx) {
return mapping.count(idx) ? mapping[idx] : idx;
}
void reset() {
mapping.clear();
n = rows * cols;
}
};
空瓶换满瓶
int numWaterBottles(int numBottles, int numExchange) {
// 比如3个空瓶=>1瓶水+1个空瓶,实际上2个空瓶=>1瓶水,但是
// 要触发这个交换,需要手中持有1个空瓶,下面的(numBottles-1)就是持有1个空瓶
return numBottles + (numBottles - 1) / (numExchange - 1);
}
第k个排列
string getPermutation(int n, int k) {
// 首元素固定后剩余元素有(n-1)!种排列,
// 第k(0-based)个排列的首元素是 k/(n-1)!
vector<int> f(n, 1); // f[i]=i!
for (int i = 1; i < n; i++)
f[i] = i * f[i-1];
string s;
for (int i = 1; i <= n; i++)
s += '0' + i;
k--; // k变成0-based
string ans;
while (n) {
int idx = k / f[n-1];
ans += s[idx];
k %= f[n-1];
n--;
s.erase(s.begin() + idx); // 剩余元素仍有序
}
return ans;
}
生成下一排列
- https://leetcode.com/problems/next-permutation
- https://leetcode.com/problems/next-greater-element-iii/
// 生成"下一排列":
// 1. 从右往左找第一个波峰前的数,即找第一个nums[i]<nums[i+1]的位置i
// 2. 在i右边、从波峰往右是个递减序列,从右往左肯定能找到第一个>nums[i]的位置j
// 3. 交换nums[i]和nums[j],交换后从波峰往右仍是个递减序列
// 4. 反转从波峰往右这个递减序列
void nextPermutation(vector<int>& nums) {
const int N = nums.size();
int i = N - 2;
while (i >= 0 && nums[i] >= nums[i+1]) i--;
if (i < 0) {
reverse(nums.begin(), nums.end());
return;
}
int j = N - 1;
while (j > i && nums[j] <= nums[i]) j--;
swap(nums[i], nums[j]);
reverse(nums.begin() + i + 1, nums.end());
}
小于n的素数有几个
int countPrimes(int n) {
// 素数筛法
vector<bool> prime(n, true);
for (int i = 2; i * i < n; i++) {
if (!prime[i]) continue;
for (int j = i * i; j < n; j += i) {
prime[j] = false;
}
}
int count = 0;
for (int i = 2; i < n; i++) {
if (prime[i]) count++;
}
return count;
}
求a^b mod 1337
b是各位数字存在数组中的大数
int superPow(int a, vector<int>& b) {
// 幂太大,将幂分解。
// 例如,37^213 mod K = 37^(210+3) mod K
// = ((37^21 mod K)^10 mod K) * (37^3 mod K)
if (b.empty()) return 1;
const int K = 1337;
int lastDigit = b.back(); b.pop_back();
return (pow(superPow(a, b), 10, K) * pow(a, lastDigit, K)) % K;
}
// 幂较小用该函数计算,a^b mod K
int pow(long a, int b, int K) {
int ans = 1;
while (b) {
if (b & 1) ans = (ans * a) % K;
a = (a * a) % K;
b >>= 1;
}
return ans;
}
不用*、/,只用+、-、移位作两正数相乘
int minProduct(int a, int b) {
if (a > b) swap(a, b);
return rMinProduct(a, b);
}
int rMinProduct(int small, int big) {
if (small == 0) return 0;
if (small == 1) return big;
int halfProd = rMinProduct(small >> 1, big);
int prod = halfProd << 1;
if (small & 1) prod += big;
return prod;
}
任务调度
每个任务要单位时间执行,相同任务间要n单位冷却时间。有一些字母表示的任务ABC...,最少需要多少时间。
数量最多的任务t1先排,放1个空n个地排,t1间的(maxCnt-1)个空隔定出框架frame=(maxCnt - 1) * (1 + n)。最后一个t1超出框架1;次多的t2紧靠t1右侧放置,若cnt(t2)==maxCnt则最后一个t2也超出框架1,t3以此类推,共超出框架tasksWithMaxCnt,总共需要frame+tasksWithMaxCnt。其他任务用来填框架,填不满时需要如上时间,填满时需要时间taskCnt。
总时间 max{ (maxCnt - 1) * (1 + n) + tasksWithMaxCnt, tackCnt }
ACCCBBEEE 2
放C: CXXCXXC // CXXCXX算frame,frame=(3-1)*(1+2)=6
放E:CEXCEXCE // 加上最后的CE后frame+tasksWithMaxCnt=8
放A: CEACEXCE
放B: CEACEBCE
CEABCEBCE // 总时间=max{frame+tasksWithMaxCnt,taskCnt}=max(8,9)=9
int leastInterval(vector<char>& tasks, int n) {
vector<int> count(26, 0);
for (char t : tasks) count[t - 'A']++;
int maxCnt = INT_MIN;
int tasksWithMaxCnt = 0;
for (int cnt : count) {
if (cnt > maxCnt) {
maxCnt = cnt;
tasksWithMaxCnt = 1;
} else if (cnt == maxCnt) {
tasksWithMaxCnt++;
}
}
return max((maxCnt - 1) * (1 + n) + tasksWithMaxCnt, (int)tasks.size());
}
按行按列翻转01位矩阵、各行之数的和最大多少
int matrixScore(vector<vector<int>>& A) {
// 2^n > 2^(n-1) + 2^(n-2) + ... + 1 = 2^n-1,越高位贡献越大。
// 所以 行先翻转,使首列都为1;非首列再翻转,使该列更多1。
// 不做实际的翻转,假想首列已变为1,某格是否为1都根据A[r][c]==A[r][0]判断。
if (A.empty()) return 0;
const int R = A.size(), C = A[0].size();
int ans = 0;
for (int c = 0; c < C; c++) {
int col1s = 0; // 某列有多少个1
for (int r = 0; r < R; r++) {
col1s += A[r][c] == A[r][0];
}
col1s = max(col1s, R - col1s);
int score = 1 << (C - 1 - c); // 某列的1代表多少分
ans += col1s * score;
}
return ans;
}
森林中的兔子数
某些兔子将告诉你多少兔子和它同色,问总共最少有多少兔子?
int numRabbits(vector<int>& answers) {
// 数x每组有x+1个,共有ceil(count[x]/(x+1)) = (count[x]+x)/(x+1)组,
// 所以报数x的兔子数为 (count[x]+x)/(x+1)*(x+1)
unordered_map<int, int> count;
for (int num : answers) count[num]++;
int ans = 0;
for (auto &e : count) {
int x = e.first;
ans += (count[x] + x) / (x + 1) * (x + 1);
}
return ans;
}
洗衣机传递衣服
洗衣机排成一排,每次某些洗衣机向邻居传一件衣服,要几次能使各洗衣机衣服数相等?
int findMinMoves(vector<int>& machines) {
const int N = machines.size();
int total = 0;
for (int machine : machines)
total += machine;
if (total % N != 0) return -1;
// 1. 每个洗衣机要发出衣服:out[i] = machines[i]-avg,
// 一次只能发一件,需要out[i]次
// 2. 通过本机向另一侧发送的件数 x = abs( sum(out[0..i] )),
// sum为正向右流、为负向左流,又需要x次
int avg = total / N;
int sum = 0;
int ans = 0;
for (int i = 0; i < N; i++) {
int out = machines[i] - avg;
sum += out; // sum(out[0..i])
ans = max({ans, out, abs(sum)});
}
return ans;
}
镜子反射
int mirrorReflection(int p, int q) {
// 反射相当于方形折叠展开、光线穿透直射
// 当 (k*q)%p == 0 时到达接收器,k最小时 k*q == lcm(p,q) == p*q/gcd(p,q)
// 所以,横向方形数k = p/gcd(p,q),纵向方形数k*q/p = q/gcd(p,q)
// 只需考查横向、纵向方形数的奇偶性
int g = gcd(p, q);
p /= g, p %= 2; // 横向
q /= g, q %= 2; // 纵向
if (p == 0) return 2;
if (q == 0) return 0;
return 1;
}
int gcd(int p, int q) {
if (q == 0) return p;
return gcd(q, p % q);
}
关键是对删除后的剩余情况重新编号,再看新编号怎么映射回原先编号。
约瑟夫环问题
0 ~ i-1的i个数字排成环,每次数到m删一个数字,最后剩下哪个数?
第一次删除数字(m-1)%i,删除数字后开始重新编号0 ~ i-2,比如被删除数后面的数((m-1)+1)%i = m%i =重编号=> 0。反过来,0=映射回=> m%i,从删除后=映射回=>删除前 p(x)=(x+m)%i,i是删除前的个数。从最终只剩x=0逆推。
int ysf(int n, int m) {
// 例如,(1+m) % len => 1,反过来就是 1 => (1+m) % len
// 推广到 x => (x+m) % len
int x = 0; // 涉及到模运算,从0开始
for (int i = 2; i <= n; i++) {
x = (x + m) % i;
}
return x + 1; // 从1开始编号
}
最小因数分解
Given a positive integer
a, find the smallest positive integerbwhose multiplication of each digit equals toa.If there is no answer or the answer is not fit in 32-bit signed integer, then return 0.
Example 1
Input:48Output:
68Example 2
Input:15Output:
35
int smallestFactorization(int a) {
if (a < 10) return a;
// a要分解成单数字因子,且个数越少越好,所以从9到2地尝试分解
long ans = 0, base = 1;
for (int i = 9; i >= 2; i--) {
while (a % i == 0) {
ans += i * base;
if (ans > INT_MAX) return 0;
a /= i;
base *= 10;
}
}
return (a == 1) ? ans : 0;
}
-2进制转换
string baseNeg2(int N) {
// 不能用%-2操作,因为需要余数为正
string ans;
while (N) {
int rem = N & 1;
ans = to_string(rem) + ans;
N = -(N >> 1);
}
return !ans.empty() ? ans : "0";
}
删除9
Start from integer 1, remove any integer that contains 9 such as 9, 19, 29...
So now, you will have a new integer sequence: 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, ...
Given a positive integer
n, you need to return the n-th integer after removing. Note that 1 will be the first integer.Example 1:
Input: 9 Output: 10Hint: n will not exceed
9 x 10^8.
int newInteger(int n) {
// 删除9后的数正好就是九进制数,这题就是把十进制转成九进制
// 扩展:如果删除7,还是九进制,只是结果需映射7=>8,8=>9。
int ans = 0, base = 1;
while (n) {
ans += (n % 9) * base; // 从后往前拼上
n /= 9;
base *= 10;
}
return ans;
}
相似的RGB颜色
string similarRGB(string color) {
return "#" + similarColor(color.substr(1, 2))
+ similarColor(color.substr(3, 2))
+ similarColor(color.substr(5, 2));
}
string similarColor(const string &ab) {
const string mapping = "0123456789abcdef";
// #ab要变成#xx,#xx=16*x+x=17x,所以要找最接近#ab的17的倍数
int num = stoi(ab, NULL, 16);
int x = (num + 8) / 17; // "四舍五入"
return string(2, mapping[x]);
}
n!末尾有多少个0
- https://leetcode.com/problems/factorial-trailing-zeroes/
int trailingZeroes(int n) {
// n!末尾0的个数 等于 [1..n]中因子5的个数
// 即 n/5 + n/25 + n/125 + ...
int ans = 0;
while (n /= 5) {
ans += n;
}
return ans;
}
满足x!末尾刚好K个0的数x有多少个
参见二分搜索的应用
0到n中有多少数字'1'
int countDigitOne(int n) {
// 从低位到高位一位位地看,某一位有多少个'1',要看该位是 <1、>1、=1?
// 需要用该位代表的数base、左边(不含该位)构成的数higher、右边(不含该位)构成的数lower。
// 该位<1 => higher*base
// 如2025的百位(100、...、199、1100、...、1199)=> 2*100
// 该位>1 => (higher+1)*base
// 如2225的百位(100、...、199、1100、...、1199、2100、...、2199)=> (2+1)*100
// 该位=1 => higher*base + (lower+1)
// 如2125的百位(100、...、199、1100、...、1199、2100、...、2125)=> 2*100+26
if (n <= 0) return 0;
int ans = 0;
long base = 1;
while (base <= n) {
int lower = n % base, curr = (n / base) % 10, higher = (n / base) / 10;
if (curr < 1) {
ans += higher * base;
} else if (curr > 1) {
ans += (higher + 1) * base;
} else {
ans += higher * base + (lower + 1);
}
base *= 10;
};
return ans;
}
整数变换成1
题目:n是偶数时变成n/2,n是奇数时变成n-1或n+1,最终变成1。怎样使替换操作最少?
int integerReplacement(int n) {
if (n == INT_MAX) return 32; // n=2^31-1
// n为奇数时到底往上n+1还是往下n-1?选下下步是偶数的那个。
// 设n=2k+1 (k>=0),有n-1=2k、n+1=2(k+1),下下步的k和k+1一奇一偶。
// k是偶数时,(n-1)%4==0,选n-1分支;否则,选n+1分支。
// 特例是n==3时选n-1分支要2步、选n+1分支要3步,选n-1分支。
int count = 0;
while (n > 1) {
if (n % 2 == 0) {
n /= 2;
} else {
if ((n - 1) % 4 == 0 || n == 3) {
n--;
} else {
n++;
}
}
count++;
}
return count;
}
第k步走+k或-k,到达数轴上某点的最少步骤
int reachNumber(int target) {
// target<0往数轴左侧和target>0往数轴右侧情况对称
target = abs(target);
// 给1,2,...,k中一些数加上负号,设要加正号的和为A,要加负号的和为B
// A+B=1+2+...+k=sum,A-B=target,B=(sum-target)/2
// 要满足sum>=target且sum-target为偶数
int sum = 0, k = 0;
while (sum < target)
sum += ++k;
int delta = sum - target;
if (delta % 2 == 0) return k;
// 最多再走两步,sum-target总能变成偶数
// 走两步,是因为第一次加的k可能是偶数
delta += ++k;
if (delta % 2 == 0) return k;
return k + 1 ;
}
所有子序列的最大最小差值之和
int sumSubseqWidths(vector<int>& A) {
// 子序列的最大最小值,与顺序无关,排序数组
// 比A[i]大的数有N-1-i个,A[i]作最小值的子序列有2^(N-1-i)个,A[i]对ans贡献2^(N-1-i)*(-A[i])
// 比A[i]小的数有i个,A[i]作最大值的子序列有2^i个,对ans贡献2^i*A[i]
// 所以,A[i]对ans贡献 A[i] * (2^i - 2^(N-1-i))
sort(A.begin(), A.end());
const int N = A.size(), MOD = 1e9 + 7;
vector<int> pow2(N);
pow2[0] = 1;
for (int i = 1; i < N; i++) {
pow2[i] = (pow2[i-1] << 1) % MOD;
}
int ans = 0;
for (int i = 0; i < N; i++) {
ans = (ans + (long)A[i] * (pow2[i] - pow2[N-1-i])) % MOD;
}
return ans;
}
所有满足 子序列最大最小之和<=target 的子序列个数
int numSubseq(vector<int>& A, int target) {
// 子序列的最大最小值与顺序无关,排序数组
sort(A.begin(), A.end());
// 预先计算pow2[]
const int N = A.size(), MOD = 1e9 + 7;
vector<int> pow2(N);
pow2[0] = 1;
for (int i = 1; i < N; i++) {
pow2[i] = (pow2[i-1] << 1) % MOD;
}
// 对A[i],找A[i]+A[j]<=target的最大j,两指针法
int ans = 0;
for (int i = 0, j = N - 1; i <= j; ) {
if (A[i] + A[j] > target) {
j--;
} else {
// 非空子序列,选了A[i]后,(i..j]间的数可选或不选,有2^(j-i)种可能
ans = (ans + pow2[j-i]) % MOD;
i++;
}
}
return ans;
}
所有奇数长子段的总和
int sumOddLengthSubarrays(vector<int>& A) {
// 看A[i]对奇数长子段贡献了多少值
// 左边包含A[i]的子段A[..i]有i+1个,右边包含A[i]的子段A[i..]有N-i个,包含A[i]的子段共(i+1)*(N-i)个。
// 设包含A[i]的奇数子段有odd个,偶数子段有even个,由举例归纳的、应该没错的结论为 odd=even或even+1。
// 注:比如举例统计[1 2 3 4 5]中包含2的长度1子段数、长度2子段数、长度3子段数...,
// 所以奇数长子段有 odd = ((i+1)*(N-i)+1)/2个
const int N = A.size();
int ans = 0;
for (int i = 0; i < N; i++) {
ans += ((i+1)*(N-i)+1)/2 * A[i];
}
return ans;
}
联想:所有子段最小值的和
一些点按顺序能否构成凸包
只要判断每三个点顺序构成的两向量都向一个方向拐。判断两向量向哪个方向拐,只要看叉积:>0逆时针、<0顺时针、=0共线。
向量a和b的叉积\[a \times b = \begin{vmatrix} x_1 & x_2 \ y_1 & y_2 \end{vmatrix} = x_1y_2 - x_2y_1\]
bool isConvex(vector<vector<int>>& points) {
const int N = points.size();
if (N < 3) return false;
int prev = 0;
for (int i = 0; i < N; i++) {
int curr = getDirection(points[i], points[(i+1) % N], points[(i+2) % N]);
if (curr == 0) continue;
if (prev == 0) prev = curr;
else if (curr != prev) return false;
}
return true;
}
int getDirection(const vector<int> &x, const vector<int> &y, const vector<int> &z) {
// x、y、z三点构成a、b两向量,计算a、b的叉积,返回1、-1、0表示拐向
vector<int> a = { y[0] - x[0], y[1] - x[1] };
vector<int> b = { z[0] - y[0], z[1] - y[1] };
int crossProd = a[0] * b[1] - a[1] * b[0];
return crossProd > 0 ? 1 : (crossProd < 0 ? -1 : 0);
}
最大三角形面积
double largestTriangleArea(vector<vector<int>>& points) {
const int N = points.size();
double ans = 0;
for (int i = 0; i < N; i++) {
for (int j = i + 1; j < N; j++) {
for (int k = j + 1; k < N; k++) {
ans = max(ans, area(points[i], points[j], points[k]));
}
}
}
return ans;
}
double area(const vector<int> &x, const vector<int> &y, const vector<int> &z) {
// x、y、z三点可以构造x->y向量a、y->z向量b,计算a、b的叉积
// 根据平行四边形面积=abs(叉积),三角形面积=abs(叉积)*0.5
vector<int> a = { y[0]-x[0], y[1]-x[1] };
vector<int> b = { z[0]-y[0], z[1]-y[1] };
int crossProd = a[0] * b[1] - a[1] * b[0];
return abs(crossProd) * 0.5;
}
四个点能否围成正方形
四个点按x排、x相同再按y排,编号p0p1p2p3。不管怎么歪,四条边都是p0p1、p1p3、p3p2、p2p0,对角线是p0p3、p1p2。只要验证四条边相等、两对角线相等。
bool validSquare(vector<int>& p1, vector<int>& p2, vector<int>& p3, vector<int>& p4) {
vector<vector<int>> p = { p1, p2, p3, p4 };
sort(p.begin(), p.end(), [](vector<int> &a, vector<int> &b) {
return a[0] < b[0] || (a[0] == b[0] && a[1] < b[1]);
});
int side = dist(p[0], p[1]);
return side != 0 && side == dist(p[1], p[3]) && side == dist(p[3], p[2]) && side == dist(p[2], p[0])
&& dist(p[0], p[3]) == dist(p[1], p[2]);
}
int dist(vector<int> &a, vector<int> &b) {
return (a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]);
}
最近点对
先按x坐标x=M将n个点分成两半,左半递归得minDistLeft,右半递归得minDistRight,再来考虑两点各在左右的情况。现在已知最小距离minDist=min(minDistLeft, minDistRight),所以只需考虑x坐标在[M-minDist, M+minDist]区间的点。将这些点按y坐标排序,只考虑 (i.x<M ^ j.x<M) && (j.y - i.y < minDist) 的点对,看能否缩小minDist。
string multiply(string num1, string num2) {
const int M = num1.size(), N = (int)num2.size();
string ans(M + N, 0); // 乘积最多M+N位
for (auto &c : num1) c -= '0';
for (auto &c : num2) c -= '0';
// 将num1和num2从低位算起的第i1位和第i2位相乘,结果加到ans从低位算起的第i1+i2位
// 换成从高位算起的视角,num1[M-1-i1] * num2[N-1-i2] +=> ans[M+N-1-(i1+i2)]
// 设i=M-1-i1,j=N-1-i2,num1[i] * num2[j] +=> ans[i+j+1]
for (int i = M - 1; i >= 0; i--) {
for (int j = N - 1; j >= 0; j--) {
int tmp = num1[i] * num2[j] + ans[i+j+1];
ans[i+j+1] = tmp % 10;
ans[i+j] += tmp / 10;
}
}
for (auto &c : ans) c += '0';
int i = 0;
while (ans[i] == '0') i++;
if (i == ans.size()) return "0";
return ans.substr(i);
}
自定义比较的priority_queue
auto cmp = [](const vector<int> &a, const vector<int> &b) {
return a[0] < b[0]; // priority_queue用less是最大堆
};
priority_queue<vector<int>, vector<vector<int>>, decltype(cmp)> taken(cmp);
另:用multiset作为可支持删除的优先队列
二分搜索函数lower_bound(t)找第一个>=t的位置
// nums中找第一个>=t的位置
auto it = lower_bound(nums.begin(), nums.end(), t);
// 第一个<=t的位置
auto it = upper_bound(nums.begin(), nums.end(), t); // >t的位置
prev(it, 1); // 回退一位,就是<=t的位置
随机数
构造函数中:srand(time(NULL));
使用时:rand() % 10 // 生成[0..9]
自定义比较的set
struct Cmp {
bool operator()(const Interval &a, const Interval &b) {
// 若有插入操作,st的比较函数一定要写全各子段,否则插入只根据部分子段就判重
return a.start < b.start || (a.start == b.start && a.end < b.end);
}
};
set<Interval, Cmp> st;
或者比较函数写在Interval结构中
struct Inteval {
int start, end;
bool operator<(const Interval &rhs) const {
...
}
};
set<Interval> st;
第k小的数
// k是1-based,midptr要加上k-1偏移
auto midptr = nums.begin() + (k-1);
nth_element(nums.begin(), midptr, nums.end());
int median = *midptr;
// 中位数是第(n+1)/2小的数
auto midptr = nums.begin() + (n-1)/2; // (n+1)/2-1=(n-1)/2
// 如果是找第k大的数
nth_element(nums.begin(), midptr, nums.end(), greater<int>());
前k个数排序
partial_sort(A.begin(), A.begin() + K, A.end());
bitset
bitset<10>(h << 6 | m).count()
取绝对值
long x = labs(INT_MIN); // abs(INT_MIN)==INT_MIN,因为溢出
unsigned数中比特1的个数
__builtin_popcount(a)
进制转换
stoi(ab, NULL, 16)
生产者消费者问题
缓冲区不满生产者才能放,缓冲区不空消费者才能拿
#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;
void producer() {
while(TRUE) {
int item = produce_item();
down(&empty); // empty--和后面full++,一个意思两个变量
down(&mutex); // 保护临界区的mutex要写在里面
insert_item(item);
up(&mutex);
up(&full);
}
}
void consumer() {
while(TRUE) {
down(&full);
down(&mutex);
int item = remove_item();
consume_item(item);
up(&mutex);
up(&empty);
}
}
读者写者问题
可以多个读,但不能同时读-写、写-写
typedef int semaphore;
semaphore data_mutex = 1; // 对写数据操作加锁
semaphore count_mutex = 1; // 对count变量加锁
int count = 0; // 读者计数
void reader() {
while(TRUE) {
down(&count_mutex);
count++;
if(count == 1) down(&data_mutex); // 第一个读者要对写数据操作加锁
up(&count_mutex);
read();
down(&count_mutex);
count--;
if(count == 0) up(&data_mutex);
up(&count_mutex);
}
}
void writer() {
while(TRUE) {
down(&data_mutex);
write();
up(&data_mutex);
}
}
哲学家就餐问题
方法一:打破"占有和等待",必须同时拿起两根筷子。
#define N 5
#define LEFT (i + N - 1) % N // 左邻居
#define RIGHT (i + 1) % N // 右邻居
#define THINKING 0
#define HUNGRY 1
#define EATING 2
typedef int semaphore;
int state[N]; // 跟踪每个哲学家的状态
semaphore mutex = 1; // 对state[]加锁
semaphore s[N]; // 每个哲学家一个信号量
void philosopher(int i) {
while(TRUE) {
think();
take_two(i);
eat();
put_two(i);
}
}
void take_two(int i) {
down(&mutex);
state[i] = HUNGRY;
test(i);
up(&mutex);
down(&s[i]);
}
void put_two(i) {
down(&mutex);
state[i] = THINKING;
test(LEFT); // 尝试唤醒邻居
test(RIGHT);
up(&mutex);
}
void test(i) { // 尝试拿起两把筷子
if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) {
state[i] = EATING;
up(&s[i]);
}
}
方法二:打破“环路等待”,所有筷子从0~N-1编号,必须先拿小编号再拿大编号。这样所有哲学家先左手后右手,但某位哲学家先右手后左手。见ctci p451。
死锁检测
用锁顺序有向图检测无环
不会死锁的LockFactory类:factory中存有无环的用锁顺序有向图G。进程向factory声明一个用锁顺序,factory尝试把用锁顺序对应的边加到G上,然后在新增用锁上dfs检查是否有环(在dfs遍历时访问到VISITING状态的节点)。若有环则删除该用锁顺序对应的边;没环就允许这个用锁顺序,并在个map中记下进程id=>用锁顺序。
见ctci p453
银行家算法
已知各进程已占有多少各资源、还需要多少各资源,总共还剩多少各资源。找出还需各资源数<剩余各资源数的进程,标记为完成,释放该进程占有的资源到剩余资源中。如此继续,最后若能标记完所有进程,则无死锁。
单例模式
双重校验+volatile (线程安全)
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile可以禁止JVM的指令重排。比如instance = new Singleton();其实分三步:1. 为新实例分配内存 2. 初始化新实例 3. 将instance指向新实例的内存地址。由于JVM具有指令重排的特性,执行顺序有可能变成1>3>2。这在单线程环境下没问题,但多线程环境下会导致某线程获得未初始化的实例。如线程T1执行了1和3,此时T2调用getInstance()发现instance不为空并返回instance,但此时instance指向的空间还未初始化。
静态内部类 (线程安全)
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
当调用getInstance()时内部类才被加载。