C++算法竞赛|二分查找与二分答案:边界模板、浮点二分、STL

文章目录

二分查找

基本概念

  • 二分查找,也称为折半查找(Binary Search),是一种在有序数组中查找特定元素的搜索算法。

基本思想是 将目标值与数组中间的元素进行比较,如果目标值小于中间元素,则在数组的左半部分继续查找,否则在右半部分查找

优点

  • 可以将最朴素的做法 (从头到尾的遍历数组)时间复杂度从 O(n)优化到O(logn)
  • 比较次数少,速度快,效率高

查找过程:

二分查找:在一个已知的有序数据集上进行二分地查找

二分答案:答案有一个区间,在这个区间中二分,直到找到最优答案

二分查找写法

while(l<r)

下标边界二分

向左找目标

cpp 复制代码
//在a数组中寻找第一个>=x的数,并返回其下标
int find(ll l, ll r)
{
    while (l < r)
    {
        ll mid = l + r >> 1;//或(l+r)/2
        if (a[mid]>=x)
        {
         r = mid;
        }
        else 
         {
          l = mid + 1;
         }
    }
    return l;//因为while循环当l==r时终止,return r也可以
}

向右找目标

cpp 复制代码
//在a数组中寻找最后一个<=x的数,并返回其下标
int find(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;//(l+r+1)/2
        if (a[mid]<=x)
        {
          l = mid;
        }
        else 
        {
         r = mid - 1;
        }
    }
    return l;
}
cpp 复制代码
// while(l<r) 找第一个 > x
int find(ll l, ll r)
{
    while (l < r)
    {
        ll mid = l + (r - l) / 2;
        if (a[mid] > x)
        {
            r = mid;
        }
        else
        {
            l = mid + 1;
        }
    }
    return l;
}

while(l<=r)

下标二分

cpp 复制代码
//在a数组中寻找第一个>=x的数,并返回其下标
//可以先将res设置为一个很大的数,如果while循环终止后res没变,便没找到
int find(int l, int r)
{
    int res=0x3f3f3f3f;
    while (l <= r)
    {
        int mid = l + r >> 1;//或写成(l+r)/2
        if (a[mid]>=x)
        {
        	r = mid-1;
        	res=mid;
        } 
        else 
        {
         l = mid + 1;
        }
    }
    return res;
}
cpp 复制代码
//在a数组中寻找最后一个<=x的数,并返回其下标
int find(int l, int r)
{
	int res=-1;
    while (l <= r)
    {
        int mid = l + r >> 1;//或(l+r)/2
        if (a[mid]<=x)
        {
        	l = mid+1;
        	res=mid;
        } 
        else 
        {
         r = mid - 1;
        }
    }
    return res;
}
cpp 复制代码
// while(l<=r)+res 找第一个 > x
int find(int l, int r)
{
    int res = 0x3f3f3f3f;
    while (l <= r)
    {
        int mid = l + (r - l) / 2;
        if (a[mid] > x)
        {
            res = mid;
            r = mid - 1;
        }
        else
        {
            l = mid + 1;
        }
    }
    return res;
}

二分答案

二分答案信号

「最大值最小化」

「最小值最大化」

「至少需要多少个」

「最多能分成多少段」

「能否在 x 时间内完成」

「求最小 / 最大的满足条件的值」

最大化最小值

cpp 复制代码
ll l=1,r=0x3f3f3f3f;
ll ans=0;
while(l<=r)
{
	ll mid=(l+r)/2;
	if(check(mid))
	{
		ans=max(mid,ans); // mid是可行答案,记录下来
		l=mid+1;          // 尝试往更大答案搜索
	}
	else
	{
		r=mid-1;          // mid不可行,要变小
	}
}
cout<<ans;

最小化最大值

cpp 复制代码
//最小化最大值模板
ll l=1,r=1e9;
ll ans=0;
while(l<=r)
{
    ll mid=l+(r-l)/2;
    if(check(mid))
    {
        ans=mid;
        r=mid-1; //可行,尝试找更小答案
    }
    else
    {
        l=mid+1; //不可行,放大
    }
}
cout<<ans;

区分

  1. 下标二分:答案一定是数组中已经存在的某个元素,目标求元素的位置。
  2. 二分答案:答案不一定是输入数组里面的值;可以写一个check(x)函数判断x这个候选答案是否可行;题目特征:最大化最小值、最小化最大值。
