二分核心:具有一定的单调性 / 可分段判定 → 二分枚举答案 ,再用check函数验证可行性。国赛 90% 二分不是裸查找数组,是二分找答案 + check 函数。
国赛二分基本就 4 种考法:最小最大值、最大最小值、满足条件最小 / 最大、实数二分。
目录
[一、经典 1:最大值最小化](#一、经典 1:最大值最小化)
[二、经典 2:最小值最大化](#二、经典 2:最小值最大化)
[三、经典 3:满足条件的最大 x(反向临界)](#三、经典 3:满足条件的最大 x(反向临界))
[四、二分 + 前缀和(子数组 / 区间和高频)](#四、二分 + 前缀和(子数组 / 区间和高频))
一、经典 1:最大值最小化
常见例如:分成 k 段 / 选 k 个物品,求分割后的最大值尽可能小
模板思路
l=下界,r=上界
while(l<r):
mid=(l+r)/2
if check(mid): r=mid
else: l=mid+1
最后l就是答案
check(x):判断 x 能不能满足划分要求
典型例题:分割木板
给定 n 块木板长度an,切成 k 段,每段连续,求分割后最长一段的最小长度。
题意:n 块木板按序排放连成一整条,只能在木板缝隙切(不能把单块木板劈两半),一共切 k−1 刀,分成 k 段(每段含有连续若干块原木板),让这「k 段里最长的那一段尽可能短」,求最长的那段的最小值。
理清题目要我们干什么:
- 只能从木板之间分割、不能切开单块木板,所以 任意一段长度 ≥ 数组里单个木板最大值
- 必须恰好分成 k 个连续段
- 每种分割方案都有一个「最长段的长度」
- 在所有合法分割方案里,找出最小的那个最长段的值(答案)
解题套路:二分→猜答案
目标:找到 最小的 X:按原顺序切木板,分成 ≤k 段,每段的总和≤X
X的取值范围:
- 下界(最小) left=max(a)(不能切碎木板,最长的那段最少装一块最大木板)
- 上界(最大)
(不切,一整个就一段)
- 二分 mid(在取值范围的两端取了一个中间值,作为X):检验如果每段不超过 mid,最少需要切成几段 cnt
-
cnt<=k:说明 mid 可以再缩小一点,试一下更小答案 right=mid,因为题目中就是要最小的X。(就比如:现在是mid是4,最长的段长是4,假设分成4,4可分成两段,如果mid=2,最长的段长是2,就可以分成4段了,更压缩了,就可以分更多段。分成更多段,意味着更均匀,更细化,求最小的max(段长))
-
cnt>k:不满足要求k段,说明mid太小了,需要变大 left=mid+1
import java.util.*;
public class Main {
static int[] a;
static int k;//校验mid是否合法 static boolean check(long mid) { int cnt = 1; //初始1段 long sum = 0; for (int x : a) { if (sum + x > mid) { //加超了,说明需要在这里分割了,这个x需要算成下一段的开端,让sum=x,继续往后累加 cnt++; sum = x; } else sum += x; } return cnt <= k; } public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); k = sc.nextInt(); a = new int[n]; long L = 0, R = 0; for (int i = 0; i < n; i++) { a[i] = sc.nextInt(); L = Math.max(L, a[i]); R += a[i]; } //整数二分模板 while (L < R) { long mid = (L + R) / 2; if (check(mid)) R = mid; else L = mid + 1; } System.out.println(L); }}
-
二、经典 2:最小值最大化
典型例题:距离分配问题
题目:n 个牛棚的坐标 xn 升序,放 m 头牛,每头牛不在同一棚,求两头牛最小间距的最大值(X)。
思路:还是用二分法去求那个X
- L=1,R=xn-1-x0 (相距最远)
- check (mid):贪心放牛,第一个位置必放,下一个坐标 - 上次坐标≥mid 才放,统计能放多少头牛
- 能放的数量≥m:mid 可行,尝试更大的距离 L=mid+1,保存答案
- 放不下,说明相距的最小距离太大了,需要把值调小:R=mid-1
代码实现
import java.util.*;
public class Main {
static int[] x;
static int m;
static boolean check(int dis) {
int cnt = 1;
int last = x[0];
for (int i = 1; i < x.length; i++) {
if (x[i] - last >= dis) {
cnt++;
last = x[i];
}
}
return cnt >= m;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
m = sc.nextInt();
x = new int[n];
for (int i = 0; i < n; i++) x[i] = sc.nextInt();
Arrays.sort(x);
int L = 1, R = x[n - 1] - x[0];
int ans = 0;
while (L <= R) {
int mid = (L + R) / 2;
if (check(mid)) {
ans = mid;
L = mid + 1;
} else R = mid - 1;
}
System.out.println(ans);
}
}
三、经典 3:满足条件的最大 x(反向临界)
x 越小越容易满足,找最大合法 x。例:在限定数量下小车最多载重
mid=(l+r+1)/2
if check(mid):l=mid
else:r=mid-1
向上取整 mid 避免死循环是国赛易错点
典型例题:切绳子
注:绳子可以切断,多余的废料可以不要,只留下满足要求的,不同于分木板
题目:n 根绳子长度 an ,裁成 k 段等长小段,小段长度保留 2 位小数,求可以切的最长长度。
思路:
- 实数二分:L=0,R=max(a),循环 100 次(精度 1e-12 足够)
- check (len):每根绳子能切 ai / len 向下取整,总和≥k 则可行
代码实现:
import java.util.*;
public class Main {
static double[] a;
static int k;
static boolean check(double len) {
int cnt = 0;
for (double x : a) cnt += (int)(x / len);
return cnt >= k;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
k = sc.nextInt();
a = new double[n];
double L = 0, R = 0;
for (int i = 0; i < n; i++) {
a[i] = sc.nextDouble();
R = Math.max(R, a[i]);
}
//实数二分,循环100次保证精度
for (int t = 0; t < 100; t++) {
double mid = (L + R) / 2;
if (check(mid)) L = mid;
else R = mid;
}
System.out.printf("%.2f", L);
}
}
四、二分 + 前缀和(子数组 / 区间和高频)
题目:给定数组an正数,找连续子数组 和≥S 的最小长度
a=2,3,1,2,4,3,S=7 →答案 2(4,3)
思路:
要求最小长度,就二分长度len,check函数:滑动窗口 / 前缀和,是否存在区间长度 = len,区间和≥S;
check函数:子数组和/区间和用前缀和实现
- 存在:尝试更小长度 R=mid
- 不存在:L=mid+1
代码实现:
import java.util.*;
public class Main {
static int[] a;
static int S;
static boolean check(int len) {
long sum = 0;
//初始化第一个窗口
for (int i = 0; i < len; i++) sum += a[i];
if (sum >= S) return true;
//滑动窗口
for (int i = len; i < a.length; i++) {
sum += a[i] - a[i - len];
if (sum >= S) return true;
}
return false;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
S = sc.nextInt();
a = new int[n];
for (int i = 0; i < n; i++) a[i] = sc.nextInt();
int L = 1, R = n;
int ans = n;
while (L <= R) {
int mid = (L + R) / 2;
if (check(mid)) {
ans = mid;
R = mid - 1;
} else L = mid + 1;
}
System.out.println(ans);
}
}
五、二分易错点
- 求最小:
mid=(l+r)/2,check→r=mid - 求最大:
mid=(l+r+1)/2,check→l=mid - 实数二分不能用整数边界条件,循环 100 次
- l、r 初始化范围要拉满(不要卡小,防止答案出界)
总结
题目要求什么的最值X,就二分谁,check函数把mid值传进去,在mid的前提下根据题目中的约束条件看是否满足题目的要求。然后做对应的left or right的调整
-
整数二分:最小化可行值(分木板模板)
while(L < R){
long mid = L+(R-L)/2; //防溢出
if(check(mid)) R=mid;
else L=mid+1;
}
//答案L -
整数二分:最大化可行值(放牛模板)
while(L <= R){
int mid = L+(R-L)/2;
if(check(mid)){ans=mid; L=mid+1;}
else R=mid-1;
}
//答案ans -
实数二分模板(循环 100 次)
for(int i=0;i<100;i++){
double mid=(L+R)/2;
if(check(mid)) L=mid;
else R=mid;
}