二分答案算法详解:从理论到实践解决最优化问题

所处专栏:算法通关_拼好饭和她皆失的博客-CSDN博客

什么是二分法?

二分法是一种基于分治思想的高效搜索算法 ,通过在有序序列中不断将搜索区间对半分割来快速定位目标值。

核心原理

每次比较中间元素,根据比较结果舍弃一半的搜索区间,将时间复杂度从 O(n) 降为 O(log n)

什么时候使用二分法?

必要条件(缺一不可)

  1. 数据必须有序(单调递增或递减)

  2. 可以通过索引直接访问元素(随机访问)

二分查找

先说一下最基础的二分查找,我们先手搓一下

cpp 复制代码
// 查找目标值,返回下标,找不到返回-1
int binary_search(vector<int>& nums, int target) {
    int left=0,right=nums.size()-1;  // 闭区间
    
    while(left<=right){ 
        int mid=left+(right-left)/2; //防溢出
        
        if(nums[mid]==target) {
            return mid;  // 找到目标
        }else if (nums[mid] < target) {
            left=mid+1;  //右半边
        }else{
            right=mid-1;  //左半边
        }
    }
    
    return -1;  //没找到
}

二分查找在c++stl容器里也有

cpp 复制代码
vector<int> nums = {1, 3, 5, 7, 9};
// lower_bound: 返回第一个≥target的迭代器
auto it1 = lower_bound(nums.begin(), nums.end(), 5);//
if (it1 != nums.end()) {
    int index = it1 - nums.begin();  // 获取下标
}

// upper_bound: 返回第一个>target的迭代器  
auto it2 = upper_bound(nums.begin(), nums.end(), 5);
if (it2 != nums.end()) {
    int index = it2 - nums.begin();
}

重点在下面

二分答案

二分答案是一种将最优化问题转化为判定问题 的技巧,通过二分搜索来寻找满足特定条件的最优值

1. 问题特征

  • 问题要求找到最大/最小值

  • 答案具有单调性:如果某个值可行,那么比它更大(或更小)的值也可行(或不可行)

  • 难以直接求解,但给定一个值后,容易判断是否可行

2. 转换思想

我们可以将求最优解,变换成给你一个值,这个值行不行,

这时候如果值可行我们可能会试试更大的或更小的值看行不行,或者值不行我们会试试更小的或更大的值看行不行,此时,单调性这个要求就显现出来了,我们必须要保证答案具有单调性

3.区间问题

这个狠狠狠重要

问题类型 二分模板 最终答案
最大值问题 (求满足条件的最大值) while(l <= r) 可行→l=mid+1 不可行→r=mid-1 rl-1
最小值问题 (求满足条件的最小值) while(l <= r) 可行→r=mid-1 不可行→l=mid+1 lr+

4.模板

整数求最大值

cpp 复制代码
 int l = MIN_VAL;      // 最小值
    int r = MAX_VAL;     // 最大值
    
    while (l <= r) {
        int mid = l + (r - l ) / 2;
        
        if (check(mid)) {    // mid可行
            l = mid+1;      // 尝试更大的值
        } else {
            r = mid - 1; // 需要更小的值
        }
    }
    
    return r;  // r就是满足条件的最大值

整数求最小值

cpp 复制代码
 int l = MIN_VAL;      // 最小值
    int r = MAX_VAL;     // 最大值
    
    while (l <= r) {
        int mid = l + (r - l ) / 2;
        
        if (check(mid)) {    // mid可行
            r = mid-1;      // 尝试更大的值
        } else {
            l = mid + 1; // 需要更小的值
        }
    }
    
    return l;  // r就是满足条件的最大值

求浮点数最大值

cpp 复制代码
    const double eps = 1e-7;  // 精度要求,根据题目调整
    
    while (r - l > eps) {
        double mid = (l + r) / 2;
        
        if (check(mid)) {
            l = mid;   // 或者 r = mid,根据单调性决定
        } else {
            r = mid;  // 或者 l = mid,根据单调性决定
        }
    }
    
    return r;  //或l根据单调性

举个例题

例题链接:875. 爱吃香蕉的珂珂 - 力扣(LeetCode)

珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。

珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。

珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。

返回她可以在 h 小时内吃掉所有香蕉的最小速度 kk 为整数)。