项目 while(l < r)边界二分 (lower_bound) while(l <= r)传统值查找
用途 找第一个≥x、第一个 > x(边界) 查找是否存在某确切数值
循环结束 l == r,下标重合 l > r,区间为空
边界更新 r=mid / l=mid+1,mid 不排除 r=mid‑1 / l=mid+1,mid 直接排除
数组重复元素 支持,拿到首次出现位置 找到任意一个,不一定是第一次
返回之后 必须额外判断 a l==x 找到就直接 return,不用二次判断

记忆口诀while(l<=r) 时,mid 直接排除,靠 l=mid+1r=mid-1 收缩;用 res 记录最近一次可行答案,循环结束后 res 即为结果。

快速识别二分答案题:

  1. 问题具备单调性:存在阈值,一侧全部不可行,另一侧全部可行。
  2. 可以实现O(n)的check(x),快速验证候选x是否可行。
  3. 题目要求:最大化最小值 / 最小化最大值。

例题

P2249 【深基13.例1】查找 - 洛谷

模板题

cpp 复制代码
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define ull unsigned long long
#define fi first
#define se second
#define PLL pair<string, ll>
#define YES cout<<"YES"<<endl;
#define NO cout<<"NO"<<endl;
using namespace std;
const ll MAXN=1005;
const ll mod=1e9+7;
using namespace std;
ll n,m;
ll b;
ll find(ll b,vector<ll>&a)
{
	ll l=0;
	ll r=n-1;
	while(l<r)
	{
		ll mid=(r+l)/2;
		if(a[mid]>=b)
		{
			r=mid;
		}
		else
		{
			l=mid+1;
		}
	}
	return l;
}
int main()
{
	IOS
	cin>>n>>m;
	vector<ll>a(n);
	for(ll i=0;i<n;i++)
	{
		cin>>a[i];
	}
	while(m--)
	{
		cin>>b;
		ll q=find(b,a);
		if(a[q]==b)
		{
			cout<<q+1<<' ';
		}
		else
		{
			cout<<-1<<' ';
		}
	}
	
//	cout<<fixed<<setprecision(x)<< ;
	return 0;
}

进击的奶牛(整型二分)

P1824 [USACO05FEB 进击的奶牛 Aggressive Cows G - 洛谷](https://www.luogu.com.cn/problem/P1824)

解题过程

  • 二分距离,判断函数check(x):间距至少 x 时最多能放下多少头牛。排序坐标;
  • 二分区间l=1,r=最大坐标;check(mid)满足(能放下≥m 头牛),说明距离 mid 可行,尝试更大值:ans=mid,l=mid+1;不满足则缩小距离:r=mid-1

代码实现

cpp 复制代码
//5303
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define ull unsigned long long
#define fi first
#define se second
using namespace std;
ll n,m;
vector<ll>a;
bool check(ll x)
{
	ll cnt=1;
	ll head=a[0];
	for(ll i=1;i<n;i++)
	{
		if(a[i]-head>=x)
		{
			cnt++;
			head=a[i];
		}
	}
	return cnt>=m;
}
int main()
{
	IOS
	cin>>n>>m;
	for(ll i=0;i<n;i++)
	{
		ll A;
		cin>>A;
		a.push_back(A);
	}
	sort(a.begin(),a.end());
	ll l=1,r=0x3f3f3f3f3f3f3f3f;//longlong 型
	ll ans=0;
	while(l<=r)
	{
		ll mid=(l+r)/2;
		if(check(mid))
		{
			ans=max(mid,ans);
			l=mid+1;
		}
		else
		{
			r=mid-1;
		}
	}
	cout<<ans<<endl;
//	cout<<fixed<<setprecision(x)<< ;
	return 0;
}

切绳子(浮点型二分)

P1577 切绳子 - 洛谷

解题过程

  • 二分单段绳子长度,check(x) 统计能切出多少段长度≥x 的绳子
  • 二分循环固定迭代 100 次(替代整数边界判断,保证精度);
  • 满足段数≥k,记录答案并向右二分找更长长度:ans=mid,l=mid;
  • 不满足则向左缩小长度:r=mid;
    最后向下取整保留两位小数输出。

代码实现

cpp 复制代码
//1577
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define ull unsigned long long
#define fi first
#define se second
using namespace std;
ll n,k;
vector<double>a;
bool check(double x)
{
	ll cnt=0;
	for(ll i=0;i<n;i++)
	{
			cnt+=(ll)(a[i]/x);
	}
	return cnt>=k;
}
int main()
{
	IOS
	cin>>n>>k;
	double r=0;
	for(ll i=0;i<n;i++)
	{
		double A;
		cin>>A;
		a.push_back(A);
		r=max(r,A);
	}
//	sort(a.begin(),a.end());
	double l=0;
	double  ans=0;
	for(ll i=1;i<=100;i++)
	{
		double mid=(l+r)/2;
		if(check(mid))
		{
			ans=mid;
			l=mid;
		}
		else
		{
			r=mid;
		}
	}
	ans=floor(ans*100)/100;//向下取整保留两位小数
	cout<<fixed<<setprecision(2)<<ans<<endl ;
	return 0;
}

不能直接 cout << fixed << setprecision(2) << ans;

setprecision(2) 默认是四舍五入。比如真实答案是 2.009,直接输出会变成 2.01,题目要求向下取整,必须手动 floor(ans*100)/100

整型二分 vs 浮点二分

  1. 整型二分

区间边界是整数,可以用 l <= r 循环,靠 mid±1 收缩边界

cpp 复制代码
ll l=1, r=1e9;
while(l <= r)
{
    ll mid = (l + r) / 2;
    if(check(mid))
    {
        ans=mid;
        l=mid+1; // 可行,往右找更大
    }
    else
        r=mid-1; // 不可行,往左缩小
}

原理:每次直接排除mid,不会死循环。

  1. 浮点二分

实数有无穷多个,不能用 l<=r,无法精准边界,会陷入循环;

标准写法:固定循环 100 次(100 次二分后精度远超题目要求的 1e-6/1e-2)。

cpp 复制代码
double l=0, r=max_len;
for(int i=1;i<=100;i++)
{
    double mid = (l + r) / 2;
    if(check(mid))
    {
        ans=mid;
        l=mid; // 可行,右边界移到mid
    }
    else
        r=mid; // 不可行,左边界移到mid
    }
}

