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 题这种题其实应该是要做出来的,水平还是不够。

posted @ 2025-07-25 20:18  liruixiong0101  阅读(35)  评论(0)    收藏  举报