题解:UOJ484 【UR #18】绝对不鸽 / QOJ2071 Interesting Game

题解:UOJ484 【UR #18】绝对不鸽 / QOJ2071 Interesting Game

题目描述

成为鸽王的三个要求之一,出色的咕咕咕能力。

为了比拼咕咕咕能力,主持人交给了码农同学\(n\)个待办事项,最初第\(i\)个待办事项还剩下\(a_i\)天的待办时间。

接下来码农同学和主持人会进行\(9^{9^9}\)轮交互,每一轮交互如下:

首先,码农同学可以找到不超过\(A\)个借口,并将这些借口任意分配给各个待办事项,每个借口都可以使某一个的待办事项增加\(1\)天的待办时间。

然后,主持人会选择\(B\)个待办事项,要求码农同学立即完成这些待办事项,将这些待办事项的待办时间清零。当然,这些待办事项的待办时间依然可以在后续轮次的交互中被增加。

\(9^{9^9}\)轮交互全部完成之后,码农同学发现在整个交互过程中,在他某一次找借口后,某一个待办事项的待办时间达到了历史最大值\(M\)天,他可以借这个数值\(M\)来吹嘘自己的咕咕咕能力。

码农同学自然希望数值\(M\)尽量大,而主持人的目的则是让数值\(M\)尽量小。

观看大赛的观众们想要请你预测,假如码农同学和主持人一直使用最优策略,最终的数值\(M\)会是多少。

对于 \(100 \%\) 的数据,\(1 \le B \le n \le 10^5, 0 \le a_i, A \le 10^{12}\)

题解

Subtask 2

\(n \le 100, A \le 100, a_i \le 100\)

主持人的策略一定是删空最大的 \(B\) 个数。可以改一下,看作一条无限长的纸带,纸带上前 \(n\) 个数字是 \(a_1, a_2, \cdots, a_n\),后面都是 \(0\),并强制码农同学维护好这条纸带的单调不升性,这样主持人每次就是删掉前 \(B\) 个数,然后码农同学只能去加纸带上的前 \(n\) 个数。

枚举最终答案取在 \(k\) 的位置,并把 \(k\) 以后的部分删掉。这样以后就暴力模拟操作,每次 \(+1\) 都是去找当前能操作的 \(n\) 个数中最小的那个数,就会有一个不断从后往前推平 \(a_i\) 序列的过程,可以稍微想一下怎么实现,具体见后文 71 分代码。单次复杂度可以线性。

然后有一个以后都要用到的小优化是 \(k\) 总是形如 \(xB+1\),否则不优。

Subtask 3

\(a_i=0\)

可以发现这样一个事情:忽略掉一些原地踏步的操作,序列中超过 \(V=\lfloor \frac{A - 1}{B} \rfloor\) 的数是在不断变少的。而当序列中只存在不超过 \(V\) 的数时,我们可以轻松将序列变为 \(n-B\)\(V\)\(B\)\(0\) 的形式,方法是先让主持人随便请零,然后每次操作就将 \(B\) 个数加到 \(V\),然后再找其它的一个数 \(+1\),剩下的往那 \(B\)\(V\) 上加,这样就能稳定地到达 \(n-B\)\(V\)\(B\)\(0\) 的形式。因此当 \(a_i\) 全零时,可以将其转化为 \(n-B\)\(V\)\(B\)\(0\) 的形式,或者说,任意序列都可以先转化为这个形式,答案不会小于这个形式下的答案。

到这里继续考虑 Subtask 2 的贪心,有一个我不会具体证的东西是 \(k<n+B\),请阅读官方题解[1]。然后就可以继续使用 Subtask 2 的算法。

注意

注意我们需要计算的东西:

  • \(k<n+B\) 的所有 \(k\) 需要计算一次答案,其中对于 \(k>n\) 的唯一一个 \(k\) 可以通过预先进行一轮操作转化为 \(k\leq n\) 的情况。
  • \(k\geq n+B\) 的情况等价于 \(n-B\)\(V=\lfloor \frac{A - 1}{B} \rfloor\)\(B\)\(0\) 的序列的 \(k\leq n\) 的情况。

