告别无脑遍历!二分查找の降维打击:让100万数据搜索只需眨眼20次

🌟 你用过微信"跳一跳"吗?每次按住屏幕的时间决定了小人的跳跃距离,有没有发现高手总能精准落在目标点?这背后藏着程序员的秘密武器——二分算法!今天让我们来揭开这个让搜索效率提升10000倍的魔法!

一、珍珠奶茶里的二分哲学

想象你要在500页的《奶茶配方大全》里找“黑糖珍珠”的做法。菜鸟会一页页翻(线性搜索),而老手会这样做:

  1. 翻到250页,发现是"奶盖制作"

  2. 往后翻到375页,看到"芋圆配方"

  3. 往前翻到312页...
    这就是二分查找!每次排除一半错误答案,3步就找到目标,而菜鸟可能需要300步!


 二、手把手教你写不会翻车的二分代码 🚗

二分算法的模板在网上找至少能找出三个以上。但是来到这里你算走运啦,你只需要掌握这一个并一直用下去就行了。

2.1 二分算法的模板

	// 二分查找左端点 
	int l = 0, r = n; 
	while(l < r)
	{
		int mid = (l + r) >> 1;
		if(check(mid)) r = mid;
		else l = mid + 1;
	}
	// 一定要注意检查答案的正确性 
	// 二分查找右端点 
	int l = 0, r = n; 
	while(l < r)
	{
		int mid = (l + r + 1) >> 1;
		if(check(mid)) l = mid;
		else r = mid - 1;
	}
	// 一定要注意检查答案的正确性 

记忆方法:仅需记住一点,if/else 中出现 -1 时,求mid时 +1 即可。


2.2 小试牛刀

题⽬来源: LeetCode
题⽬链接: 34. 在排序数组中查找元素的第⼀个和最后⼀个位置
难度系数: ★

【解题】:非递减顺序排列数组内查找目标值开始和结束的位置 -> 查找一堆目标值区间的左右端点 -> 二分算法。

问题一:万一找不到怎么办? 我们用二分算法本质是按照顺序往答案收敛,在原区间内找不到就意味着答案在区间之外(左右两侧),也就是说最后l < r,不满足跳出循环时 l 会在一侧,此时我们只需要对 l 位置检测一下是否为目标值即可。

🖥️code:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int> ans;
        int n = nums.size();
        if(n == 0) return {-1, -1};
        int left = 0, right = n - 1;
        while(left < right)
        {
            int mid = (left + right) >> 1;
            if(nums[mid] >= target) right = mid;
            else left = mid + 1; 
        }
        if(nums[left] != target) return {-1, -1};
        ans.push_back(left);
        left = 0; right = n - 1;
        while(left < right)
        {
            int mid = (left + right + 1) >> 1;
            if(nums[mid] <= target) left = mid;
            else right = mid - 1;
        }
        ans.push_back(left);
        return ans;
    }
};

2.3 趁热打铁

题⽬来源: Acwing
难度系数: ★

【解题】:一个数n的三次方根m,在它左侧 m^3 < n,它的右侧 m^3 > n,我们已经找出二段性,每次对mid判断时就可以舍弃一半的区间啦。

问题一:我们之前模板用的是整数二分,这题是浮点数二分? 这个可以当个模板来记忆,思想一模一样,只是多个精度

🖥️code:

#include <iostream>

using namespace std;

const double eps = 1e-12; // 精度


double n;

int main()
{

    cin >> n;
    double l = -1e5, r = 1e5 + 10;
    // l r 的差值小于精度时就可以认为找到了
    while(l + esp < r)
    {
      double mid = (l + r) / 2;
      if(mid * mid * mid > n) r = mid;
      else l = mid;
    }
    printf("%.6lf\n", l);
  return 0;
}

2.4 算法进阶

题⽬来源: Acwing
难度系数: ★★
【解题】:本题有点困难,假如我们真的去二分 -100到100 这个区间会发现这个区间根本没有二段性,但是题目中告诉我们了,f(x1) * f(x2) < 0,必定有一个解,也就是说我们可以枚举所有长度为1的区间判断区间是否存在解,存在就二分找解。
问题一:二分的check函数怎么写? f(l) * f(x2) 即可。
问题二:r和l如何转移? 
🖥️code:
#include <iostream>

using namespace std;

const double eps = 1e-7;// 精度

double a, b, c, d;

double f(double x)
{
	return a * x * x * x + b * x * x + c * x + d;
}

// 二分
double find(double l, double r)
{
	while(r - eps > l)
	{
		double mid = (l + r) / 2;
		if(f(l) * f(mid) < 0) r = mid;
		else l = mid;
	}
	return l;
}