示例 1:

复制代码
输入:piles = [3,6,7,11], h = 8
输出:4

示例 2:

复制代码
输入:piles = [30,11,23,4,20], h = 5
输出:30

示例 3:

复制代码
输入:piles = [30,11,23,4,20], h = 6
输出:23

提示:

  • 1 <= piles.length <= 104
  • piles.length <= h <= 109
  • 1 <= piles[i] <= 109

思路 这题可以说是非常经典的一个二分答案题了

我们可以看一下题,题目让我们求出最小的速度k能在一定时间吃完香蕉,那我们就对k进行二分,然后知道找到合适的值,我们需要一个check函数来判断,最后就解决了,我们看代码

cpp 复制代码
class Solution {
public:
    bool check(int m,vector<int>& piles,int h){
        long  sum=0;
        for(int i=0;i<piles.size();i++){
           sum+=(piles[i]+m-1)/m;//向上求整
        }
        if(sum<=h) return true;
        
        return false;
    }
    int minEatingSpeed(vector<int>& piles, int h) {
        int r=0;
        for(int num: piles)
        r=max(num,r);//找到r最大的值是多少
        int l=1 ;
        int k;
        while(l<=r){
            int m=l+(r-l)/2;
            if(check(m,piles,h)){
                r=m-1;
                k=m;
            }
            else{
                l=m+1;
            }
        } 
        return  k;    
    }
};

通过这个题其实就已经熟悉了一下二分答案了,他就是通过不断二分找到一个值进行判读是否符合答案


接下来我们再来看一下较难的题

练习部分

题目一

链接:通往奥格瑞玛的道路 - 洛谷 P1462 - Virtual Judge

Background

在艾泽拉斯大陆上有一位名叫歪嘴哦的神奇术士,他是部落的中坚力量。

有一天他醒来后发现自己居然到了联盟的主城暴风城。

在被众多联盟的士兵攻击后,他决定逃回自己的家乡奥格瑞玛。

Description

在艾泽拉斯,有 n个城市。编号为 1,2,3,...,n。

城市之间有 mm 条双向的公路,连接着两个城市,从某个城市到另一个城市,会遭到联盟的攻击,进而损失一定的血量。

每次经过一个城市,都会被收取一定的过路费(包括起点和终点)。路上并没有收费站。

假设 1 为暴风城,n 为奥格瑞玛,而他的血量最多为 b,出发时他的血量是满的。如果他的血量降低至负数,则他就无法到达奥格瑞玛。

歪嘴哦不希望花很多钱,他想知道,在所有可以到达奥格瑞玛的道路中,对于每条道路所经过的城市单次收费的最大值,其最小值为多少。

Input

第一行 3个正整数,n,m,b。分别表示有 n 个城市,m 条公路,歪嘴哦的血量为 b。

接下来有 n 行,每行 1 个非负整数,fi​。表示经过城市 i,需要交费 fi​ 元。

再接下来有 m 行,每行 3 个正整数,ai,bi,ci(1≤ai,bi≤n)。表示城市 ai​ 和城市 bi​ 之间有一条公路,如果从城市 ai​ 到城市 bi​,或者从城市 bi​ 到城市 ai​,会损失 ci​ 的血量。

Output

仅一个整数,表示歪嘴哦经过城市单次交费最大值的最小值。

如果他无法到达奥格瑞玛,输出 AFK

样例

输入

4 4 8

8

5

6

10

2 1 2

2 4 1

1 3 4

3 4 3
输出

10

对于 60% 的数据,满足 n≤200,m≤104,b≤200;

对于 100% 的数据,满足 1≤n≤10^4,1≤m≤5×10^4,1≤b≤10^9;

对于 100% 的数据,满足 1≤ci​≤10^9,0≤fi​≤10^9,可能有两条边连接着相同的城市。

思路:这题用了dijkstra算法,不会的可以看看我的另一篇文章去练习一下Dijkstra算法实战:最短路问题全解析_洛谷dijkstra算法题目-CSDN博客

这题可以看到题目给了个血量和到每个城市所花费的钱,题目后面问经过城市单次交费最大值的最小值。我们不可能去拿每个城市的话费去试,能不能满足,这样肯定超时,所以我们用二分法,我们去二分城市的花费,然后去判断能不能走出一条在血量b耗完前走到n,去看代码

