普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月1日LeetCode 每日一题题解

每日一题-同时运行 N 台电脑的最长时间🔴

2025年12月1日 00:00

你有 n 台电脑。给你整数 n 和一个下标从 0 开始的整数数组 batteries ,其中第 i 个电池可以让一台电脑 运行 batteries[i] 分钟。你想使用这些电池让 全部 n 台电脑 同时 运行。

一开始,你可以给每台电脑连接 至多一个电池 。然后在任意整数时刻,你都可以将一台电脑与它的电池断开连接,并连接另一个电池,你可以进行这个操作 任意次 。新连接的电池可以是一个全新的电池,也可以是别的电脑用过的电池。断开连接和连接新的电池不会花费任何时间。

注意,你不能给电池充电。

请你返回你可以让 n 台电脑同时运行的 最长 分钟数。

 

示例 1:

输入:n = 2, batteries = [3,3,3]
输出:4
解释:
一开始,将第一台电脑与电池 0 连接,第二台电脑与电池 1 连接。
2 分钟后,将第二台电脑与电池 1 断开连接,并连接电池 2 。注意,电池 0 还可以供电 1 分钟。
在第 3 分钟结尾,你需要将第一台电脑与电池 0 断开连接,然后连接电池 1 。
在第 4 分钟结尾,电池 1 也被耗尽,第一台电脑无法继续运行。
我们最多能同时让两台电脑同时运行 4 分钟,所以我们返回 4 。

示例 2:

输入:n = 2, batteries = [1,1,1,1]
输出:2
解释:
一开始,将第一台电脑与电池 0 连接,第二台电脑与电池 2 连接。
一分钟后,电池 0 和电池 2 同时耗尽,所以你需要将它们断开连接,并将电池 1 和第一台电脑连接,电池 3 和第二台电脑连接。
1 分钟后,电池 1 和电池 3 也耗尽了,所以两台电脑都无法继续运行。
我们最多能让两台电脑同时运行 2 分钟,所以我们返回 2 。

 

提示:

  • 1 <= n <= batteries.length <= 105
  • 1 <= batteries[i] <= 109

两种方法:二分答案 / 排序+贪心(Python/Java/C++/Go)

作者 endlesscheng
2022年1月16日 15:41

方法一:二分答案

如果可以让 $n$ 台电脑同时运行 $x$ 分钟,那么必然可以同时运行 $x-1,x-2,\ldots$ 分钟(要求更宽松);如果无法让 $n$ 台电脑同时运行 $x$ 分钟,那么必然无法同时运行 $x+1,x+2,\ldots$ 分钟(要求更苛刻)。

据此,可以二分猜答案。关于二分算法的原理,请看 二分查找 红蓝染色法【基础算法精讲 04】

假设可以让 $n$ 台电脑同时运行 $x$ 分钟,那么对于电量大于 $x$ 的电池,其只能被使用 $x$ 分钟,因此每个电池的使用时间至多为 $\min(\textit{batteries}[i], x)$。累加所有电池的使用时间,记作 $\textit{sum}$。那么要让 $n$ 台电脑同时运行 $x$ 分钟,必要条件是 $n\cdot x\le \textit{sum}$。

下面证明该条件也是充分的,即如果 $n\cdot x\le \textit{sum}$ 成立,那么一定存在一种安排电池的方式,可以让 $n$ 台电脑同时运行 $x$ 分钟。

构造方法如下:

对于电量 $\ge x$ 的电池,我们可以让其给一台电脑供电 $x$ 分钟。由于一个电池不能同时给多台电脑供电,因此该电池若给一台电脑供电 $x$ 分钟,那它就不能用于其他电脑了(因为电脑运行时间就是 $x$ 分钟)。我们可以将所有电量 $\ge x$ 的电池各给一台电脑供电。

对于其余电池,设其电量和为 $\textit{sum}'$,剩余 $n'$ 台电脑未被供电。我们可以随意选择剩下的电池,供给剩余的第一台电脑(用完一个电池就换下一个电池),多余的电池电量与剩下的电池一起供给剩余的第二台电脑,依此类推。注意由于这些电池的电量均小于 $x$,按照这种做法是不会出现同一个电池在同一时间供给多台电脑的(如果某个电池供给了两台电脑,可以将这个电池的供电时间划分到第一台电脑的末尾和第二台电脑的开头)。

由于 $\textit{sum}'=\textit{sum}-(n-n')\cdot x$,结合 $n\cdot x\le \textit{sum}$ 可以得到 $n'\cdot x\le \textit{sum}'$,按照上述供电方案(用完一个电池就换下一个电池),这 $n'$ 台电脑可以运行至少 $x$ 分钟。充分性得证。

细节