int main()
{
	cin >> a >> b >> c >> d;
    // 遍历-100 到 100 区间
	for(double i = -100; i < 100; i++)
	{
        // 根恰好为整数
		if(f(i) == 0) printf("%.2lf ", i);
        // 根在i 和 i + 1之间
		else if(f(i) * f(i + 1) < 0) printf("%.2lf ", find(i, i + 1));
	}
	return 0;
}


三、对二分算法的灵魂拷问🔥

3.1 为什么快?

每次都将问题的规模缩小一半。

  • 1000个元素 → 10次搞定(2¹⁰=1024)

  • 100万数据 → 20次解决!比线性搜索快5万倍!

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

3.2 什么时候可以用二分算法?

当我们的解具有二段性的时候(下面习题),就可以使用二分算法得出答案:

  • 待查找的区间一定有二段性

  • 根据待查找的区间中点位置,分析得出答案会出现在哪一侧。

  • 接下来舍弃一半的待查找区间,转而在有答案的区间内继续使用二分算法查找结果。

 3.3 二分算法的做题流程?

  • 画图分析!确定使用左端点模板还是右端点模板,还是两者同时使用。

  • 二分结果之后,不要忘记判断结果是否存在,二分问题细节众多,一定要分析全面。


 四、二分法的七十二变 🎭

4.1 二分算法其他练习

4.1.1 牛可乐和魔法封印

题目来源:牛客网
题目链接: 牛可乐和魔法封印
难度系数: ★
【解题】:先来一道模板题练练手~

🖥️code:

注意一定要开longlong 要不然求mid会溢出。

#include <iostream>

using namespace std;

typedef long long LL;

const int N = 1e5 + 10;

int a[N], n, q;

int solve(int x, int y)
{
	LL l = 1, r = n;
	while(l < r)
	{
		LL mid = (l + r) >> 1;
		if(a[mid] >= x) r = mid;
		else l = mid + 1;
	}
    if(a[l] < x) return 0;
	int lowerbound = l;
	l = 1, r = n;
	while(l < r)
	{
		LL mid = (l + r + 1) >> 1;
		if(a[mid] <= y) l = mid;
		else r = mid - 1;
	}
    if(a[l] > y) return 0;
	return l - lowerbound + 1;
}

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> a[i];
	cin >> q;
	while(q--)
	{
		int x, y; cin >> x >> y;
		cout << solve(x, y) << endl;
	}
	
	return 0;
}


4.1.2 P1102 A-B 数对 - 洛谷

题目来源: 洛谷
题目链接: P1102 A-B 数对 - 洛谷
难度系数: ★
【解题】:要求计算出A−B=C数对的个数,其实遍历数组当做B然后找满足A = B + C的A的个数,就是找小于等于B和大于等于B区间的长度,就可以用到二分算法啦~
C++STL中的low_bound 和 upper_bound:
  • low_bound:传入要查询区间的左右迭代器(注意左闭右开, 如果是数组就是左右指针)以及要查询的值k, 然后返回 >= k的第一个位置。

  • upper_bound:传入要查询区间的左右迭代器(注意左闭右开, 如果是数组就是左右指针)以及要查询的值k, 然后返回 > k的第一个位置。

tip: 这两个函数只能用在有序序列中,我们二分的模板可以用在任意有二段性题目中。

🖥️code:

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 2e5 + 10;
LL a[N];
LL n, c;
LL ans;

LL search(LL A)
{
	auto it1 = lower_bound(a + 1, a + n + 1, A);
	auto it2 = upper_bound(a + 1, a + n + 1, A);
	return it2 - it1; 
}

int main()
{
	cin >> n >> c;
	for(int i = 1; i <= n; i++) cin >> a[i];
	
	sort(a + 1, a + n + 1);
	
	// 遍历数组当b 
	for(int i = 1; i <= n; i++)
	{
		ans += search(a[i] + c);		
	}
	
	cout << ans << endl;
	
	return 0;
}

4.1.3 P1678 烦恼的高考志愿 - 洛谷

题⽬来源: 洛⾕
难度系数: ★
【解题】:一般看到这种最小值会下意识想到贪心,没错每个学生的不满意度最小累加起来就是答案(因为这些学校没有人数限制)。
问题一:怎么找到是每个学生不满意度最小的分数呢?二分出 >= 学生分数的第一个数,结果要么就是pos要么是pos-1位置。
细节一:pos - 1 会导致越界,我们要 加上左右护法避免这种情况,这也是常用的技巧。
🖥️code:
#include <iostream>
#include <algorithm> 

using namespace std;

const int N = 1e5 + 10, INF = 1e7 + 10;
int n, m;
int a[N];
long long ans;

