速通蓝桥杯省一:二分算法

二分算法

  • 解集具"二段性"即可用:前一段不满足,后一段一定满足,或反之

  • 两套模板

    不用死记,根据题目思考一下就能知道用哪个

  1. 找左端点,满足条件的最小下标(≥x 的最小元素)
cpp 复制代码
int l = 1, r = n;
while (l < r)
 {
    int mid = l + (r - l) / 2;   // 防溢出
    if (check(mid)) r = mid;     // 条件真,答案≤mid
    else            l = mid + 1; // 条件假,答案>mid
}
// 结束时 l==r,再验证 a[l] 是否真满足

2.找右端点,满足条件的最大下标(≤y 的最大元素)

cpp 复制代码
int l = 1, r = n;
while (l < r) {
    int mid = l + (r - l + 1) / 2; // 向上取整
    if (check(mid)) l = mid;       // 条件真,答案≥mid
    else            r = mid - 1;   // 条件假,答案<mid
}
  • STL
cpp 复制代码
#include <algorithm>
vector<int> a;
auto it1 = lower_bound(a.begin(), a.end(), x); // ≥x 最小元素
auto it2 = upper_bound(a.begin(), a.end(), x); // >x 最小元素
// 返回迭代器,解引用 *it,下标 it - a.begin()。
// 想拿 ≤x 的最大元素:--it2(先判 it2!=a.begin())

时间复杂度

O(log n)

二分答案

  • 最大值最小/最小值最大
cpp 复制代码
bool check(int mid) { /* 判定"答案≤mid"是否可行 */ }

int left = MINAns, right = MAXAns;
while (left < right) {
    int mid = left + (right - left) / 2;   // 左端点版
    if (check(mid)) right = mid;           // 可行,尝试更小
    else            left  = mid + 1;       // 不可行,需更大
}
// left 即为"最小化最大值"

时间复杂度

O(log n)


在解题时如何想到二分?

当想要枚举数据时发现时间太长,并且答案是区间某一个确定值;可以试着分析题目条件是否具有二段性

复制代码
// 典型的问题描述:
// - 找到最大的x使得条件满足
// - 找到最小的x使得条件满足  
// - 第一个不满足条件的点
// - 最后一个满足条件的点

从"过程思维"到"结果思维"

从贪心转换到二分

  • 贪心:关注每一步该做什么(过程)
  • 二分答案 :先假设知道答案,再验证(结果)
    二分关注全局可行性(给定总时间,判断能否完成任务)

直接上例题(来源洛谷)

P1083 借教室

题目描述

在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。

面对海量租借教室的信息,我们自然希望编程解决这个问题。

我们需要处理接下来 nnn 天的借教室信息,其中第 iii 天学校有 rir_iri 个教室可供租借。共有 mmm 份订单,每份订单用三个正整数描述,分别为 dj,sj,tjd_j,s_j,t_jdj,sj,tj,表示某租借者需要从第 sjs_jsj 天到第 tjt_jtj 天租借教室(包括第 sjs_jsj 天和第 tjt_jtj 天),每天需要租借 djd_jdj 个教室。

我们假定,租借者对教室的大小、地点没有要求。即对于每份订单,我们只需要每天提供 djd_jdj 个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。

借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。这里的无法满足指从第 sjs_jsj 天到第 tjt_jtj 天中有至少一天剩余的教室数量不足 djd_jdj 个。

现在我们需要知道,是否会有订单无法完全满足。如果有,需要通知哪一个申请人修改订单。

输入格式

第一行包含两个正整数 n,mn,mn,m,表示天数和订单的数量。

第二行包含 nnn 个正整数,其中第 iii 个数为 rir_iri,表示第 iii 天可用于租借的教室数量。

接下来有 mmm 行,每行包含三个正整数 dj,sj,tjd_j,s_j,t_jdj,sj,tj,表示租借的数量,租借开始、结束分别在第几天。

每行相邻的两个数之间均用一个空格隔开。天数与订单均用从 111 开始的整数编号。

输出格式

如果所有订单均可满足,则输出只有一行,包含一个整数 000。

否则(订单无法完全满足)输出两行,第一行输出一个负整数 −1-1−1,第二行输出需要修改订单的申请人编号。

输入输出样例 #1

输入 #1

复制代码
4 3 
2 5 4 3 
2 1 3 
3 2 4 
4 2 4

输出 #1

复制代码
-1 
2

说明/提示

【输入输出样例说明】

第 111 份订单满足后,444 天剩余的教室数分别为 0,3,2,30,3,2,30,3,2,3。第 222 份订单要求第 222 天到第 444 天每天提供 333 个教室,而第 333 天剩余的教室数为 222,因此无法满足。分配停止,通知第 222 个申请人修改订单。

