2528. 最大化城市的最小电量

题目

给你一个下标从 0 开始长度为 n 的整数数组 stations ,其中 stations[i] 表示第 i 座城市的供电站数目。

每个供电站可以在一定 范围 内给所有城市提供电力。换句话说,如果给定的范围是 r ,在城市 i 处的供电站可以给所有满足 |i - j| <= r 且 0 <= i, j <= n - 1 的城市 j 供电。

  • |x| 表示 x 的 绝对值 。比方说,|7 - 5| = 2 ,|3 - 10| = 7 。

一座城市的 电量 是所有能给它供电的供电站数目。

政府批准了可以额外建造 k 座供电站,你需要决定这些供电站分别应该建在哪里,这些供电站与已经存在的供电站有相同的供电范围。

给你两个整数 r 和 k ,如果以最优策略建造额外的发电站,返回所有城市中,最小电量的最大值是多少。

这 k 座供电站可以建在多个城市。

示例 1:

输入:stations = [1,2,4,5,0], r = 1, k = 2 输出:5 解释: 最优方案之一是把 2 座供电站都建在城市 1 。 每座城市的供电站数目分别为 [1,4,4,5,0] 。

  • 城市 0 的供电站数目为 1 + 4 = 5 。
  • 城市 1 的供电站数目为 1 + 4 + 4 = 9 。
  • 城市 2 的供电站数目为 4 + 4 + 5 = 13 。
  • 城市 3 的供电站数目为 5 + 4 = 9 。
  • 城市 4 的供电站数目为 5 + 0 = 5 。 供电站数目最少是 5 。 无法得到更优解,所以我们返回 5 。

示例 2:

输入:stations = [4,4,4,4], r = 0, k = 3 输出:4 解释: 无论如何安排,总有一座城市的供电站数目是 4 ,所以最优解是 4 。

提示:

  • n == stations.length
  • 1 <= n <= 10^5
  • 0 <= stations[i] <= 10^5
  • 0 <= r <= n - 1
  • 0 <= k <= 10^9

这是一个典型的“最大化最小值”问题,通常使用二分答案结合贪心策略来解决。

核心解题思路

  1. 二分答案 (Binary Search on Answer):
    • 题目要求我们求“最小电量的最大值”。
    • 假设我们设定一个目标最小电量值 target,如果我们能用不超过 k 个额外供电站使得所有城市的电量都 $\ge$ target,那么对于任何比 target 小的值我们也一定能做到。反之,如果 target 做不到,那么比它大的值更做不到。
    • 这种单调性允许我们在可能的答案范围内(从 0 到一个足够大的数)进行二分查找。
  2. 贪心策略 (Greedy Strategy) 用于 check 函数:
    • 在二分过程中,我们需要一个 check(target) 函数来判断:是否能在增加不超过 k 个供电站的情况下,让所有城市的电量至少为 target
    • 贪心决策:当我们从左到右遍历城市时,如果发现某个城市 i 的电量不足 target,我们就需要建造新的供电站来覆盖它。为了让这个新供电站尽可能多地“造福”后续的城市,我们应该把它建在能覆盖城市 i 的最右边位置
    • 能覆盖城市 i 的供电站位置范围是 [i - r, i + r]。在不越界的情况下,最右边的位置是 min(n - 1, i + r)
    • 我们在这个最佳位置建造所需的供电站,这些站点的覆盖范围将一直延伸到 min(n - 1, i + r) + r
  3. 滑动窗口/差分数组优化:
    • 为了高效地计算每个城市的初始电量以及在 check 过程中动态维护新增供电站的影响,我们需要使用滑动窗口或差分数组的技术,将每次检查的时间复杂度控制在 $O(N)$。

步骤 1: 预处理初始电量

首先,我们需要快速算出不加任何新站点时,每个城市的初始电量。

设 initial_power[i] 为城市 i 的初始电量。

朴素计算需要 $O(N \cdot R)$,太慢。我们可以用滑动窗口在 $O(N)$ 时间内算出来:

  • 城市 0 的电量是 stations[0...min(r, n-1)] 的和。
  • 当从城市 i 移动到城市 i+1 时,覆盖窗口从 [i-r, i+r] 变为 [i+1-r, i+1+r]
  • 我们需要减去滑出窗口左侧的站点 stations[i-r](如果存在),并加上滑入窗口右侧的站点 stations[i+1+r](如果存在)。

步骤 2: 设计 check(target) 函数

这个函数返回布尔值:能否在 k 个预算内达到目标 target

  1. 我们需要一个数组(或差分数组)来记录新增加的供电站对当前城市电量的贡献。
  2. 从左到右遍历城市 i = 0n-1
    • 维护一个变量 current_extra_power,表示此前在 [i-r, i+r] 范围内新增的供电站总数。
    • 如果有些新增站点在到达城市 i 时已经超出了覆盖范围(即它们覆盖的最后一座城市是 i-1),需要先把它们从 current_extra_power 中减去。
    • 计算城市 i 的总电量:total_power = initial_power[i] + current_extra_power
    • 如果 total_power < target,说明电量不够:
      • 计算缺口:needed = target - total_power
      • 如果剩余的 k 不够填补这个缺口,直接返回 false
      • 贪心操作:在位置 pos = min(n - 1, i + r) 处增加 needed 个供电站。
      • 这些新站点从现在开始生效,所以 current_extra_power += needed
      • 记录这些站点将在过了城市 min(n - 1, pos + r) 后失效。我们可以用一个差分数组 diff 来记录失效位置:diff[pos + r + 1] -= needed
  3. 如果遍历完所有城市,总共消耗的额外供电站数量 $\le k$,返回 true