int main()
{
	cin >> n >> m;
	for(int i = 1; i <= n; i++) cin >> a[i];
	// 左右护法
	a[0] = a[n + 1] = INF; 
	sort(a + 1, a + n + 1);
	
	while(m--)
	{
		int x; cin >> x;
		auto it1 = lower_bound(a + 1, a + n + 1, x);
		auto it2 = it1 - 1;
//		cout << *it1 << " " << *it2 << endl;
		ans += min(abs(*it1 - x), abs(*it2 - x));
	}
	
	cout << ans << endl;
	
	return 0;
}

4.2 二分答案

二分答案可以处理最大值最小,以及最小值最大的问题。如果解空间在从小到大的变化过程中,判断答案的结果出现二段性,此时我们就可以二分这个解空间,通过判断找出最优解。

大家学习数学一定对应用题不陌生,没错二分答案就是二分算法的应用题,在这种题要善于找到其中隐藏的二段性。

4.2.1 P2440 木材加工 - 洛谷

题目来源: 洛谷
题目链接: P2440 木材加工 - 洛谷
难度系数: ★
【解题】:题目中出现了最大值最小的字眼,二分答案八九不离十。
我们发现:当 l 的长度减小时:切出的段数 k 增大。
                  当 l 的长度增大时:切出的段数 k 减小。
🖥️code:
#include <iostream>

using namespace std;

const int N = 1e5 + 10;
int a[N];
int n, aim;

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

int main()
{
	int maxlen = 0;
	cin >> n >> aim;
	for(int i = 1; i <= n; i++) 
	{
		cin >> a[i];
		maxlen = max(maxlen, a[i]);
	}
	int l = 0, r = maxlen;
	while(l < r)
	{
		int mid = (l + r + 1) >> 1;
		int k = calc(mid);
		if(k >=aim) l = mid;
		else r = mid - 1; 
	}
	cout << l << endl;
	
	return 0;
}

4.2.2 P1873 [COCI 2011/2012 #5] EKO / 砍树 - 洛谷

 题目来源: 洛谷
难度系数: ★
【解题】:本题同样是求解 最大值最小问题(最大的伐木机高度,最小满足条件的木材长度) 二段性也很明显:
伐木机的高度为H,得到木材长度为len
  • H越高,len越小。

  • H越低,len越大。

问题一:如何求得切的木材长度? 遍历一遍就好,其实这点容易想到,关系的问题是能不能过。答案是可以的时间复杂度 O(nlogn)
🖥️code:
#include <iostream>

using namespace std;

typedef long long LL;

const int N = 1e6 + 10;
int n, m, maxlen;
int a[N];

LL calc(int 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];
		maxlen = max(maxlen, a[i]);
	} 
	int l = 0, r = maxlen;
	while(l < r)
	{
		int mid = (l + r + 1) >> 1;
		LL h = calc(mid);
		if(h >= m) l = mid;
		else r = mid - 1;
	}
	cout << l << endl;
	
	return 0;
}

4.2.3 P2678 [NOIP 2015 提高组] 跳石头 - 洛谷

 题目来源: 洛谷
难度系数: ★★
【解题】:最小值最大(最短的跳跃距离尽可能长)。
移动的石头数量n,最短跳跃长度len,最终结果为ret。
二段性:
  • len越长,n越多。

  • len越短,n越少。

于是在整个解空间中: 

  • len <= ret 时,n <= M(给定的移动石头数量):当每次跳跃的最短距离 <= 最终结果,需要移动的石头数量少于或等于M。if(n <= M) l = mid; 

  • len > ret 时,n > M(给定的移动石头数量):当每次跳跃的最短距离 > 最终结果,需要移动的石头数量多于M。if(n >M) r = mid - 1; 

问题一:如何求在给定最短距离len下的移动石头次数n?  滑动窗口或前后指针法:两个指针i,j,从前往后扫描过程中第一次发现a[j] - a[i] >= len时,把j + 1 和 i - 1之间的石头搬走即可。

细节一:本题的数据最后一个位置是单独给的,处理时要n(这里表示数据个数)++,然后把最后一个也放入数组。

🖥️code:

#include <iostream>

using namespace std;

const int N = 5e4 + 10;
int a[N];
int len, n, k, m;

int calc(int x)
{
	int 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 >> len >> n >> k;
	for(int i = 1; i <= n; i++) cin >> a[i];
	a[++n] = len;
	
	int l = 1, r = len; 
	while(l < r)
	{
		int mid = (l + r + 1) >> 1;
		int t = calc(mid);
		if(t <= k) l = mid;
		else r = mid - 1; 
	}
	
	cout << l << endl;
	
	return 0;
}

完。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值