分配给商店的最多商品的最小值 图解题解
这道题到底在问什么
- 输入
- n=7, quantities=[15,10,10]
- 输出
- 5
- 输入
- n=6, quantities=[11,6]
- 输出
- 3
先想最直接的笨办法
三种商品要的店数加起来是 6 间。这不超过 7 间,说明每店 8 件是放得下的。既然放得下,就试试更小的上限,把右界收到 mid 等于 8,答案可能还在更左边。新区间是 [1, 8]。(动画第 9 步)
最优解:为什么这么做
一句话答案:LeetCode 2064 分配给商店的最小最大值:n 家店分 m 种商品、一店只放一种,让件数最多的店尽量轻。上限抬高店只减不增,这条单调让上限可二分,判定按 ⌈件数÷x⌉ 累计店数 ≤n。时间 O(m·log C)、空间 O(1)。
n 间店、m 种商品,这题要把哪个数压到最小
有 n 间零售商店和 m 种商品,第 i 种有 quantities[i] 件。一间店只放一种商品、件数不限;记所有店里商品数目的最大值为 x,要让 x 尽量小。题面 n=7、quantities=[15,10,10] 时答案是 5:每店至多 5 件,三种商品刚好用 7 间店装下,再小一档就塞不进了。
从每店 1 件起一档档试上限,会白试掉一大段
每店上限 x 最小取 1,最大不必超过件数最多的那种商品 15(取到 15,每种商品一间店就装下,才 3 间、没用满 7 间)。笨办法是从 1 起逐个试上限,把各商品店数加起来看超不超 n。可最坏要试到那条分界档才停,次数和上限范围一样多,是 O(m·C)(C 是件数值域),件数一大就试不动了。
每店上限抬高,需要的店只减不增
把每个上限对应的总店数排一排:上限越大,每店装得越多,需要的店数只减不增、越容易塞进 n 间,这就是单调。于是店数够的上限连成一片、不够的连成另一片,答案正是够用那片最左那个——第一个够用的上限,二分就能逼近它。这里二分的是每店上限这个答案、不是商品下标,这类猜答案再验证的解法就叫二分答案。
判定函数怎么数店数,够用和不够又各收哪半
判定函数 check(x) 这样数:某种商品件数、每店至多 x 件,要 ⌈件数 ÷ x⌉ 间店——⌈⌉ 是向上取整,把件数按每堆 x 件分,凑不满一堆的零头也单占一间。各商品店数加起来不超过 n 就够用。二分在 [lo, hi] 取中点 mid:check(mid) 够用就把 hi 收到 mid(可能就是答案,得留);不够用,lo 跳到 mid+1。缩到 lo、hi 相遇即答案。
拿 n=7、[15,10,10] 一轮轮列出 mid 和店数
起点 lo=1、hi=15。第一轮 mid=(1+15)//2=8,⌈15÷8⌉+⌈10÷8⌉+⌈10÷8⌉=2+2+2=6 ≤ 7 够用,hi 收到 8。第二轮 lo=1、hi=8,mid=4,⌈15÷4⌉+⌈10÷4⌉+⌈10÷4⌉=4+3+3=10 > 7 不够,lo 跳到 5。第三轮 lo=5、hi=8,mid=6,⌈15÷6⌉+⌈10÷6⌉+⌈10÷6⌉=3+2+2=7 ≤ 7 够用,hi 收到 6。第四轮 lo=5、hi=6,mid=5,⌈15÷5⌉+⌈10÷5⌉+⌈10÷5⌉=3+2+2=7 ≤ 7 够用,hi 收到 5。此时 lo=hi=5,循环停,答案 5。
下界搁 0 会当场除零,收右界写 mid−1 会丢答案
二分把上限档数压到约 log C,每档 check 扫 m 种商品,时间 O(m·log C)、空间 O(1)。下界必须从 1 起:写成 0,⌈件数 ÷ 0⌉ 直接除零崩掉,每店本就至少放 1 件。够用时只能写 hi=mid,写成 hi=mid−1 会把可能正是答案的 mid 挤出区间,结果偏大一档。判据也得写累计店数 ≤ n 才够用,写成 ≥ n 就方向全反、答案错位。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记牢这一句:二分答案 x,判据是把每种商品要的店数 ⌈qi 除以 x⌉ 加起来,累计不超过 n 就可行,收右界去找更小的 x;超了就收左界。下面每一帧都在套它。
- 4上面这排是三种商品的件数 15、10、10。右边面板记二分状态。为什么上界取最大件数 15,因为哪怕每店只放到 15 件,每种商品最多也只占一间店,一共才 3 间、不超过 7 间,肯定放得下,所以答案不会比 15 更大。下界取 1,每店至少能放 1 件。答案就落在 1 到 15 之间。
- 5当前二分区间是左界 1、右界 15,取正中间 mid 等于 8。现在假设每间店最多放 8 件,来数一数这样够不够 7 间店装下所有商品。累计门店从 0 开始。
- 6紫色指到第一种商品,它有 15 件。每间店至多 8 件,把 15 除以 8 向上取整,得 2 间店。这 2 间店足够装下它的 15 件。累计门店加到 2 间。
- 7紫色指到第二种商品,它有 10 件。每间店至多 8 件,把 10 除以 8 向上取整,得 2 间店。这 2 间店足够装下它的 10 件。累计门店加到 4 间。
- 8紫色指到第三种商品,它有 10 件。每间店至多 8 件,把 10 除以 8 向上取整,得 2 间店。这 2 间店足够装下它的 10 件。累计门店加到 6 间。
- 9三种商品要的店数加起来是 6 间。这不超过 7 间,说明每店 8 件是放得下的。既然放得下,就试试更小的上限,把右界收到 mid 等于 8,答案可能还在更左边。新区间是 [1, 8]。
- 10当前二分区间是左界 1、右界 8,取正中间 mid 等于 4。现在假设每间店最多放 4 件,来数一数这样够不够 7 间店装下所有商品。累计门店从 0 开始。
- 11紫色指到第一种商品,它有 15 件。每间店至多 4 件,把 15 除以 4 向上取整,得 4 间店。这 4 间店足够装下它的 15 件。累计门店加到 4 间。
- 12紫色指到第二种商品,它有 10 件。每间店至多 4 件,把 10 除以 4 向上取整,得 3 间店。这 3 间店足够装下它的 10 件。累计门店加到 7 间。
- 13紫色指到第三种商品,它有 10 件。每间店至多 4 件,把 10 除以 4 向上取整,得 3 间店。这 3 间店足够装下它的 10 件。累计门店加到 10 间。
- 14三种商品要的店数加起来是 10 间。这超过了 7 间,说明每店 4 件太挤、放不下。得放宽上限,把左界收到 mid 加 1 等于 5,答案在更右边。新区间是 [5, 8]。
- 15当前二分区间是左界 5、右界 8,取正中间 mid 等于 6。现在假设每间店最多放 6 件,来数一数这样够不够 7 间店装下所有商品。累计门店从 0 开始。
- 16紫色指到第一种商品,它有 15 件。每间店至多 6 件,把 15 除以 6 向上取整,得 3 间店。这 3 间店足够装下它的 15 件。累计门店加到 3 间。
- 17紫色指到第二种商品,它有 10 件。每间店至多 6 件,把 10 除以 6 向上取整,得 2 间店。这 2 间店足够装下它的 10 件。累计门店加到 5 间。
- 18紫色指到第三种商品,它有 10 件。每间店至多 6 件,把 10 除以 6 向上取整,得 2 间店。这 2 间店足够装下它的 10 件。累计门店加到 7 间。
- 19三种商品要的店数加起来是 7 间。这不超过 7 间,说明每店 6 件是放得下的。既然放得下,就试试更小的上限,把右界收到 mid 等于 6,答案可能还在更左边。新区间是 [5, 6]。
- 20当前二分区间是左界 5、右界 6,取正中间 mid 等于 5。现在假设每间店最多放 5 件,来数一数这样够不够 7 间店装下所有商品。累计门店从 0 开始。
- 21紫色指到第一种商品,它有 15 件。每间店至多 5 件,把 15 除以 5 向上取整,得 3 间店。这 3 间店足够装下它的 15 件。累计门店加到 3 间。
- 22紫色指到第二种商品,它有 10 件。每间店至多 5 件,把 10 除以 5 向上取整,得 2 间店。这 2 间店足够装下它的 10 件。累计门店加到 5 间。
- 23紫色指到第三种商品,它有 10 件。每间店至多 5 件,把 10 除以 5 向上取整,得 2 间店。这 2 间店足够装下它的 10 件。累计门店加到 7 间。
- 24三种商品要的店数加起来是 7 间。这不超过 7 间,说明每店 5 件是放得下的。既然放得下,就试试更小的上限,把右界收到 mid 等于 5,答案可能还在更左边。新区间是 [5, 5]。
- 25二分到左界和右界相遇,都停在 5,区间只剩这一个值,循环结束。这个 5 就是最小的最大值:每间店至多 5 件时,三种商品刚好用 7 间店装下;再小一点就装不下。和开头说的答案对上了。
⚠️ 容易写错的地方
✗ 错:可行性判据方向写反,把累计店数 ≥ n 当可行
✓ 对:累计店数 ≤ n 才可行
每种商品要 ⌈qi/x⌉ 间店,总店数不超过 n 才装得下;写成大于等于会把该收右界的往左收,答案错位
✗ 错:每种商品的店数用普通整除 qi/x
✓ 对:必须向上取整 ⌈qi/x⌉,即 (qi+x-1)/x
整除会丢掉余下的零头件数,少算一间店,把放不下的误判成放得下
✗ 错:二分下界从 0 开始
✓ 对:下界从 1 开始
x 是每店件数上限,取 0 会让 ⌈qi/0⌉ 除以零;每店至少能放 1 件,下界 1 才合法
✗ 错:收右界写成 right = mid - 1
✓ 对:可行时 right = mid,保留 mid 这个候选
mid 本身是一个可行答案,减 1 会把它丢掉;二分找最左可行值时,可行的 mid 要留在区间里
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def minimizedMaximum(self, n: int, quantities: List[int]) -> int:
def check(x):
return sum((v + x - 1) // x for v in quantities) <= n
return 1 + bisect_left(range(1, 10**6), True, key=check)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int minimizedMaximum(int n, vector<int>& quantities) {
int left = 1, right = 1e5;
while (left < right) {
int mid = (left + right) >> 1;
int cnt = 0;
for (int& v : quantities) {
cnt += (v + mid - 1) / mid;
}
if (cnt <= n) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
};Java
import java.util.*;
class Solution {
public int minimizedMaximum(int n, int[] quantities) {
int left = 1, right = (int) 1e5;
while (left < right) {
int mid = (left + right) >> 1;
int cnt = 0;
for (int v : quantities) {
cnt += (v + mid - 1) / mid;
}
if (cnt <= n) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
}复杂度
时间
O(m·log C)
C 是件数值域(上界)。二分把区间每次减半,做 log C 轮;每一轮的可行性检查要遍历 m 种商品各算一次向上取整,是 O(m)。两者相乘
空间
O(1)
只用了 left、right、mid、cnt 这几个变量,不随商品数或件数增长(Python 版的 range 是惰性区间,不真正展开)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 分配给商店的最多商品的最小值 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这道题凭什么能二分,不直接算?+
看两点:答案是「求最小的每店上限」这类最值,且它单调——上限越大越容易在 n 间店内装下,存在一条分界,分界及以上的上限都够用、以下都不够。凡是答案本身单调、又能写出一个判定函数验证某个候选行不行,就能把「直接推答案」换成「猜一个上限再判定」。运输能力、分割数组最大值这类题都是同一套路。
判定里为什么必须向上取整,普通整除不行?+
一间店只放一种商品,某种商品的件数按每店 x 件装,最后凑不满 x 件的零头还得单占一间店。所以店数是 ⌈件数 ÷ x⌉:比如 15 件、每店 8 件,第一间装 8、第二间装 7,要 2 间,正是 ⌈15÷8⌉=2。写成整除 15÷8=1 会漏掉那间装零头的店,把店数算少,反而把装不下的上限误判成够用。
和珂珂吃香蕉(LC875)、袋子里最少数目的球(LC1760)是同一类吗?+
是同一族:都在答案值域上二分,判定也都把各组按上限向上取整算份数再求和。差别只在判据比的是谁——珂珂比总小时数 ≤ h,袋子分球比拆分次数 ≤ maxOperations,这题比总店数 ≤ n。识别信号都是「最小化某个上限、上限越大越宽松」,套路一样、只换判据里的那个量。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 分配给商店的最多商品的最小值 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。