原理:实数区间不能 mid±1,只能不断缩小区间范围,靠迭代保证精度。

浮点:存在浮点数精度丢失,不能直接输出;

如切绳子需要向下取整保留两位小数:ans=floor(ans*100)/100,配合setprecision(2)输出。

奶牛晒衣服

P1843 奶牛晒衣服 - 洛谷

解题过程

  1. 二分答案
    二分总晾晒时间s,check(s)判断s时间内能否晒干所有衣服:
    每件衣服自然风干s*a,剩余水分需要烘干机;
    统计所有衣服烘干总次数,若总次数≤s 则时间 s 可行,尝试更小时间:ans=mid,r=mid-1;
    不可行则增大时间:l=mid+1。
  2. 优先队列暴力
    每次取出含水量最大的衣服烘干 b 水分,循环直到所有衣服自然风干即可

代码实现

cpp 复制代码
// 二分
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define ull unsigned long long
#define fi first
#define se second
using namespace std;
ll n,aa,b;
//ll s;
ll mx,ti;
vector<ll>a;
bool check(ll s)
{
	ll sum=0;
	for(ll i=0;i<n;i++)
	{
		if(a[i]<=s*aa)
		continue;
		ll re=a[i]-s*aa;
		sum+=(re+b-1)/b;
		if(sum>s)
		return false;
	}
	return sum<=s;
}
int main()
{
	IOS
	cin>>n>>aa>>b;
	for(ll i=0;i<n;i++)
	{
		ll x;
		cin>>x;
		a.push_back(x);
	}
	sort(a.begin(),a.end());
	ll l=0,r=a[n-1];
	ll ans=0;
	while(l<=r)
	{
		ll mid=(l+r)/2;
		if(check(mid))
		{
			ans=mid;
			r=mid-1;
		}
		else
		{
			l=mid+1;
		}
	}
	cout<<ans<<endl;
	return 0;
}
cpp 复制代码
//优先队列
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define ull unsigned long long
#define fi first
#define se second
using namespace std;
ll n,a,b;
ll mx,ti;
int main()
{
	IOS
	priority_queue<ll>q;
	cin>>n>>a>>b;
	for(ll i=0;i<n;i++)
	{
		ll x;
		cin>>x;
		q.push(x);
	}
	mx=q.top();
	q.pop();
	while(mx>ti*a)
	{
		ti++;
		mx-=b;
		q.push(mx);
		mx=q.top();
		q.pop();
	}
	cout<<ti<<endl;

	return 0;
}

逛画展

P1638 逛画展 - 洛谷

说明:滑动窗口