一共需要调用 \(3n\)solve(k)

Subtask 5

无特殊限制。

这里就将 \(k\leq n\) 的所有 solve(k) 全部压缩到一起,想法是:对于一次 solve(k),可以把它填平的过程分成两阶段,以第一次到达“所有没有删除的数的极差 \(\leq 1\)”的状态为分界点。这样有什么好处呢?我们发现我们只需要找到 \(a_i\) 什么时候填平,就能快速跳过第一阶段,而第二阶段的形式统一,可以递推,下面详细解说一下。

第一阶段

对于每个 \(k\),我们找出一个最小的 \(l\) 使得

\[sum(a[l..k]) + blo(l)\times A \leq a[l] \times (k-l+1) \]

\(blo(l)\) 表示 \(l\) 所在块,块是按照 \(B\) 个一块划分的,\(blo(l)\)\(\left\lceil l/B\right\rceil\)。这里的意思是我们现在正处于主持人第 \(blo(l)\) 次操作之前,我们进行加操作,填平到了 \(a[l]\),下一次主持人就删掉 \(l\) 所在的块,就进入了第二阶段(注意可能需要特判一下 \(l=k\))。

进入第二阶段,我们维护 \(f_i\) 表示序列剩下 \(iB+1\) 个数没有删,这时总和的最大值是多少,因为这个填平的状态可能不是完全平的,已经记录了长度,干脆直接记录总和来表示当前的序列。

第二阶段

原算法的第二阶段只能暴力 \(O(k/B)\) 进行,所以我们将它们压缩了,并进行递推:从大到小枚举 \(i\)\(f_{i-1}\)\(func(f_i + A, iB+1, B)\) 取最大值,其中 \(func(sum, len, b)\) 表示长度为 \(len\) 的极差 \(\leq 1\) 的单调不升序列,删去前 \(b\) 个数后的总和是多少。

最后 \(f_0\) 就是答案。复杂度 \(O(n\log n)\) 瓶颈是排序。

代码

71 分:

#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define endl "\n"
#define debug(...) void(0)
#endif
using LL = long long;
constexpr int N = 1e5 + 10;
int n, B;
LL A, a[N], s[N];
LL solve(int k) {
  int p = k, q = k + 1;
  LL val = a[k];
  for (int i = 1; i <= (k - 1) / B; i++) {
    int tmpl = (i - 1) * B + 1;
    if (q <= tmpl) --val, p = tmpl, q = k + 1;
    else if (p <= tmpl) p = tmpl;
    LL rest = A;
    while (p > tmpl && rest >= (a[p - 1] - val) * (k - p + 1) + k - q + 1) {
      rest -= (a[p - 1] - val) * (k - p + 1) + k - q + 1;
      val = a[--p], q = k + 1;
    }
    int len = k - p + 1;
    val += rest / len, rest %= len;
    q += (int)rest;
    if (q > k + 1) q += p - k - 1, ++val;
  }
  int tmpl = k;
  if (q <= tmpl) --val, p = tmpl, q = k + 1;
  else if (p <= tmpl) p = tmpl;
  if (k <= 50) debug("solve(%d) = %lld\n", k, val + A);
  return val + A;
}
int main() {
#ifndef LOCAL
  cin.tie(nullptr)->sync_with_stdio(false);
#endif
  cin >> n >> A >> B;
  for (int i = 1; i <= n; i++) cin >> a[i];
  sort(a + 1, a + n + 1, greater<>{});
  LL ans = 0;
  for (int i = 1; i <= n; i += B) ans = max(ans, solve(i));
  sort(a + 1, a + n + 1, greater<>{});
  for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
  for (int i = n; i >= 0; i--) {
    if (!i || a[i] * (n - i) > s[n] - s[i] + A) {
      auto rest = A;
      for (int j = i + 1; j <= n; j++) rest -= a[i + 1] - a[j], a[j] = a[i + 1];
      for (int j = i + 1; j <= n; j++) a[j] += rest / (n - i);
      rest %= n - i;
      for (int j = i + 1; j <= i + rest; j++) a[j] += 1;
      break;
    }
  }
  for (int i = 1; i <= B; i++) a[i] = 0;
  sort(a + 1, a + n + 1, greater<>{});
  for (int i = 1; i <= n; i++) debug("%lld%c", a[i], " \n"[i == n]);
  for (int i = 1; i <= n; i += B) ans = max(ans, solve(i));
  for (int i = 1; i <= n - B; i++) a[i] = (A - 1) / B;
  for (int i = n - B + 1; i <= n; i++) a[i] = 0;
  for (int i = 1; i <= n; i++) debug("%lld%c", a[i], " \n"[i == n]);
  for (int i = 1; i <= n; i += B) ans = max(ans, solve(i));
  cout << ans << endl;
  return 0;
}

