目录
例题:806 div4 Problem - F - Codeforces
例题:34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
例题一:[美国国际航空公司 2016年1月]愤怒的奶牛 (nowcoder.com)
例题三:[NOIP2012]借教室 (nowcoder.com)
二分查找
二分查找是一种在有序数组中查找某一特定元素的搜索算法。
使用前提:1.数列有序。 2.数列使用顺序存储结构。
时间复杂度:O(logn)
证明:假设有n个数,经过k次二分查找,每次减少一半,直到最后剩下一个数,即n,n/2,n/4...1,也就是n*[(1/2)^k]=1,解得k=log2 n
模板
关键点:
- while(l<=r) 小于等于。
- mid=(l+r)>>1也可以写成mid=l+((r-l)>>1),两种写法看题目类型选取,第二种写法一定不会出错,既可用于实数情况,也可用于数组情况,但第一种写法最好只用于数组情况,因为如果数据量太大,l+r可能就爆int范围了。
- l=mid+1,r=mid-1,写错容易死循环。
int Binary_search(int a[n],int t) {
//左指针指向数组的第一个数,右指针指向数组的最后一个数
int l = 1, r = n, mid;
while(l<=r){
mid=(l+r)>>1;
if(a[mid]==t){
return mid;
}
else if(a[mid]>t){
r=mid-1;
}
else{
l=mid+1;
}
}
return l;
}
对于二分查找,如果可以找到这个数,那么就返回这个数的下标;如果找不到这个数,那么一定是由于不满足while循环的条件而退出循环,所以左指针在右指针的右边(r,l),左指针指向第一个一个大于它的数(也是这个数应该出现的位置),右指针指向最后一个小于它的数,返回值返回左指针还是右指针看具体题目来定。要特别注意:如果要查找的数不存在,并且它比非递减数列的第一个数还要小,那么此时左指针指向第一个数,若其下标为0,那么右指针的值则为-1,如果返回的是右指针,一定在主函数中要特判一下,不能直接写a[r],这样会导致越界问题出现;同理,如果要查找的数比非递减数列的最后一个数还要大,那么右指针指向最后一个数,左指针越界,返回左指针时要特判。
例题:1. 两数之和 - 力扣(LeetCode)
对于一个数a,要去数组中查找target-a,很显然我们会想到二分查找,但二分查找的前提是有序数组,所以要先给数组排序,由于要返回数组下标,所以排序时原下标不能丢,可以开一个结构体存数值和下标,也可以直接用再开一个数组存下标,对这个数组根据原数组的大小进行排序。也可以直接利用unordered_map(元素可重复)的find函数去查找。
代码如下:
class Solution {
public:
int binarysearch(vector<int>& nums,vector<int>& Id,int i,int target)
{
int l=i,r=Id.size()-1,mid;
while(l<=r)
{
mid=(l+r)/2;
if(nums[Id[mid]]>target)
{
r=mid-1;
}
else if(nums[Id[mid]]<target)
{
l=mid+1;
}
else{
return mid;
}
}
return -1;
}
vector<int> twoSum(vector<int>& nums, int target)
{
vector<int>ans(2);
vector<int>Id(nums.size());
int i,ret;
for(i=0;i<nums.size();i++)
{
Id[i]=i;
}
//不改变原数组的顺序,Id中存储原数组从小到大排序后各元素的下标
sort(Id.begin(),Id.end(),[&](int i,int j){return nums[i]<nums[j];});
for(i=0;i<Id.size();i++)
{
//传i+1是因为循环没结束,所以前面已经遍历过的数不可能和现在的数匹配上,
//所以左指针不需要从第一个数开始找,直接从它后一个开始就可以
ret=binarysearch(nums,Id,i+1,target-nums[Id[i]]);
if(ret!=-1)
{
ans[0]=Id[i];
ans[1]=Id[ret];
break;
}
}
return ans;
}
};
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int>ans(2);
unordered_map<int,int>t;
int i;
for(i=0;i<nums.size();i++){
t.insert(make_pair(nums[i],i));
}
for(i=0;i<nums.size();i++){
//找到了,并且不是同一个
if(t.find(target-nums[i])!=t.end()&&t.find(target-nums[i])->second!=i){
ans[0]=i;
ans[1]=t.find(target-nums[i])->second;
}
}
return ans;
}
};
lower_bound( )和upper_bound( )
二分查找也可以直接使用lower_bound( )和upper_bound( )。
lower_bound( )
常规版本:在一个非递减的数组中从first位置到last-1位置去查找第一个大于等于val的数,找到了就返回这个数的地址,找不到则返回last。
template <class ForwardIterator, class T> ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val);重载版本:在一个非递增的数组中从first位置到last-1位置去查找第一个小于等于val的数,找到了就返回这个数的地址,找不到则返回last。区别仅仅只有数组整体是递增的还是递减的和>=与<=,但我们需要在函数参数中多加一个仿函数greater<T>() 。
template <class ForwardIterator, class T, class Compare> ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);如果想要直接得到元素的下标,而不是它的地址(一个指向它的指针),我们直接用返回地址减去起始地址即可。
例:
#include<iostream> #include<algorithm> #include<functional> using namespace std; int main() { int a1[8] = { 10,20,30,40,50,60,70,80 }; int a2[8] = { 80,70,60,50,40,30,20,10 }; int pos1 = lower_bound(a1, a1 + 8, 20) - a1; int pos2 = lower_bound(a2, a2 + 8, 50, greater<int>()) - a2; cout << pos1 << endl; cout << pos2 << endl; return 0; }
upper_bound( )
(其实与lower_bound基本一致,区别仅仅只是它没有等号)
常规版本:在一个非递减的数组中从first位置到last-1位置去查找第一个大于val的数,找到了就返回这个数的地址,找不到则返回last。
template <class ForwardIterator, class T> ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last, const T& val);重载版本:在一个非递增的数组中从first位置到last-1位置去查找第一个小于val的数,找到了就返回这个数的地址,找不到则返回last。区别仅仅只有数组整体是递增的还是递减的和>=与<=,但我们需要在函数参数中多加一个仿函数greater<T>() 。
template <class ForwardIterator, class T, class Compare> ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);
例题:806 div4 Problem - F - Codeforces
你会得到一个数组 a1,a2,...。计算指数 1≤i,j≤n 对数,使 ai<i<aj<j。
输入
第一行包含一个整数 t (1≤t≤1000) — 测试用例的数量。
每个测试用例的第一行包含一个整数 n (2≤n≤2⋅105) — 数组的长度。
每个测试用例的第二行包含 n 个整数 a1,a2,...,an (0≤ai≤109) — 数组的元素。
保证所有测试用例中的 n 之和不超过 2⋅105。
输出
对于每个测试用例,输出一个整数 — 满足语句中条件的索引对数。
请注意,某些测试用例的答案不适合 32 位整数类型,因此您应该在编程语言中使用至少 64 位整数类型(如 long long 表示 C++)。
题解:首先由于要满足 ai<i<aj<j,对于ai<i和aj<j我们很简单就可以判断出来,如果输入的那个数大于等于它的下标(i,j是从1开始的),我们直接就跳过这个数,那么我们最后就只需要找满足i<aj的数了。由于i、j的含义是数组中的第几个数,因为有i<j这个要求在,所以j一定在i的后面,我们遍历数组中的数时,只需要找在它前面的,并且和aj这个数比较的是i而不是ai,所以我们可以直接再开一个数组存前面的数的下标(一定是递增的),二分查找第一个大于等于它的i,如果i前面还有下标,那前面的一定是满足i<aj的。
代码如下:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
#define ll long long
int a[200005];
int main(){
ios::sync_with_stdio(false);
cout.tie(0);cin.tie(0);
int t,n,i;
cin>>t;
while(t--){
cin>>n;
vector<int>v;
ll ans=0;
for(i=1;i<=n;i++){
cin>>a[i];
}
for(i=1;i<=n;i++){
if(a[i]>=i)continue;
//得到的是第一个大于等于aj的i在v里面的下标,
//若为0说明前面没数,为1说明前面有一个数小于aj...
ans+=lower_bound(v.begin(),v.end(),a[i])-v.begin();
v.push_back(i);
}
cout<<ans<<endl;
}
return 0;
}
特别应用(范型写法)
例:在0000111111中查找最后一个0,第一个1。
模板:
区别在于:当查找到目标值时,并不立即返回其下标,而是在其上进一步进行查找,返回值根据r在l的左边来确定(也需要注意特判)。
例如在查找最后一个0时,当我们查找到0时,并不能肯定它就是最后一个0,所以我们还要继续往右找,也就是说要把return mid改成l=mid+1,返回值就是return r。
在查找第一个1时,当我们查找到1时,并不能肯定它就是第一个1,所以我们还要继续往左找,也就是说要把return mid改成r=mid-1,返回值就是return l。
并且,查找第一个1,最后一个0这样的题目,代码也不需要写两个函数,完成其中一个即可。例如我们写了查找第一个1这个函数,那么我们显然就能得到最后一个0的下标一定是第一个1的下标减去1,举一个更普适的例子,223336666,我们查找最后一个3,那么主函数中直接调用查找第一个4的函数就可以了,因为如果数组中有4,那么和上面000111111的例子相同,而如果数组中没有4,前面也说了,左指针指向第一个大于它的数,右指针指向最后一个小于它的数,而函数返回的是左指针,也就是第一个6,减一也就是右指针了,指向最后一个小于它的数3,也就是我们所找的最后一个3。
代码如下:
//第一个1 len是数组大小
int Binary_Search(int a[], int target,int len) {
int l = 0, r = len - 1, mid;
while (l <= r) {
mid = l + ((r - l) >> 1);
/*
if (a[mid] == target) {
r = mid - 1;
}
else if (a[mid] > target) {
r = mid - 1;
}
else {
l = mid + 1;
}
*/
//合并后:
if (a[mid] < target) {
l = mid + 1;
}
else {
r = mid - 1;
}
}
return l;
}
例题:34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
前面说的两个函数写哪个都行,如果用查找最后一个0,结束位置就正常查找,开始位置就查找target-1的结束位置+1即可。
代码如下:(查找第一个1)
class Solution {
public:
int binarysearch(vector<int>& nums, int target){
int l=0,r=nums.size()-1,mid;
while(l<=r){
mid=(l+r)/2;
if(nums[mid]<target){
l=mid+1;
}
//找到了也要继续往左找
else{
r=mid-1;
}
}
return l;
}
vector<int> searchRange(vector<int>& nums, int target) {
vector<int>v={-1,-1};
//数组为空
//特判的左指针越界,说明没有找到,要找的数比最大的还大
//虽然左指针没有越界,但它对应的值和要找的数不一样,说明这个数不存在,左指针指向第一个大于它的数
if(nums.empty()||binarysearch(nums,target)>=nums.size()||nums[binarysearch(nums,target)]!=target){
return v;
}
v[0]=binarysearch(nums,target);
v[1]=binarysearch(nums,target+1)-1;
return v;
}
};
(查找最后一个0):
class Solution {
public:
//最后一个0
int Binary_Search(vector<int>& nums,int target){
int l=0,r=nums.size()-1,mid;
while(l<=r){
mid=l+((r-l)>>1);
if(nums[mid]>target){
r=mid-1;
}
else{
l=mid+1;
}
}
return r;
}
vector<int> searchRange(vector<int>& nums, int target) {
vector<int>ans{-1,-1};
int p1=Binary_Search(nums,target-1)+1,p2=Binary_Search(nums,target);
//数组为空
//特判的右指针越界,要找的值比数组中最小的数还要小
//要找的数不存在
if(nums.empty()||p2<0||nums[p2]!=target){
return ans;
}
ans[0]=p1;
ans[1]=p2;
return ans;
}
};
——————————————————————————————————————————
二分答案
简而言之就是答案在一个区间里,我们通过二分选择一个数带入题目,如果不符合就直接砍掉一半的区间,如果符合就继续往一侧区间去找最优解。
使用条件:
1、答案在一个区间内。
2、直接搜索不好搜,但是容易判断一个答案可不可行。
3、该区间对题目具有单调性,即:在区间中的值越大或越小,题目中的某个量对应增加或减少。其实也就是可以根据选择的数可不可行来选择下一个区间。
典型特征:求...最大值的最小 、 求...最小值的最大。
根据题目理解:
例题一:[美国国际航空公司 2016年1月]愤怒的奶牛 (nowcoder.com)
题解:首先题目给了我们所有干草的位置,很明显需要先对它们进行排序。接下来我们就需要思考半径为多少才能把这些坐标全部覆盖,并且还要让半径尽可能的小,我们很简单就能想到半径的取值范围,最小是1,最大是最远的那个点的坐标,但直接做我们也不太知道该从哪开始,这已经很符合二分答案的特征了,我们进一步去思考,如果我们选择了一个半径,用它引爆所有的干草所需要的奶牛个数如果超过了k,那么很显然这个半径选小了,比这个半径更小的那一半就更不可能了,可以直接舍掉一半的区间;而如果它所需要的奶牛个数小于等于k,那么这就是符合题意的解了,它说明选择的半径可能偏大了,我们并不能保证它就是最优解,所以我们继续往它的左边(让半径减小)寻找看有没有更优解。
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
int a[50005];
int main(){
int n,k,i,l=1,r,mid,ans;
cin>>n>>k;
for(i=0;i<n;i++){
cin>>a[i];
}
sort(a,a+n);
r=a[n-1];
while(l<=r){
int sum=0,begin=0,num=1;
mid=(l+r)>>1;
//选第一个点作为起点,如果目前覆盖范围小于2*mid,就继续添入下一个点,
//直至若再加入下一个点会导致距离超过最远可覆盖范围,就把这个点当成新起点此时奶牛个数加1
for(i=0;i<n;i++){
sum=a[i]-a[begin];
if(sum>2*mid){
begin=i;
num++;
}
}
if(num<=k){
r=mid-1;
ans=mid;
}
else{
l=mid+1;
}
}
cout<<ans;
return 0;
}
例题二:跳石头 (nowcoder.com)
题目描述
一年一度的“跳石头”比赛又要开始了!
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。
为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能移走起点和终点的岩石)。
输入描述:
输入文件第一行包含三个整数 L,N,M,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。 接下来 N 行,每行一个整数,第 i 行的整数 Di(0 < Di < L)表示第 i块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。
输出描述:
输出文件只包含一个整数,即最短跳跃距离的最大值。
输入
25 5 2 2 11 14 17 21
输出
4
说明
将与起点距离为 2 和14 的两个岩石移走后,最短的跳跃距离为 4(从与起点距离17的岩石跳到距离 21的岩石,或者从距离 21 的岩石跳到终点)。
备注:
对于20%的数据,0 ≤ M ≤ N ≤ 10。 对于50%的数据,0 ≤ M ≤ N ≤ 100。 对于100%的数据,0 ≤ M ≤ N ≤ 50,000,1 ≤ L ≤ 1,000,000,000。
题解:我们要让最短跳跃距离尽可能长,也就是最长的最短跳跃距离,正好是二分答案的标志词,我们接下来分析是否可以使用二分答案完成此题。首先,最短跳跃距离最小取所有石块间距最小的值,最大取起点石头到终点石头的距离。我们用变量now记录当前所处位置,从now往前跳,如果它和前面那块石头的距离小于我们所选取的最短距离,我们就把这块石头移开,因为最短距离已经定了,不能比它更小了,移开的石头数量+1,以此类推,要注意的是最后从now到终点石头要特别拿出来判断,因为终点石头不能移走,所以不能和上面的过程写在一个循环中,不然终点石头就可能会被移走。最后,如果移走的石头个数大于M,说明移走的过多,最短距离太长了,那么比这个距离还长的区间就可以直接舍去了,因为它们只会导致更多的石头被移走;如果移走的石头个数小于等于M,说明移走的偏少,最短距离取短了,我们就到它右侧取寻找更优解。
代码如下:
#include<iostream>
#include<limits.h>
using namespace std;
int a[50005];
int main() {
int L, N, M, i, ans, l = INT_MAX, r = 0, mid;
cin >> L >> N >> M;
for (i = 1; i <= N; i++) {
cin >> a[i];
l = min(l, a[i] - a[i - 1]);
}
a[N+1]=L;
r = L;
l=min(l,a[N+1]-a[N]);
while (l <= r) {
mid = (l + r) >> 1;
//now记录当前所处位置,num记录移走的石块个数
int now = 0, num = 0;
for (i = 1; i <= N; i++) {
if (a[i] - a[now] < mid) {
num++;
}
else {
now = i;
}
}
//终点特判:终点不应该被移走,所以如果终点在所选mid中需要被移走说明这种情况不成立,舍去
if((a[N+1]-a[now]<mid)||num>M){
r=mid-1;
}
else {
l = mid + 1;
ans = mid;
}
}
cout << ans;
return 0;
}
例题三:[NOIP2012]借教室 (nowcoder.com)
题目描述
在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。
面对海量租借教室的信息,我们自然希望编程解决这个问题。
我们需要处理接下来n天的借教室信息,其中第i天学校有ri个教室可供租借。共有m份订单,每份订单用三个正整数描述,分别为dj, sj, tj,表示某租借者需要从第sj天到第tj天租借教室(包括第sj天和第tj天),每天需要租借dj个教室。
我们假定,租借者对教室的大小、地点没有要求。即对于每份订单,我们只需要每天提供dj个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。
借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。这里的无法满足指从第sj天到第tj天中有至少一天剩余的教室数量不足dj个。
现在我们需要知道,是否会有订单无法完全满足。如果有,需要通知哪一个申请人修改订单。
输入描述:
第一行包含两个正整数n, m,表示天数和订单的数量。
第二行包含n个正整数,其中第i个数为ri,表示第i天可用于租借的教室数量。
接下来有m行,每行包含三个正整数dj, sj, tj,表示租借的数量,租借开始、结束分别在第几天。
每行相邻的两个数之间均用一个空格隔开。天数与订单均用从1开始的整数编号。
输出描述:
如果所有订单均可满足,则输出只有一行,包含一个整数0。否则(订单无法完全满足)输出两行,第一行输出一个负整数-1,第二行输出需要修改订单的申请人编号。
输入
4 3 2 5 4 3 2 1 3 3 2 4 4 2 4
输出
-1 2
说明
第1 份订单满足后,4 天剩余的教室数分别为0,3,2,3。 第2 份订单要求第2 天到第4 天每天提供3 个教室,而第3 天剩余的教室数为2,因此无法满足。分配停止,通知第2个申请人修改订单。
备注:
对于10%的数据,有1≤n,m≤10;
对于30%的数据,有1≤n,m≤1000;
对于70%的数据,有1≤n,m≤105;
对于100%的数据,有1≤n, m≤106, 0≤ri, dj≤109, 1≤sj≤tj≤ n。
题解:首先,完成每个订单后sj到tj的教室数量都会减少dj个,对于这种同时修改一段区间的问题,我们很明显要用到差分。然后我们在考虑二分答案,区间最小值是1,最大值是最后一个订单m,然后我们选一个订单作为无法满足的订单,如果最后check发现它可以被满足,那么他左边的一定都可以满足,不然到不了它,如果他不能被满足,那么我们就往它左边找,看谁是第一个不能被满足的订单。
代码如下:
#include<iostream>
using namespace std;
const int Max = 1e6 + 5;
int a[Max];//原数组
int d[Max];//差分数组
int D[Max];
struct ding {
int d, s, t;
}b[Max];
int n, m, i;
bool check(int mid) {
int i;
//用d[]赋值D[],对D进行修改
//因为我们如果我们直接对d进行修改,那么下一次使用check函数,差分数组就不再是一开始的数组了,而是被修改后的数组
for (i = 1; i <= n; i++) {
D[i] = d[i];
}
//修改差分数组
for (i = 1; i <= mid; i++) {
D[b[i].s] -= b[i].d;
D[b[i].t + 1] += b[i].d;
}
//差分是前缀和的逆运算
for (i = 1; i <= n; i++) {
a[i] = D[i] + a[i - 1];
//说明订单不能被满足
if (a[i] < 0) {
return true;
}
}
return false;
}
int main() {
cin >> n >> m;
for (i = 1; i <= n; i++) {
cin >> a[i];
d[i] = a[i] - a[i - 1];
}
for (i = 1; i <= m; i++) {
cin >> b[i].d >> b[i].s >> b[i].t;
}
int l = 1, r = m, mid, ans = 0;
while (l <= r) {
mid = (l + r) >> 1;
//订单不满足,往前找第一个不满足的
if (check(mid)) {
r = mid - 1;
ans = mid;
}
else {
l = mid + 1;
}
}
//说明订单一直被满足,ans没有被修改过
if (ans == 0) {
cout << 0 << endl;
return 0;
}
else {
cout << -1 << endl << ans;
}
return 0;
}