Bootstrap

【2024暑训】洛谷P1102 A-B数对&二分查找的一些感想

题目背景

出题是一件痛苦的事情!

相同的题目看多了也会有审美疲劳,于是我舍弃了大家所熟悉的 A+B Problem,改用 A-B 了哈哈!

题目描述

给出一串正整数数列以及一个正整数 C,要求计算出所有满足 A−B=C 的数对的个数(不同位置的数字一样的数对算不同的数对)。

输入格式

输入共两行。

第一行,两个正整数 N,C。

第二行,N 个正整数,作为要求处理的那串数。

输出格式

一行,表示该串正整数中包含的满足 A−B=C 的数对的个数。

输入输出样例

输入

4 1
1 1 2 3

输出

3

说明/提示

对于 75%的数据,1≤N≤2000。

对于 100% 的数据,1≤N≤2×10^5,0≤ai<2^30,1≤C<2^30。

2017/4/29 新添数据两组

思路

(我)数据结构

1.用vector保存数组。

2.用set保存,自动排序和去重。

3.用map记录各元素出现次数。(map<int,int> mp)

4.指针it便利set内每一个元素,result+=mp[*it]*mp[*it+c]。(*it+c不存在即mp[*it+c]==0,所以不用专门去判断set内是否存在*it+c)

优点:操作很无脑,库函数会用就行。

缺点:时间和空间复杂度明显过高。(虽然这道题能AC掉)

#include<iostream>
#include<vector>
#include<set>
#include<map>
using namespace std;
//qxt_version
int main(){
    long long int c,result;
    int n;
    result=0;
    cin>>n>>c;
    vector<long long int> vec(n,0);
    for(int i=0;i<n;i++){    //用vector保存数组
        cin>>vec[i];
    }
    set<long long int> st(vec.begin(),vec.end());    //用vector初始化set
    map<long long int,long long int> mp;    //创建map
    for(int i=0;i<n;i++){
        mp[vec[i]]++;                //遍历vector,保存元素及重复次数
    }
    for(auto it=st.begin();it!=st.end();++it){
        result+=mp[(*it)+c]*mp[(*it)];    //加组合数
    }
    cout<<result<<endl;
    return 0;
}

(trm)二分查找(膜拜)

1.预先开一个2e5+5的数组nums,保存输入数组。

2.遍历数组,已知c和nums[i],用二分查找去找元素为c+nums[i]的范围的上下界l和r(左开右开区间(l,r))

3.(l,r)长度r-l-1即result增加值。

优点:节约时间和空间

缺点:二分查找上下界难写

#include<iostream>
#include<algorithm>
int nums[200005];
int down(int x,int n){    //找到值为x的元素相邻左边元素的下标
    int l,r;
    l=0;r=n+1;            //左闭右开区间(找左下标)
    while(l+1<r){
        int mid=(l+r)/2;
        if(nums[mid]>=x) r=mid;    //保证闭区间闭的边界为左边相邻元素的下标
        else l=mid;
    }
    return l;
}
int up(int x, int n){    //找到值为x的元素相邻右边元素的下标
    int l,r;
    l=0;r=n+1;
    while(l+1<r){        //左开右闭区间(找右下标)
        int mid=(l+r)/2;
        if(nums[mid]>x) r=mid;    //保证闭区间闭的边界为右边相邻元素的下标
        else l=mid;
    }
    return r;
}
int main(){
    int n,c;
    std::cin>>n>>c;
    long long int ans=0;
    for(int i=1;i<=n;i++) std::cin>>nums[i];
    nums[n+1]=0x3f3f3f3f;    //数组[1,n],涉及开区间,要初始化[n+1]为inf
    std::sort(nums+1,nums+n+1);    //对[1,n+1]排序
    for(int i=1;i<=n;i++){
        int x=c+nums[i];
        int l=down(x,n);
        int r=up(x,n);
        ans+=r-l-1;
    }
    std::cout<<ans;
    return 0;
}

个人疑惑

1.为什么初始化nums[0]=0而nums[n+1]=inf?

sort的范围[1,n+1],n+1的值非读入,取inf最大值占位,保证必在最右侧。

2.为什么down和up函数内初始化l=0,r=n+1?

因为我们找的是左/右侧相邻元素下标,数组范围[1,n],左侧为0,右侧为n+1

心得

奥义·区间维护

在不开编译器的情况下手搓二分,一次成功的关键在于有意识维护区间。

1.对于左闭右闭区间

对于[0,n-1]:

while判断条件:

        找到了:l==r,所以l<r是不断循环的前提,当l==r时就会跳出这个循环。在while外判断该元素是否为target

l和r的更新:

        对于nums[mid]>=target,在[mid,r]内没有排除==的情况,所以r=mid而不是r=mid-1

        对于nums[mid]<target,则在[l,mid]内排除了==的情况,所以l=mid+1而不是l=mid

判断是否找到target:

        nums[l](或nums[r])==target?true:false

//左闭右闭
bool binary_search(int n, int target){
    int l=0,r=n-1;
    while(l<r){
        int mid=(l+r)>>1;
        if(nums[mid]>=target){
            r=mid-1;
        } else {
            l=mid;
        }
    }
    return nums[l]==target?true:false;
}
2.对于左闭右开区间

对于[0,n):

while判断条件:

        找到了:l+1==r,所以l+1<r是不断循环的前提。

l和r的更新:

        对于nums[mid]>target,[mid,r]内排除了==,因为右侧为开区间,r=mid

        对于nums[mid]<=target,[l,mid]内没排除==,左侧为闭区间,l=mid

判断是否找到target:

        nums[l](或nums[r-1])==target?true:false

//左闭右开
bool binary_search(int n, int target){
    int l=0,r=n;
    while(l+1<r){
        int mid=(l+r)>>1;
        if(nums[mid]>target){
            r=mid;
        } else {
            l=mid;
        }
    }
    return nums[l]==target?true:false;
}

为什么不把nums[mid]==target的情况直接写在while里呢(?)trm说是在数学上好证,没有破坏完整性。。有点听不太懂哈哈。。

;