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

前言

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

提高熟练度的方法:多敲几遍,大约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)} rl>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. 找到分界点xq[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[i1][j]+S[i][j1]S[i1][j1]+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][y11]S[x11][y2]+S[x11][y11]

代码:

#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 差分

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

然后我们构造一个数组bb[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循环lr区间,时间复杂度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(红色部分),但我们只要求lr区间加上c, 因此还需要执行 b[r+1] - c,让a数组中a[r+1]及往后的区间再减去c(绿色部分),这样对于a[r] 以后区间的数相当于没有发生改变。

因此我们得出一维差分结论:给a数组中的[ l, r]区间中的每一个数都加上c,只需对差分数组bb[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]重复则右移js[a[j]]要减1) -> 确定j及更新当前长度i - j + 1r

代码:

//朴素做法:
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 数组元素的目标和

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

代码:

#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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

For 丶I Forever

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

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

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

打赏作者

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

抵扣说明:

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

余额充值