建图用

邻接表就行,我用了链式前向星

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
const int M=5e4+10;
struct edge{
	int to;
	int next;
	int w;
}graph[M*2];
int cnt,head[N],f[N];
void add_edge(int u,int v,int w){
	cnt++;
	graph[cnt].w=w;
	graph[cnt].to=v;
	graph[cnt].next=head[u];
	head[u]=cnt;
}
int n,m,b;
bool check(int x){
    vector<int>distance_(N,-1); 
	//如果开头和结尾已经超过了二分出来的血量就代表这个收费不是正解 
	if(f[1]>x||f[n]>x) return false;
	distance_[1]=b;
	priority_queue<pair<int,int>>pq;//构建大根堆 ({剩余血量,节点})
	pq.push({b,1});
	while(!pq.empty()){
		int dist=pq.top().first;
		int u=pq.top().second;
		pq.pop();
		if(dist<distance_[u]) continue;
		for(int i=head[u];i;i=graph[i].next){
			int to=graph[i].to;
			int w=graph[i].w;
			if(f[to]>x) continue;//如果下一个要走的城市所花费大于二分出来的值,就直接不走这个路 
			if(distance_[to]<dist-w&&dist-w>=0){
				//如果要走的节点to里存的最大血量可以更新
				distance_[to]=dist-w;
				pq.push({distance_[to],to});
			}
		}
	}
	return distance_[n]!=-1;//如果不等于-1说明二分出来的答案可以 
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n>>m>>b;
	int l=1e9+10,r=0;
	for(int i=1;i<=n;i++){
	  cin>>f[i];	
	  l=min(l,f[i]);
	  r=max(r,f[i]);
	} 
	for(int i=0;i<m;i++){
		int u,v,w; 
		cin>>u>>v>>w;
		if(u==v) continue;
		add_edge(u,v,w);
		add_edge(v,u,w);
	}
	int ans=-1;
	while(l<=r){
		int mid=(l+r)/2;
		if(check(mid)){
			ans=mid;
			r=mid-1;
		}
		else{
			l=mid+1;
		}
	}
	if(ans==-1) cout<<"AFK";
	else{
		cout<<ans;
	}
} 

题目二

链接:进击的奶牛 Aggressive Cows G - 洛谷 P1824 - Virtual Judge

题目展示

农夫约翰建造了一座有 nn 间牛舍的小屋,牛舍排在一条直线上,第 ii 间牛舍在 xixi​ 的位置,但是约翰的 mm 头牛对小屋很不满意,因此经常互相攻击。约翰为了防止牛之间互相伤害,因此决定把每头牛都放在离其它牛尽可能远的牛舍。也就是要最大化最近的两头牛之间的距离。

牛们并不喜欢这种布局,而且几头牛放在一个隔间里,它们就要发生争斗。为了不让牛互相伤害。约翰决定自己给牛分配隔间,使任意两头牛之间的最小距离尽可能的大,那么,这个最大的最小距离是多少呢?

Input

第一行用空格分隔的两个整数 nn 和 mm;

下面 nn 行为 nn 个用空格隔开的整数,表示位置 xixi​。

Output

一行一个整数,表示最大的最小距离值。

Sample 1

输入

5 3

1

2

8

4

9

输出

3

思路:题目要找最大的最小距离,那我们就去二分这个距离,然后通过check函数去看看这个距离满足不就行,详细的题解在代码注释

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int n,m,sum;
int x[100050];
bool check(int mid){
	int last=x[0];//记录上一只牛的位置,开始时第一只牛一定在第一个牛栏 
	int num=0;
	for(int i=1;i<n;i++){
		if(x[i]-last<mid) num++;//发现这个x[i]这个牛栏不能放牛,就空过去
		else{
			last=x[i];//更新上一次的牛栏位置 ,即上一头牛放的位置 
		} 
		if(num>sum) return false;
	}
	return true;
}
int main(){
	cin>>n>>m;
	int l,r;
	for(int i=0;i<n;i++){
		cin>>x[i];
	
	}
	l=1;
	r=x[n-1]-x[0];//可能情况的最大值 
	sum=n-m;//最大剩余牛栏数 
	sort(x,x+n);//由题意可知,这道题可以直接排序来进行二分 
	while(l<=r){
		int mid=l+(r-l)/2;
		if(check(mid)){
			l=mid+1;
		}
		else{
			r=mid-1;
		}
	}
	cout<<r;//这里输出r,因为最后l+1造成的不满足l<=r所以最后的正确的值是r而不是l 
} 

