【算法系列】二分、排序、高精度、前缀和、双指针、差分

排序和高精度算法总结 用时一周多,再接再厉 阅读详情

前言

本博客从常见的算法模板出发,对常见算法进行归类并给出相应的模板,希望对您能有所帮助。

提高熟练度的方法:多敲几遍,大约3~5遍。

一、二分模板

整数二分的一般步骤:

  1. 确定一个区间[L,R],使得目标值在该区间中
  2. 找一个判断条件,使得该判断条件:①具有二段性 ②答案是二段性的分界点。(二段性:前半段满足、后半段不满足)

第一类:ans是红色区间的右端点
在这里插入图片描述

while(l<r)
{
	int mid=(l+r+1)/2;
	if(m红色) l=m;
	else r=mid-1;
}

第二类:ans是绿色区间的左端点

while(l<r)
{
   int mid=(l+r)/2;
   if(m绿色) r=m;
   else l=mid+1;
}
  1. 分析中点M在该判断条件下是否成立,如果成立,考虑答案在哪个区间;如果不成立,考虑答案在哪个区间
  2. 更新方式写的是 R = M i d R=Mid R=Mid,则不用做任何处理;如果更新方式写的是 L = M i d L=Mid L=Mid,则需要在计算 M i d Mid Mid时加上1。

模板一:

int l=0,r=n-1;
while(l<r)
{
	int mid=(l+r)/2;
	if(q[mid]>=k) r=mid;
	else l=mid+1;
}
if(q[l]==k) cout << l << endl;

模板二:

int l=0,r=n-1;
while(l<r)
{
	int mid=(l+r+1)/2;
	if(q[mid]<=k) l=mid;
	else r=mid-1;
}
if(q[l]==k) cout << l << endl;

1.1 整数范围(整数二分)

在这里插入图片描述
求解代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 100010;
int q[N];
int n,m;

int main()
{
    cin >> n >> m;
    for(int i=0;i<n;i++) cin >> q[i];
    
    while (m -- )
    {
        int k;
        cin >> k;
        int l=0,r=n-1;
        while(l<r)
        {
            int mid=(l+r)/2;
            if(q[mid]>=k) r=mid;
            else l=mid+1;
        }
        if(q[l]==k)
        {
            cout << l << ' ';
            r=n-1;
            while(l<r)
            {
                int mid=l+r+1>>1;
                if(q[mid]<=k) l=mid;
                else r=mid-1;
            }
            if(q[l]==k) cout << r << endl;
        }
        else cout << "-1 -1" << endl;
    }
    return 0;
}

1.2 数的三次方根(浮点数二分)

在这里插入图片描述
如果答案保留$k$位小数,那么二分时区间长度只需满足: r − l > 1 0 − ( k + 2 ) r-l>10^{-\left( k+2 \right)} r−l>10−(k+2)

求解代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int main()
{
    double n;
    cin >> n;
    double l=-100,r=100;
    while(r-l>1e-8)
    {
        double mid=(l+r)/2;
        if(mid*mid*mid>=n) r=mid;
        else l=mid;
    }
    printf("%.6f",l);
}

1.3 机器人跳跃问题

在这里插入图片描述
check函数:找到能满足check函数最小的e,因此,满足check时,另r=mid。

求解代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 100010;
int H[N],n;

bool check(int e)
{
    for(int i=1;i<=n;i++)
    {
        e=2*e-H[i];
        if(e<0) return false;
        if(e>1e5) return true;
    }
    return true;
}

int main()
{
    cin >> n;
    for(int i=1;i<=n;i++) cin >> H[i];
    
    int l=0,r=1e5;
    while(l<r)
    {
        int mid=l+r>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    }
    cout << l << endl;
}

1.4 剪绳子

在这里插入图片描述
最优化问题转通过增加一个条件换为判定性问题

假设如果我们知道绳子需要切成多长例如为x,然后对每段绳子除以x,累加总和后看是否大于等于需要切成的数量。

如何求x,那我们就可以通过对区间分割的方式也就是二分的方式来进行确定。

判断能总共剪出长度为 mid 的绳子多少根:总共能剪出的根数=每一根绳子能剪出的根数之和。

如果能剪出长度为 m i d mid mid的绳子,则 l = m i d l=mid l=mid,如果不能,则 r = m i d r=mid r=mid。关键:如果mid成立,任何小于等于mid的值一定成立。

求解代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 100010;
int L[N];
int n,m;

bool check(double x)
{
    int res=0;
    for(int i=0;i<n;i++)
    {
        res+=L[i]/x;
    }
    return res>=m;
}

int main()
{
    cin >> n >> m;
    for(int i=0;i<n;i++) cin >> L[i];
    
    double l=0,r=1e9;
    while(r-l>1e-4)
    {
        double mid=(l+r)/2;
        if(check(mid)) l=mid;
        else r=mid;
    }
    printf("%.2lf",l);
}

1.5 分巧克力

在这里插入图片描述
分析思路同上题:剪绳子

求解代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 100010;
int h[N],w[N];
int n,m;

bool check(int x)
{
    int res=0;
    for(int i=0;i<n;i++)
        res+=(h[i]/x)*(w[i]/x);
    return res>=m;
}

int main()
{
    cin >> n >> m;
    for(int i=0;i<n;i++) cin >> h[i] >> w[i];
    
    int l=0,r=1e5;
    while(l<r)
    {
        int mid=(l+r+1)/2;
        if(check(mid)) l=mid;
        else r=mid-1;
    }
    cout << l << endl;
}

1.6 四平方和

在这里插入图片描述

求解代码:


