Leetcode 大部分是medium难度不怎么按顺序题解(中)
前言
万万没想到,一年过去了,我还在刷leetcode。。。
上一篇题解里面字太多,编辑器都卡住了,所以另开一篇文档
第一轮面试阴沟里翻车,祈祷我后天二轮能过155551
期末季面试真太顶了,一个礼拜之前接到邮件,因为中间有考试愣是最后只剩下两天准备面试,好家伙
upd: 2021.2.7
凉了凉了,下半年再战吧
感觉最近写代码还是太慢了。小心翼翼不想写出bug的代价就是码代码本身速度变得很慢orz
upd:2021.9.20
我怎么还在刷leetcode???
这一篇东西太多也变卡了。下次再开一篇新的。
我觉得我刷LeetCode就是纯粹找找手感,写完自己都忘了,题解也不好好写,完全没有参考意义。惭愧惭愧。
220. 存在重复元素III
看到这题,第一眼:主席树!第二眼:离散化+树状数组!
然后看了一眼题解学了一下这个hash做法,还是很精妙的
这个题里面有两个距离:k和t。k可以用滑动窗口解决,而t就要把数字分组,比如t=3的时候就分成[0,2][3,5][6,9]....这样。
可以发现,当加入一个数字的时候,如果它那个组里已经有一个数,那就一定输出true。
另外答案还有可能出现在相邻的两个组里。
由于刚才说如果一个组里有两个数直接输出答案,那么可以知道在每个状态下一个组里最多只有一个数。记一下这个数的下标,每次拿出来比较就可以了。
但是这个题主要是,它细节是真的多。。。
最重要的问题就是数字范围问题。由于我这里想直接用数字整除t的结果来分组,必须要把t加一。而这就导致在t=2147483647的时候会爆int,所以我这个写法需要很多强转long long
另外,直接整除在处理负数的时候也会遇到问题。因为这个整除它严格来说是“向零取整”。还是以t=2为例,这会导致0所在的那个组范围实际上是[-2,2]而不是[0,2],就会出问题。
所以我需要让负数向下取整。需要一个特判,在getkey函数中。
另外就是,用map这类东西做哈希表的时候要区分“组为空”或“组里的数字是0”。因为map元素如果是空,访问出来的结果也是0。。。
这里直接就把记录在map里的下标都从1开始了。
写的贼丑。
class Solution {
private:
long long getkey(long long num, long long t) {
if (num >= 0) return num/t;
else return (num/t)-1;
}
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int _t) {
unordered_map<long long, int> table;
int n = nums.size();
if (n == 1) return false;
long long t = (long long)_t+1;
k = k + 1;
for (int i = 0; i < min(n, k); i ++) {
long long key = getkey(nums[i], t);
if (table[key] != 0) return true;
table[key] = i + 1;
if (table[key-1] != 0) {
int tar = table[key-1];
if (abs((long long)nums[i] - nums[tar-1]) < t)
return true;
}
if (table[key+1] != 0) {
int tar = table[key+1];
if (abs((long long)nums[i] - nums[tar-1]) < t)
return true;
}
}
for (int i = k; i < n; i ++) {
long long key = getkey(nums[i-k], t);
table[key] = 0;
key = getkey(nums[i], t);
if (table[key] != 0) return true;
table[key] = i + 1;
if (table[key-1] != 0) {
int tar = table[key-1];
if (abs((long long)nums[i] - nums[tar-1]) < t)
return true;
}
if (table[key+1] != 0) {
int tar = table[key+1];
if (abs((long long)nums[i] - nums[tar-1]) < t)
return true;
}
}
return false;
}
};
221. 最大正方形
二分。因为有边长为k的正方形就一定有边长比k小的正方形,反之亦然。
预处理一个二维前缀和,判定的时候枚举所有边长为mid的正方形即可。
注意处理全0矩阵的情况。
class Solution {
private:
int n, m;
vector<vector<int>> s;
void buildSum(vector<vector<char>> &matrix) {
for (int i = 0; i <= n; i ++) {
vector<int> tmp;
tmp.clear();
for (int j = 0; j <= m; j ++)
tmp.push_back(0);
s.push_back(tmp);
}
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
s[i][j] = (matrix[i-1][j-1]-'0') + s[i-1][j] + s[i][j-1] - s[i-1][j-1];
}
int calc(int x, int y, int xx, int yy) {
return s[xx+1][yy+1] - s[x][yy+1] - s[xx+1][y] + s[x][y];
}
bool check(int len) {
for (int i = 0; i < n-len+1; i ++)
for (int j = 0; j < m-len+1; j ++) {
int sum = calc(i, j, i+len-1, j+len-1);
if (sum == len * len) return true;
}
return false;
}
int divide(int l, int r) {
int mid, ans = l;
while (l <= r) {
mid = (l+r)>>1;
if (check(mid)) {
ans = max(ans, mid);
l = mid + 1;
} else r = mid - 1;
}
return ans*ans;
}
public:
int maximalSquare(vector<vector<char>>& matrix) {
n = matrix.size();
m = matrix[0].size();
buildSum(matrix);
if (s[n][m] == 0) return 0;
return divide(1, min(n, m));
}
};
222. 完全二叉树的节点个数
这个题O(n)的做法很简单,遍历就可以了。
要缩减复杂度的话,考虑它是一个完全二叉树。完全二叉树决定节点个数的就只有两个量:它的高度和它最底层的节点数目。
它的高度可以通过一直顺着左儿子走来求出,而最底层的节点数目可以发现满足二分单调性:从某一个点开始,左边全都有点,右边全都没有点。只要找到这个分界点就可以了。
那么就是每次二分一个mid,检查最底层的第mid个节点存不存在。
可以发现,如果把最底层的节点从左往右从0开始编号,那么它的编号的二进制就描述了从根节点走到它的路径。
举个栗子,就题目里样例的那棵二叉树。底层节点可以顺序编号为0 1 2 3(3号节点不存在)。那么如果要看2号节点存不存在,2号节点的二进制是10
(因为从顶到底最多只需要走两步,所以保留二进制的后两位即可。一般地,一个高度为h的二叉树保留h-1位即可)
2号节点的二进制是10,说明要先往右走一步(1)、再往左走一步(0)。
这样判定的复杂度是\(O(h)\)也就是\(O(logn)\),然后二分的复杂度也是\(O(logn)\),总的复杂度是\(O(log^2n)\)。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
int h;
int getdepth(TreeNode *root) {
int res = 0;
while (root != NULL) {
res ++;
root = root->left;
}
return res;
}
bool check(TreeNode *root, int id) {
for (int i = h-1; i >= 1; i --) {
if ((id >>(i-1)) & 1)
root = root->right;
else root = root->left;
if (root == NULL) return false;
}
return true;
}
int divide(TreeNode *root, int l, int r) {
int mid, ans = 1;
while (l <= r) {
mid = (l+r) >> 1;
if (check(root, mid-1)) {
ans = max(ans, mid);
l = mid + 1;
} else r = mid - 1;
}
return ans;
}
public:
int countNodes(TreeNode* root) {
if (root == NULL) return 0;
h = getdepth(root);
int lastdep = divide(root, 1, 1<<(h-1));
return (1<<(h-1))-1 + lastdep;
}
};
223. 矩形面积
这个题一眼就可以分类讨论,但情况特别多(完全包含的;相离的;左边覆盖的;右边覆盖的等等),如果要算覆盖面积的话非常不好做。
这里采用了离散化+填格子的方法。横纵坐标分别离散化,坐标范围缩小到4*4。然后枚举两个矩阵覆盖的每个格子,覆盖到的+1。最后计算每个被覆盖过的格子面积总和就可以了。
注意最后计算格子面积的时候坐标要用离散化之前的。
class Solution {
private:
int va[10], vb[10], ca, cb;
int mat[10][10];
void trans(int *v, int &cnt, int &n1, int &n2, int &n3, int &n4) {
v[1] = n1; v[2] = n2; v[3] = n3; v[4] = n4;
sort(v+1, v+4+1);
cnt = unique(v+1, v+4+1) - v - 1;
n1 = lower_bound(v+1, v+cnt+1, n1) - v;
n2 = lower_bound(v+1, v+cnt+1, n2) - v;
n3 = lower_bound(v+1, v+cnt+1, n3) - v;
n4 = lower_bound(v+1, v+cnt+1, n4) - v;
}
void add(int x, int y, int xx, int yy) {
for (int i = x; i < xx; i ++)
for (int j = y; j < yy; j ++)
mat[i][j] ++;
}
int getArea(int x, int y) {
int l1 = va[x+1] - va[x];
int l2 = vb[y+1] - vb[y];
return l1 * l2;
}
public:
int computeArea(int A, int B, int C, int D, int E, int F, int G, int H) {
trans(va, ca, A, C, E, G);
trans(vb, cb, B, D, F, H);
add(A, B, C, D);
add(E, F, G, H);
int ans = 0;
for (int i = 1; i <= ca; i ++)
for (int j = 1; j <= cb; j ++)
if (mat[i][j] != 0)
ans += getArea(i, j);
return ans;
}
};
227. 基本计算器
双栈法计算中缀表达式。
需要注意字符串末尾有空格的情况。
class Solution {
private:
int pri(char c) {
if (c == '+' || c == '-')
return 1;
if (c == '*' || c == '/')
return 2;
return 0;
}
int calc(int a, int b, char opt) {
switch (opt) {
case '+': return a+b;
case '-': return a-b;
case '*': return a*b;
case '/': return a/b;
}
return 0;
}
int getnum(int &ptr, string &s, int len) {
int x = 0;
while (ptr < len && (s[ptr] < '0' || s[ptr] > '9'))
++ptr;
while (ptr < len && s[ptr] <= '9' && s[ptr] >= '0') {
x = x * 10 + (s[ptr] - '0');
ptr ++;
}
return x;
}
char getopt(int &ptr, string &s, int len) {
while (ptr < len && pri(s[ptr]) == 0)
++ptr;
if (ptr >= len) return 0;
return s[ptr++];
}
public:
int calculate(string s) {
int ptr = 0, len = s.size();
int now, res;
char opt;
if (len == 0) return 0;
deque<int> nums;
deque<char> ops;
now = getnum(ptr, s, len);
nums.push_back(now);
while (ptr < len) {
opt = getopt(ptr, s, len);
if (ptr >= len) break;
now = getnum(ptr, s, len);
res = now;
if (ops.empty()) ops.push_back(opt);
else {
if (pri(ops.back()) < pri(opt)) {
res = calc(nums.back(), res, opt);
nums.pop_back();
} else ops.push_back(opt);
}
nums.push_back(res);
}
res = nums.front(); nums.pop_front();
while (!nums.empty()) {
opt = ops.front(); ops.pop_front();
now = nums.front(); nums.pop_front();
res = calc(res, now, opt);
}
return res;
}
};
229. 求众数
第一眼看到这个题就想起来那个求出现次数大于n/2的数字的题。那个题是让不同的数字相互抵消,最后剩下的那个就是大于n/2的数字。
然后就考虑把那个做法套到这个题里面。因为出现次数大于n/3的数字最多有两个,所以就维护两个数。
还是考虑相互抵消的思路。在求大于n/2的题目里,一个数字最多会被抵消n/2次。又保证众数存在,所以出现次数大于n/2的那个数最后一定能留下。
在这道题里也要保证一个数字最多会被抵消的次数不能超过n/3。否则众数就有可能被抵消掉。
但是也要保证最坏情况下(有数字恰好出现了n/3次,例如原题的第三个样例[1,1,1,2,2,3,3,3])抵消的次数一定要达到n/3,否则就消不掉“坏数”。
那么思路就是每次遇到不同的数字时,让维护的那两个数字同时被抵消一次。
这样,每次抵消都会少三个数(新来的和维护的两个),总共只有n个数,所以抵消次数不会超过n/3。
对于任意一个出现次数不大于n/3的数字,一定有超过2*(n/3)个数字和它不相等。即使每次抵消要消耗两个数字,也足够把它全部消掉。
class Solution {
public:
vector<int> majorityElement(vector<int>& nums) {
int ext[2], cnt[2];
int n = nums.size();
cnt[0] = cnt[1] = 0;
for (int i = 0; i < n; i ++) {
bool addin = false;
for (int j = 0; j < 2; j ++)
if (cnt[j] > 0 && ext[j] == nums[i]) {
++cnt[j]; addin = true; break;
}
if (addin) continue;
for (int j = 0; j < 2; j ++)
if (cnt[j] == 0) {
cnt[j] = 1; ext[j] = nums[i];
addin = true; break;
}
if (addin) continue;
cnt[0] --; cnt[1] --;
}
vector<int> ans; ans.clear();
for (int i = 0; i < 2; i ++)
if (cnt[i] > 0) {
int check = 0;
for (int j = 0; j < n; j ++)
if (nums[j] == ext[i]) ++check;
if (check > n/3) ans.push_back(ext[i]);
}
return ans;
}
};
230. 二叉搜索树中第K小的元素
就普通的平衡树Find操作,算个size然后从顶至底查就行了。时间复杂度\(O(n)\)(因为要dfs一遍)
按理来说其实也可以不dfs做,就用普通的平衡树FindNext那种操作每次找下一个节点。
具体我记得是如果当前节点有右子树就找右子树最靠左的儿子,如果没有右子树就往上找父亲,找到最近的一个从左儿子上去的父亲就是。
这样做的话时间复杂度应该是\(O(klogn)\)的。因为每次查找的最坏复杂度是\(O(logn)\)。
k比较小的时候这样一个个找会比较好吧。。。k一大就退化成\(O(nlogn)\),还不如dfs一遍。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
unordered_map<TreeNode*, int> size;
void dfs(TreeNode *root) {
size[root] = 1;
if (root->left != NULL) {
dfs(root->left);
size[root] += size[root->left];
}
if (root->right != NULL) {
dfs(root->right);
size[root] += size[root->right];
}
}
TreeNode* Find(TreeNode* root, int k) {
int val;
if (root->left != NULL)
val = size[root->left];
else val = 0;
if (k == val + 1)
return root;
if (k <= val) return Find(root->left, k);
else return Find(root->right, k - val - 1);
}
public:
int kthSmallest(TreeNode* root, int k) {
size.clear();
if (root == NULL) return 0;
dfs(root);
return Find(root, k)->val;
}
};
232. 用栈实现队列
随便翻的时候翻到这个题,想起自己两年前计概考试的时候这题就没做出来,吓出一身冷汗,赶紧做一做。
进了A栈一堆元素以后,要“出队”出的是那个栈底的元素,那就必须要把栈底的元素翻到上面来,那就必定要一个一个往外弹,弹到另一个栈(B栈)里存起来。
一开始脑子有点叉劈,非得想维护栈里元素正确的顺序,就觉得应该把B栈里的元素再倒回A栈里。实际上没必要,因为B栈里就是按照正确的“出队”顺序存的,出的时候可以直接从B里面出。
而入栈的时候还是往A栈里面入,显然,B没空的时候不能从A栈往B栈倒元素,否则就破坏了B的正确顺序。但是当B出空了,就可以把A的元素倒过去。
这样每个元素一定会入A栈一次,入B栈一次,然后就被弹出去。均摊复杂度是O(n)的。
class MyQueue {
public:
/** Initialize your data structure here. */
stack<int> ins, outs;
MyQueue() {
while (!ins.empty()) ins.pop();
while (!outs.empty()) outs.pop();
}
/** Push element x to the back of queue. */
void push(int x) {
ins.push(x);
}
void trans() {
while (!ins.empty()) {
int tmp = ins.top();
ins.pop();
outs.push(tmp);
}
}
/** Removes the element from in front of queue and returns that element. */
int pop() {
if (outs.empty()) trans();
int res = outs.top();
outs.pop();
return res;
}
/** Get the front element. */
int peek() {
if (outs.empty()) trans();
return outs.top();
}
/** Returns whether the queue is empty. */
bool empty() {
return ins.empty() && outs.empty();
}
};
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/
238. 除自身以外数组的乘积
显然就是搞一个前缀和,一个后缀和,每次乘起来就行了。
不让开额外空间也比较简单,因为它说输出数组不算额外空间,所以就先把对应位置的前缀和存在输出数组里,然后从后往前遍历,一边维护后缀和一边往输出数组里面乘就可以了。
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
vector<int> ans; ans.clear();
int n = nums.size(), tmp;
if (n == 0) return ans;
tmp = 1; ans.push_back(1);
for (int i = 0; i < n-1; i ++) {
tmp = tmp * nums[i];
ans.push_back(tmp);
}
tmp = 1;
for (int i = n-1; i >= 1; i --) {
tmp = tmp * nums[i];
ans[i-1] = ans[i-1] * tmp;
}
return ans;
}
};
239. 滑动窗口最大值
惊了,真就年纪大了连个单调队列都写不对。。。
忘了单调队列要用双向的,整个单向的队列在那搞来搞去。。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> q;
int n = nums.size();
vector<int> ans;
for (int i = 0; i < n; i ++) {
while (!q.empty() && q.front() <= i-k) q.pop_front();
while (!q.empty() && nums[i] > nums[q.back()]) q.pop_back();
q.push_back(i);
if (i >= k-1) ans.push_back(nums[q.front()]);
}
return ans;
}
};
241. 为运算表达式设计优先级
这里用了搜索的方法,对于一个表达式,枚举最后一个计算的运算符是谁,然后就可以把这个表达式分为两部分递归处理
左右两边可能的结果都存在vector里返回,最后再把它们两两相乘合并到最终的结果里就可以了。
搜索的时间复杂度大约是O(n!)的,n是表达式中运算符的个数。
我的计算方法是考虑搜索树。一个具有n个运算符的串需要枚举n次,每次枚举会产生两个串,这两个串总共有n-1个运算符。
所以递推式子就是\(T(n)=\sum\limits_{i=1}^{n-1}T(i)T(n-i-1)\)
是卡特兰数的递推式。可以知道\(T(n)={{C(2n, n)} \over {n+1}}\),所以T(n)=O(n!)
class Solution {
private:
int is_digit(string &input, int l, int r) {
int x = 0;
while (l <= r && input[l] == ' ') l ++;
while (l <= r && input[l] <= '9' && input[l] >= '0') {
x = x * 10 + input[l] - '0';
l ++;
}
while (l <= r && input[l] == ' ') l ++;
if (l <= r) return -1;
return x;
}
int calc(int a, int b, char opt) {
switch (opt) {
case '+': return a+b;
case '-': return a-b;
case '*': return a*b;
}
return -1;
}
void merge_res(vector<int> &res, vector<int> &res1, vector<int> &res2, char opt) {
int l1 = res1.size(), l2 = res2.size();
for (int i = 0; i < l1; i ++)
for (int j = 0; j < l2; j ++)
res.push_back(calc(res1[i], res2[j], opt));
}
vector<int> dfs(int l, int r, string &input) {
int x = is_digit(input, l, r);
vector<int> res; res.clear();
if (x >= 0) {
res.push_back(x);
return res;
}
vector<int> res1, res2;
for (int i = l; i <= r; i ++)
if (input[i] == '+' || input[i] == '-' || input[i] == '*') {
res1 = dfs(l, i-1, input);
res2 = dfs(i+1, r, input);
merge_res(res, res1, res2, input[i]);
}
return res;
}
public:
vector<int> diffWaysToCompute(string input) {
int len = input.size();
vector<int> res;
res.clear();
if (len == 0) return res;
if (input[0] == '-')
input = "0" + input;
res = dfs(0, len-1, input);
return res;
}
};
260. 只出现一次的数字
设要求的两个数字分别是x和y
可以想到把数组里的数字全都异或起来得到的就是x^y
那么只需要再求出x和y其中一个就可以了。
考虑x^y这个数字。它的某一位如果是0,那么可以推断要么是x和y都有这一位,要么是x和y都没有这一位。这个性质无法区分x和y。
但如果x^y的某一位是1,说明x和y的这一位有且仅有一个为1。
那么只要找到所有这一位是1的数字,把它们异或起来,得到的就是x和y中的某一个(因为其它这一位是1的数字也都是成对出现的,异或会消掉)
找一个是1的位这里使用了lowbit=x^(-x)。注意这里有一个-x,所以当x是-2147483648的时候需要注意。这里图省事直接全用long long了。
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
long long x = 0, y = 0, mak = 0;
int n = nums.size();
for (int i = 0; i < n; i ++)
x ^= nums[i];
mak = x & (-x);
for (int i = 0; i < n; i ++)
if (nums[i] & mak)
y ^= nums[i];
x ^= y;
vector<int> ans; ans.clear();
ans.push_back(x);
ans.push_back(y);
return ans;
}
};
275. H指数II
首先如果序列长度为n,那么H指数的值肯定不会超过n。
如果h指数是x,那么[0,n-x)这个下标区间内的数字都小于x,[n-x, n)这个下标区间内的数字都大于等于x。
那么,对于一个数字t,如果[n-t, n)这个下标内存在小于t的数字,说明t这个数字作为H指数来说偏大了。
如果t偏大,那么比t大的所有数字都是偏大的。满足二分单调性。
每次二分的时候,只需要判断第n-mid个数字是不是大于等于t就可以了。时间复杂度O(logn)。
class Solution {
private:
bool check(int n, int h, vector<int> &citations) {
return (citations[n-h] >= h);
}
int divide(int l, int r, int n, vector<int> &citations) {
int mid, ans = 0;
while (l <= r) {
mid = (l+r)>>1;
if (check(n, mid, citations)) {
ans = max(ans, mid);
l = mid + 1;
} else r = mid - 1;
}
return ans;
}
public:
int hIndex(vector<int>& citations) {
int n = citations.size();
if (n == 0) return 0;
return divide(1, n, n, citations);
}
};
279. 完全平方数
简单DP
class Solution {
public:
int numSquares(int n) {
int f[n+1];
memset(f, 31, sizeof(f));
f[0] = 0;
for (int i = 1; i * i <= n; i ++)
f[i*i] = 1;
for (int i = 1; i <= n; i ++)
for (int j = 1; j * j <= i; j ++)
f[i] = min(f[i], f[i - j*j] + 1);
return f[n];
}
};
765. 情侣牵手
LeetCode这个每日一题真是很会哦,结合时事.jpg (来自2021.2.14)
一开始看这个蒙了半天,每次看到这种求最值的就老是想DP。。
后来突然领(xia)悟(cai)到一个事情:如果一个序列的未匹配对数比其它某个序列要小,那么它的答案一定更优
【太长不看版:贪!!都可以贪!!从前往后一对对扫,把不匹配的交换一次调整成匹配,一定是最优解】
因为它可以任意交换,那么每一次交换它的目的一定是匹配上某一对,不存在说先拆开某一对为后面的交换制造方便的情况(因为任意交换这个条件已经是最宽松的了)
那么交换A和B,那起码A和B有一个是原先没匹配,交换后匹配了的。这样就会减少一个未匹配数。
可是有没有可能比如说交换以后A匹配上了,但反而把B拆开了,导致总匹配数未变呢?
———— 显然是不可能的,因为匹配是唯一的。
比如说一开始情况是【X A …… B Y】,交换以后A和Y匹配了,那么B和Y一开始一定是不匹配的,因为Y只能和A匹配。
所以只要交换以后能匹配上一对新的,那么总匹配数至少会+1。
但是只这样还不足以让我们放心地使用贪心算法。
因为对于一对不匹配的数字,有两种交换方法:换走左边的或者右边的。
有没有可能说,换走左边的能让匹配数+1,换走右边的能让匹配数+2?
【太长不看版:由于匹配的唯一性,对于一个不匹配的数对,换走左边和换走右边造成的效果一定是一样的,要么都+1,要么都+2】
实际上是不可能的。还是举个栗子,比如情况是【Z C …… X A …… B Y】,A和B匹配。现在我们要拿【X A】这一对开刀。
如果说换走某一边能让匹配数一下+2,那么肯定是X和Y要匹配。这时候可以发现,换走左边和换走右边实际上是一码事,都会让匹配数+2。
如果换走某一边能让匹配数+1,比如说交换X和B,因为已知匹配数+1,所以X和Y一定是不匹配的。
综上所述,我们只需要每次找到一个不匹配的对,把它调整到匹配就可以了。
顺着扫描一遍数列即可。这里用反表记录了每个数字的位置便于查找。总复杂度O(n),
class Solution {
public:
int minSwapsCouples(vector<int>& row) {
int n = row.size();
int rev[n];
int ans = 0;
for (int i = 0; i < n; i++)
rev[row[i]] = i;
for (int i = 0; i < n; i += 2)
if ((row[i] ^ row[i+1]) != 1) {
int tar = row[i] ^ 1;
int pos = rev[tar];
rev[tar] = i+1;
rev[row[i+1]] = pos;
swap(row[i+1], row[pos]);
ans ++;
}
return ans;
}
};
697. 数组的度
又是LeetCode的每日一题。
关键就是想到,整个数组的度数由出现次数最多的那个(或那几个)数字决定。
那么如果要找长度最短的子数组,只需要恰好把一个出现次数最多的数字全包括进去就可以了。
扫描数组,统计数字出现的次数,记录每个数字第一个和最后一个出现的位置。
对于每个出现次数最多的数字,正好把它全部包括进去的区间长度就是备选答案,用来更新即可。
class Solution {
public:
int findShortestSubArray(vector<int>& nums) {
int head[50000], tail[50000], cnt[50000];
memset(head, -1, sizeof(head));
memset(tail, -1, sizeof(tail));
memset(cnt, 0, sizeof(cnt));
int n = nums.size(), d = 0, ans = n;
for (int i=0; i<n; i ++) {
cnt[nums[i]] ++;
if (head[nums[i]] == -1)
head[nums[i]] = i;
tail[nums[i]] = i;
}
for (int i=0; i<n; i ++)
d = max(d, cnt[nums[i]]);
for (int i=0; i<n; i ++)
if (cnt[nums[i]] == d)
ans = min(ans, tail[nums[i]]-head[nums[i]]+1);
return ans;
}
};
1052. 爱生气的书店老板
又双叒叕是LeetCode的每日一题
老板每天的收益就是grumpy[i]==0的位置的customer数目之和
而连续X分钟不生气就是保证那X分钟之内grumpy都是0。
将这X分钟放在某个位置的收益显然就是这段时间内\(grumpy[i]==1\)的那些位置的customer数目之和
(因为这些customer一开始没计算进去)
那么首先求出不放置那X分钟的初始收益
然后使用滑动窗口统计每一段连续的X分钟内\(grumpy==1\)的位置的收益,取最大值作为增量收益
初始收益与增量收益相加就得到了最大收益
时间复杂度O(n),空间复杂度O(1)
class Solution {
public:
int maxSatisfied(vector<int>& customers, vector<int>& grumpy, int X) {
int n = customers.size();
int sum = 0, now = 0, Max = 0;
for (int i = 0; i < n; i ++)
if (grumpy[i] == 0)
sum += customers[i];
for (int i = 0; i < X; i ++)
now += grumpy[i] * customers[i];
Max = max(Max, now);
for (int i = X; i < n; i ++) {
now = now - grumpy[i-X] * customers[i-X];
now = now + grumpy[i] * customers[i];
Max = max(Max, now);
}
return sum + Max;
}
};
45. 跳跃游戏 II
显然是个DP。裸的DP很好写,每次枚举一个位置能往后到达的所有位置更新就可以了。
但这样的最坏复杂度是\(O(n^2)\)的。
观察发现,DP数组是单调不下降的。所以一个位置A只要被第k个位置更新过,后面枚举k+1、k+2……等位置时一定不会再更新它了,因为k往后的位置肯定不会比k位置的结果更好。
所以维护一个单调的指针指向目前被更新到的位置,顺序枚举已经求出来的位置看它能不能再往后更新就行了。
时间复杂度\(O(n)\)
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0;
int f[n], ptr;
memset(f, 30, sizeof(f));
f[0] = 0; ptr = 1;
for (int i = 0; i < n; i ++) {
if (i + nums[i] >= ptr) {
while (ptr < n && ptr <= i + nums[i]) {
f[ptr] = f[i] + 1;
ptr ++;
}
}
if (ptr >= n) break;
}
return f[n-1];
}
};
42. 接雨水
一眼单调栈,第二眼不会做(x)
单调栈题目思考的第一步必然是先确定要用单增的栈还是单减的栈
这两种栈的区别在于栈内保留的元素不同。
而栈中保留的元素必然是后面还有可能会用到的元素。
对于本题来说,如果某个位置出现了一个高度为k的柱子,那么它前面所有高度小于等于k的柱子之后都不会再起到影响了。
反而,如果它前面有高度大于k的柱子,这个高度更大的柱子还有可能与后面某个很高的柱子合作来接更多的水。
于是就确定了,需要保留它前面大于它的。所以要维护单调递减的栈。
接下来考虑怎么统计答案。
在单调栈中,统计答案的时机要么就是弹栈时,要么就是新元素入栈时。
当一个新元素(A)入栈时,它必然要弹出栈顶元素直到栈为空或当前栈顶元素大于它。
设当前栈顶元素为B。显然,由于A的到来,A与B之间形成了新的一块可以装水的空间。我们需要把这一块的答案统计进去。
在弹到B之前,A是木桶的“长板”。由于栈自顶向下递增,每次发现一个新的可出栈元素,都意味着这个“桶”能装的水又多了一块。
举个栗子,栈目前的状态是【h[1]=5, h[3]=3, h[5]=1】,要入栈的元素是h[6]=4。
弹出h[5]的时候,能装水的区域是h[5]和h[6]之间,高度为1的一块。这块因为宽度是0(位置5和位置6之间没有空隙了),所以累加答案0;
弹出h[3]的时候,“桶”高了一块,比之前增加的高度是3-1=2。高出来的这块宽度是2(位置3和位置6之间有两个单位长度的空隙),所以累加答案2*2=4;
h[1]不满足出栈条件,停止。
但仅仅在出栈的时候统计答案是不够的,刚才那个例子就能说明。
在刚才的例子中,h[1]让“桶”又增高了一块,又能装更多的水。增高的高度是4-3=1。
这一块需要在新元素入栈的时候统计进去。
总之每次统计答案的时候都是计算:“增量”乘以“长度”,“增量”即桶增高的高度,“长度”即当前增量对应的位置差。
代码中采用的计算方法是用一个临时变量last记录上次循环时“桶”的高度,初始为0。每次用当前高度减一下就可以了。
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
if (n == 0) return 0;
stack<int> st;
int ans = 0;
for (int i = 0; i < n; i ++) {
int pos, last = 0;
while (!st.empty() && height[st.top()] <= height[i]) {
pos = st.top();
ans += (height[pos] - last) * (i - pos - 1);
last = height[pos]; st.pop();
}
if (!st.empty())
ans += (height[i] - last) * (i - st.top() - 1);
st.push(i);
}
return ans;
}
};
84. 柱状图中最大的矩形
又是一个单调栈题。
首先可以确定维护的栈是单调上升的。因为前面比较小的高度可以往后延伸得比较远,贡献答案的时间比较长。
而如果一个很大的A后面跟了一个很小的B,那么A就被B截断了,A自己没法往后延伸了。
接下来确定统计答案的方法。
设栈稳定下来以后,有两个相邻的元素A和B。显然A比B小。
而序列中位于A和B之间的元素,必然都比A或B要大。
(因为如果中间某元素比A小,它会使得A出栈;否则如果中间某元素比B小,那么A和B必然不相邻)
显然,A和B中间的这一块已经被A和B“截断”了。(参考【2,5,6,3】这个序列,5和6被两边的2和3截断了,5和6本身不能再往两侧延伸)
那么问题是:到底应该看成A往右延伸,还是B往左延伸呢?
注意到一个问题:如果看成A往右延伸,把A和B之间的这段长度累加到A上,那么当A被弹出栈的时候,这块长度会被丢掉。显然是不合理的。
所以应该把A和B之间的长度累加到B上,看成是B往左延伸的长度。
于是算法就有了。开一个数组记录每个元素往左延伸的最大长度,每次弹出一个元素就记录它保有的长度,新元素入栈时一并累加到新元素的记录里。
而统计答案的时候,要记录前面所有元素延伸的长度之和。当栈顶元素A将要被弹出时,这个累加的和就代表了A这个长度右边能延伸的最大值。
而A自己的记录里有A能往左延伸的最大值。左右相加,就是A这个长度能扩展出的最大矩形。更新答案即可。
注意这里手动在序列的末尾加了个0,这样这个0可以把栈内所有元素都弹出来,保证所有答案都被枚举到。
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n = heights.size(), ans = 0;
if (n == 0) return 0;
stack<int> st;
int len[n+1];
for (int i = 0; i < n; i ++) len[i] = 1;
heights.push_back(0);
for (int i = 0; i <= n; i ++) {
int now = 0;
while (!st.empty() && heights[st.top()] >= heights[i]) {
now += len[st.top()];
ans = max(ans, heights[st.top()] * now);
st.pop();
}
len[i] += now;
st.push(i);
}
return ans;
}
};
85. 最大矩形
显然,划出一条“底线”之后,这个题就变成了上面那个柱状图最大矩形的题。
关键点是如何确定“柱状图”的高度。
显然应该每一列分别考虑。而因为要求是一个完整的矩形,如果某一列中间出现了一个0,那么这个0上面的所有1全都不算数。
于是依次枚举每一行作为“底线”,每次更新的时候,如果当前列是0,就重新计算;否则对应的“柱状图”高度加1。
然后套上面那个题的函数就行了。
时间复杂度是\(O(n^2)\)的。基本上是个最优算法了。
class Solution {
private:
int work(vector<int>& heights) {
int n = heights.size(), ans = 0;
if (n == 0) return 0;
stack<int> st;
int len[n+1];
for (int i = 0; i < n; i ++) len[i] = 1;
heights.push_back(0);
for (int i = 0; i <= n; i ++) {
int now = 0;
while (!st.empty() && heights[st.top()] >= heights[i]) {
now += len[st.top()];
ans = max(ans, heights[st.top()] * now);
st.pop();
}
len[i] += now;
st.push(i);
}
return ans;
}
void update(int k, int m, vector<vector<char>>& matrix, vector<int>& height) {
for (int i = 0; i < m; i ++)
if (matrix[k][i] == '0')
height[i] = 0;
else height[i] ++;
}
public:
int maximalRectangle(vector<vector<char>>& matrix) {
int n, m, ans = 0;
n = matrix.size();
if (n == 0) return 0;
m = matrix[0].size();
if (m == 0) return 0;
vector<int> height;
for (int i = 0; i < m; i ++)
height.push_back(0);
for (int i = 0; i < n; i ++) {
update(i, m, matrix, height);
ans = max(ans, work(height));
}
return ans;
}
};
354. 俄罗斯套娃信封问题
二维偏序模板题。设f[i]表示元素i可以套多少个信封。答案就是所有f[i]的最大值。
先按照某一维排序。这样从左往右扫描的时候就可以找到所有第一维小于当前信封的元素,只需要考虑第二维就行了。
考虑第二维,就是要找所有扫描过的元素中,第二维小于当前元素的f[i]最大值。这个值+1就是当前元素的f[i]值。
显然是一个以第二维作为下标的前缀最大值问题,树状数组即可。
注意一定要保证查询的都是“小于”当前元素的。
具体就是第一维处理的时候要先把相等的一组拿出来询问完了再一块加入树状数组;第二维查询的时候要减一。
class Solution {
private:
int lowbit(int i){return i&(-i);}
int askmax(int *s, int x) {
int ans = 0;
if (x <= 0) return 0;
for (int i = x; i != 0; i -= lowbit(i))
ans = max(ans, s[i]);
return ans;
}
void add(int *s, int x, int val, int N) {
for (int i = x; i <= N; i += lowbit(i))
s[i] = max(s[i], val);
}
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
vector< pair<int, int> > envs;
int n = envelopes.size();
if (n == 0) return 0;
int v[n+1], t, f[n+1], s[n+1];
memset(s, 0, sizeof(s));
memset(f, 0, sizeof(f));
for (int i = 1; i <= n; i ++)
v[i] = envelopes[i-1][1];
sort(v+1, v+n+1);
t = unique(v+1, v+n+1) - v - 1;
for (int i = 0; i < n; i ++) {
int now = lower_bound(v+1, v+t+1, envelopes[i][1]) - v;
envs.push_back(make_pair(envelopes[i][0], now));
}
sort(envs.begin(), envs.end());
int L = 0, R = 0;
while (L < n) {
while (R < n && envs[R].first == envs[L].first)
R++;
for (int i = L; i < R; i ++)
f[i] = max(1, askmax(s, envs[i].second-1) + 1);
for (int i = L; i < R; i ++)
add(s, envs[i].second, f[i], t);
L = R;
}
int ans = 0;
for (int i = 0; i < n; i ++)
ans = max(ans, f[i]);
return ans;
}
};
233.数字1的个数
经典数位DP题。
f[i][j][0/1]表示从高位往低位填数,填到第i个数,目前有j个1,是否卡上界,这样的数字个数。
这里为了处理简便,无论上界有几位,固定填满10位,允许填前导0。
这样统计答案的时候只需要扫描填满了10位的部分,累加j*f[i][j][k]即可。
注意因为f数组里存的是“数字个数”,所以f数组一定不会爆int;但最后统计出来的答案不一定。
实测2000000000已经爆int了,网站的std返回的也是负数。
class Solution {
private:
int f[20][20][2], R[20];
void split(int n) {
for (int i = 10; i >= 1; i --) {
R[i] = n % 10; n /= 10;
}
}
public:
int countDigitOne(int n) {
memset(f, 0, sizeof(f));
split(n);
for (int i = 0; i <= R[1]; i ++)
if (i == R[1])
f[1][(i == 1)][1] = 1;
else f[1][(i == 1)][0] = 1;
for (int i = 1; i < 10; i ++)
for (int j = 0; j <= i; j ++)
for (int k = 0; k <= 1; k ++)
if (f[i][j][k] != 0)
for (int h = 0; h <= 9; h ++) {
int lim;
if (k == 1 && h > R[i+1])
break;
lim = (k == 1 && h == R[i+1]);
if (h == 1)
f[i+1][j+1][lim] += f[i][j][k];
else f[i+1][j][lim] += f[i][j][k];
}
long long ans = 0;
for (int i = 1; i <= 10; i ++)
for (int j = 0; j <= 1; j ++)
ans += (long long)i * f[10][i][j];
return ans;
}
};
503. 下一个更大元素
很容易想到是寻找类似“拐点”的东西。只要存在一个元素比它前一个元素大,那么这个元素就可能作为某一个或某几个元素的答案。
这让我们想到单调栈,而且是单调递减的栈。
可以想象,在维护单调递减栈的过程中,如果第i个元素比第i-1个元素大,那么它会开始弹栈。而弹出栈的这些元素,就可以用第i个元素来更新答案。
这样找到的一定是离他最近的第一个大于它的。
因为这个过程中我们能保证没有更新过答案的元素都在栈里面。元素是从前往后扫描的,那么这个元素第一次被弹出,一定是后面遇到了一个大于它的。
从这道题中可以总结出单调栈的一个重要性质:在单减(增)栈中,一个元素一定会被第一个大于(小于)它的元素弹出去。
最后栈内可能还剩下一些元素。这些元素就是无法更新答案的,也就是数组内最大的元素。
注意因为这个题要求循环数组,所以要做两遍,相当于把原数组复制一遍并且接在后面。
这会造成某些位置重复被出栈。然而只有第一次出栈的时候需要更新答案。后面重复扫到就跳过即可。
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
stack<int> st;
vector<int> ans;
int n = nums.size();
for (int i = 0; i < n; i ++)
ans.push_back(-1);
for (int i = 0; i < n; i ++) {
while (!st.empty() && nums[st.top()] < nums[i]) {
if (ans[st.top()] == -1)
ans[st.top()] = i;
st.pop();
}
st.push(i);
}
for (int i = 0; i < n; i ++) {
while (!st.empty() && nums[st.top()] < nums[i]) {
if (ans[st.top()] == -1)
ans[st.top()] = i;
st.pop();
}
st.push(i);
}
for (int i = 0; i < n; i ++)
if (ans[i] != -1)
ans[i] = nums[ans[i]];
return ans;
}
};
480. 滑动窗口中位数
第一眼是两个堆维护中位数的问题。但问题在于这个题需要随机移除元素(因为每次滑动窗口移动时要移除窗口内第一个元素)。
C++的priority_queue是不方便实现这种功能的。随机增删并且维护有序,可以想到二叉搜索树。
于是这里使用了C++的multiset。multiset内部使用红黑树实现,查找和增删的平均复杂度是log级别的。
做法是首先加入最开始的k个元素。两棵红黑树big存放较大的一半,small存放较小的一半。通过迭代器可以访问到multiset的最大值和最小值。
然后枚举后面的元素。首先删掉最开始的元素。这里采用的做法是用multiset的find方法,分别在两棵树内查找对应的元素。找到了就删掉。
然后是加入新元素。这里有一个细节是,如果刚才删除的是small里面的元素,新元素就先加入big里,然后再弹出big内最小的元素加入small。反之亦然。
这样可以省去很多麻烦的比较和分类。否则如果删除的是small里面的元素,新元素又加入了small,有可能新加入的这个元素很大。此时就又需要与big内最小的元素比较,判断是不是需要调整一下。
其实这个做法还是麻烦了。用multiset做这个东西显然有点杀鸡用牛刀的感觉。
题解的做法是延迟删除,就是仍然用堆完成这些操作,但随机删除的时候先把这个操作用哈希表记下来。等需要删除的元素来到堆顶再将它真正删除。
class Solution {
private:
multiset<int> small, big;
public:
vector<double> medianSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
vector<double> ans;
if (n == 0) return ans;
multiset<int>::iterator it;
for (int i = 0; i < k; i ++) {
big.insert(nums[i]);
it = big.begin();
if (big.size() > k/2) {
small.insert(*it);
big.erase(it);
}
}
it = small.end(); it --;
if (k%2) ans.push_back(*it);
else ans.push_back(((double)*(big.begin()) + *it) / 2.0);
for (int i = k; i < n; i ++) {
it = small.find(nums[i-k]);
if (it == small.end()) {
it = big.find(nums[i-k]);
big.erase(it);
small.insert(nums[i]);
it = small.end(); it --;
big.insert(*it);
small.erase(it);
} else {
small.erase(it);
big.insert(nums[i]);
it = big.begin();
small.insert(*it);
big.erase(it);
}
it = small.end(); it --;
if (k%2) ans.push_back(*it);
else ans.push_back(((double)*(big.begin()) + *it) / 2.0);
}
return ans;
}
};
1828. 统计一个圆中点的数目
好久没写题了,先写个简单的练练手。
最近在公司实习写python写多了,再写c++代码要么就是不加分号,要么就是不加类型声明。。。我麻了
总之这个题就是枚举每一个询问,再枚举每一个点,\(O(n^2)\)判就行了。它题目里用整数的含义就是让你别用sqrt开根,两边平方判断圆心距就行了。
class Solution {
private:
#include <cmath>
int check(vector<int> &point, int x, int y, long long r) {
long long dx = abs(point[0] - x);
long long dy = abs(point[1] - y);
return dx * dx + dy * dy <= r * r;
}
public:
vector<int> countPoints(vector<vector<int>>& points, vector<vector<int>>& queries) {
vector<int> answer;
answer.clear();
for (int i = 0; i < queries.size(); i ++) {
int cx = queries[i][0], cy = queries[i][1], r = queries[i][2];
int cnt = 0;
for (int j = 0; j < points.size(); j ++) {
cnt += check(points[j], cx, cy, r);
}
answer.push_back(cnt);
}
return answer;
}
};
1689. 十-二进制的最少数目
这题面看起来花里胡哨的,实际上就是个扫描数组找最大值。。。
一开始看这个涉及到十进制加法,还在想这个会不会涉及到进位,所以不同位之间可能互相影响之类的
但实际上不可能,因为你可以自由控制每个数字的某一位填“0”还是“1”,所以不管用了几个数字,一定可以通过多填“0”来避免进位。
在不进位的情况下,需要的最多的“1”的个数就是给定的数字中最大的那一位了。
具体的构造方式是这样的:
- 从最高位开始扫描。最高位是k,那么至少要k个数字,这k个数字的最高位都是1。
- 看下一位:
- 如果下一位的数字x小于等于k,那么从k个数字里随便选出x个,当前位填“1”,其他数字当前位填“0”。
- 如果下一位的数字x大于k,那么现在已经有的k个数字当前位都要填“1”,另外增加x-k个数字,当前位是“1”。
- 令k=x,重复上一步。
class Solution {
public:
int minPartitions(string n) {
int len = n.size();
int mx = n[0] - '0';
for (int i = 1; i < len; i ++) {
mx = max(mx, n[i] - '0');
}
return mx;
}
};
673. 最长递增子序列的个数
好久没写题了,写一个练练手。
果然好久没写了,一个是初始化搞错,又忘了vector是从0开始的。
就是一边DP一边维护方案数目。f数组表示以某个元素结尾的最长递增子序列长度,g数组维护以某个元素结尾的最长递增子序列个数。
如果f得到了更大的值,就要把一开始维护的g值覆盖掉;如果f得到了和已有答案相同的值,g就要加上当前这个元素贡献的答案。
最后要扫一遍统计答案。因为可能有题目中样例2这样的情况出现。
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0;
int f[2010], g[2010];
f[0] = g[0] = 0;
for (int i = 1; i <= n; i ++) {
f[i] = 1; g[i] = 1;
for (int j = 1; j < i; j ++)
if (nums[j - 1] < nums[i - 1]) {
if (f[j] + 1 > f[i]) {
f[i] = f[j] + 1;
g[i] = g[j];
} else if (f[j] + 1 == f[i]) {
g[i] += g[j];
}
}
}
int mx = 0, ans = 0;
for (int i = 1; i <= n; i ++)
if (f[i] > mx) {
mx = f[i]; ans = g[i];
} else if (f[i] == mx) {
ans += g[i];
}
return ans;
}
};
188. 买卖股票的最佳时机IV
DP,f[i][j]表示在前i天最多买卖j次股票的最大收益。
首先是状态的继承。第i天当然可以什么也不做,就是继承f[i-1][j]。
又因为是最多买卖j次,所以可以继承f[i][j-1]。
如果第i天完成了一次买卖,那么一定是第i天卖出,第i天之前的某一天买入。
枚举哪一天买入,计算这次买卖的收益然后更新答案即可。
最后答案就是f[n][k]。
当然也可以把状态设置为前i天恰好买卖k次的收益,这样不需要继承f[i][j-1],但最后统计答案的时候要枚举所有k。
做的时候忘了所有状态更新都要取max,还WA了一次。无能狂怒.jpg
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
if (n == 0 || k == 0) return 0;
int f[1010][110];
memset(f, 0, sizeof(f));
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= k; j ++) {
for (int l = 1; l < i; l ++) {
f[i][j] = max(f[i - 1][j], f[i][j]);
f[i][j] = max(f[i][j - 1], f[i][j]);
f[i][j] = max(f[i][j], f[l - 1][j - 1] + prices[i - 1] - prices[l - 1]);
}
}
return f[n][k];
}
};
299. 猜数字游戏
简单的模拟。可以发现“A”的优先级最高,所以先判掉A,并且把判过A的位置都打上标记(代码中标记为字符x)
然后判B。B就是字符一样但位置不一样的。如果一个串里剩下x个,另一个串里剩下y个,那就是x和y取min然后累加到答案里。
class Solution {
public:
string getHint(string secret, string guess) {
int A = 0, B = 0, len = secret.size();
int cS[10], cG[10];
for (int i = 0; i < len; i ++)
if (secret[i] == guess[i]) {
A ++;
secret[i] = guess[i] = 'x';
}
for (int i = 0; i < 10; i ++)
cS[i] = cG[i] = 0;
for (int i = 0; i < len; i ++) {
if (secret[i] != 'x')
cS[secret[i] - '0'] ++;
if (guess[i] != 'x')
cG[guess[i] - '0'] ++;
}
for (int i = 0; i < 10; i ++)
B += min(cS[i], cG[i]);
string ans;
ans = to_string(A) + "A" + to_string(B) + "B";
return ans;
}
};
306. 累加数
逻辑不难,但细节比较多的一个题。
显然,如果知道了前两个数是多少,就可以顺着判断出来这个串是不是累加数。
所以一个简单的做法就是枚举前两个数字的位置,然后依次判断。
判断的基本逻辑就是记录当前判断到了什么位置,求出前两个数的和以后看能不能在字符串对应的位置找到。
注意在开头要判断一下有没有第三个数的位置,如果第二个数结尾已经到整个串的结尾了,就直接返回false。
比较麻烦的是对于前导零的判断。这里题目中说“不能以0开头”是说不能有前导零,如果这个数本身就是0还是可以的。
所以判断的时候,只有当前数字长度大于1并且开头是0才算不合法。
class Solution {
private:
bool check(string num, int p1, int p2) {
if (p1 + 1 > 1 && num[0] == '0') return false;
if (p2 - p1 > 1 && num[p1 + 1] == '0') return false;
long long x = stoll(num.substr(0, p1 + 1));
long long y = stoll(num.substr(p1 + 1, p2 - p1));
int ptr = p2 + 1, len = num.size();
if (ptr >= len) return false;
while (ptr < len) {
string sum = to_string(x + y);
if (sum.size() > 1 && num[ptr] == '0') return false;
string tar = num.substr(ptr, sum.size());
if (sum != tar) return false;
ptr += sum.size();
x = x + y;
swap(x, y);
}
return true;
}
public:
bool isAdditiveNumber(string num) {
int n = num.size();
for (int i = 0; i < n; i ++)
for (int j = 0; j < i; j ++)
if (check(num, j, i))
return true;
return false;
}
};
309. 最佳买卖股票时机含冷冻期
f[i]表示第i天的最大交易,第i天允许卖出。
每次递推的时候枚举最后一次购买是哪一天,从那一天的前一天的前一天(因为有一天冷冻期)来递推结果。
注意可以允许某一天什么也不做,这个时候f[i]就继承f[i-1]。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n <= 1) return 0;
int *f = new int[n + 1];
f[0] = 0;
for (int i = 1; i <= n; i ++) {
f[i] = max(prices[i - 1] - prices[1 - 1], 0);
for (int j = 2; j < i; j ++) {
f[i] = max(f[i], f[j - 2] + prices[i - 1] - prices[j - 1]);
}
f[i] = max(f[i], f[i - 1]);
}
int ans = 0;
for (int i = 1; i <= n; i ++)
ans = max(ans, f[i]);
return ans;
}
};
310. 最小高度树
我们想要知道每个节点作为根时的树高。
树高的求法就是所有儿子的高度最大值+1。
基本思路是递推,不断把根节点从一个点移动到它的儿子。
当根节点从一个点u移动到它的某个儿子v时,有两个节点的高度会发生变化:u和v。(怎么觉得说了些废话)
而因为节点u作为根时的答案已经求出来了(递推前提),我们只关心v的高度变化。
v的高度由两部分贡献:v原来的儿子们,以及u去掉v这个儿子组成的子树(v的新儿子)
重点是“u去掉v这个儿子组成的子树”。因为u的高度是max得来的,去掉v这个儿子以后max值可能会发生变化。
这就要求我们维护一个最大值和一个次大值,并且维护最大值的来源。
如果u的最大值是从v来的,新子树的高度就要从u的次大值得到;否则新子树的高度就等于u作为根节点时u的高度。
那么首先dfs一遍求出节点1作为根时每个节点的高度(数组h1)和次大高度(数组h2,从次大的儿子递推出来的高度)
然后dfs第二遍。在第二遍dfs过程中h1和h2的意义发生了变化,h1表示当前节点作为根时的高度,h2表示当前节点作为根时的次大高度。
因为还是从节点1开始dfs,而节点1本来就是根,所以递推前提是满足的。
每次枚举当前节点的每个儿子,用儿子原本的高度(h1[a[i]])和父亲形成的新儿子的高度(h1[u]+1或h2[u]+1)去更新即可。
注意更新儿子的时候也要同时维护最大值、次大值和最大值位置。所以代码看起来比较丑。其实如果把更新过程封装在函数里就会清晰很多(不过我懒得改了)
class Solution {
private:
int tot, p[20010], a[40010], nxt[40010];
int h1[20010], h2[20010], hp[20010];
void add(int x, int y) {
tot++; a[tot] = y; nxt[tot] = p[x]; p[x] = tot;
}
void dfs(int u, int fa) {
h1[u] = h2[u] = 0;
for (int i = p[u]; i != 0; i = nxt[i])
if (a[i] != fa) {
dfs(a[i], u);
if (h1[a[i]] + 1 > h1[u]) {
h2[u] = h1[u];
h1[u] = h1[a[i]] + 1;
hp[u] = a[i];
} else {
h2[u] = max(h2[u], h1[a[i]] + 1);
}
}
}
void dfs_again(int u, int fa) {
for (int i = p[u]; i != 0; i = nxt[i])
if (a[i] != fa) {
if (a[i] == hp[u]) {
if (h2[u] + 1 > h1[a[i]]) {
h2[a[i]] = h1[a[i]];
h1[a[i]] = h2[u] + 1;
hp[a[i]] = u;
} else {
h2[a[i]] = max(h2[a[i]], h2[u] + 1);
}
} else {
if (h1[u] + 1 > h1[a[i]]) {
h2[a[i]] = h1[a[i]];
h1[a[i]] = h1[u] + 1;
hp[a[i]] = u;
} else {
h2[a[i]] = max(h2[a[i]], h1[u] + 1);
}
}
dfs_again(a[i], u);
}
}
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
memset(p, 0, sizeof(p));
for (auto item = edges.begin(); item != edges.end(); item ++) {
add((*item)[0] + 1, (*item)[1] + 1);
add((*item)[1] + 1, (*item)[0] + 1);
}
dfs(1, 1);
dfs_again(1, 1);
int mn = n;
vector<int> ans;
for (int i = 1; i <= n; i ++)
mn = min(mn, h1[i]);
for (int i = 1; i <= n; i ++)
if (h1[i] == mn)
ans.push_back(i - 1);
return ans;
}
};
313. 超级丑数
一个比较暴力的思路是维护一个红黑树(set),每次弹出一个最小的数字就把这个数字和所有的质数乘一遍放进set里。重复n次。
不过这样有两个问题:第一个是会产生大量的重复数字(所以用set而不是priority_queue,但重复数字显然是冗余工作);第二个是产生的数字太多了,会有\(O(nm)\)这个数量级。n是询问的个数,m是primes数组的长度。
首先解决第一个问题:重复数字。
如果要避免重复,我们只需要让生成数字时选择的质因数都是递增的就可以了。
也就是说,对于每个数字,我们记录它最大的质因数是谁,下次用它生成数字的时候只把它跟更大的那些质数相乘。
primes数组给定的就是递增顺序,这显然是很方便的。那么现在就可以用小根堆代替红黑树了。
第二个问题是产生的数字太多。
考虑刚才的思路,我们实际上每次弹出一个数字会生成一串递增的新数字。但是因为我们维护的是小根堆,所以我们只关心最小的那个数字是什么。
只有当一个序列中最小的数字被弹出了,我们才会关心第二小的是谁。所以我们对每个序列只需要维护它的“开头”。
一个序列是由被弹出的那个数字和primes数组里的数字依次相乘得来的。所以只需要维护被弹出的数字是谁,以及这个序列遍历到了primes数组的哪个位置,就相当于记录了整个序列的信息。
这样每次弹出一个数字只会压入两个新数字,空间复杂度是\(O(n)\)的。
需要注意的问题是int溢出问题。虽然题目保证了答案不会爆int,但在维护数字的过程中还是要判一下溢出。
一开始我以为让它自然溢出不会影响,但这是有符号整数,溢出可能会变为负数,就会影响小根堆的维护。所以还是要把超出int的判掉。
不过也不一定需要用long long。用unsigned int就可以放任它自然溢出,而且速度应该还会比long long快。
class Solution {
private:
struct Item {
long long from, val;
int index;
Item(){}
Item(long long _v, long long _f, int _i) {
val = _v; from = _f; index = _i;
}
bool operator < (const Item &a) const {
return val > a.val;
}
};
priority_queue<Item> q;
const long long MAX_INT = 2147483647;
public:
int nthSuperUglyNumber(int n, vector<int>& primes) {
int len = primes.size(), ans;
if (n == 1) return 1;
while (!q.empty()) q.pop();
q.push(Item(primes[0], 1, 0));
for (int i = 2; i <= n; i ++) {
Item u = q.top();
q.pop();
ans = u.val;
if (u.index + 1 < len && u.from * primes[u.index + 1] <= MAX_INT)
q.push(Item(u.from * primes[u.index + 1], u.from, u.index + 1));
if (u.val * primes[u.index] <= MAX_INT)
q.push(Item(u.val * primes[u.index], u.val, u.index));
}
return ans;
}
};
316. 去除重复字母
这个题一开始想的是贪心,但不管是从小的字母开始贪还是从大的字母开始贪都不大对,贪心的规则太繁琐了。
最后看了一眼题解写的是单调栈。
考虑维护一个字符串让它没有重复字母且字典序最小。
每次扫描到一个字母的时候,首先已经维护的字符串里应该没有和它相等的字母才会考虑加入它。
理想情况下我们希望维护出来的字符串是单增的,所以要维护单调递增的栈。
如果新加入的字母不改变递增性质则直接入栈,否则要考虑弹出一些字母。
因为每个字母至少要出现一次,所以弹出的字母必定是“后面还有”的。如果一个字母弹出以后后面没有机会加入了,就不能弹出它,弹栈终止。
所以除了维护一个单增的栈,还要维护两个信息:某个字母是否还在栈中;某个字母是否还有未扫描到的。
class Solution {
public:
string removeDuplicateLetters(string s) {
int len = s.size();
int cnt[30], st[110], top = 0;
bool ext[30];
memset(cnt, 0, sizeof(cnt));
memset(ext, false, sizeof(ext));
string ans;
for (int i = 0; i < len; i ++)
cnt[s[i] - 'a'] ++;
for (int i = 0; i < len; i ++) {
if (ext[s[i] - 'a'] == false) {
while (top != 0 && st[top] > s[i] && cnt[st[top] - 'a'] > 0) {
ext[st[top] - 'a'] = false;
top --;
}
st[++top] = s[i];
ext[s[i] - 'a'] = true;
}
cnt[s[i] - 'a'] --;
}
for (int i = 1; i <= top; i ++)
ans.push_back(st[i]);
return ans;
}
};
318. 最大单词长度乘积
为什么同是medium题,难度差别能这么大。。
位运算记录每个单词里有哪些字母即可。
class Solution {
private:
int get_stat(string str) {
int len = str.size();
int stat = 0;
for (int i = 0; i < len; i ++)
stat |= (1 << (str[i] - 'a'));
return stat;
}
public:
int maxProduct(vector<string>& words) {
int stat[1010], len[1010], ans = 0;
int n = words.size();
for (int i = 0; i < n; i ++) {
stat[i] = get_stat(words[i]);
len[i] = words[i].size();
}
for (int i = 0; i < n; i ++)
for (int j = 0; j < i; j ++)
if ((stat[i] & stat[j]) == 0)
ans = max(ans, len[i] * len[j]);
return ans;
}
};
319. 灯泡开关
这题还怪好玩的。
首先每个灯泡都是互相独立的。先考虑一个灯泡最后的亮灭怎么得到。
很容易发现,一个灯泡的编号有多少个因数,它就会被开关几次。
有奇数个因数(比如4号)的最后就是亮的;有偶数个因数(比如2)的最后就是灭的。
但是n的范围很大,显然不能一个个枚举。
需要注意到一个事实是,因数都是以xy=n的方式成对出现的,有一个就会有另一个。
什么时候一个数字会有奇数个因数呢?就是因为它有一对因数是相同的,也就是满足xx=n。
实际上就是完全平方数。只有完全平方数才会有奇数个因数,其他数字都有偶数个因数。
于是只需要求出n以内的完全平方数个数就可以了。
class Solution {
public:
int bulbSwitch(int n) {
int i;
for (i = 0; i * i <= n; i ++);
return i - 1;
}
};