常用基础算法

二分

二分是一种非常常用的算法,通常可以解决一类问题——这类问题的答案往往具有单调性,例如从0~N都满足题意而到了N+1再往后都不满足题意。
这是我们可以采用折半查找的方式来查找,这个介绍一个比较通用的模板。
整数二分例题:AcWing789
代码实现:

#include<iostream>
using namespace std;
const int N=1e5+10;
int n,q;
int arr[N];
int main(){
    scanf("%d%d",&n,&q);
    for(int i=0;i<n;i++){
        scanf("%d",arr+i);
    }
    while(q--){
        int x;
        scanf("%d",&x);
        int l=0,r=n-1;
        while(l<r){//二分查找>=k的最小的数
            int mid=l+r>>1;
            if(arr[mid]>=x){//如果当前数大于等于x,则答案一定在左边(包括当前位置),把r更新为mid
                r=mid;
            }
            else{//否则答案一定在右边(不包括当前位置)
                l=mid+1;
            }
        }
        if(arr[l]==x)   printf("%d ",l);//最后l和r相等,输出任意一个
        else printf("-1 ");
        l=0,r=n-1;
        while(l<r){
            int mid=l+r+1>>1;//这里必须+1,否则当l=0,r=1时若满足arr[mid]<=x则会陷入死循环
            if(arr[mid]<=x){//同样的分析方法
                l=mid;
            }
            else{
                r=mid-1;
            }
        }
        if(arr[l]==x)   printf("%d\n",l);
        else printf("-1\n");
    }
    return 0;
}

小数二分例题:AcWing790
代码实现:

#include<iostream>
using namespace std;
double n;
int main(){
    double l=-100,r=100;
    scanf("%lf",&n);
    while(r-l>1e-8){//浮点数存储有误差,一般两数之差在一个很小的范围内视为相等
        double mid=(l+r)/2;//无需考虑边界问题
        if(mid*mid*mid>=n)  r=mid;
        else l=mid;
    }
    printf("%lf",l);
    return 0;
}

高精度

我们知道,在C++中,即使unsigned long long的范围也是挺有限的,当我们不能用这些类型来存储数字时,我们可以用数组或者别的数据结构存储每一位数字,计算时就按照小学时的步骤来计算,本人喜欢用vector来写,虽然效率上可能稍逊,但写起来很方便。

加法

例题:AcWing791
代码实现:

#include<iostream>
#include<vector>
using namespace std;
vector<int> a,b;
vector<int> ans;
void add(vector<int>&a,vector<int>&b){//引用传值,效率更高
    int tmp=0;//tmp代表进位
    int m=max(a.size(),b.size());
    for(int i=0;i<m;i++){
        if(i<a.size())  tmp+=a[i];
        if(i<b.size())  tmp+=b[i];
        ans.push_back(tmp%10);
        tmp/=10;
    }
    if(tmp) ans.push_back(tmp);//处理最后的进位
}
int main(){
    string s1,s2;
    cin>>s1>>s2;
    for(int i=s1.size()-1;i>=0;i--){
        a.push_back(s1[i]-'0');
    }
    for(int i=s2.size()-1;i>=0;i--){
        b.push_back(s2[i]-'0');
    }
    add(a,b);
    for(int i=ans.size()-1;i>=0;i--)    printf("%d",ans[i]);
    return 0;
}

减法

例题:AcWing792
代码实现:

#include<iostream>
#include<vector>
using namespace std;
vector<int> a,b,ans;
void sub(vector<int>&a,vector<int>&b){
    int t=0;//t代表借位
    for(int i=0;i<a.size();i++){
        t=a[i]-t;
        if(i<b.size())  t-=b[i];
        ans.push_back((t+10)%10);
        t=(t<0?1:0);
    }
    while(ans.size()>1&&ans.back()==0)    ans.pop_back();//删除前导0,注意如果只剩一位且为0的情况,此时不能删除
}
bool cmp(vector<int>&a,vector<int>&b){
    if(a.size()!=b.size())  return a.size()>b.size();
    for(int i=a.size()-1;i>=0;i--){
        if(a[i]!=b[i])  return a[i]>b[i];
    }
    return true;
}
int main(){
    string x,y;
    cin>>x>>y;
    for(int i=x.size()-1;i>=0;i--){
        a.push_back(x[i]-'0');
    }
    for(int i=y.size()-1;i>=0;i--){
        b.push_back(y[i]-'0');
    }
    if(cmp(a,b)){//需要对a,b根据大小情况分类
        sub(a,b);
    }
    else{
        cout<<"-";
        sub(b,a);
    }
    for(int i=ans.size()-1;i>=0;i--)    cout<<ans[i];
    return 0;
}

乘法(高精乘低精)

例题:AcWing793
代码实现:

#include<iostream>
#include<vector>
using namespace std;
vector<int> v;
void mul(vector<int>& a,int b){
    int t=0;//t为进位
    for(int i=0;i<a.size();i++){
        int tmp=t;//这里用一个临时变量记录t,不然很麻烦
        t=(tmp+a[i]*b)/10;
        a[i]=(a[i]*b+tmp)%10;
    }
    while(t){//处理剩余的进位
        v.push_back(t%10);
        t/=10;
    }
    while(a.size()>1&&a.back()==0)  a.pop_back();
}
int main(){
    string a;
    int b;
    cin>>a>>b;
    for(int i=a.size()-1;i>=0;i--){
        v.push_back(a[i]-'0');
    }
    mul(v,b);
    for(int i=v.size()-1;i>=0;i--)  printf("%d",v[i]);
    return 0;
}

高精乘高精其实也很好写,就和小学的列竖式计算一样,这里本人就不再赘述。

除法(高精除低精)

例题:
代码实现:

#include<iostream>
#include<vector>
using namespace std;
vector<int> d,ans;
void div(vector<int>&d,int b,int& r){//r为余数,这里用的是小学除法的思想
    for(int i=d.size()-1;i>=0;i--){
        ans.push_back((r*10+d[i])/b);
        r=(r*10+d[i])%b;
    }
    while(ans.size()>1&&ans.front()==0) ans.erase(ans.begin());//去除前导0
}
int main(){
    string a;
    int b;
    cin>>a>>b;
    int r=0;
    for(int i=a.size()-1;i>=0;i--){
        d.push_back(a[i]-'0');
    }
    div(d,b,r);
    for(int i=0;i<ans.size();i++) cout<<ans[i];
    puts("");
    cout<<r;
    return 0;
}

前缀和与差分

一维前缀和

前缀和主要是用来优化多次求不同区间内所有数之和的问题的,试想有一个数列a1,a2,a3,a4,……an,我们算出来s1,s2,s3,s4……,sn后,如果我们想求ak+ak+1+ak+2+……+am,那我们就可以直接用sm-sk-1来算了,这样我们就能够在O(1)时间内求出区间和,而sn=sn-1+an,这样我们就能用O(N)的时间预处理s数组,从而大大方便了后面的查询。
例题:AcWing795
代码实现:

#include<iostream>
using namespace std;
const int N=1e5+10;
int a[N],s[N];//a为原来的数组,s为前缀和数组,前缀和数组一般下标从1开始,当用到i-1时就不用再考虑边界问题了
int n,m;
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)   scanf("%d",a+i);
    for(int i=1;i<=n;i++)   s[i]=s[i-1]+a[i];//下标从1开始
    while(m--){
        int x,y;
        scanf("%d%d",&x,&y);
        printf("%d\n",s[y]-s[x-1]);
    }
    return 0;
}

二维前缀和

同样地,二维前缀和有利于我们快速地求出子矩阵的和,我们先预处理出来s数组,s[i][j]即表示如图所示子矩阵的和。
在这里插入图片描述
当我们预处理出来一个这样的数组后,我们就很容易求出任意一个子矩阵的和了。
在这里插入图片描述
我们发现,红色子矩阵的和等于黑色子矩阵的和减去两个蓝色子矩阵的和再加上绿色子矩阵的和(因为刚刚它被减了两次)。即子矩阵的和为s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]
那我们怎么预处理出来一个s数组呢?和求子矩阵方法一样!相当于求出当前点外当前子矩阵的和再加上当前点,自己画个图更容易理解哦。
例题:AcWing796
代码实现:

#include<iostream>
using namespace std;
const int N=1010;
int n,m,q;
int a[N][N],s[N][N];
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>a[i][j];
            s[i][j]=s[i-1][j]+s[i][j-1]+a[i][j]-s[i-1][j-1];
        }
    }
    while(q--){
        int x1,y1,x2,y2;
        cin>>x1>>y1>>x2>>y2;
        cout<<s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]<<endl;
    }
    return 0;
}

一维差分

差分相当于前缀和的逆运算吧,当我们需要多次给不同段区间都加上一个相同的值时,暴力的做法复杂度很高,这时我们想到我们的前缀和思想,由此联想到差分,什么意思呢,假设我们需要对一段区间都加上x,我们只需对该区间的第一个数加上x,该区间最后一个数的后一个数减上x,我们就完成了差分操作。
为什么这么说呢?我们发现,当我们对这个数组求前缀和后,就等效于在这个区间内的数各加上x了。
在这里插入图片描述
例题:AcWing797
代码实现:

#include<iostream>
using namespace std;
int n,m;
const int N=1e5+10;
int a[N],s[N];//a为差分数组,s为前缀和数组
void insert(int l,int r,int c){
    a[l]+=c;
    a[r+1]-=c;
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int tmp;
        cin>>tmp;
        insert(i,i,tmp);//这也就是对i这一个点的区间差分,求前缀和后就相当于只给i上的数加上了tmp
    }
    while(m--){
        int l,r,c;
        cin>>l>>r>>c;
        insert(l,r,c);
    }
    for(int i=1;i<=n;i++){
        s[i]=s[i-1]+a[i];//求前缀和
        cout<<s[i]<<" ";
    }
    return 0;
}

二维差分

二维差分和一维差分思路一致,我们同样用insert操作了简化代码
我们画个图来加深一下对差分操作的理解。
在这里插入图片描述

例题:AcWing798
代码实现:

#include<iostream>
using namespace std;
int n,m,q,c;
const int N=1010;
int a[N][N],s[N][N];
void insert(int x1,int y1,int x2,int y2,int c){//差分操作
    a[x1][y1]+=c;
    a[x2+1][y1]-=c;
    a[x1][y2+1]-=c;
    a[x2+1][y2+1]+=c;
}
int main(){
    int tmp;
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>tmp;
            insert(i,j,i,j,tmp);//差分处理
        }
    }
    while(q--){
        int x1,x2,y1,y2;
        cin>>x1>>y1>>x2>>y2>>c;
        insert(x1,y1,x2,y2,c);
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            s[i][j]=s[i-1][j]+s[i][j-1]+a[i][j]-s[i-1][j-1];//求前缀和
            cout<<s[i][j]<<" ";
        }
        cout<<endl;
    }
    return 0;
}

双指针算法

即一前一后两个指针,该方法简单易懂,使用起来有时候还是比较巧妙地。
直接用例题来理解。
例题:AcWing799
代码实现:

#include<iostream>
using namespace std;
int n;
const int N =1e5+10;
int a[N];
int cnt[N];
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)   cin>>a[i];
    int ans=0;
    for(int str=1,ed=1;str<=n;str++){//一前一后两个指针,维护整个区间
        cnt[a[str]]++;//前指针后移,区间内a[str]对应数的数量++
        while(cnt[a[str]]>1)   cnt[a[ed]]--,ed++;//如果此数数量超1,让后指针前移,并让后指针指向数的数量--,直至此数数量为1
        ans=max(ans,str-ed+1);//动态更新区间长度
    }
    cout<<ans;
    return 0;
}

例题:AcWing802
代码实现:

#include<iostream>
using namespace std;
int n,m,x;
const int N=1e5+10;
int a[N],b[N];
int main(){
    cin>>n>>m>>x;
    for(int i=1;i<=n;i++)   cin>>a[i];
    for(int i=1;i<=m;i++)   cin>>b[i];
    int i=n,j=1;//由于两个序列都是单调的,我们让一个指针从一个序列最后一个数开始,另一个指针从另一个序列第一个数开始
    while(1){
        if(a[i]+b[j]>x) i--;//超过目标,让i指针--
        else if(a[i]+b[j]<x)    j++;//小于目标,让j指针++
        else break;
    }
    cout<<i-1<<" "<<j-1;
    return 0;
}

例题:AcWing2816
代码实现:

//两个序列都是单调的,使用双指针再爽不过
#include<iostream>
using namespace std;
int n,m;
const int N=1e5+10;
int a[N],b[N];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)   cin>>a[i];
    for(int j=1;j<=m;j++)   cin>>b[j];
    int i=1,j=1;
    while(i<=n&&j<=m){
        if(a[i]==b[j])  i++;//如果两数相等,i后移
        j++;//否则只有j后移
    }
    if(i==n+1)  puts("Yes");//如果i移动到了n+1,说明b数组中包含a中的前n个数
    else
        puts("No");
    return 0;
}

位运算

位运算也是很简单的,只要见过了后面再用就easy了
例题:AcWing801
代码实现:

#include<iostream>
using namespace std;
int n;
int lowbit(int x){//这个方法很巧妙,假设一个数的二进制为1110110,那么它将返回10,我们让原数减去这个值,再调用此函数,则返回100,这样知道此数为0,函数调用次数就是二进制中1的个数
    return x&(-x);//为什么这个计算式有这种效果呢,这和源码、反码、补码之间的换算规则有关,x的源码符号位取反后就是-x的源码,假设x为正数,则-x的补码为源码符号位不变,其它位按位取反,再加上1,这样,两数&操作后就只剩最低的一位1和后面的0了
}
int main(){
    cin>>n;
    while(n--){
        int tmp;
        cin>>tmp;
        int ans=0;
        while(tmp){
            tmp-=lowbit(tmp);
            ans++;
        }
        cout<<ans<<" ";
    }
    return 0;
}

另外,在状态压缩dp中我们经常会遇到求一个数的二进制位中有没有两个相邻的1的问题,我们只需求一下x&(x>1)就行了,如果答案为0,则说明没有两个相邻的1,否则有。

离散化

我们之前讲过前缀和操作,那万一它给我们的区间非常分散,从1~10的9次方不等,那我们咋办呢?这是我们可以用到离散化思想。
例题:AcWing802
代码实现:

//其实用map也行,这里只是学习一下这个思想
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int n,m;
vector<pair<int,int>> add,query;
vector<int> gap;
const int N=3e5+10;//最多有这么多分界点
int s[N],a[N];
//我们用一个gap的vector存储我们所有要用到的区间点,并从大到小分别用1~n来代指它们
int find(int x){
    int l=0,r=gap.size()-1;
    while(l<r){
        int mid=l+r>>1;
        if(gap[mid]>=x) r=mid;
        else l=mid+1;
    }
    return l+1;
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int x,c;
        cin>>x>>c;
        gap.push_back(x);//存入需要分的区间的分界点
        add.push_back({x,c});//先把需要的操作存下来
    }
    for(int i=1;i<=m;i++){
        int l,r;
        cin>>l>>r;
        gap.push_back(l);//存入分界点
        gap.push_back(r);
        query.push_back({l,r});//存入查询操作
    }
    sort(gap.begin(),gap.end());//从小到大排序
    gap.erase(unique(gap.begin(),gap.end()),gap.end());//去重
    for(auto tmp:add){//遍历add,执行操作
        int x=find(tmp.first);//二分查找当前分界点的对应下标
        a[x]+=tmp.second;
    }
    for(int i=1;i<=gap.size();i++)  s[i]=s[i-1]+a[i];//求前缀和
    for(auto tmp:query){
        int x=find(tmp.first),y=find(tmp.second);//找到查询的区间的对应下标
        cout<<s[y]-s[x-1]<<endl;
    }
    return 0;
}