1.7 最佳牛围栏

在这里插入图片描述
在这里插入图片描述
算法思想:

求解代码:


二、排序模板

2.1 快速排序

2.1.1 快速排序

在这里插入图片描述
算法思想:分治法求解

  1. 确定分界点,直接取左边界q[l],取中间值q[(l+r)/2],取右边界q[r],随机取一个数。
  2. 调整区间,将区间一分为二。保证左边区间里面所有的数都≤x,右边区间里面所有的数都≥x。重点
  3. 递归处理左右两段,将两段拼接在一块。

在这里插入图片描述代码求解:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 100010;
int n;
int q[N];

void quick_sort(int q[],int l,int r)
{
    if(l>=r) return; //递归的终止情况。判断边界,区间中没有数或者只有一个数,不用排序
    //第一步:分解子问题
    int x=q[(l+r)/2],i=l-1,j=r+1;//分界点x,左右两个指针
    while(i<j)
    {
        do i++;while(q[i]<x);//保证左边的数小于x
        do j--;while(q[j]>x);//保证右边的数大于x
        if(i<j) swap(q[i],q[j]);//两个指针还没有相遇
    }
    //第二步:递归处理子问题
    quick_sort(q,l,j);//递归处理左右两边
    quick_sort(q,j+1,r);
    //第三步:子问题合并,快排这一步不需要,但归并排序的核心在这一步
}

int main()
{
    cin >> n;
    for(int i=0;i<n;i++) cin >> q[i];
    quick_sort(q,0,n-1);
    for(int i=0;i<n;i++) cout << q[i] <<' ';
    return 0;
}

2.1.2 第k个数

在这里插入图片描述
算法思想:
将k值当做物理地址的值,比如第5个数其实就是数组4的位置,第2个数就是数组1的位置

每次只需要判断k在左区间还是右区间,一直递归查找k所在区间
最后只剩一个数时,只会有数组[k]一个数,返回数组[k]的值就是答案

  1. 找到分界点x,q[L],q[(L+R)/2],q[R]
  2. 左边所有数≤x,右边所有数≥x
  3. 递归排序Left,递归排序Right
    k≤S[L],S[L]为左边≤x的数的个数,此时递归Left
    k>S[L],此时递归Right,第k-S[L]个小的数

代码求解:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
int q[N];
int n,k;

int quick_sort(int l,int r,int k)
{
    if(l>=r) return q[l];
    
    int x=q[(l+r)/2],i=l-1,j=r+1;
    while(i<j)
    {
        do i++;while(q[i]<x);
        do j--;while(q[j]>x);
        if(i<j) swap(q[i],q[j]);
    }
    int sl=j-l+1;
    if(k<=sl) return quick_sort(l,j,k);
    return quick_sort(j+1,r,k-sl);
}

int main()
{
    cin >> n >> k;
    
    for(int i=0;i<n;i++) cin >> q[i];
    cout << quick_sort(0,n-1,k);
    return 0;
}

2.2 归并排序

2.2.1 归并排序

在这里插入图片描述
算法思想:分治

  1. 确定分界点mid:mid=(l+r)/2
  2. 递归排序左边q[l…mid]和右边q[mid+1…r]
  3. 归并:合二为一。步骤:①主体合并:至少有一个小数组添加到tmp数组中,②收尾:可能存在的剩下的一个小数组的尾部直接添加到 tmp 数组中,③复制回来:tmp 数组覆盖原数组。

时间复杂度: O ( n l o n g n ) O(nlong_n) O(nlongn​)

求解代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
int n;
int q[N],tmp[N];

void merge_sort(int q[],int l,int r)
{
    if(l>=r) return;
    int mid=(l+r)/2;
    merge_sort(q,l,mid),merge_sort(q,mid+1,r);
    
    int k=0,i=l,j=mid+1;
    while(i<=mid && j<=r)
    {
        if(q[i]<=q[j]) tmp[k++]=q[i++];
        else tmp[k++]=q[j++];
    }
    while(i<=mid) tmp[k++]=q[i++];
    while(j<=r) tmp[k++]=q[j++];
    
    for(int i=l,j=0;i<=r;i++,j++) q[i]=tmp[j];
}

int main()
{
    cin >> n;
    for(int i=0;i<n;i++) cin >> q[i];
    merge_sort(q,0,n-1);
    for(int i=0;i<n;i++) cout << q[i] << ' ';
    return 0;
}

2.2.2 逆序对数量

在这里插入图片描述
算法思想:

  1. 左半边内部的逆序对数量:merge_sort(L,mid)
  2. 右半边内部的逆序对数量:merge_sort(mid+1,R)
  3. S[j]=mid-i+1

求解代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
int n;
int q[N],tmp[N];

long long merge_sort(int l,int r)
{
    if(l>=r) return 0;
    
    int mid=(l+r)/2;
    long long res=merge_sort(l,mid)+merge_sort(mid+1,r);
    
    int k=0,i=l,j=mid+1;
    while(i<=mid && j<=r)
    {
        if(q[i]<=q[j]) tmp[k++]=q[i++];
        else 
        {
            tmp[k++]=q[j++];
            res+=mid-i+1;
        }
    }
    while(i<=mid) tmp[k++]=q[i++];
    while(j<=r) tmp[k++]=q[j++];
    
    for(int i=l,j=0;i<=r;i++,j++) q[i]=tmp[j];
    
    return res;
}

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

三、高精度

vector相关内容:
头文件

#include<vector>

vector<int> A;
A.push_back(1);

3.1 高精度加法(两个大整数相加)

在这里插入图片描述

