2025-07-25 模拟赛总结 😮
预期:\(100+100+100+35=335\)。
实际:\(100+100+100+35=335\)。
排名:\(rk25/138\)。
比赛链接:http://oj.daimayuan.top/contest/366、
A - 子段乘积:
题意:
给定长度为 \(n\) 的序列 \(a\),求 \(\displaystyle\max_{l_1\le r_1\lt l_2\le r_2}(\sum_{i=l_1}^{r_1}a_i)(\sum_{i=l_2}^{r_2}a_i)\)。
思路:
枚举分割点,乘积最大即要求两边要么均为最大子段和,要么均为最小子段和,直接用线段树维护区间最大 / 小子段和即可,时间复杂度 \(O(n\log n)\)。
但是我们注意到,这是前后缀最大 / 小子段和可以 \(O(n)\) dp 求,时间复杂度 \(O(n)\)。
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 2e5 + 5;
int T, n, a[kMaxN];
long long f[kMaxN], g[kMaxN], h[kMaxN], l[kMaxN], ans = -1e18;
int main() {
ios::sync_with_stdio(0), cin.tie(0);
for (cin >> T; T; T--, ans = -1e18) {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 0; i <= n + 1; i++) {
f[i] = h[i] = -1e18, g[i] = l[i] = 1e18;
}
for (int i = 1; i <= n; i++) {
f[i] = max(0LL + a[i], f[i - 1] + a[i]);
g[i] = min(0LL + a[i], g[i - 1] + a[i]);
}
for (int i = n; i; i--) {
h[i] = max(0LL + a[i], h[i + 1] + a[i]);
l[i] = min(0LL + a[i], l[i + 1] + a[i]);
}
for (long long i = 1, maxn = -1e18, minn = 1e18; i < n; i++) {
maxn = max(maxn, f[i]), minn = min(minn, g[i]);
ans = max({ans, maxn * h[i + 1], minn * l[i + 1]});
}
cout << ans << '\n';
}
return 0;
}
B - 玩偶:
题意:
有 \(n\) 种玩偶,第 \(i\) 种玩偶的大小为 \(h_i\),扔掉一个玩偶的损失为 \(c_i\),第 \(i\) 种玩偶的数量为 \(p_i\)。
现在需要扔掉一些玩偶,使得剩下的玩偶中,最大的玩偶的数量之和严格大于扔完后剩下玩偶总数的一半,求最小损失。
思路:
首先枚举最大的玩偶的大小,然后需要丢掉比他大的玩偶,接着可以计算出 \(k\) 表示最少需要删除多少个比它小的玩偶,贪心的选择 \(k\) 个损失最小的玩偶,将大小比它小的玩偶按照 \(c_i\) 从小到大排序,暴力选择,时间复杂度 \(O(n^2)\)。
但是我们可以维护大小比它小的玩偶的 \(c_i\) 的值域线段树,然后在线段树上二分,找到前缀玩偶数量和第一个大于 \(k\) 的位置,然后在这里计算答案。
运用线段树二分时间复杂度为 \(O(n\log n)\),当然你也可以用树状数组倍增做到更小的常数。
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 5e5 + 5;
int n;
long long ans = 1e18, p[kMaxN], w[kMaxN], sum[kMaxN << 2], cnt[kMaxN << 2];
// cnt: S(a[i].p) sum: S(a[i].p * a[i].c)
struct P {
int h, c, p;
} a[kMaxN];
void PushUp(int u) {
sum[u] = sum[u << 1] + sum[u << 1 | 1];
cnt[u] = cnt[u << 1] + cnt[u << 1 | 1];
}
void Update(int u, int l, int r, int p, long long x, long long y) {
if (l == r) {
cnt[u] += x, sum[u] += x * y;
return;
}
int mid = l + r >> 1;
if (p <= mid) Update(u << 1, l, mid, p, x, y);
else Update(u << 1 | 1, mid + 1, r, p, x, y);
PushUp(u);
}
long long Query(int u, int l, int r, long long k) {
if (l == r) return cnt[u] == 0 ? 0 : k * sum[u] / cnt[u];
int mid = l + r >> 1;
if (cnt[u << 1] >= k) return Query(u << 1, l, mid, k);
else return Query(u << 1 | 1, mid + 1, r, k - cnt[u << 1]) + sum[u << 1];
}
long long Get(int l, int r, long long ret = 0) {
if (l < 1 || a[l].h != a[r].h || a[r].h == a[r + 1].h) return 1e18;
long long cnt = p[r] - p[l - 1], Cnt = p[n] - p[l - 1];
long long sum = w[l - 1], k = max(0LL, Cnt - cnt - cnt + 1);
return sum + Query(1, 1, 5e5, k);
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i].h >> a[i].c >> a[i].p;
}
sort(a + 1, a + 1 + n, [](P i, P j) { return i.h > j.h; });
for (int i = 1; i <= n; i++) {
p[i] = p[i - 1] + a[i].p;
w[i] = w[i - 1] + 1LL * a[i].c * a[i].p;
if (i != 1) Update(1, 1, 5e5, a[i].c, a[i].p, a[i].c);
}
for (int i = 1, j = 1; i <= n; ) {
for (; i < n && a[i + 1].h == a[i].h; i++, Update(1, 1, 5e5, a[i].c, -a[i].p, a[i].c)) {
ans = min(ans, Get(j, i));
}
ans = min(ans, Get(j, i));
for (; j < i && a[j].h != a[i].h; j++) {
}
for (; j < i && a[j].h == a[i].h; j++) {
ans = min(ans, Get(j, i));
}
ans = min(ans, Get(j, i));
i++;
if (i <= n) Update(1, 1, 5e5, a[i].c, -a[i].p, a[i].c);
}
cout << ans;
return 0;
}
C - 无人机:
题意:
给你一个无向图,每个节点有一个高度 \(a_i\),保证 \(a_1=a_n=0\)。现在有一个无人机在 \(1\) 点高度为 \(0\) 处,每分钟,无人机可以垂直上升、高度不变经过一条边、高度加 / 减一经过一条边,求你最快可以多久到 \(n\)。
思路:
赛时:
观察到,无人机的高度序列肯定是单峰的,我们可以枚举最高的那个点 \(i\),从 \(1\) 走到 \(i\),再从 \(i\) 走到 \(n\),这两个过程是对称的,所以只需要考虑一个就行了,现在的问题就是如何求出 \(1\) 到 \(i\) 的最短时间。
考虑一个 naive 的做法,维护当前的高度 \(h\),如果下一个节点的高度小于等于它,那么就不上升,否则就边走边上升一次,在上升若干次,再维护一个 \(dis\) 数组,跑 dijkstra。
可惜这样的做法是错的,因为当下一个节点的高度小于等于它的时候,你其实可以上升,为以后更高的节点做准备。
考虑另一种状态,在每个节点维护当前高度 \(h\) 和之前高度不变经过一条边的次数 \(c\),那么总时间就是 \(h+c\)。如果下一个节点高度小于等于它,那么就不改变高度走,并将 \(c\gets c+1\),否则可以用 \(c\) 来抵消竖直向上走,以 \(h+c\) 作为 \(dis\) 跑 dijkstra 就可以了,这样做是对的。
赛后:
注意到我们至多做一次不升高的移动。首先若当前高度不为最高点,那么进行升高肯定不劣,若当前高度为最高点,若经过的边为偶数,那么可以转化为一段上升一段下降,否则可以转化为一段上升一段平一段下降。我们设当前点的高度为 \(h\),那么 \(h_i\) 其实就是时间,要求 \(h\) 最小即可,我们直接这样跑 dijkstra,可以证明是正确的。
代码:
赛时:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int kMaxN = 2e5 + 115;
int n, m, a[kMaxN], h1[kMaxN], c1[kMaxN], h2[kMaxN], c2[kMaxN], ans = 1e18;
bool vis[kMaxN];
vector<int> g[kMaxN];
priority_queue<pair<int, int>> q;
void Dijkstra(int s, int *h, int *c) {
for (int i = 0; i <= n + 10; i++) {
h[i] = c[i] = 1e18;
}
memset(vis, 0, sizeof(vis));
for (h[s] = c[s] = 0, q.push({0, s}); q.size(); ) {
int u = q.top().second;
q.pop();
if (vis[u]) continue;
vis[u] = 1;
for (int v : g[u]) {
int H, C;
if (h[u] < a[v]) {
int dh = a[v] - h[u];
int s = min(c[u] + 1, dh);
C = c[u] + 1 - s, H = a[v];
} else {
H = h[u], C = c[u] + 1;
}
if (h[v] + c[v] > H + C) {
h[v] = H, c[v] = C;
q.push({-h[v] - c[v], v});
}
}
}
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
g[u].push_back(v), g[v].push_back(u);
}
Dijkstra(1, h1, c1);
Dijkstra(n, h2, c2);
for (int i = 1; i <= n; i++) {
if (h1[i] == h2[i] && h1[i] == a[i]) ans = min(ans, h1[i] + h2[i] + c1[i] + c2[i]);
}
cout << ans;
return 0;
}
赛后:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int kMaxN = 2e5 + 115;
int n, m, a[kMaxN], d1[kMaxN], d2[kMaxN], ans = 1e18;
bool vis[kMaxN];
vector<int> g[kMaxN];
priority_queue<pair<int, int>> q;
void Dijkstra(int s, int *d) {
for (int i = 0; i <= n + 10; i++) {
d[i] = 1e18;
}
memset(vis, 0, sizeof(vis));
for (q.push({d[s] = 0, s}); q.size(); ) {
int u = q.top().second;
q.pop();
if (vis[u]) continue;
vis[u] = 1;
for (int v : g[u]) {
if (d[v] > max(d[u] + 1, a[v])) {
q.push({-(d[v] = max(d[u] + 1, a[v])), v});
}
}
}
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
g[u].push_back(v), g[v].push_back(u);
}
Dijkstra(1, d1), Dijkstra(n, d2);
for (int i = 1; i <= n; i++) {
ans = min(ans, 2 * max(d1[i], d2[i]));
for (int j : g[i]) {
ans = min(ans, 2 * max(d1[i], d2[j]) + 1); // 唯一一段平走
}
}
cout << ans;
return 0;
}
D - 交集:
题意:
给定 \(n\) 条线段,你需要把它们分成恰好 \(k\) 份非空的线段,你需要求出这 \(k\) 份线段的交的和,需要保证每一份线段的交非空。
思路:
赛时:
有一个很 naive 的想法,将这些线段按照 \(l\) 排序,然后猜这每一份线段一定是连续的,然后再猜它可以 wqs 二分。(比赛开始 35min 左右的时候)
写完后,发现这是不对的,于是弃掉。
写完 C 题,只剩最后 10min,想了一会后,于是开始写暴力,花了 5min 拿到了 35 pts。
赛后:
事实上,我的感觉还是很对的。
考虑两个区间的关系,首先包含情况很好解决,如果区间 \(i\) 包含区间 \(j\),那么要么 \(i\) 和 \(j\) 一组,要么 \(i\) 单独一组,因为与 \(j\) 合并不会减少交集长度。所以我们可以将所有的 \(i\) 分离出来,剩下只有互不包含的区间了,那么这些区间将 \(l\) 排序后肯定 \(r\) 也被排序了,这样我们就可以用上面那个 naive 的做法了,但是当然不需要 wqs 二分。dp 式子写出来后可以用前缀 min 优化,但是每一份线段的交非空,所以可以用单调队列优化。时间复杂度:\(O(n^2)\)。
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 5005;
int n, k, f[kMaxN][kMaxN], cnt, tot, len[kMaxN], ans, q[kMaxN], h, t;
bool vis[kMaxN];
struct L {
int l, r;
} a[kMaxN], b[kMaxN];
struct cmp {
bool operator()(const int &x, const int &y) {
return a[x].r < a[y].r;
}
};
priority_queue<int, vector<int>, cmp> pq;
int X(int i, int j) { return f[i][j - 1] + b[i + 1].r; }
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i].l >> a[i].r;
}
sort(a + 1, a + 1 + n, [](L i, L j) { return i.l != j.l ? i.l < j.l : i.r > j.r; });
for (int i = 1; i <= n; i++) {
while (pq.size() && a[pq.top()].r >= a[i].r) {
vis[pq.top()] = 1, pq.pop();
}
pq.push(i);
}
for (int i = 1; i <= n; i++) {
if (vis[i]) {
len[++cnt] = a[i].r - a[i].l + 1;
} else {
b[++tot] = a[i];
}
}
memset(f, 0xc0, sizeof(f));
f[0][0] = 0;
for (int l = 1; l <= k; l++) {
h = 1, t = 0, q[++t] = 0;
for (int i = 1; i <= tot; i++) {
for (; h <= t && b[q[h] + 1].r < b[i].l; h++) {
}
f[i][l] = X(q[h], l) - b[i].l + 1;
for (; h <= t && X(q[t], l) <= X(i, l); t--) {
}
q[++t] = i;
}
}
sort(len + 1, len + 1 + cnt, greater<int>());
for (int i = 1; i <= k; i++) {
if (k - i > cnt) continue;
int ret = 0;
for (int j = 1; j <= k - i; j++) {
ret += len[j];
}
ans = max(ans, ret + f[tot][i]);
}
cout << ans;
return 0;
}
反思:
遇到很多区间的题目,若区间之间的关系难以处理,可以考虑两个区间的情况,排除掉包含 / 相离的情况,后面可能就简单了。Day1 D 也是类似的思路。
多种元素分组求最大 / 最小代价的题目,分组可能很难处理,我们可以先贪心处理(例如:排序,删除不必要的元素),然后发现分组肯定是一个连续段分一组,可以用 dp 做。
总结:
时间分配:\(30+60+90+50\)。
写完 A 后,直接去看 D 了,写了 20min 假做法,然后去写 B,写了 1h,然后去做 C,C 在最后 10min 过的(😮好惊险),然后写了 D 的暴力就没了。
这次比赛 B 题写太久了,代码能力还是不行,写了个双指针都写好久,导致最后 D 的暴力还差 15pts 没写。
D 题这种题其实应该是要做出来的,水平还是不够。

浙公网安备 33010602011771号