区间合并

这个具体的用途我还不太清楚,反正好像2019美团笔试考了一个二维的
看题目一维的描述吧:AcWing803
代码实现:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
void merge(vector<PII> &segs){
    vector<PII> res;//pair默认按第一个数为第一关键字,第二个数为第二关键字排序
    sort(segs.begin(), segs.end());//按开始点从小到大排序
    int st = -2e9, ed = -2e9;//初始化为一个很小的值,维护合并后最后一个区间的左右端点
    for (auto seg : segs)//遍历所有区间
        if (ed < seg.first){//如果开头大于上一个结尾
            if (st != -2e9) res.push_back({st, ed});//如果不是初始值,直接加入答案(相当于一旦要更新区间,就把上一个区间加入答案)
            st = seg.first, ed = seg.second;//更新新的str和ed
        }
        else ed = max(ed, seg.second);//否则就只更新结尾
    if (st != -2e9) res.push_back({st, ed});//将最后一个区间加入,要特判此时st是否是初始值,因为可能有空集情况
    segs = res;
}
int main(){
    int n;
    scanf("%d", &n);
    vector<PII> segs;
    for (int i = 0; i < n; i ++ ){//先读入所有区间
        int l, r;
        scanf("%d%d", &l, &r);
        segs.push_back({l, r});
    }
    merge(segs);
    cout << segs.size() << endl;//最后vector的大小就是答案
    return 0;
}

美团笔试题链接:AcWing759
代码实现:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
struct pos{
    int str,ed,x;//开始位置、结束位置与行号/列号
};
int n;
long long res;
vector<pos> row,col;//记录每一行、每一列的情况
bool cmp(pos& p1,pos& p2){
    if(p1.x!=p2.x)  return p1.x<p2.x;
    else return p1.str<p2.str;
}
void merge(vector<pos>& v){
    sort(v.begin(),v.end(),cmp);//按行号排序
    vector<pos> ans;
    int str=-2e9,ed=-2e9,k=-2e9;
    for(auto tmp:v){//遍历每个区间
        if(tmp.x==k){//是当前行
            if(ed<tmp.str){//按上一题的思路(这里一定不是第一个区间了,第一个区间必定去else了)
                ans.push_back({str,ed,k});
                res+=ed-str+1;
                str=tmp.str,ed=tmp.ed;
            }else{
                ed=max(ed,tmp.ed);
            }
        }else{//不是当前行
            if(str!=-2e9){//如果不是第一个区间,就加入答案,同样相当于每次到一个新的区间或行/列,就把上一个加入答案
                ans.push_back({str,ed,k});
                res+=ed-str+1;//并更新res
            }   
            k=tmp.x,str=tmp.str,ed=tmp.ed;//k赋值为当前行/列,并更新起始、终止位置
        }
    }
    if(str!=-2e9){//更新最后剩余的区间
        ans.push_back({str,ed,k});
        res+=ed-str+1;
    }   
    v=ans;//把col这个vector更新为ans,这样判交叉时不会多个区间重判(因为我们计算答案时就是按合并后的区间计算的,因此同一列/行内没有重复)
}
int main(){
    scanf("%d",&n);
    while(n--){
        int x1,x2,y1,y2;
        scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
        if(x1==x2)  row.push_back(pos{min(y1,y2),max(y1,y2),x1});//如果是行
        else col.push_back({min(x1,x2),max(x1,x2),y1});//如果是列
    }
    merge(row),merge(col);//合并每行每列,首先保证单独的一行一列中每个格子只算一次,最后再处理横竖交叉的情况
    for(auto c:col)//处理交叉情况
        for(auto r:row)
            if(r.x>=c.str&&r.x<=c.ed&&r.str<=c.x&&r.ed>=c.x)    res--;
    printf("%lld",res);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_bxzzy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值