A+B 位数 1 0 6 10^6 106
算法思想:

  1. 大整数存储。将每一位存到数组中,考虑到进位,让低位在前,第0位存个位。
  2. 大整数运算。模拟人工加法,Ai+Bi+t(进位)

不压位做法:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;
const int N = 100010;

vector<int> add(vector<int> &A,vector<int> &B)
{
    vector<int> C;
    int t=0;
    for(int i=0;i<A.size()||i<B.size();i++)
    {
        if(i<A.size()) t+=A[i];
        if(i<B.size()) t+=B[i];
        C.push_back(t%10);
        t/=10;
    }
    if(t) C.push_back(1);
    return C;
}

int main()
{
    string a,b;
    vector<int> A,B;
    
    cin >> a >> b;
    for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
    for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0');
    
    auto C=add(A,B);
    for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
}

压位做法:压9位

#include <iostream>
#include <vector>

using namespace std;

const int base = 1000000000;

vector<int> add(vector<int> &A, vector<int> &B)
{
    if (A.size() < B.size()) return add(B, A);

    vector<int> C;
    int t = 0;
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % base);
        t /= base;
    }

    if (t) C.push_back(t);
    return C;
}

int main()
{
    string a, b;
    vector<int> A, B;
    cin >> a >> b;

    for (int i = a.size() - 1, s = 0, j = 0, t = 1; i >= 0; i -- )
    {
        s += (a[i] - '0') * t;
        j ++, t *= 10;
        if (j == 9 || i == 0)
        {
            A.push_back(s);
            s = j = 0;
            t = 1;
        }
    }
    for (int i = b.size() - 1, s = 0, j = 0, t = 1; i >= 0; i -- )
    {
        s += (b[i] - '0') * t;
        j ++, t *= 10;
        if (j == 9 || i == 0)
        {
            B.push_back(s);
            s = j = 0;
            t = 1;
        }
    }

    auto C = add(A, B);

    cout << C.back();
    for (int i = C.size() - 2; i >= 0; i -- ) printf("%09d", C[i]);
    cout << endl;

    return 0;
}

3.2 高精度减法(两个大整数相减)

在这里插入图片描述

A-B 位数 1 0 6 10^6 106

算法思想:

  1. 判断,若A≥B,直接算A-B
  2. 若A<B,算-(B-A),保证大数减去小数

求解代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 1000010;

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;
}

vector<int> sub(vector<int> &A,vector<int> &B)
{
    vector<int> C;
    int t=0;//t表示进位
    for(int i=0;i<A.size();i++)
    {
        t=A[i]-t;
        if(i<B.size()) t=t-B[i];//判断B是否存在,B的位数比A的位数少
        C.push_back((t+10)%10);//t≥0 或者 t<0
        if(t<0) t=1;//借位
        else t=0;
    }
    while(C.size()>1 && C.back()==0) C.pop_back();//去掉前导0
    return C;
}

int main()
{
    string a,b;
    vector<int> A,B;
    
    cin >> a >> b;
    for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
    for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0');
    
    if(cmp(A,B))
    {
        auto C=sub(A,B);
        
        for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
    }
    else
    {
        auto C=sub(B,A);
        printf("-");
        for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
    }
    return 0;
}

3.3 高精度乘法

A*a l e n ( A ) ≤ 1 0 6 len(A)≤10^6 len(A)≤106 a≤10000
算法思想:

在这里插入图片描述
求解代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

vector<int> mul(vector<int> &A,int &b)
{
    vector<int> C;
    int t=0;
    for(int i=0;i<A.size() || t;i++)
    {
        if(i<A.size()) t+=A[i]*b;
        C.push_back(t%10);
        t/=10;
    }
    while (C.size() > 1 && C.back() == 0) C.pop_back();
    
    return C;
}

int main()
{
    string a;
    int b;
    cin >> a >> b;
    vector<int> A;
    for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
    
    auto C=mul(A,b);
    
    for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
    return 0;
}

3.4 高精度除法

在这里插入图片描述
A/a
算法思想:
除法从最高位开始算

求解代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

vector<int> div(vector<int> A,int b,int &r)//r引用类型
{
    vector<int> C;//尚
    r=0;
    for(int i=A.size()-1;i>=0;i--)//要从高位开始进行运算
    {
        r=r*10+A[i];
        C.push_back(r/b);
        r%=b;
    }
    reverse(C.begin(),C.end());//C[0]存储的是低位,要将C反转
    
    while(C.size()>1 && C.back()==0) C.pop_back();//除去前导0
    
    return C;
}

int main()
{
    string a;
    int b;
    vector<int> A;
    cin >> a >> b;
    
    for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
    
    int r;
    auto C=div(A,b,r);
    
    for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
    cout << endl << r << endl;
    return 0;
}

四、前缀和模板

4.1 前缀和思想

算法思想:

s [ i ] = a [ 1 ] + a [ 2 ] + . . . + a [ n ] s[i]=a[1]+a[2]+...+a[n] s[i]=a[1]+a[2]+...+a[n]

  • 如何求 S [ i ] S[i] S[i]?s[i]=s[i-1]+a[i],s[0]=0
  • 作用:快速求出原数组中一段数的和
  • 例如:[l,r]一段区间中的和?s[r]-s[l-1]

4.2 一维前缀和

在这里插入图片描述

代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 100010;
int n,m;
int a[N],s[N];

int main()
{
    cin >> n >> m;
    for(int i=1;i<=n;i++) cin >> a[i];
    
    for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
    
    while (m -- )
    {
        int l,r;
        cin >> l >> r;
        cout << s[r]-s[l-1] << endl;
    }
    return 0;
}