【数据范围】

对于 10%10\%10% 的数据,有 1≤n,m≤101\le n,m\le 101≤n,m≤10;

对于 30%30\%30% 的数据,有 1≤n,m≤10001\le n,m\le 10001≤n,m≤1000;

对于 70%70\%70% 的数据,有 1≤n,m≤1051 \le n,m \le 10^51≤n,m≤105;

对于 100%100\%100% 的数据,有 1≤n,m≤1061 \le n,m \le 10^61≤n,m≤106,0≤ri,dj≤1090 \le r_i,d_j\le 10^90≤ri,dj≤109,1≤sj≤tj≤n1 \le s_j\le t_j\le n1≤sj≤tj≤n。

NOIP 2012 提高组 第二天 第二题

2022.2.20 新增一组 hack 数据

题解

⼒破万法:线段树。但是杀鸡焉⽤宰⽜⼑,可以⽤更简单的做法

设⽆法完成的订单的天数为 ret 。针对某⼀天 x ,根据题意可得:

  • 当 x ≥ ret 时,处理完 [1, x] 天的订单,⼀定⽆法完成;
  • 当 x < ret 时,处理完 [1, x] 天的订单,⼀定可以完成。

在解空间中,根据 的位置,可以将解集分成两部分,具有「⼆段性」,那么我们就可以「⼆分答案」。

接下来的问题就是给定⼀个天数 x ,如何判断能否完成 [1, x] 天内的所有订单:

利⽤「差分」数组

处理完 [1, x] 区间的修改,还原「原数组」之后判断是否「全部 ≥ 0 」即可。

时间复杂度: O(n log n) 。

cpp 复制代码
#include <iostream>
using namespace std;

const int N = 1e6 + 10;  // 定义最大数据范围
int n, m;                // n-天数,m-订单数
int r[N];                // 每天可用的教室数量
int d[N], s[N], t[N];    // 订单信息:d-每天用量,s-开始日,t-结束日
int f[N];                // 差分数组

// 检查处理前x个订单是否可行
bool check(int x)
{
    // 初始化差分数组,表示每天剩余的教室数量
    for(int i = 1; i <= n; i++)
    {
        f[i] = r[i] - r[i - 1];  // 构建差分数组
    }
    
    // 处理前x个订单
    for(int i = 1; i <= x; i++)
    {
        // 在差分数组上应用订单:从s[i]到t[i]每天减少d[i]个教室
        f[s[i]] -= d[i];         // 开始位置减去需求量
        f[t[i] + 1] += d[i];     // 结束位置的下一天加回需求量
    }
    
    // 通过前缀和还原实际的每天剩余教室数量
    for(int i = 1; i <= n; i++)
    {
        f[i] = f[i - 1] + f[i];  // 计算前缀和,得到每天实际剩余教室
        if(f[i] < 0) return false; // 如果某天教室不足,返回false
    }
    return true;  // 所有订单都能满足,返回true
}

int main()
{
    cin >> n >> m;  // 输入天数和订单数
    
    // 输入每天可用的教室数量
    for(int i = 1; i <= n; i++) cin >> r[i];
    
    // 输入订单信息
    for(int i = 1; i <= m; i++) cin >> d[i] >> s[i] >> t[i];
    
    // 二分查找:找到第一个无法满足的订单
    int l = 1, r = m;  // 搜索范围:从第1个订单到第m个订单
    while(l < r)
    {
        int mid = (l + r) / 2;  // 取中间点
        if(check(mid)) 
            l = mid + 1;  // 前mid个订单可行,检查更大的范围
        else 
            r = mid;      // 前mid个订单不可行,检查更小的范围
    }
    
    // 输出结果
    if(check(l)) 
        cout << 0 << endl;        // 所有订单都能满足
    else 
        cout << -1 << endl << l << endl;  // 输出-1和第一个无法满足的订单编号
    
    return 0;
}

P1843 奶牛晒衣服

题目背景

熊大妈决定给每个牛宝宝都穿上可爱的婴儿装 。但是由于衣服很湿,为牛宝宝晒衣服就成了很不爽的事情。于是,熊大妈请你(奶牛)帮助她完成这个重任。

题目描述

一件衣服在自然条件下用一秒的时间可以晒干 aaa 点湿度。抠门的熊大妈只买了一台烘衣机 。使用用一秒烘衣机可以让一件衣服额外烘干 bbb 点湿度(一秒晒干 a+ba+ba+b 湿度),但在同一时间内只能烘一件衣服。现在有 nnn 件衣服,第 iii 衣服的湿度为 wiw_iwi(保证互不相同),要你求出弄干所有衣服的最少时间(湿度为 000 为干 )。

输入格式

第一行三个整数,分别为 n,a,bn,a,bn,a,b。