下面代码采用开区间二分,这仅仅是二分的一种写法,使用闭区间或者半闭半开区间都是可以的。

  • 开区间左端点初始值:$0$。不运行任何电脑,一定满足要求。
  • 开区间右端点初始值:平均值加一,即 $\left\lfloor\dfrac{\sum \textit{batteries}[i]}{n}\right\rfloor + 1$。一定无法满足要求。
class Solution:
    def maxRunTime(self, n: int, batteries: List[int]) -> int:
        l, r = 0, sum(batteries) // n + 1
        while l + 1 < r:
            x = (l + r) // 2
            if n * x <= sum(min(b, x) for b in batteries):
                l = x
            else:
                r = x
        return l
class Solution:
    def maxRunTime(self, n: int, batteries: List[int]) -> int:
        r = sum(batteries) // n
        # 二分找最小的不满足要求的 x+1,那么最大的满足要求的就是 x
        check = lambda x: n * (x + 1) > sum(min(b, x + 1) for b in batteries)
        return bisect_left(range(r), True, key=check)
class Solution {
    public long maxRunTime(int n, int[] batteries) {
        long tot = 0;
        for (int b : batteries) {
            tot += b;
        }

        long l = 0;
        long r = tot / n + 1;
        while (l + 1 < r) {
            long x = l + (r - l) / 2;
            long sum = 0;
            for (int b : batteries) {
                sum += Math.min(b, x);
            }
            if (n * x <= sum) {
                l = x;
            } else {
                r = x;
            }
        }
        return l;
    }
}
class Solution {
public:
    long long maxRunTime(int n, vector<int>& batteries) {
        long long tot = reduce(batteries.begin(), batteries.end(), 0LL);
        long long l = 0, r = tot / n + 1;
        while (l + 1 < r) {
            long long x = l + (r - l) / 2;
            long long sum = 0;
            for (long long b : batteries) {
                sum += min(b, x);
            }
            (n * x <= sum ? l : r) = x;
        }
        return l;
    }
};
func maxRunTime(n int, batteries []int) int64 {
tot := 0
for _, b := range batteries {
tot += b
}

return int64(sort.Search(tot/n, func(x int) bool {
x++
sum := 0
for _, b := range batteries {
sum += min(b, x)
}
return n*x > sum
}))
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(m\log (S/n))$,其中 $m$ 是 $\textit{batteries}$ 的长度,$S$ 是 $\textit{batteries}$ 的元素和。
  • 空间复杂度:$\mathcal{O}(1)$。

方法二:排序 + 贪心

受解法一的启发,我们可以得出如下贪心策略:

记电池电量和为 $\textit{sum}$,则理论上至多可以供电 $x=\Big\lfloor\dfrac{\textit{sum}}{n}\Big\rfloor$ 分钟。我们对电池电量从大到小排序,然后从电量最大的电池开始遍历:

  • 若该电池电量超过 $x$,则将其供给一台电脑,问题缩减为 $n-1$ 台电脑的子问题。

  • 若该电池电量不超过 $x$,则其余电池的电量均不超过 $x$,此时有

    $$
    n\cdot x=n\cdot\Big\lfloor\dfrac{\textit{sum}}{n}\Big\rfloor \le \textit{sum}
    $$

    根据解法一的结论,这些电池可以给 $n$ 台电脑供电 $x$ 分钟。

由于随着问题规模减小,$x$ 不会增加,因此若遍历到一个电量不超过 $x$ 的电池时,可以直接返回 $x$ 作为答案。

class Solution:
    def maxRunTime(self, n: int, batteries: List[int]) -> int:
        batteries.sort(reverse=True)
        s = sum(batteries)
        for b in batteries:
            if b <= s // n:
                return s // n
            s -= b
            n -= 1
class Solution {
    public long maxRunTime(int n, int[] batteries) {
        Arrays.sort(batteries);

        long sum = 0;
        for (int b : batteries) {
            sum += b;
        }

        for (int i = batteries.length - 1; ; i--) {
            if (batteries[i] <= sum / n) {
                return sum / n;
            }
            sum -= batteries[i];
            n--;
        }
    }
}
class Solution {
public:
    long long maxRunTime(int n, vector<int>& batteries) {
        ranges::sort(batteries, greater());
        long long sum = reduce(batteries.begin(), batteries.end(), 0LL);
        for (int i = 0; ; i++) {
            if (batteries[i] <= sum / n) {
                return sum / n;
            }
            sum -= batteries[i];
            n--;
        }
    }
};
func maxRunTime(n int, batteries []int) int64 {
slices.Sort(batteries)
sum := 0
for _, b := range batteries {
sum += b
}
for i := len(batteries) - 1; ; i-- {
if batteries[i] <= sum/n {
return int64(sum / n)
}
sum -= batteries[i]
n--
}
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(m\log m)$,其中 $m$ 是 $\textit{batteries}$ 的长度。瓶颈在排序上。
  • 空间复杂度:$\mathcal{O}(1)$。忽略排序的栈开销。

专题训练

  1. 二分题单的「§2.2 求最大」。
  2. 贪心题单的「§1.1 从最小/最大开始贪心」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

二分答案的check函数的思考方式

作者 onjoujitoki
2022年1月16日 14:30

本题来自ABC 227D。这题很容易想到二分答案,但是check函数稍微有点难想。
借用了张图来表达一下。原日文博客
设本题电脑同时运行时间为P,这也是我们不断二分得到的结果。
设一共有K台电脑,我们的目的是在P的时间内不断运转他们。
因此,我们的目的其实是看电池的状态能不能填满P*K的矩形。

image.png
上部分代表了一种电池的合法分布情况。
很明显,当Batteries0(黄色)的数量超过了P,我们其实只需要P即可,剩下的都只能抛弃。
当Batteries1(橘色)的数量小于P,我们需要把当前电池全部用完。同时提前借用别的电池来填充该列。

然而,下面也有NG的情况。我们把橘色电池容量-1,红色的+1,再来看看我们构造的矩形。因为一行不能存在2个同样的颜色(即不能存在一个电池给2个电脑续航的情况),所以红色的电池会浪费掉一个(对应了代码里的min(p, 红色电池容量)),最终导致矩形的构造失败。
总结一下可以用这个心态来构造矩形:小于P的时候,贪心地利用多个电池,但是同时不能在一行里有相同的颜色。

###C++

auto check = [&](i64 mid) {
            i64 sum = 0;
            for(int x : batteries) sum += min(mid, (i64)x);
            return sum >= n * mid;
        };

全部代码:

###C++

typedef long long i64;
class Solution {
public:
    long long maxRunTime(int n, vector<int>& batteries) {
        auto check = [&](i64 mid) {
            i64 sum = 0;
            for(int x : batteries) sum += min(mid, (i64)x);
            return sum >= n * mid;
        };
        i64 l = 0, r = 1e16/n;
        while (l < r) {
            i64 mid = l + r + 1>> 1;
            if (check(mid)) l = mid;
            else r = mid - 1;
        }
        return l;
    }
};

二分答案(证明+图解)

作者 newhar
2022年1月16日 12:08

解法:二分法

假设所有电脑同时运行 $t$ 分钟。因为一个电池同时只能给一台电脑供电,所以一个电池最多有 $t$ 分钟的供电时间。我们只需要统计所有电池的可供电时间总和$\displaystyle{S = \sum_i{\min(t, batteries_i)}}$ ,然后检查它们是否可以给 $n$ 台电脑供电即可(即 $\displaystyle{\frac{S}{t} \ge n}$)。

为什么这个解法是正确的?实际上,如果 $\displaystyle{\frac{S}{t} \ge n}$ ,那么我们总可以找出一种符合要求的方案来支持 $n$ 台电脑的运行。

如下图所示,我们依次分配电池 $0 \sim m$ 给电脑 $0 \sim n$。图中,横轴代表时间,各个栏目代表各个电脑的电池分配情况。首先我们把电池 $0$ (蓝色)分配给电脑 $0$。电池 $0$ 给电脑 $0$ 供电时段为 $0 \sim t_2$。然后,电脑 $0$ 由电池 $1$ 继续供电,而电池 $1$ 的余下电量用于供给电脑 $1$。然后,我们继续安排电池 $2,3,4... m-1$ 即可。

image.png

我们可以得出以下结论:只要每个电池的供电时间不超过 $t$,那么每个电池的供电时间就不会发生重叠,也就不会发生同一个电池给多台电脑的情况。

因此,每个电池的 最大可供电时间 = $\min(电池电量, t)$。只要最大可供电时间的 总和 可以 覆盖 所有的电脑的时间总和($n \times t$),那么这个供电方案就是可行的。

class Solution:
    def maxRunTime(self, n: int, batteries: List[int]) -> int:
        l, r = 1, 10 ** 15
        while l < r:
            m = (l + r + 1) >> 1
            if sum(min(x, m) for x in batteries) >= n*m:
                l = m
            else:
                r = m-1
        return l
class Solution {
public:
    long long maxRunTime(int n, vector<int>& batteries) {
        auto check = [&](long long t) {
            long long sum = 0;
            for(int i : batteries) sum += min(t, (long long)i);
            return sum / t >= n;
        };
        
        long long l = 1, r = 1e15;
        while(l < r) {
            long long m = (l + r + 1) / 2;
            if(check(m)) {
                l = m;
            }
            else {
                r = m - 1;
            }
        }
        return l;
    }
};
❌
❌