4.3 二维前缀和——子矩阵的和

在这里插入图片描述
在这里插入图片描述
思想:

  • S[i][j]如何求?
    S [ i ] [ j ] = S [ i − 1 ] [ j ] + S [ i ] [ j − 1 ] − S [ i − 1 ] [ j − 1 ] + a [ i ] [ j ] S[i][j]=S[i-1][j]+S[i][j-1]-S[i-1][j-1]+a[i][j] S[i][j]=S[i−1][j]+S[i][j−1]−S[i−1][j−1]+a[i][j]
  • ( x 1 , y 1 ) (x_1,y_1) (x1​,y1​), ( x 2 , y 2 ) (x_2,y_2) (x2​,y2​)这一子矩阵中的所有数之和为:
    S [ x 2 ] [ y 2 ] − S [ x 2 ] [ y 1 − 1 ] − S [ x 1 − 1 ] [ y 2 ] + S [ x 1 − 1 ] [ y 1 − 1 ] S[x_2][y_2]-S[x_2][y_1-1]-S[x_1-1][y_2]+S[x_1-1][y_1-1] S[x2​][y2​]−S[x2​][y1​−1]−S[x1​−1][y2​]+S[x1​−1][y1​−1]

代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;
int a[N][N],s[N][N];
int n,m,q;

int main()
{
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            cin >> a[i][j];
        }
    }
    
    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]-s[i-1][j-1]+a[i][j];//求前缀和
        }
    }
    
    while(q--)
    {
        int x1,y1,x2,y2;
        cin>>x1>>y1>>x2>>y2;
        printf("%d\n",s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]);//求子矩阵的部分和
    }
}

五、差分模板

5.1 差分

类似于数学中的求导和积分,差分可以看成前缀和的逆运算。
差分数组:
首先给定一个原数组a:a[1], a[2], a[3],,,,,, a[n];

然后我们构造一个数组b :b[1] ,b[2] , b[3],,,,,, b[i];

使得a[i] = b[1] + b[2 ]+ b[3] +,,,,,, + b[i]

也就是说,a数组是b数组的前缀和数组,反过来我们把b数组叫做a数组的差分数组。换句话说,每一个a[i]都是b数组中从头开始的一段区间和。

考虑如何构造差分b数组?

最为直接的方法
如下:
a[0 ]= 0;

b[1] = a[1] - a[0];

b[2] = a[2] - a[1];

b[3] =a [3] - a[2];

........

b[n] = a[n] - a[n-1];

图示:
在这里插入图片描述
我们只要有b数组,通过前缀和运算,就可以在O(n) 的时间内得到a数组 。

话说有这么一个问题:

给定区间[l ,r ],让我们把a数组中的[ l, r]区间中的每一个数都加上c,即 a[l] + c , a[l+1] + c , a[l+2] + c ,,,,,, a[r] + c;

暴力做法是for循环l到r区间,时间复杂度O(n),如果我们需要对原数组执行m次这样的操作,时间复杂度就会变成O(n*m)。有没有更高效的做法吗? 考虑差分做法。

始终要记得,a数组是b数组的前缀和数组,比如对b数组的b[i]的修改,会影响到a数组中从a[i]及往后的每一个数。

首先让差分b数组中的 b[l] + c ,a数组变成 a[l] + c ,a[l+1] + c,,,,,, a[n] + c;

然后我们打个补丁,b[r+1] - c, a数组变成 a[r+1] - c,a[r+2] - c,,,,,,,a[n] - c;

为啥还要打个补丁?

我们画个图理解一下这个公式的由来:
在这里插入图片描述
b[l] + c,效果使得a数组中 a[l]及以后的数都加上了c(红色部分),但我们只要求l到r区间加上c, 因此还需要执行 b[r+1] - c,让a数组中a[r+1]及往后的区间再减去c(绿色部分),这样对于a[r] 以后区间的数相当于没有发生改变。

因此我们得出一维差分结论:给a数组中的[ l, r]区间中的每一个数都加上c,只需对差分数组b做 b[l] + = c, b[r+1] - = c。时间复杂度为O(1), 大大提高了效率。

总结:
在这里插入图片描述

5.1.1 问题

在这里插入图片描述
核心:

将 a [ l   r ] a[l~r] a[l r]全部加上c,等价于 b [ l ] + = c b[l]+=c b[l]+=c, b [ r + 1 ] − = c b[r+1]-=c b[r+1]−=c

代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 100010;
int a[N],b[N];
int n,m;

void insert(int l,int r,int c)
{
    b[l]+=c;
    b[r+1]-=c;
}

int main()
{
    cin >> n >> m;
    for(int i=1;i<=n;i++) cin >> a[i];//输入数据
    
    for(int i=1;i<=n;i++) insert(i,i,a[i]);//刚开始a[]是有值的,可以看成在全是0的基础上进行了n步的插入操作,在i,i位置上加上了a[i]
    
    while (m -- )//完成操作
    {
        int l,r,c;
        cin >> l >> r >> c;
        insert(l,r,c);
    }
    
    for(int i=1;i<=n;i++) b[i]+=b[i-1];//求原数组,即差分数组b[]前缀和
    
    for(int i=1;i<=n;i++) printf("%d ",b[i]);//输出原数组
    return 0;
}

5.2 二维差分——差分矩阵

如果扩展到二维,我们需要让二维数组被选中的子矩阵中的每个元素的值加上c,是否也可以达到O(1)的时间复杂度。答案是可以的,考虑二维差分。

