目录
二分查找适用题型的一般特征
1.题目要查找合法区间的临界或者是要求所有数据中符合题意的数据个数。
2.题目所给的数据区间特别大
二分查找的代码逻辑
二分查找嵌套着极限的数学思想。在二分查找的题目中,我们所求的答案往往是(或者可以转换成)去求一个极限。我们的思路一般是通过构造一个包含所求极限的有序集合/数组,然后取中值,再根据极限的性质正确缩小区间,不断逼近答案,最终将答案逼出。
二分查找的两种方式
1.递归(代码相对简洁,但难度稍微要大一些)
2.循环(个人比较推荐)
例题:子串简写
题目正解代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<vector>
#include<iostream>
typedef long long ll;
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
vector<ll> AllTheC1Index;
ll K;
ll ans = 0;
string s;
char c1, c2;
cin >> K >> s >> c1 >> c2;
//这里的i充当指向当前c2的整形指针
for (ll i = 0 ; i < (ll)s.size();++i) {
if (s[i] == c1)
//注意这里存的是c1在s中的索引值,而不是c1本身。
AllTheC1Index.push_back(i);
if (s[i] == c2) {
//临界情况特殊处理:c1集合的无任何元素
if (!AllTheC1Index.size())
continue;
//程序进到这里说明二分法有序区间的右临界已找到,下面则是暂停循环,开始二分
ll left = 0;
ll right = (ll)AllTheC1Index.size() - 1;
while (left < right)
{
ll mid = (left + right + 1) >> 1;
if ( i - AllTheC1Index[mid] + 1 >= K)
{
left = mid;
}
else {
right = mid - 1;
}
}
//+if条件是为了进行临界情况特殊处理:c1集合中所有的元素均不符合题意
if (i - AllTheC1Index[left] + 1 >= K)
ans += right + 1;
}
}
cout << ans << endl;
return 0;
}
思路分析
根据题目所给的数据范围,若是直接暴力搜索,大部分的数据一定会超时。所以考虑使用二分法来减少查找过程中检查的数据个数。
No.1 构造有序集合
当前c2前面所有c1的索引值。vector<ll> AllTheC1Index中有几个值,就说明当前c2前面有几个c1。
No.2 找所求极限
这里所求的极限是:有序集合中满足i - AllTheC1Index[left] +1 >= K 的最大索引。
极限的性质:
极限左边的元素均满足:i - AllTheC1Index[left] +1 >= K
极限右边的元素均满足:i - AllTheC1Index[left] +1 < K
No.3 开始写一般情况下的代码。
正确缩小区间,最终将答案逼出。
No.4 一些临界情况进行单独处理。
一般情况代码无法处理的特殊情况有:
1.当前c2前面根本没有c1,即此时AllTheC1Index集合大小为0,此情况若不单独拎出来处理,程序在执行过程中访问该集合时,会出现越界访问问题,导致答案错误。
//临界情况特殊处理:c1集合的无任何元素
if (!AllTheC1Index.size())
continue;
2.当前c2与有序集合中第一个对应c1之间构成的子串长度仍小于K。
//+if条件是为了进行临界情况特殊处理:c1集合中所有的元素均不符合题意
if (i - AllTheC1Index[left] + 1 >= K)
ans += right + 1;
二分查找中间值的取法
ll mid = (left + right + 1) >> 1;
取法解释
当区间内元素个数为奇数个时,中间值即取此区间唯一的中间值。
当区间内元素个数为偶数个时,此时区间存在两个中间值,代码会令mid去取从左往右第二个中间值。
如此取的好处
这样的中间值涵盖了二分查找时的某些临界情况(注意,不是全部),无需对这些临界情况再进行单独的代码处理。
一些感悟
1. 做算法题时,题目中的一些临界情况可以利用小技巧巧妙地融合进一般情况的代码中,进行处理。但是,题目中有的临界情况必须拿出来单独代码处理。
所以做题时,将一般情况处理好后,一定要根据题意去找到那些一般情况代码无法处理的临界情况,拎出来,处理。
2. vector.size()返回值是一个无符号整形,会导致同行代码数据类型不一致,从而导致程序运行错误。因此这里必须强转一次。
ll right = (ll)AllTheC1Index.size() - 1;
所以,我们在调用一些容器自带的方法时,一定要关注方法的返回值是什么,方法所需参数的类型与自己所传参数的类型是否一致。