接下来 222 到 n+1n+1n+1 行,第 iii 行输入 wiw_iwi。

输出格式

一行,弄干所有衣服的最少时间。

输入输出样例 #1

输入 #1

复制代码
3 2 1
1
2
3

输出 #1

复制代码
1

说明/提示

样例解释

让机器烘第三件衣服即可一秒完成。

数据范围

1≤wi,a,b,n≤5×1051 \le w_i,a,b,n \le 5 \times 10^51≤wi,a,b,n≤5×105

题解

错误解

第一眼直接[[贪心]]

  • 每次都烘最湿的衣服
  • 优先处理湿度大的衣服
cpp 复制代码
n=3, a=1, b=2
湿度:[3, 3, 3]

如果每次都烘最湿的,可能需要2秒。但最优解是1秒内烘任意一件,自然晾干其他的。

正解

思路

  1. 问题问的是"最少时间" → 可能二分
  2. 时间具有单调性
    • 如果x分钟能烘干所有衣服,那么x+1分钟肯定也能
    • 如果x分钟不能烘干,那么x-1分钟肯定也不能

设经过的时间是 x 。根据题意,我们可以发现如下性质:

  • 经过的时间如果是 x 的话,烘⼲机的「使⽤次数」最多也是 x ;
  • 当 x 在「增⼤」的时候,能弄⼲的⾐服在「增多」;
  • 当 x 在「减⼩」的时候,能弄⼲的⾐服也在「减少」。那么在整个「解空间」⾥⾯,设弄⼲所有⾐服的最少时间是 ret ,于是有:
  • 当 x ≥ ret 时,我们「能弄⼲」所有⾐服;
  • 当 x < ret 时,我们「不能弄⼲」所有⾐服。

在解空间中,根据 ret的位置,可以将解集分成两部分,具有「⼆段性」,那么我们就可以「⼆分答案」。

接下来的重点就是,给定⼀个时间x ,判断「是否能把所有的⾐服全部弄⼲」。当时间为x时,所

有⾐服能够⾃然蒸发a* x 的湿度,于是:

  • 如果 w[i] ≤ a ⋅ x :让它⾃然蒸发;
  • 如果 w[i] > a ⋅ x:需要⽤烘⼲机烘⼲ t = w[i] − a ⋅ x的湿度,次数为 t / b + (t % b == 0 ? 0 : 1)
    那么我们可以遍历所有的⾐服,计算烘⼲机的「使⽤次数」:
  • 如果使⽤次数「⼤于」给定的时间 x ,说明不能全部弄⼲;
  • 如果使⽤次数「⼩于等于」给定的时间 x ,说明能全部弄⼲。
cpp 复制代码
#include <iostream>
using namespace std;
typedef long long LL;  // 定义长整型别名,防止数据溢出
const int N = 5e5 + 10;  // 定义数组最大大小

LL n, a, b;    // n-衣服数量,a-自然晾干速度,b-烘干机额外速度
LL w[N];       // 存储每件衣服的湿度

// 检查在x秒内是否能烘干所有衣服
bool check(LL x)
{
    LL cnt = 0;  // 需要烘干机工作的总时间(秒)
    
    for(int i = 1; i <= n; i++)
    {
        // 如果自然晾干就能满足,跳过
        if(w[i] <= a * x) continue;
        
        // 计算需要烘干机补足的湿度
        LL d = w[i] - a * x;
        
        // 计算这件衣服需要烘干机工作的时间
        // 向上取整:d/b 如果有余数就多需要1秒
        cnt = cnt + d / b + (d % b == 0 ? 0 : 1);
        
        // 注意:这里应该提前判断,如果cnt已经超过x,可以直接返回false优化性能
        // if(cnt > x) return false;
    }
    
    // 总烘干机时间不超过x秒(因为只有一台机器)
    return cnt <= x;
}

int main()
{
    // 输入数据
    cin >> n >> a >> b;
    for(int i = 1; i <= n; i++) cin >> w[i];
    
    // 二分查找最小时间
    LL l = 1, r = 5e5;  // 时间范围:1秒到最大可能时间
    
    while(l < r)
    {
        LL mid = (l + r) / 2;  // 取中间时间
        
        if(check(mid)) 
            r = mid;      // mid秒可行,尝试更小的时间
        else 
            l = mid + 1;  // mid秒不可行,需要更多时间
    }
    
    // 输出最小可行时间
    cout << l << endl;
    
    return 0;
}

P3853 路标设置

题目背景

B 市和 T 市之间有一条长长的高速公路,这条公路的某些地方设有路标,但是大家都感觉路标设得太少了,相邻两个路标之间往往隔着相当长的一段距离。为了便于研究这个问题,我们把公路上相邻路标的最大距离定义为该公路的"空旷指数"。