a[][]数组是b[][]数组的前缀和数组,那么b[][]是a[][]的差分数组

原数组: a[i][j]

我们去构造差分数组: b[i][j]

使得a数组中a[i][j]是b数组左上角(1,1)到右下角(i,j)所包围矩形元素的和。

如何构造b数组呢?

我们去逆向思考。

同一维差分,我们构造二维差分数组目的是为了 让原二维数组a中所选中子矩阵中的每一个元素加上c的操作,可以由O(n*n)的时间复杂度优化成O(1)

已知原数组a中被选中的子矩阵为 以(x1,y1)为左上角,以(x2,y2)为右下角所围成的矩形区域;

始终要记得,a数组是b数组的前缀和数组,比如对b数组的b[i][j]的修改,会影响到a数组中从a[i][j]及往后的每一个数。

假定我们已经构造好了b数组,类比一维差分,我们执行以下操作
来使被选中的子矩阵中的每个元素的值加上c

b[x1][y1] + = c;

b[x1,][y2+1] - = c;

b[x2+1][y1] - = c;

b[x2+1][y2+1] + = c;

每次对b数组执行以上操作,等价于:

for(int i=x1;i<=x2;i++)
  for(int j=y1;j<=y2;j++)
    a[i][j]+=c;

我们画个图去理解一下这个过程:
在这里插入图片描述
b[x1][ y1 ] +=c ; 对应图1 ,让整个a数组中蓝色矩形面积的元素都加上了c。
b[x1,][y2+1]-=c ; 对应图2 ,让整个a数组中绿色矩形面积的元素再减去c,使其内元素不发生改变。
b[x2+1][y1]- =c ; 对应图3 ,让整个a数组中紫色矩形面积的元素再减去c,使其内元素不发生改变。
b[x2+1][y2+1]+=c; 对应图4,让整个a数组中红色矩形面积的元素再加上c,红色内的相当于被减了两次,再加上一次c,才能使其恢复。
在这里插入图片描述
我们将上述操作封装成一个插入函数:

void insert(int x1,int y1,int x2,int y2,int c)
{     //对b数组执行插入操作,等价于对a数组中的(x1,y1)到(x2,y2)之间的元素都加上了c
    b[x1][y1]+=c;
    b[x2+1][y1]-=c;
    b[x1][y2+1]-=c;
    b[x2+1][y2+1]+=c;
}

我们可以先假想a数组为空,那么b数组一开始也为空,但是实际上a数组并不为空,因此我们每次让以(i,j)为左上角到以(i,j)为右下角面积内元素(其实就是一个小方格的面积)去插入 c=a[i][j],等价于原数组a中(i,j) 到(i,j)范围内 加上了 a[i][j] ,因此执行n*m次插入操作,就成功构建了差分b数组.

这叫做曲线救国。

代码如下:

for(int i=1;i<=n;i++)
{
    for(int j=1;j<=m;j++)
    {
        insert(i,j,i,j,a[i][j]);    //构建差分数组
    }
}

在这里插入图片描述

5.2.1 问题

在这里插入图片描述
在这里插入图片描述

代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;
int a[N][N],b[N][N];
int n,m,q;

void insert(int x1,int y1,int x2,int y2,int c)
{
    b[x1][y1]+=c;
    b[x2+1][y1]-=c;
    b[x1][y2+1]-=c;
    b[x2+1][y2+1]+=c;
}

int main()
{
    scanf("%d %d %d",&n,&m,&q);
    for(int i=1;i<=n;i++)//读入数据
        for(int j=1;j<=m;j++)
            cin >> a[i][j];
    
    for(int i=1;i<=n;i++)//刚开始a[]是有值的,可以看成在全是0的基础上进行了n步的插入操作,在(i,j)-(i,j)位置上加上了a[i][j]
        for(int j=1;j<=m;j++)
            insert(i,j,i,j,a[i][j]);
            
    while (q -- )
    {
        int x1,y1,x2,y2,c;
        cin >> x1 >> y1 >> x2 >> y2 >> c;
        insert(x1,y1,x2,y2,c);
    }
    
    for(int i=1;i<=n;i++)//求原数组即b[][]数组的前缀和
        for(int j=1;j<=m;j++)
            b[i][j]+=b[i-1][j]+b[i][j-1]-b[i-1][j-1];
    
    for(int i=1;i<=n;i++)//打印输出b[][]数组
    {
        for(int j=1;j<=m;j++)
        {
            printf("%d ",b[i][j]);
        }
        printf("\n");
    }
    return 0;
}

六、双指针算法

算法模板:

for(int i=0,j=0;i<n;i++)
{
	while(j<i && check(i,j))
		j++;
	//每道题目的具体逻辑
}

作用:

for(int i=0;i<n;i++)
	for(int j=0;j<n;j++)
	O(n^2)	
//将上面的朴素算法优化到O(n)

例子:将字符串中的单词分隔开来

#include <iostream>
#include <cstring>
#include <algorithm>
#include <string.h>

using namespace std;

int main()
{
    char str[1000];
    
    gets(str);
    
    for(int i=0;i<n;i++)
    {
        int j=i;
        while(j<n && str[j]!=' ') j++;
        
        for(int k=i;k<j;k++) cout << str[k];
        cout << endl;
        
        i=j;
    }
    return 0;
}

6.1 最长连续不重复子序列