题目三

链接:Pie - POJ 3122 - Virtual Judge

题目展示

我的生日快到了,按照传统,我会准备派。不是一块派,而是有 N 块,口味和大小各异。我的 F 个朋友会来参加我的聚会,每个人都会得到一块派。这应该是一块完整的派,而不是几块小的,因为那看起来很乱。不过,这一块可以是一整块派。

我的朋友们非常烦人,如果其中一个人得到的块比其他人大,他们就会开始抱怨。因此,所有人都应该得到大小相等(但不一定形状相同)的块,即使这导致一些派被浪费(这总比浪费聚会要好)。当然,我也想要一块派,而那块的大小也应该相同。

我们都能得到的最大可能的块大小是多少?所有的派都是圆柱形的,且它们的高度都是 1,但派的半径可以不同。

输入

第一行是一个正整数:测试用例的数量。然后对于每个测试用例:

  • 一行包含两个整数 N 和 F,1 ≤ N, F ≤ 10 000:派的数量和朋友的数量。
  • 一行包含 N 个整数 ri,1 ≤ ri ≤ 10 000:派的半径。

输出

对于每个测试用例,输出一行,包含我和我的朋友们都能得到的最大可能体积 V。答案应以浮点数形式给出,绝对误差最多为 10−3。

示例

输入

3

3 3

4 3 3

1 24

5

10 5

1 4 2 3 4 5 6 5 4 2

输出

25.1327

3.1416

50.2655

思路 这题思路是哪个比较简单,但是要注意他是浮点数二分,这时候咱的l和r每次不能变成mid+1或-1了,要变mid,防止误差

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const double P=acos(-1.0);
int n,f;
double R[10020];
bool check(double mid){
	int sum=0;
	for(int i=0;i<n;i++){
		sum+=(int)(R[i]/mid);
	}
	return sum>=f+1;
}
int main(){
	int t;
	cin>>t;
	while(t--){
		cin>>n>>f;
		for(int i=0;i<n;i++) {
			int x;
			cin>>x;
			R[i]=x*x;
		}
		sort(R,R+n);
		double l=0,r=R[n-1];
		while(r-l>=0.00001){
			double mid=(l+r)/2;
			if(check(mid)){
				l=mid;
			}
			else{
				r=mid;
			}
		}
		cout<<fixed<<setprecision(4)<<r*P<<endl;
	}
}

后面会继续更新练习题目及题解来帮助更深刻理解

码字不易,大家三连支持支持

相关推荐
weixin_457760002 小时前
逻辑回归(Logistic Regression)进行多分类的实战
算法·分类·逻辑回归
月明长歌2 小时前
【码道初阶】Leetcode234进阶版回文链表:牛客一道链表Hard,链表的回文结构——如何用 O(1) 空间“折叠”链表?
数据结构·链表
元亓亓亓2 小时前
LeetCode热题100--215. 数组中的第K个最大元素--中等
算法·leetcode·职场和发展
CoderYanger2 小时前
C.滑动窗口-求子数组个数-越长越合法——2962. 统计最大元素出现至少 K 次的子数组
java·数据结构·算法·leetcode·职场和发展
小满、2 小时前
Redis:高级数据结构与进阶特性(Bitmaps、HyperLogLog、GEO、Pub/Sub、Stream、Lua、Module)
java·数据结构·数据库·redis·redis 高级特性
Eiceblue2 小时前
通过 C# 将 RTF 文档转换为图片
开发语言·算法·c#
alphaTao2 小时前
LeetCode 每日一题 2025/12/8-2025/12/14
算法·leetcode
玖日大大2 小时前
ModelEngine 可视化编排实战:从智能会议助手到企业级 AI 应用构建全指南
大数据·人工智能·算法
月明长歌2 小时前
【码道初阶】Leetcode面试题02.04:分割链表[中等难度]
java·数据结构·算法·leetcode·链表