步骤 3: 二分执行

  • 确定二分范围 [left, right]left 可以是 0,right 可以是 $2 \times 10^{14}$(大约是最大可能的初始电量 + 最大 K)。
  • 进行标准的二分查找,找到满足 check(mid) == true 的最大 mid

具体代码

C++

class Solution {
public:
    long long maxPower(vector<int>& stations, int r, long long k) {
        int n = (int)stations.size();

        // 1) 预处理:计算每个城市当前电量 power[i] = sum_{j in [i-r, i+r]} stations[j]
        vector<long long> pref(n + 1, 0);
        for (int i = 0; i < n; ++i) pref[i + 1] = pref[i] + stations[i];

        auto getRangeSum = [&](int L, int R) -> long long {
            L = max(L, 0);
            R = min(R, n - 1);
            if (L > R) return 0LL;
            return pref[R + 1] - pref[L];
        };

        vector<long long> power(n, 0);
        for (int i = 0; i < n; ++i) {
            power[i] = getRangeSum(i - r, i + r);
        }

        // 2) 二分答案:最大化所有城市的最小电量
        long long low = *min_element(power.begin(), power.end());
        long long high = *max_element(power.begin(), power.end()) + k; // 上界安全:把 k 全堆到能覆盖某城的位置

        auto can = [&](long long x) -> bool {
            // 线性检查是否能用 <= k 个新电站,让所有城市电量 >= x
            // 技巧:用“到期数组 expire”维护新增电站对滑动窗口的影响结束时间
            vector<long long> expire(n + 1, 0);
            long long used = 0;        // 已经新增的电站总数
            long long extra = 0;       // 当前 i 的新增电量贡献(窗口内新增站的总数)

            for (int i = 0; i < n; ++i) {
                // 移除已经过期的新增影响
                extra -= expire[i];

                long long curr = power[i] + extra;
                if (curr < x) {
                    long long need = x - curr; // 还差这么多电量
                    used += need;
                    if (used > k) return false;

                    // 贪心:把 need 个新电站尽量放在最右端位置 pos = min(i + r, n - 1)
                    // 这样既能立刻覆盖当前城市,也能尽量覆盖后面的城市,便于后续修补
                    int pos = min(n - 1, i + r);
                    extra += need;                    // 这些站从现在起对窗口生效
                    int endIdx = pos + r + 1;        // 它们对窗口影响在 endIdx 时刻过期(不包含 endIdx)
                    if (endIdx <= n) expire[endIdx] += need;
                }
            }
            return true;
        };

        while (low < high) {
            long long mid = (low + high + 1) >> 1; // 取上中位,避免死循环
            if (can(mid)) low = mid;
            else high = mid - 1;
        }
        return low;
    }
};

Python

from typing import List

class Solution:
    def maxPower(self, stations: List[int], r: int, k: int) -> int:
        n = len(stations)

        # 1) 预处理:计算每个城市当前电量 power[i] = sum_{j in [i-r, i+r]} stations[j]
        pref = [0] * (n + 1)
        for i, v in enumerate(stations):
            pref[i + 1] = pref[i] + v

        def range_sum(L: int, R: int) -> int:
            L = max(L, 0)
            R = min(R, n - 1)
            if L > R:
                return 0
            return pref[R + 1] - pref[L]

        power = [0] * n
        for i in range(n):
            power[i] = range_sum(i - r, i + r)

        # 2) 二分答案:最大化所有城市的最小电量
        lo = min(power)
        hi = max(power) + k  # 全部新增电站堆到最短板附近的安全上界

        def can(x: int) -> bool:
            """
            检查是否能在 <= k 个新增电站下,使所有城市电量 >= x
            贪心:从左到右,若 power[i] + extra < x,则在 pos=min(i+r, n-1) 处补 need
            用 expire 数组记录这些新增的“影响何时到期”,以 O(1) 维护 extra
            """
            used = 0
            extra = 0
            expire = [0] * (n + 1)  # expire[t] 表示在下标 t 时,这些新增电站的影响到期并从 extra 中扣除

            for i in range(n):
                # 移除到期影响
                extra -= expire[i]

                curr = power[i] + extra
                if curr < x:
                    need = x - curr
                    used += need
                    if used > k:
                        return False
                    pos = min(n - 1, i + r)
                    extra += need
                    end_idx = pos + r + 1  # 影响区间到 [i, pos+r],在 end_idx 时刻过期
                    if end_idx <= n:
                        expire[end_idx] += need
            return True

        while lo < hi:
            mid = (lo + hi + 1) // 2
            if can(mid):
                lo = mid
            else:
                hi = mid - 1

        return lo