在这里插入图片描述
算法思想:

  • 遍历数组a中的每一个元素a[i], 对于每一个i,找到j使得双指针[j, i]维护的是以a[i]结尾的最长连续不重复子序列,长度为i - j + 1, 将这一长度与r的较大者更新给r。
  • 对于每一个i,如何确定j的位置:由于[j, i - 1]是前一步得到的最长连续不重复子序列,所以如果[j, i]中有重复元素,一定是a[i],因此右移j直到a[i]不重复为止(由于[j, i - 1]已经是前一步的最优解,此时j只可能右移以剔除重复元素a[i],不可能左移增加元素,因此,j具有“单调性”、本题可用双指针降低复杂度)。
  • 用数组s记录子序列a[j ~ i]中各元素出现次数,遍历过程中对于每一个i有四步操作:cin元素a[i] -> 将a[i]出现次数s[a[i]]加1 -> 若a[i]重复则右移j(s[a[j]]要减1) -> 确定j及更新当前长度i - j + 1给r。

代码:

//朴素做法:
for(int i=0;i<n;i++)
	for(int j=0;j<=i;j++)
		if(check(i,j))
		{
			res=max(res,i-j+1);
		}
//双指针算法:O(n) 单调性
for(int i=0,j=0;i<n;i++)
{
	while(j<=i && check(i,j)) j++;
	
	res=max(res,i-j+1);
}
//i从0到n枚举   j往左最远能到什么地方

AC代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;
int n;
int a[N],s[N];

int main()
{
    cin >> n;
    for(int i=0;i<n;i++) cin >> a[i];
    
    int res=0;
    for(int i=0,j=0;i<n;i++)
    {
        s[a[i]]++;//记录当前数出现的次数为1
        while(s[a[i]]>1)//若当前数出现的次数为2,要剔除当前数
        {
            s[a[j]]--;//将a[j]移出
            j++;
        }
        res=max(res,i-j+1);//每次更新res,求得最大值
    }
    cout << res << endl;
}

6.2 数组元素的目标和

在这里插入图片描述
在这里插入图片描述
算法思想:
先写暴力做法,找i和j之间的单调关系,降低时间复杂度

代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std;
const int N = 100010;
int a[N],b[N];
int n,m,x;

int main()
{
    cin >> n >> m >> x;
    for(int i=0;i<n;i++) cin >> a[i];
    for(int i=0;i<m;i++) cin >> b[i];
    
    for(int i=0,j=m-1;i<n;i++)
    {
        while(j>=0 && a[i]+b[j]>x) j--;
        if(a[i]+b[j]==x) printf("%d %d",i,j);
    }
    return 0;
}

6.3 判断子序列

在这里插入图片描述
在这里插入图片描述
算法思想:

双指针算法

1.j指针用来扫描整个b数组,i指针用来扫描a数组。若发现a[i]==b[j],则让i指针后移一位。
2.整个过程中,j指针不断后移,而i指针只有当匹配成功时才后移一位,若最后若i==n,则说明匹配成功。

为什么双指针做法是正确的?

整个过程中j指针不断扫描b数组并且向后移动,相当于不断给i指针所指向的a数组创建匹配的机会,只有匹配成功时i指针才会向后移动一位,当i==n时,说明全部匹配成功。

在这里插入图片描述

代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 100010;
int n,m;
int a[N],b[N];

int main()
{
    cin >> n >> m;
    for(int i=0;i<n;i++) cin >> a[i];
    for(int i=0;i<m;i++) cin >> b[i];
    
    int i=0,j=0;
    while(i<n&&j<m)
    {
        if(a[i]==b[j]) i++;
        j++;
    }
    
    if(i==n) cout << "Yes" << endl;
    else cout << "No" << endl;
}

6.4 日志统计

在这里插入图片描述
在这里插入图片描述
二元组排序
算法思想:

  • 1.对所有的赞按照时间从小到大排序
  • 2.通过双指针i,j维护长度不大于d的区间,并记录该区间的中所有帖子获得的赞数

代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#define x first
#define y second

using namespace std;

typedef pair<int,int> PII;
const int N = 100010;
PII logs[N];
int n,d,k;
int cnt[N];//用来记录一个id号获得的赞数,表示形式为cnt[id]++;
bool st[N];//用来标记id号,因为id <= 1e5,所以可以利用遍历来输出。

int main()
{
    scanf("%d %d %d",&n,&d,&k);
    for(int i=0;i<n;i++) scanf("%d %d",&logs[i].x,&logs[i].y);
    
    sort(logs,logs+n);//排序时默认以first为准排序
    //双指针算法, i走在前面,j走在后面
    for(int i=0,j=0;i<n;i++)
    {
        int id=logs[i].y;//t表示为i时刻的id号
        cnt[id]++;//在j时刻,id号为t的大佬日记获得了一个赞,给t大佬加一分
        //两个指针跨越的时间超过了d,早期的赞过期了
        while(logs[i].x-logs[j].x>=d)
        {
            cnt[logs[j].y]--;//就是这位大佬,获赞的时间太久远了,赞作废了,哭去吧
            j++;//在logs[j].x时刻的太久远了,往前挪挪。
            //这个循环,直到最后一个赞不过期为止。
        }
        //记录热帖的id号,好知道谁才是大佬
        if(cnt[id]>=k) st[id]=true;
    }
    //遍历一遍id号,展现大佬
    for(int i=0;i<=100000;i++)
        if(st[i])
            printf("%d\n",i);
            
    return 0;
}

七、位运算

7.1 二进制中1的个数

在这里插入图片描述
算法思想:

1.n的二进制表示中第k位是几?n>>k&1
n=15=(1111)2
①先把第k位数字移到最后一位,n>>k
②看个位是几 x&1
2.返回n的最后一位1:lowbit(n)=n& -n

