文章目录

‹
›

积木游戏 题解

积木游戏 题解

题意

数轴上 \(n\) 个积木点,第 \(i\) 个坐标 \(x_i\)(升序),叠有 \(a_i\) 个积木。选定起点 \(p\) 后,每移动一单位坐标耗时 1 秒,手上最多持 1 块,拿起/放下不耗时。求 \(T\) 秒内 \(p\) 上最多能有多少块积木。

\(n \le 5\times 10^5\),\(T \le 10^{18}\),\(\sum a \le 5\times 10^9\)。

赛时思路:枚举 + 排序贪心

赛时我的做法是直接枚举 \(p\)。把位置 \(i\) 的一块积木搬到 \(p\) 需走一个来回,代价 \(2|x_i-x_p|\);\(p\) 自身的积木代价为 0。于是对每个 \(p\),把 \((代价, 个数)\) 排序后从小到大取:

复杂度 \(O(n^2\log n)\),只能通过 \(n \le 1000\) 的部分分。

瓶颈

相邻的 \(p\) 与 \(p+1\),所有距离只变化 \(\pm(x_{p+1}-x_p)\),两次计算的信息几乎完全重叠。暴力的浪费在于每个 \(p\) 都从零重算。要优化,就必须让 \(p\) 从左到右扫描时增量维护。

但直接维护"最多能堆几个"做不到:取的个数本身是变量,要维护的对象没有固定形状。下面的两步转化把问题压成可维护的形式。

转化一:二分答案

答案单调:能堆 \(mid\) 个必能堆 \(mid-1\) 个。二分答案,化为判定问题:

这一步的关键作用是把取的个数固定为 \(mid\)。个数固定后,"取哪些"才有稳定结构。

转化二:固定 \(p\) 时,最优解是一段连续区间

\(p\) 固定、个数固定为 \(mid\),最小化总时间,显然取距离 \(p\) 最近的 \(mid\) 个。又因为坐标有序,最近 \(mid\) 个在原序列上是连续区间 \([l_p, r_p]\)(两端点处可能只取部分)。

于是每个 \(p\) 的最优解由两个端点唯一确定,无需排序。

转化三(核心):端点单调不降

结论:\(l_i, r_i\) 关于 \(i\) 单调不降。

理由:\(p\) 右移时,左侧所有积木距离变大,右侧所有积木距离变小。若最优区间发生变化,只可能是用右边的积木替换左边的,绝不反向。

端点只右移不回头,\(p\) 扫一遍的过程中端点总移动次数 \(O(n)\)。

增量维护

窗口表示

当前取 \([L, R]\) 上共 \(mid\) 个,其中:

  • \([L+1, R-1]\) 全取;
  • \(Lx\) = 位置 \(L\) 上未取的个数,\(Rx\) = 位置 \(R\) 上已取的个数(端点处允许部分取)。

和 \(a\) 的前缀和 \(pre\),任意一段内"取了多少个"可 \(O(1)\) 查询。

时间更新:\(p\) 从 \(i-1\) 移到 \(i\)

设 \(d = x_i - x_{i-1}\)。每块积木往返搬运,距离变 \(d\) 则时间变 \(2d\):

  • 位置 \(\ge i\) 的已取积木每个省 \(2d\):共 \((pre[R-1]-pre[i-1]) + Rx\) 个;
  • 位置 \(\le i-1\) 的已取积木每个多花 \(2d\):共 \((pre[i-1]-pre[L-1]) - Lx\) 个。

窗口更新:近替换远

移动后,若右端外未取的积木比左端内已取的更近,最优区间就应右移:

若 \(x_R - x_i \le x_i - x_L\),则不取 \(L\) 转而取 \(R\)。

  • 每换一块,时间变化 \(2\big((x_R-x_i)-(x_i-x_L)\big) \le 0\),只减不增,故换到条件不满足为止即为当前 \(p\) 的最优;
  • 一次换的个数由两侧剩余量决定:\(L\) 可退 \(a_L-Lx\) 个,\(R\) 可补 \(a_R-Rx\) 个,少者先换完,端点取空/取满则推进指针;
  • 条件取等号:交换不亏,且保证窗口位置规范,维护单调性。

复杂度

\(O(n\log\sum a)\)

思维回顾

  • 暴力的贪心正确,问题在于逐 \(p\) 重算;
  • 想增量维护,但个数不定无法维护 → 二分答案固定个数;
  • 个数固定后,最优取法是连续区间 → 只需维护端点;
  • 左远右近 ⇒ 端点单调 → 滑动窗口成立;

AC code

#include <bits/stdc++.h>
using namespace std;

using ll = long long;

const int N = 5e5 + 5;

ll n, T;
ll x[N], a[N];

ll pre[N];

bool check(ll mid) {
    ll tim = 0;
    ll L = 1, R = n + 1;
    ll lx = 0, rx = 0;

    ll tot = 0;
    for (int i = 1; i <= n; i ++) {
        if (tot + a[i] <= mid) {
            tot += a[i];
            tim += 2ll * (x[i] - x[1]) * a[i];
        }
        else {
            rx = mid - tot;
            tim += 2ll * (x[i] - x[1]) * rx;
            R = i;
            break;
        }
    }

    if (tim <= T) return true;

    for (int i = 2; i <= n; i ++) {
        ll d = x[i] - x[i - 1];

        tim -= 2ll * (pre[R - 1] - pre[i - 1] + rx) * d;
        tim += 2ll * (pre[i - 1] - pre[L - 1] - lx) * d;

        while (R <= n) {
            ll disl = x[i] - x[L];
            ll disr = x[R] - x[i];
            if (disr > disl) break;

            ll cntl = a[L] - lx;
            ll cntr = a[R] - rx;

            if (cntl <= cntr) {
                tim += 2ll * (disr - disl) * cntl;
                rx += cntl;
                L ++;
                lx = 0;
                if (rx == a[R]) R ++, rx = 0;
            }
            else {
                tim += 2ll * (disr - disl) * cntr;
                lx += cntr;
                R ++, rx = 0;
            }
        }

        if (tim <= T) return true;
    }

    return false;
}

int main () {
    freopen("block.in", "r", stdin);
    freopen("block.out", "w", stdout);

    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> T;
    for (int i = 1; i <= n; i ++) cin >> x[i];
    for (int i = 1; i <= n; i ++) cin >> a[i];

    for (int i = 1; i <= n; i ++) pre[i] = pre[i - 1] + a[i];

    ll l = 0, r = pre[n], ans = 0;
    while (l <= r) {
        ll mid = (l + r) / 2;
        if (check(mid)) {
            ans = mid;
            l = mid + 1;
        }
        else r = mid - 1;
    }

    cout << ans << '\n';

    return 0;
}
posted @ 2026-10-20 20:56  lvwangshu  阅读(4)  评论(0)    收藏  举报