解题过程

  1. 用两个指针 l(窗口左边界)、r(窗口右边界)维护一个动态区间:

    1. 右指针 r 不断向右扩张窗口,把新画作纳入窗口,统计窗口内画作种类数量 kind

    2. 当窗口集齐全部 m种画(kind == m)说明当前区间合法;

      持续收缩左指针 l,尽可能缩短区间长度;

      每次收缩前更新最短区间答案;

    3. 左指针移出某类画,且该画在窗口内数量变为 0 时,种类数 kind 减一,停止收缩,继续右移 r

代码实现

cpp 复制代码
//1843
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define ull unsigned long long
#define fi first
#define se second
using namespace std;
ll n,m;
int main()
{
	IOS
	cin>>n>>m;
	vector<ll>a(n+2);
	vector<ll>cnt(m+2,0);
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	ll l=1;
	ll kind=0;
	ll ansx=0,ansy=0;
	ll mincur=LLONG_MAX;
	for(ll r=1;r<=n;r++)
	{
		if(cnt[a[r]]==0)
		{
			kind++;
		}
		cnt[a[r]]++;
		while(kind==m)
		{
			ll cur=r-l+1;
			if(cur<mincur||(cur==mincur&&l<ansx))
			{
			mincur=cur;
			ansx=l;
			ansy=r;
			}
			cnt[a[l]]--;
			if(cnt[a[l]]==0)
			kind--;
			l++;
		}
	}
	cout<<ansx<<" "<<ansy<<endl;
	return 0;
}

STL内置二分函数

头文件 <algorithm>,数组必须升序

返回迭代器,it-a.begin()得到下标

  1. lower_bound(a.begin(),a.end(),x):第一个 ≥ x
  2. upper_bound(a.begin(),a.end(),x):第一个 > x
  3. equal_range(a.begin(),a.end(),x) 返回pair,等于x的区间[L,R)
  4. binary_search(a.begin(),a.end(),x) 返回bool,只判断是否存在

最后一个 <=x

cpp 复制代码
auto it = upper_bound(a.begin(),a.end(),x);
if(it != a.begin()) 
--it;

逻辑:

upper_bound 返回第一个 > x的位置。

  • 它前一个元素,就是最后一个 ≤ x
  • it == a.begin():代表全部元素都大于 x,不存在<=x,不能--it(越界)。

示例:a={1,3,5,7,9},x=6

  • upper_bound 找第一个 > 6 -> 指向 7(下标 3)
  • --it 指向 5,5 就是数组最后一个 ≤6。

最后一个 < x

cpp 复制代码
auto it = lower_bound(a.begin(),a.end(),x);
if(it != a.begin())
 --it;

逻辑:

lower_bound 返回第一个 >= x的位置。

  • 它前一个元素,就是最后一个 < x
  • it == a.begin():全部元素都≥x,不存在<x,禁止--it

示例:a={1,3,5,7,9},x=5

  • lower_bound 找第一个≥5 →指向 5(下标 2)
  • --it指向 3,3 就是数组最后一个 <5。

set/map使用成员函数 .lower_bound(x),不要用全局函数,防止退化O(n)

cpp 复制代码
auto it = s.lower_bound(x); // 正确
// auto it = lower_bound(s.begin(),s.end(),x); 错误

7.24的库存

相关推荐
sel_91 小时前
【OPD论文导读(二)】OPD(On-Policy Distillation)全景调研:十篇论文讲透“在自己生成的内容上学习“这件事
人工智能·python·深度学习·学习·算法·语言模型
动词ing1 小时前
【学习笔记】数据结构(数组长度和关键特性+快慢指针+左右指针)
数据结构·笔记·学习
老当益壮梁奶奶1 小时前
Linux 软件编程学习笔记(六):线程编程入门与实践
linux·c语言·c++·笔记·学习
小小龙学IT1 小时前
Dear ImGui 开源即时模式 GUI 库深度解析
c++·ui·开源
金幄科技2 小时前
RAG第四篇:重排序 Rerank——粗召回之后,为什么还需要一次精排
人工智能·算法·ai·ai编程
M78佐菲2 小时前
Linux学习笔记:文件IO
linux·笔记·学习·算法
ShineWinsu2 小时前
对于TRAE中配置Qt的解析
开发语言·c++·ide·vscode·qt·ai·trae
小小龙学IT2 小时前
第一节 QML 背景介绍
c++·qt·js
水饺编程2 小时前
第5章,[Win32 章节] :创建、选择和删除画笔
c语言·c++·windows·visual studio