题目描述

现在政府决定在公路上增设一些路标,使得公路的"空旷指数"最小。他们请求你设计一个程序计算能达到的最小值是多少。请注意,公路的起点和终点保证已设有路标,公路的长度为整数,并且原有路标和新设路标都必须距起点整数个单位距离。

输入格式

第 111 行包括三个数 L,N,KL,N,KL,N,K,分别表示公路的长度,原有路标的数量,以及最多可增设的路标数量。

第 222 行包括递增排列的 NNN 个整数,分别表示原有的 NNN 个路标的位置。路标的位置用距起点的距离表示,且一定位于区间 [0,L][0,L][0,L] 内。

输出格式

输出 111 行,包含一个整数,表示增设路标后能达到的最小"空旷指数"值。

输入 #1

复制代码
101 2 1
0 101

输出 #1

复制代码
51

说明/提示

公路原来只在起点和终点处有两个路标,现在允许新增一个路标,应该把新路标设在距起点 505050 或 515151 个单位距离处,这样能达到最小的空旷指数 515151。

50%50\%50% 的数据中,2≤N≤1002 \leq N \leq 1002≤N≤100,0≤K≤1000 \leq K \leq 1000≤K≤100。

100%100\%100% 的数据中,2≤N≤1000002 \leq N \leq 1000002≤N≤100000, 0≤K≤1000000 \leq K \leq1000000≤K≤100000。

100%100\%100% 的数据中,0<L≤100000000 < L \leq 100000000<L≤10000000。

题解

[[二分]]答案

⼆段性:设最优解为 ret :

  • 如果 x >= ret ,需要设置路标的数量就⼩于等于 k ;
  • 如果 x < ret ,需要设置路标的数量就⼤于 k 。
    对于⼀个数 x ,如何判断设置路标的数量:
  • 设 d = a[i] - a[i - 1] ,那么需要路标的数量就是 d / x ;
  • 如果 d % x == 0 ,此时就可以少⽤⼀个路标。
cpp 复制代码
#include <iostream>
using namespace std;

const int N = 1e5 + 10;  // 定义最大数组大小

int len, n, k;  // len:公路长度, n:原有路标数量, k:最多可增设路标数量
int a[N];       // 存储原有路标位置

// 检查函数:判断当最大间隔为x时,需要增设的路标数量是否不超过k
bool check(int x)
{
    int cnt = 0;  // 记录需要增设的路标数量
    
    // 遍历所有相邻的原有路标
    for(int i = 2; i <= n; i++)
    {
        int d = a[i] - a[i - 1];  // 计算相邻路标间的距离
        
        cnt += d / x;  // 在距离d中,每x长度需要增设一个路标
        
        // 如果距离正好能被x整除,最后一个间隔不需要增设路标
        if(d % x == 0) cnt--;
    }
    
    // 判断需要增设的路标数量是否在允许范围内
    return cnt <= k;
}

int main()
{
    // 输入公路长度、原有路标数量、最多可增设路标数量
    cin >> len >> n >> k;
    
    // 输入原有路标的位置(按递增顺序排列)
    for(int i = 1; i <= n; i++) 
        cin >> a[i];
    
    // 二分查找最小空旷指数
    int l = 1, r = len;  // 最小可能间隔为1,最大可能间隔为公路全长
    
    while(l < r)
    {
        int mid = (l + r) / 2;  // 取中间值作为候选的最大间隔
        
        if(check(mid)) 
            r = mid;    // 如果mid可行,尝试更小的间隔(往左搜索)
        else 
            l = mid + 1; // 如果mid不可行,需要更大的间隔(往右搜索)
    }
    
    // 输出最小的可能空旷指数
    cout << l << endl;
    
    return 0;
}
相关推荐
炽烈小老头1 小时前
【 每天学习一点算法 2026/05/08】最小覆盖子串
学习·算法
lbb 小魔仙1 小时前
DolphinDB:以“存算一体“重新定义工业时序数据的边界
开发语言·人工智能·python·langchain·jenkins
callJJ1 小时前
Codex 联动 OpenSpec 提效方法论
java·开发语言·codex·openspec
上弦月-编程1 小时前
Java编程:跨平台开发利器
java·开发语言
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题】【Java基础篇】第38题:两个对象的hashCode()相同,则 equals()是否也一定为 true?
java·开发语言·后端·面试·hash-index
java1234_小锋1 小时前
什么是可重入锁ReentrantLock?
java·开发语言
csbysj20201 小时前
XSLFO 区域
开发语言
爱编码的小八嘎1 小时前
C语言完美演绎9-27
c语言
江南十四行1 小时前
Java并发编程中的锁机制:synchronized与Lock详解
java·开发语言