本次比赛囊括众多面试中高级知识点,具体为 差分数组,单调队列,双指针,单调栈,拓扑排序,DAG 上 dp
人口最多的年份
给定 (n) 个年份区间 ([L_{i}, R_{i}]),表示第 (i) 个人的出生年份到死亡年份
定义年份 (x) 的 人口 为这一年活着的人口数量,对于第 (i) 个人,若其被记入年份 (x) 的人口,则有 (L_{i}leq x < R_{i})
返回 人口最多 的 最早 年份
数据规定
(1leq nleq 100)
(1950leq L_{i} < R_{i}leq 2050)
题解
问题等价于,给定多个区间 ([L_{i}, R_{i}]),对区间中所有年份人口加 (1),经过数次修改后,返回年份人口最大值的最小下标
区间修改定值,离线查询,可以考虑使用 差分数组
- 区间修改定值 指的是,对于区间上每一个数,统一增减一个定值
- 离线查询 指的是,多次操作后一次性查询结果
具体来讲,预计算差分数组 (D),给 (D_{L}) 加 (1),(D_{R+1}) 减 (1),多次操作后使用 前缀和 还原原数组
之后,对数组排序,返回期望的下标即可,时间复杂度为 (O(n + mlogm)),其中 (m) 为年份的区间长度最大值
当然,本题的数据规模很小,可以使用暴力算法,暴力修改每一个区间的值,时间复杂度为 (O(nm + mlogm))
/* 差分数组 */
class Solution {
public:
int maximumPopulation(vector<vector<int>>& logs) {
vector<int> b(107);
vector<int> d(107);
for (int i = 0; i < logs.size(); ++i) {
d[logs[i][0] - 1950]++;
d[logs[i][1] - 1950]--;
}
for (int i = 1; i < 107; ++i) d[i] += d[i - 1];
for (int i = 0; i < 107; ++i) b[i] = i;
sort(b.begin(), b.end(), [&](int x, int y) {
if (d[x] == d[y]) return x < y;
return d[x] > d[y];
});
return 1950 + b[0];
}
};
/* 暴力算法 */
class Solution {
public:
int maximumPopulation(vector<vector<int>>& logs) {
vector<int> a(107);
vector<int> b(107);
for (int i = 0; i < logs.size(); ++i) {
for (int j = logs[i][0]; j < logs[i][1]; ++j) {
a[j - 1950]++;
}
}
for (int i = 0; i < 107; ++i) b[i] = i;
sort(b.begin(), b.end(), [&](int x, int y) {
if (a[x] == a[y]) return x < y;
return a[x] > a[y];
});
return 1950 + b[0];
}
};
下标中的最大值
给定两个 非递增 数组 A, B
,下标从 0
开始计数
若 A
的下标 i
,与 B
的下标 j
满足 i <= j, A[i] <= B[j]
,则称 i, j
为有效下标对,定义下标对的 距离 为 j - i
返回所有 有效 下标对的 最大距离,若不存在有效下标对,返回 (0)
数据保证
(1leq A.lengthleq 10^5)
(1leq B.lengthleq 10^5)
(1leq A_{i}, B_{i}leq 10^5)
题解
由于两个数组具有 单调性,因此可以考虑用双指针 动态扩充队列,队列维护 (B) 中元素的下标
具体来说,若 A[i] <= B[j]
,那么一定有 A[i + 1] <= B[j]
,因此可以动态扩充一个队列,对于队列中所有下标 j
,都满足 A[i] <= B[j]
- 考虑入队,只要
A[i] <= B[j]
,指针j
就可以右移,直到移动到 (B) 的右边界 - 考虑出队,每次把队首出队,因为其下标
j
不满足i <= j
由于需要队尾入队,队首出队,可以使用 双端队列 来实现
对于 A[i]
,若队列不为空,每次拿队尾的下标 j
和 i
作差即可,维护答案最大值
时间复杂度为 (O(n))
class Solution {
public:
int maxDistance(vector<int>& nums1, vector<int>& nums2) {
int n = nums1.size(), m = nums2.size();
deque<int> dq;
int ans = 0;
for (int i = 0, j = 0; i < n; ++i) {
if (!dq.empty()) dq.pop_front();
while (j < m && nums1[i] <= nums2[j])
dq.push_back(j++);
if (!dq.empty()) {
ans = max(ans, dq.back() - i);
}
}
return ans;
}
};
子数组最小乘积
定义一个数组 (A) 的 最小乘积 为数组中 最小值 乘以 数组的和
- 举例来讲,数组
[3, 2, 5]
的最小值为2
,数组和为10
,因此最小乘积为20
现在给定一个长为 (n) 的正整数数组 nums
,请计算 nums
中所有 非空子数组 的 最小乘积 的 最大值
- 举例来讲,数组
[1, 2, 3, 2]
满足条件的子数组为[2, 3, 2]
,答案为2 * (2 + 3 + 2) = 14
题目保证,存储的答案可以使用 64
位有符号整数存储,但是最终的答案需要对 (10^9 + 7) 取余
数据保证
(1leq nleq 10^5)
(1leq A_{i}leq 10^7)
题解
直观来想,如果枚举子数组,一共需要计算 (1 + 2 + .. + n = frac{n(n + 1)}{2}) 次,不考虑区间查询最小值,已经是 (O(n^2)) 的时间复杂度,无法通过全部数据规模
换个角度,枚举每一个元素 (A_{i}),考虑 (A_{i}) 所能 管辖的区域
具体来讲,我们需要为每一个 (A_{i}) 计算出一个区间 ([L, R]),使得 (A_{i}) 是 (A_{L}, A_{L + 1}, .., A_{R}) 中的最小值
那么我们需要分别计算出 (A_{i}) 右侧和左侧第一个更小值,这个可以使用 单调栈 解决,详见 下一个更大元素 I
预处理每一个元素所管辖的区间,维护答案的最大值,时间复杂度 (O(n))
class Solution {
public:
typedef long long LL;
int maxSumMinProduct(vector<int>& nums) {
const int MOD = 1e9 + 7;
int n = nums.size();
vector<LL> a(n + 1), sum(n + 1);
for (int i = 1; i <= n; ++i) {
a[i] = nums[i - 1];
sum[i] = sum[i - 1] + a[i];
}
vector<int> rmin(n + 1, -1), lmin(n + 1, -1); // -1 表示没有更小的
stack<int> mono1, mono2; // 递增
for (int i = 1; i <= n; ++i) {
while (!mono1.empty() && a[mono1.top()] > a[i]) {
rmin[mono1.top()] = i;
mono1.pop();
}
mono1.push(i);
}
for (int i = n; i >= 1; --i) {
while (!mono2.empty() && a[mono2.top()] > a[i]) {
lmin[mono2.top()] = i;
mono2.pop();
}
mono2.push(i);
}
LL ans = 0;
for (int i = 1; i <= n; ++i) {
/* 若为 -1,则左/右边没有更小元素 */
/* 管辖范围可以拓展到边界 */
int L = (lmin[i] == -1 ? 1 : lmin[i] + 1);
int R = (rmin[i] == -1 ? n : rmin[i] - 1);
ans = max(ans, a[i] * (sum[R] - sum[L - 1]));
}
return ans % MOD;
}
};
后记
这题我最早听说,是同学在今年春招面试美团后端时遇到的,后来牛客网上有同学爆料腾讯广告投放也出了这么一道题,如果不转换个枚举思路,这题是很难做的
有向图中最大颜色值
给定一个 (n) 个节点,(m) 条边的 有向图,其中 (1leq n, mleq 10^5)
给定一个长为 (n),并且由 小写字母 构成的字符串 (color),表示节点 (1, 2, .., n) 的颜色
在图论中,我们用 路径 表示一个点序列 (x_{1} ightarrow x_{2} ightarrow ... ightarrow x_{k}),其中 (x_{i} ightarrow x_{i + 1}) 表示点 (x_{i}) 和点 (x_{i + 1}) 有单向连边,下标 (i) 满足 (1leq i < k)
我们定义,路径中 出现次数最多的 颜色的节点数目为路径的 颜色值,请计算图中所有路径 最大颜色值,如果图里有环,返回 (-1)
题解
注意到给定的是 有向图
- 对于有环的情况,可以使用 拓扑排序 侦测,拓扑排序详见 课程表
- 对于无环的情况,即 有向无环图(DAG),非常适合做 动态规划(DP)
我们定义 (dp_{i, j}) 表示到第 (i) 个节点,颜色 (j) 出现的最大次数,考虑节点 (i) 的所有 前继节点 (u),我们可以轻松的写出状态转移方程
其中 (add) 为指示变量,当节点 (i) 的颜色和 (j) 相等时,颜色个数要增 (1),当颜色不同时,只要继承前继节点的颜色数量即可,具体来讲
上面的分析要求给出 前继节点,这需要对图上节点的先后关系做分析,而拓扑排序正好可以帮助我们做到这点
在本题中,拓扑排序的作用有两个
- 首先是判环
- 其次是给定节点之间的 先后关系
我们在拓扑排序的过程中对状态进行转移,最后维护每个节点的颜色最大值即可
时间复杂度为 (O(|Sigma|(n + m))),其中 (|Sigma|) 是字符集的大小
class Solution {
public:
int largestPathValue(string colors, vector<vector<int>>& edges) {
int n = colors.size();
int m = edges.size();
vector<int> ind(n);
vector<vector<int>> g(n);
vector<vector<int>> dp(n, vector<int>(26, 0));
queue<int> q;
for (int i = 0; i < m; ++i) {
ind[edges[i][1]]++;
g[edges[i][0]].push_back(edges[i][1]);
}
for (int i = 0; i < n; ++i) {
if (!ind[i]) {
q.push(i);
dp[i][colors[i] - 'a'] = 1;
}
}
int cnt = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
++cnt;
for (auto &i: g[u]) {
for (int j = 0; j < 26; ++j) {
int add = j == colors[i] - 'a' ? 1 : 0;
dp[i][j] = max(dp[i][j], dp[u][j] + add);
}
--ind[i];
if (!ind[i]) q.push(i);
}
}
if (cnt != n) return -1;
int ans = 0;
for (int i = 0; i < n; ++i)
ans = max(ans, *max_element(dp[i].begin(), dp[i].end()));
return ans;
}
};
后记
这题很好想,注意到大部分 case
面向 有向无环图 (DAG),就会往 DAG 上 dp 考虑,而对于判环和节点的先后顺序,可以使用 拓扑排序 解决,由此一来,这道题也就水到渠成了