使用lowbit操作,进行,每次lowbit操作截取一个数字最后一个1后面的所有位,每次减去lowbit得到的数字,直到数字减到0,就得到了最终1的个数。

lowbit原理:x&(-x),取反x加1

根据计算机负数表示的特点,如一个数字原码是10001000,他的负数表示形式是补码,就是反码+1,反码是01110111,加一则是01111000,二者按位与得到了1000,就是我们想要的lowbit操作

输出10的二进制各个位:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int main()
{
    int n=10;
    for(int k=3;k>=0;k--) cout << (n>>k&1) << ' ';
    return 0;
}

AC代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int lowbit(int x)
{
    return x&(-x);
}

int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        cin >> x;
        int res=0;
        while(x) x-=lowbit(x),res++;//每次减去x的最后一位1
        
        cout << res << ' ';
    }
}

求反码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int main()
{
    int n=10;
    unsigned int x=-10;
    
    for(int i=31;i>=0;i--) cout << (x>>i&1);
    
    return 0;
}

八、离散化–整数

8.1 区间和

在这里插入图片描述
算法思想:

代码:

九、区间合并

9.1

常用算法代码模板 Java版 (1) :基础算法 常用算法模板 Java版——基础算法:排序、二分、高精度、前缀和与差分、位运算、双指针、离散化、区间合并 阅读详情

相关推荐

AcWing算法基础课——简单算法模板

本篇文章只给出代码模板,以及自己对该模板的理解。如果想看正确的算法思路,可以移步AcWing官网看详情。如有理解错误,欢迎大家批评指正。

Beau_Will的博客 1040

高精度、快速幂与快速排序

高精度、快速幂、快速排序

m0_66115819的博客 1004

AcWing算法学习笔记:基础算法(快速排序 + 归并排序 + 二分 + 高精度 +前缀和差分 + 双指针算法 + 位运算 + 离散化 + 区间和并)

如果a[i] + b[j] > x ,则不断左移j,当j停止移动时,a[i] + b[j] 只有可能等于或小于x ,如果是小于,再右移i,增大a[i]即可。i右移,使用s数组记录选中的数,当选中区间中有重复数字出现时,j右移,当选中区间没有重复数字时j停止移动。A * a (大整数A 和 小整数a相乘,A位数为1e6,a数值为1e9)A ➗ a (大整数A 和 小整数a相乘,A位数为1e6,a数值为1e9)利用某种性质,使数组一分为二,不断缩小区间,最后找到性质的边界范围。

一只可爱的小猴子的博客 2522

二、高精度计算 三、排序算法

ACM中有时遇到精度要求极高或者几十几百位的数,超出了int、double范围,需要高精度来解决。 我认为高精度的根本就是把string转化为数组,值得注意的是数组a[]在存一个大数时,a[1]a[2]…一次存的是个十百千….位,而在string之中,是(n-1),(n-2)….位,也就是吧string到着存入数组之中。 而高精度中的难点,则是小学生就应该熟记于心的逢十进一与像高位借一,在计算

konghhhhh的博客 541

各种排序算法、高精度计算算法

各种常用算法、排序算法、精度计算代码、广度优先搜索模版 等等。

使用podman 容器化安装DIFY

本文阐述了使用podman容器化安装DIFY的过程

wengad的博客 1901

高精度排序[模板]

高精度一向是一个让本蒟蒻头秃的东西,今天居然要写高精度的排序。。。 输入数的个数,以及每个数,求出其中最大数及其编号。 大体思路 通过贪心的办法,每次输入时记录最大数的编号以及最大数的位数,将所有数存入一个二维数组(如果之后不用的话可以不存储),然后每次输入时只要比较位数就可以了,如果位数一样就比较最高位大小,如果还一样就比较次高位大小……依次类推,找出到底谁大。 核心:将...

单精度的梦 1185

从Reset到Read:深入解析Rolling Shutter曝光流水线

本文深入解析Rolling Shutter(卷帘曝光)的工作原理,重点探讨Reset和Read信号在图像传感器中的关键作用。通过Reset-Read流水线机制,详细解释了曝光控制、时序调优及运动伪影补偿等核心技术,帮助读者理解卷帘曝光在高速摄影和动态场景中的应用。

weixin_42531925的博客 254

高精度(加 减 乘 )+排序(快排 归并)

通过字符数组存储所输入的超长数字,再运用循环把字符数组转换成int数组,通过下标把两个数组加到一个全新的数组中,进行进位操作时,在新数组中,先把i下标的数值除以10取出十位数加入i+1下标的值中,i下标的数就%10赋给自己,结束后,通过下标把前导0给取消。注意当0+0时,要进行判断,否则无法输出。

weixin_70600848的博客 435

《从0到1:CTFer成长之路》书籍配套题目-[第二章 web进阶]文件上传

[第二章 web进阶]文件上传开始行动绕过第1个限制绕过第2个限制 介绍:记录一下笔记,方便以后迅速回忆使用。 《从0到1:CTFer成长之路》书籍配套题目,来源网站:《从0到1:CTFer成长之路》 先看一下题目源码: <?php header("Content-Type:text/html; charset=utf-8"); // 每5分钟会清除一次目录下上传的文件 require_once('pclzip.lib.php'); if(!$_FILES){ echo '

weixin_46703850的博客 3411

从零到一:3D Slicer与MONAI在医学影像中的AI革命

本文探讨了3D Slicer与MONAI在医学影像分析中的革命性应用,展示了如何通过开源工具实现高效的三维影像处理和AI增强分析。结合临床案例,详细介绍了从数据准备到AI分析的全流程,显著提升诊断效率和精度,推动医学影像技术迈向新纪元。

