单周赛 240 题解
本次比赛囊括众多面试中高级知识点,具体为 差分数组,单调队列,双指针,单调栈,拓扑排序,DAG 上 dp
人口最多的年份
给定 \(n\) 个年份区间 \([L_{i},\ R_{i}]\),表示第 \(i\) 个人的出生年份到死亡年份
定义年份 \(x\) 的 人口 为这一年活着的人口数量,对于第 \(i\) 个人,若其被记入年份 \(x\) 的人口,则有 \(L_{i}\leq x < R_{i}\)
返回 人口最多 的 最早 年份
数据规定
\(1\leq n\leq 100\)
\(1950\leq 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\)
数据保证
\(1\leq A.length\leq 10^5\)
\(1\leq B.length\leq 10^5\)
\(1\leq 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\) 取余
数据保证
\(1\leq n\leq 10^5\)
\(1\leq 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\) 条边的 有向图,其中 \(1\leq n,\ m\leq 10^5\)
给定一个长为 \(n\),并且由 小写字母 构成的字符串 \(color\),表示节点 \(1,\ 2,\ ..,\ n\) 的颜色
在图论中,我们用 路径 表示一个点序列 \(x_{1}\rightarrow x_{2}\rightarrow ... \rightarrow x_{k}\),其中 \(x_{i}\rightarrow x_{i + 1}\) 表示点 \(x_{i}\) 和点 \(x_{i + 1}\) 有单向连边,下标 \(i\) 满足 \(1\leq 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 考虑,而对于判环和节点的先后顺序,可以使用 拓扑排序 解决,由此一来,这道题也就水到渠成了