蓝桥杯算法精讲:二分算法之二分答案深度剖析

目录

  • 前言
  • [一、 二分算法](#一、 二分算法)
    • [1.1 二分答案](#1.1 二分答案)
      • [1.1.1 木材加工](#1.1.1 木材加工)
      • [1.1.2 砍树](#1.1.2 砍树)
      • [1.1.3 跳石头](#1.1.3 跳石头)
  • 结语

🎬 云泽Q个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列

⛺️遇见安然遇见你,不负代码不负卿~


前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、 二分算法

1.1 二分答案

1.1.1 木材加工

木材加工

解法

学习「二分答案」这个算法,基本上都会把这道比较简单的题当成例题

设要切成的长度为,能切成的段数为C。根据题意,我们可以发现如下性质,可以利用该规律衍生到二分:

  • 当x增大的时候,c在减小。也就是最终要切成的长度越大,能切的段数越少;
  • x当减小的时候,c在增大。也就是最终要切成的长度越小,能切的段数越多。

可以在这个单调性的基础上,稍微优化一下暴力解法,可以从大到小枚举切割长度x,当x在减小的过程中,c在逐渐增大,当第一次出现切割出来的段数>=7的时候,第一次出现的那个x一定是最终结果

在整个「解空间」里面,设最终的结果是ret,于是有:

  • 当x<ret时,c≥k。也就是「要切的长度」小于等于「最优长度」的时候,最终切出来的段数「大于等于」k;
  • 当x>ret时,c<k。也就是「要切的长度」大于「最优长度」的时候,最终切出来的段数「小于」k;

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

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

typedef long long LL;
const int N = 1e5 + 10;
LL a[N];
LL n, k;

LL calc(LL x)
{
	LL cnt = 0;
	for(int i = 1; i <= n; i++)
	{
		cnt += a[i] / x; 
	}
	return cnt;
}

int main()
{
	cin >> n >> k;
	for(int i = 1; i <= n; i++) cin >> a[i];
	//不管读入原木的最长长度了,
	//接按题目给的数据范围给到最大1e8
	LL left = 0, right = 1e8;
	while(left < right)
	{
		LL mid = (left + right + 1) / 2;
		if(calc(mid) >= k) left = mid;
		else right = mid - 1;
	}
	cout << left << endl;
	return 0;
}

这段代码采用二分查找策略来求解最大切割长度 l,时间复杂度如下:

  • 二分查找次数:查找区间为 [0,maxL](maxL 为原木最大长度,本题中为 108),二分次数约为 log(108)≈27 次。
  • 单次验证时间:每次二分需要调用 calc 函数,遍历所有 n 根原木(n≤105),统计可切割的总段数,时间复杂度为 O(n)。

因此,总时间复杂度为 O(n⋅logmaxL​),代入数据规模后约为 105×27=2.7×106 次操作,完全在可接受的时间范围内。

  1. 避免计算溢出 :在 calc 函数中,统计总段数 cnt 时,若切割长度 x 很小(如 x=1),每根原木可切割出的段数为 Li​,n 根原木的总段数可能达到 105×108=1013。而 int 类型的最大值仅约 2.1×109,远小于 1013,若用 int 存储 cnt,会导致整数溢出,计算结果完全错误。因此,cnt 必须用 long long 类型。

  2. 变量范围匹配

    题目中 k 的范围是 1≤k≤108,虽然 int 可以存储,但在比较 calc(mid) >= k 时,calc(mid) 返回的是 long long 类型,为避免类型不匹配导致的隐式转换问题,k 也应定义为 long long。

    原木长度 Li​ 的范围是 1≤Li​≤108,单个 Li​ 可用 int 存储,但在计算 Li​/x 并累加到 cnt 时,为避免类型转换开销,Li​ 也应定义为 long long。

1.1.2 砍树

砍树

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

const int N = 1e6 + 10;
typedef long long LL;
LL a[N];
LL n, m;

LL calc(LL x)
{
	LL ret = 0;
	for(int i = 1; i <= n; i++)
	{
		if(a[i] > x) ret += a[i] - x;
	}
	return ret;
}

int main()
{
	cin >> n >> m;
	for(int i = 1; i <= n; i++) cin >> a[i];
	LL left = 0, right = 4e5;
	while(left < right)
	{
		LL mid = (left + right + 1) / 2;
		if(calc(mid) >= m) left = mid;
		else right = mid - 1; 
	}
	cout << left << endl;
	return 0;
}

这道题时间复杂度和思路基本和上道题完全一样

1.1.3 跳石头

跳石头


解法

设每次跳的最短距离是,移走的石头块数为c。根据题意,我们可以发现如下性质:

  • 当x增大的时候,c也在增大;
  • 当x减小的时候,c也在减小。

那么在整个「解空间」里面,设最终的结果是ret,于是有:

  • 当x≤ret时,c<=M。也就是「每次跳的最短距离」小于等于「最优距离」时,移走的石头块数「小于等于」M;
  • 当x>ret时,c>M。也就是「每次跳的最短距离」大于「最优距离」时,移走的石头块数「大于」M。

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

当我们每次二分一个最短距离x时,如何算出移走的石头块数c?

  • 定义前后两个指针i,j遍历整个数组,设i≤j,每次j从i的位置开始向后移动;
  • 当第一次发现a[j]一a[i]≥x时,说明[i+1,j一1]之间的石头都可以移走;
  • 然后将i更新到j的位置,继续重复上面两步。
cpp 复制代码
#include <iostream>
using namespace std;

typedef long long LL;
const int N = 5e4 + 10;
LL l, n, m;
LL a[N];

// 当最短跳跃距离为 x 时,移走的岩石数目
LL calc(LL x)
{
    LL ret = 0;
    for(int i = 0; i <= n; i++)
    {
        int j = i + 1;
        while(j <= n && a[j] - a[i] < x) j++;
        ret += j - i - 1;
        i = j - 1;
    }
    return ret;
}

int main()
{
    cin >> l >> n >> m;
    for(int i = 1; i <= n; i++) cin >> a[i];
    a[n + 1] = l;
    n++;
    LL left = 1, right = l;
    while(left < right)
    {
        LL mid = (left + right + 1) / 2;
        if(calc(mid) <= m) left = mid;
        else right = mid - 1;
    }
    cout << left << endl;
    return 0;
}

时间复杂度
1. 二分查找部分

我们在区间 [1, L] 中寻找最大的满足条件的跳跃距离 x,其中 L≤109

二分查找的次数为 log​(109)≈30 次,时间复杂度为 O(logL)。

2. calc(x) 函数部分

calc(x) 用于计算:当最短跳跃距离至少为 x 时,需要移走的岩石数量。

代码中使用了双指针(i 和 j):

i 代表当前所在的岩石位置,j 从 i+1 开始向后寻找第一个满足 a[j] - a[i] >= x 的岩石。

由于 i 和 j 都是单向递增(j 永远不会回退,i 会被设置为 j-1),整个数组只会被遍历一次。

因此 calc(x) 的时间复杂度为 O(n)(n 为岩石总数 + 1,包含终点)。

3. 整体时间复杂度

每次二分查找都会调用一次 calc(x),因此总时间复杂度为:O(nlogL)​

代入题目数据规模验证:

n≤5×104,logL≈30

总操作数约为 5×104×30=1.5×106,完全在时间限制内,不会超时。


结语

相关推荐
小龙报1 小时前
【数据结构与算法】环与相遇:链表带环问题的底层逻辑与工程实现
c语言·数据结构·c++·物联网·算法·链表·visualstudio
佩奇大王1 小时前
P2118 排列字母
java·开发语言·算法
仟濹1 小时前
【算法打卡day20(2026-03-12 周四)算法:前缀和,二维前缀和,快慢指针,哈希表set使用技巧,哈希表map使用技巧】7个题
数据结构·算法
一叶落4381 小时前
LeetCode 67. 二进制求和(C语言详解 | 双指针模拟加法)
c语言·数据结构·算法·leetcode
寒月小酒2 小时前
3.12 OJ
算法
CoovallyAIHub2 小时前
纯合成数据训练,真实图像Pose mAP达0.97:亚琛工大用YOLOv11实现风电关键点检测
深度学习·算法·计算机视觉
铭哥的编程日记2 小时前
贪心算法解决分糖果问题
算法·贪心算法
马猴烧酒.2 小时前
【JAVA算法|hot100】贪心算法类型题目详解笔记
java·开发语言·ide·笔记·算法·spring·贪心算法