tcp8optimizer的博客 342

算法基础_基础算法【高精度 + 前缀和 + 差分 + 双指针】

本篇博客是《算法基础_基础算法》的题型总结+算法练习。主要算法涉及:高精度、前缀和、差分、双指针算法。

序属秋秋秋的博客 1619

跟着团子学SAP CO:基于成本的完工百分比结算:Cost-based POC(以项目为例)

本文以英国及北美会计准则下的基于成本的POC的销售型项目结算对PS模块中的完工百分比进行介绍

Lucas1211的博客 2万+

一、基础算法之排序、二分、高精度、前缀和与差分、双指针算法、位运算、离散化、区间合并内容。

算法思想:选择基准元素,比基准元素小的放左边,比基准元素大的放右边。每趟至少一个元素排好。

qq_52053775的博客 928

基于知识图谱的时空事件关联推理能力培养

随着信息技术的飞速发展,大量的时空事件数据被产生和收集,如交通流量数据、气象观测数据、社交媒体上的地理定位信息等。这些数据蕴含着丰富的知识和规律,但要从中提取有价值的信息并非易事。基于知识图谱的时空事件关联推理旨在通过构建知识图谱,将时空事件数据进行整合和关联,从而实现对事件之间潜在关系的挖掘和推理。本文的目的是系统地介绍基于知识图谱的时空事件关联推理能力的培养方法,包括核心概念、算法原理、数学模型、实际应用等方面。

操作系统内核探秘的博客 750

字节系,ali系,ks,pdd最新抓包方案

各大厂安卓逆向

qq_38759383的博客 1145

常用算法模板——汇总

本篇文章主要包含了算法与数据结构的一些常用模板,可适用于算法小白学习,也适用于有一定基础的同学用来复习!!

m0_66689823的博客 7023

Python爬虫实战:58同城租房信息分析与可视化,寻找高性价比房源

随着城市化进程加快,租房需求日益增长。58同城作为国内领先的本地生活服务平台,汇聚了大量房源信息。本教程将使用最新的Python爬虫技术获取58同城租房数据,通过数据分析与可视化揭示各区域租金水平,并运用机器学习算法找出性价比高的房源。本项目将使用异步爬虫、反爬对抗技术、数据清洗、可视化分析和机器学习建模等技术栈。

2201_76125261的博客 1279

基础算法-快排、归并排、二分、高精度、前缀和、差分、双指针、位运算、离散化、区间合并

应用-二进制中1的个数int n;cin>>n;int x;cin>>x;//基本思想:每次减去二进制最后一位1,看减了几次int cnt=0;return 0;

a2946501058的博客 1095

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

基础算法(排序,二分,高精度,前缀和,差分,双指针,位运算,离散化,区间合并)

排序,二分,高精度,前缀和,差分,双指针,位运算,离散化,区间合并

上烟雨心上尘的博客 1075

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

【无标题】AcWing算法基础课——基础算法

AcWing算法基础课 1.1 快速排序 快速排序属于分治算法,快速排序的算法步骤,用分治的思想 确定分界点,数组内任意一个值(这里建议取中点,即(l+r)/2); 调整区间,让x左边的数都小于x,右边的数都大于x; 递归处理左右两个区间,直到细分到不能细分 对于第二步的调整区间,具体就是弄两个指针,分别指向数组的左右边界;不断的向中间遍历,将遍历过程中左边比x大的数和右边比x小的数交换;直到i,j相遇。 快排模版 void quick_sort(int q[],int l,int r)

hdheid的博客 5362

AcWing算法基础课 第一讲小结(已完结,第二讲已开始)

AcWing的基础算法课--第一讲--基础算法小结,已完结!!! 第二讲开始

weixin_73090892的博客 4579

基础算法(排序,二分,高精度加减乘除,前缀和与差分,离散化,位运算,双指针等)介绍

基础算法(排序,二分,高精度加减乘除,前缀和与差分,离散化,位运算,双指针等)介绍

weixin_61551023的博客 887

基础算法(排序、二分、高精度、前缀与差分、位运算...)

快速排序是由C. A. R. Hoare在1962年提出,对冒泡排序的一种改进,采用了一种分治的策略。通过一趟排序将要排序的数据分割成独立的两部分,其中一部分是所以数据都比另外一部分的数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到数据变成有序序列,平均状况下时间复杂度为O(nlogn)。分成子问题递归处理子问题子问题合并差分是求前缀和的逆操作,类似于数学中的求导和积分,对于原数组a[n],构造出一个数组b[n],使得a[n]为b[n]的前缀和。

icc_hhy的博客 571

【基础算法一】 基础算法盘点,包括排序、二分、高精度、前缀和与差分、双指针算法、位运算、离散化、区间合并等

在本博客中,将盘点几个主要的基础算法,包括排序、二分、高精度、前缀和与差分、双指针算法、位运算、离散化、区间合并。本文将对各个算法详细介绍,并且给出实例,使用C++代码实现。

qq_46013293的博客 359

常用基础算法及例题(快速选择、归并求逆序对个数、二分、高精度、前缀和与差分、双指针、位运算、离散化、区间合并)

常见基础算法总结

CS_boy的博客 1453
上一篇: 【MIT 6.S081】make qemu运行出错,常见问题及解决方法
下一篇: 【MIT6.S081】xv6学习路线、用户程序的编写、系统调用编写
For 丶I  Forever
博客等级 码龄7年 49粉丝 10原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

For 丶I Forever

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

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

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

打赏作者

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

抵扣说明:

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

余额充值