100 分:

#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define endl "\n"
#define debug(...) void(0)
#endif
using LL = long long;
template <class T> T& chkmax(T& x, const T& y) { if (x < y) x = y; return x; }
template <class T> T& chkmin(T& x, const T& y) { if (x > y) x = y; return x; }
int n, B;
LL a[100010], s[100010], f[100010], A;
LL func(LL sum, int len, int b) {
  // a[1] >= a[2] >= ... >= a[len]
  // a[1] - a[len] <= 1
  // a[1] + a[2] + ... + a[len] = sum
  // return a[b + 1] + ... + a[len]
  auto val = sum / len;
  auto rest = sum % len;
  return rest <= b ? val * (len - b) : sum - b * (val + 1);
}
void solve() {
  sort(a + 1, a + n + 1, greater<>{});
  for (int i = 1; i <= n; i++) debug("%lld%c", a[i], " \n"[i == n]);
  for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
  for (int l = 1, r = 1; r <= n; r += B) {
    while (l <= n && s[r] - s[l - 1] + (l + B - 1) / B * A < a[l] * (r - l + 1)) l++;
    if (l == r) { // 这是一个特判
      chkmax(f[0], a[r] + (r - 1) / B * A);
      debug("chkmax: f[%d] <- %lld\n", 0, a[r] + (r - 1) / B * A);
      continue;
    }
    int len = r - l + 1;
    auto sum = s[r] - s[l - 1] + (l + B - 1) / B * A;
    int k = (r - 1) / B - (l + B - 1) / B;
    chkmax(f[k], func(sum, len, (l + B - 1) / B * B - l + 1));
  }
}
int main() {
#ifndef LOCAL
  cin.tie(nullptr)->sync_with_stdio(false);
#endif
  cin >> n >> A >> B;
  for (int i = 1; i <= n; i++) cin >> a[i];
  solve();
  for (int i = n; i >= 0; i--) {
    if (!i || a[i] * (n - i) > s[n] - s[i] + A) {
      auto rest = A;
      for (int j = i + 1; j <= n; j++) rest -= a[i + 1] - a[j], a[j] = a[i + 1];
      for (int j = i + 1; j <= n; j++) a[j] += rest / (n - i);
      rest %= n - i;
      for (int j = i + 1; j <= i + rest; j++) a[j] += 1;
      break;
    }
  }
  for (int i = 1; i <= B; i++) a[i] = 0;
  solve();
  for (int i = 1; i <= n - B; i++) a[i] = (A - 1) / B;
  for (int i = n - B + 1; i <= n; i++) a[i] = 0;
  solve();
  for (int i = (n - 1) / B; i >= 1; i--) {
    int len = i * B + 1;
    chkmax(f[i - 1], func(f[i] + A, len, B));
  }
  cout << f[0] + A << endl;
  return 0;
}

参考资料

一些题(九) - Y25t - 博客园

【题解】UOJ Round #18 - Qiuly - 博客园

UOJ Round #18 题解 - 博客 - whzzt的博客


  1. UOJ Round #18 题解 - 博客 - whzzt的博客 ↩︎

posted @ 2025-04-14 14:26  caijianhong  阅读(56)  评论(0)    收藏  举报