算法基础课:第一讲——基础算法

前言:

写了四天,2W字,终于把第一讲写好了,所有题都重做了一遍,算是温故而知新,有些地方写的仓促,慢慢修改吧
在这里插入图片描述

排序

1.快速排序

作用:

算法思想:

主要思想——分治

步骤 1: 选取中枢 x(可以是任意值)
步骤 2: 将区间分为两部分,左边部分满足所有值 <= x,右边部分满足所有值 >= x
步骤 3: 递归左右部分

模板:

void quick_sort(int q[], int l, int r)
{
    if (l >= r) return ;
    int x = q[(l + r) >> 1], 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]);
    }
    quick_sort(q, l, j);
    quick_sort(q, j + 1, r);
}

例题:

AcWing 786. 第k个数

AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 1e5 + 10;
int n, k;
int q[N];

void quick_sort(int q[], int l, int r)
{
    if (l >= r) return ;
    int x = q[(l + r) >> 1], 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]);
    }
    quick_sort(q, l, j);
    quick_sort(q, j + 1, r);
}

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




2.归并排序

作用:

排序

算法思想:

主要思想——分治

步骤 1: 选取中点 mid
步骤 2: 将区间分为两部分,递归排序左右两部分
步骤 3: 归并——合二为一

模板:

void merge_sort(int q[], int l, int r)
{
    if (l >= r)     return ;
    
    int mid = l + r >> 1;
    
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);
    
    int i = l, j = mid + 1, k = 0;
    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 (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}

例题:

AcWing 788. 逆序对的数量

AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 100005;

typedef long long LL;

int n;
int q[N], tmp[N];

LL merge_sort(int q[], int l, int r)
{
    if (l >= r)     return 0;
    
    int mid = l + r >> 1;
    
    LL res = 0;
    
    res += merge_sort(q, l, mid) + merge_sort(q, mid + 1, r);
    
    int i = l, j = mid + 1, k = 0;
    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 (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(q, 0, n - 1) << endl;
    return 0;
}




二分

1.整数二分

作用:

以O(log n)的时间复杂度进行查找

算法思想:

步骤 1 : 确定一个区间,使得ans在区间内
步骤 2 : 找到一个性质,性质满足:
(1)性质具有二段性,即整个区间一段满足该性质,另一段不满足
(2)ans 为二段性的分界点
在这里插入图片描述

步骤 3 : 确定使用哪个模板

第一类 : 当 ans 是 红色区间的右端点
将[L,R]分成[L, Mid - 1] 和 [Mid, R]:

if (Mid 为红色) 说明 ans 在 [Mid, R]
在这里插入图片描述
else 说明 ans 在[L, Mid - 1]
在这里插入图片描述

第二类 : 当 ans 是 绿色区间的左端点
将[L,R]分成[L, Mid] 和 [Mid + 1, R]

if (Mid 为红色) 说明 ans 在 [Mid + 1, R]
在这里插入图片描述
else 说明 ans 在[L, Mid]
在这里插入图片描述

模板:

bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}

// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

例题:

AcWing 789. 数的范围

AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 1e5 + 5;

int q[N];

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

总结:

  • 写 l = mid 时计算 mid 要 + 1,写 r = mid时计算 mid 不 + 1

2.浮点数二分

算法思想:

步骤 1 : 确定一个区间,使得ans在区间内
步骤 2 : 找到一个性质,性质满足:
(1)性质具有二段性,即整个区间一段满足该性质,另一段不满足
(2)ans 为二段性的分界点
浮点数二分无需考虑区间的划分,由于是稠密的,所以每次都会确定地划分成[L, Mid] [Mid, R],整数二分当区间只剩一个数时停止循环,而浮点数二分当区间长度足够小的时候停止 while 循环

模板:

bool check(double x) {/* ... */} // 检查x是否满足某种性质

double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

例题:

AcWing 790. 数的三次方根

AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 10005;

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("%.6lf\n", l);
    return 0;
}




高精度

1.高精度加法

作用:

解决两个大整数相加的问题

算法思想:

步骤 1 : 倒序存储
用动态数组倒序存储两个大整数,由于有进位的原因,且在vector后面插数相对容易,所以采用倒序存储

原数:1357910
在vector中 为 [ 0,1,9,7,5,3,1]

步骤 2 : 模拟加法
对于A和B的每一位,我们记为A[i],B[i],而 t[i] 为上一位进过来的进位(初始值为0)
所以对于每一位 我们只需记录 C[i] = A[i] + B[i] + t[i],若 C[i] > 10,取 C[i] % 10 存入答案,t[i +1] = 1;若 C[i] < 10,取 C[i] 存入答案, t[i +1] = 0
在这里插入图片描述
代码表示:

// 省略了 ans 变量
if (i < A.size())   t += A[i];
if (i < B.size())   t += B[i];
C.push_back(t % 10);
t /= 10;

模板:

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

注意点:

t 可能A,B遍历完了最后还需要进一位 1,所以要加到循环条件里

例题:

AcWing 791. 高精度加法

AC代码:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

string a, b;
vector<int> A, B;

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

int main( )
{
    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 --)    cout << C[i];
    
    cout << endl;
    return 0;
}

2.高精度减法

作用:

解决两个大整数相减的问题

算法思想:

步骤 1 : 倒序存储
步骤 2 : 模拟减法
类比高精度加法,C[i] = A[i] - B[i] - t[i],这里的 t[i] 取0或1,表示是否被上一位借走了,如果 C[i] < 0,则将 C[i] + 10 存入答案,t[i + 1] = 1 表示向高位借了10过来,若 C[i] > 0 ,t[i + 1] = 0 表示没有向高位借,将 C[i] 存入答案。
请添加图片描述

模板:

// C = A - B    满足 A > B > 0 
vector<int> sub(vector<int> &A, vector<int> &B)
{
    vector<int> C;
    for (int i = 0, t = 0; i < A.size(); i ++)
    {
        t = A[i] - t;
        if (i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10);
        if (t < 0) t = 1;
        else t = 0;
    }
    // 去前导0
    while (C.size() > 1 && C.back() == 0)   C.pop_back();
    return C;
}

例题:

AcWing 792. 高精度减法

AC代码:
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 1e5 + 10;

string a, b;
vector<int> A, B;

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;     // 两数相等
}

// C = A - B    满足 A > B > 0 
vector<int> sub(vector<int> &A, vector<int> &B)
{
    vector<int> C;
    for (int i = 0, t = 0; i < A.size(); i ++)
    {
        t = A[i] - t;
        if (i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10);
        if (t < 0) t = 1;
        else t = 0;
    }
    // 去前导0
    while (C.size() > 1 && C.back() == 0)   C.pop_back();
    return C;
}

int main( )
{
    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 --)    cout << C[i];
    }
    else	// 当 A < B 计算结果为 -(B - A)
    {
        auto C = sub(B, A);
        cout << '-';
        for (int i = C.size() - 1; i >= 0; i --)    cout << C[i];
    }
    cout << endl;
    return 0;
}

3.高精度乘法

作用:

解决大整数乘小整数的问题

算法思想:

步骤 1 : 倒序存储
步骤 2 : 模拟乘法
C[i] = A[i] * b ,若C[i] > 10,则将 C[i] 模上10存入答案,其余为进位 t = C[i] / 10 ,若 C[i] < 10,则直接 将 C[i] 存入答案,t = 0,无进位
在这里插入图片描述

注意点:

可能含有前导0

模板:

vector<int> mul(vector<int> &A, int b)    
{
    vector<int> C;      // 答案
    
    for (int i = 0, t = 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;
}

例题:

AcWing 793. 高精度乘法

AC代码:
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 1e5 + 10;

string a;
int b;
vector<int> A;

vector<int> mul(vector<int> &A, int b)    
{
    vector<int> C;      // 答案
    
    for (int i = 0, t = 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( )
{
    cin >> a >> b;
    // 倒序存储
    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 --)    cout << C[i];
    cout << endl;
    return 0;
}

4.高精度除法

作用:

解决大整数除小整数的问题

算法思想:

步骤 1 : 倒序存储
其实除法是从高位开始除,但是一般高精度题目中,不会光只涉及除法,所以为了考虑对加减乘除的兼容性,我们除法也采用倒序存储,最后对结果进行逆序即可
步骤 2 : 模拟除法
每次用上一位的余数,进行计算,C[i] = r[i] * 10 + a[i] ,将 C[i] / b 的整数部分存入答案,其余为余数 r[i + 1] = C[i] % b,准备进行下一位的除法运算
在这里插入图片描述

注意点:

可能含有前导0

模板:

// A ÷ B = C ...... r 
vector<int> div(vector<int> &A, int b, int &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());
    
    // 去前导0
    while(C.size() > 1 && C.back() == 0)  C.pop_back();
    
    return C;
}

例题:

AcWing 794. 高精度除法

AC代码:
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 1e5 + 10;

string a;
int b;
vector<int> A;

// A ÷ B = C ...... r 
vector<int> div(vector<int> &A, int b, int &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());
    
    // 去前导0
    while(C.size() > 1 && C.back() == 0)  C.pop_back();
    
    return C;
}

int main( )
{
    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 --)    cout << C[i];
    cout << endl << r << endl;;
    return 0;
}




前缀和与差分

1.一维前缀和

作用:

快速求出一个静态数组中某个区间的所有数的和

算法思想:

Eg: 求a数组中[L,R]的元素和
暴力求解: 时间复杂度在O(N),当需要求很多次时,会导致超时

for(int i=L;i<=R;i++)
    ans+=a[i];

步骤 1 : 处理前缀和数组
引进一个数组S,满足 S[n] 等于原数组前n个数值之和,即

S[0]=a[0];
S[1]=a[0]+a[1];
S[2]=a[0]+a[1]+a[2];

S[n]=a[0]+a[1]+a[2]+…+a[n];

步骤 2 : 求区间和
求区间 [ L,R] 的和,即求 a[L] + a[L+1] + a[L+2] + … + a[R]

由 S[R] = a[0]+a[1]+a[2]+…+a[R] 且 S[L-1]=a[0]+a[1]+a[2]+…+a[L-1]
得 a[L] + a[L+1] + a[L+2] + … + a[R] 等于 S[R] - S[L-1]

由于是数组访问,所以每次查询的时间由 O(N) 降到了 O(1)

注意点:

(1)原数组下标从 1 开始
(2)前缀和数组需要预处理 s[0]=0,即原数组前 0 个数的和(即为0)

例题:

AcWing 795. 前缀和

AC代码:
#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N=100010;         //数据范围

int a[N];                   //原数组
int s[N];                   //前缀和数组
int n,m;

int main( )
{
    cin>>n>>m;
    for(int i = 1;i <= n; i ++)
    {
        cin >> a[i];            //输入原数组
        s[i] = s[i-1] + a[i];   //同时处理前缀和数组
    }
    
    int l,r;
    
    while(m --)                  //共m个询问
    {
        cin >> l >> r;
        cout << s[r] - s[l-1] << endl;
    }
    return 0;
}

总结:

前缀和是一个简单的算法,本质上就是用了另一个数组进行了空间换时间的操作(比赛中时间很宝贵,空间往往充裕),一维前缀和只要记住两点:

  • 初始化:s[i]=s[i-1]+a[i] (s[0]=0)
  • 求值:a[l,r]=s[r]-s[l-1]

2.二维前缀和

作用:

快速求出一个静态矩阵中某个区域(子矩阵)所有数的和

算法思想:

Eg:给定一个3X4的矩阵,求出点(2,1)和点(3,4)围成的矩形中的元素之和(红色区域部分)
带尺寸的图片:

暴力解法: 两重for循环求解,时间复杂度高为O(NM)
在一维前缀和数组中,S[i]表示的是原数组前 i 个数的和,那我们应该考虑是否可以仿照一维前缀和的思路来解决二维前缀和问题。

步骤 1 : 处理前缀和矩阵
二维前缀和矩阵中,S[i][j]表示了左上角矩阵的所有数的和,比如:

S[1][1] = a[1][1];
S[2][1] = a[1][1] + a[2][1];
S[2][2] = a[1][1] + a[1][2] + a[2][1] + a[2][2];

所以原矩阵的前缀和矩阵为:


Alt
下面要考虑的就是如何推导出求S[i][j]的公式,可见图中绿色部分为S[i-1][j],红色部分为S[i][j-1],蓝色部分为S[i-1][j-1],而我们要求的S[i][j]就等于 a[i][j]单个元素 + 绿色部分 + 红色部分,由于容斥原理,蓝色部分在加的过程中被计算了两次,所以要减去一个蓝色部分
Alt
所以我们不难推导出

S[i][j] = a[i][j] + S[i-1][j] + S[i][j-1] - S[i-1][j-1]

步骤 2 :求点(x1,y1)与点(x2,y2)构成的矩形中的全部元素之和
Alt
如图所示,蓝色区域 = 红色区域 - 绿色区域 - 紫色区域 + 粉色区域(容斥原理),即

a[x1,y1 ~ x2,y2] = S[x2][y2] - S[x2][y1-1] - S[x1-1][y2] + S[x1-1][y1-1]

时间复杂度由 O(NM) 降到了 O(1)

注意点:

(1)原矩阵下标从 1 开始
(2)前缀和矩阵需要预处理一下 s[0][0~M] = 0,s[0~N][0] = 0 (其中没有元素)

例题:

AcWing 796. 子矩阵的和

AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 1010;

int n,m,q;              //矩阵大小以及询问数
int a[N][N],s[N][N];    //原矩阵、前缀和矩阵

int main( )
{
    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] = a[i][j] + s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];    //处理前缀和矩阵
        }
        
    int x1,y1,x2,y2;
    while (q --)        //q次询问
    {
        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;
}

总结:

与一维前缀和思想相同,二维前缀和也是用空间换时间的想法,只需记住两点:

  • 初始化: S[i][j] = a[i][j] + S[i-1][j] + S[i][j-1] - S[i-1][j-1] (S[0][0~M] = 0,S[0~N][0] = 0)
  • 求值: a[x1,y1 ~ x2,y2] = S[x2][y2] - S[x2][y1-1] - S[x1-1][y2] + S[x1-1][y1-1];

3.一维差分

作用:

一维数组区间 [l, r] 上的数需要被修改很多次时,能避免复杂度O(N)的循环

算法思想:

步骤 1 : 构造差分数组
构造数组 b 使得原数组 a 是 b 的前缀和,即满足 a[i] = b[1] + b[2] + b[3] + b[4] + … + b[i],例如:

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

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

步骤 2 : 区间修改
要使得 a 数组的 [l, r] 区间内每个数 += c,由于

a[l] = b[1] + b[2] + … + b[l] ;
a[l + 1] = b[1] + b[2] + … + b[l] + b[l + 1] ;
a[l + 2] = b[1] + b[2] + … + b[l] + b[l + 1] + b[l + 2];

a[r] = b[1] + b[2] + … + b[l] + b[l + 1] + b[l + 2] + … + b[r];
a[r + 1] = b[1] + b[2] + … + b[l] + b[l + 1] + b[l + 2] + … + b[r] + b[r + 1];

可见,我们只需令 b[l] += c,就可以使 a[l] 往后所有的数都 + c,同时由于我们的区间为[l, r] 所以我们需要将 a[r + 1] 往后的数都不变,即 - c,同样只需将 b[r + 1] -= c 即可,如图粉色为区间的最终变化
在这里插入图片描述
步骤 3 : 求前缀和
a[i] = a[i - 1] + b[i]

模板:

void insert(int l, int r, int c)	// 区间修改
{
	b[l] += c;
	b[r + 1] -= c;
}

注意点:

在构造差分数组时,可以将 a[ ] 看成最初全为 0 的数组,所以 b 数组初始也全为0,对于输入的每个 a[i],调用insert 函数,看成对 [i, i] 这个区间 进行+= a[i] 即可

例题:

AcWing 797. 差分

AC代码:
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100005;

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]);     // 构造差分数组
    
    while (m --)    // m次修改操作
    {
        int l, r, c;
        cin >> l >> r >> c;
        insert(l, r, c);
    }
    
    for (int i = 1; i <= n; i ++)   a[i] = a[i - 1] + b[i];
    
    for (int i = 1; i <= n; i ++)   cout << a[i] << ' ';
    
    return 0;
}

总结:

  • 差分是前缀和的逆运算

4.二维差分

作用:

二维数组区域 [ x1,y1 ~ x2,y2 ] 上的数需要被修改很多次时,能避免复杂度O(NM)的循环

算法思想:

仿照一维差分,我们只需要求得 insert 函数即可,如图,我们需要将(x2,y2)后面的数进行还原,由于容斥原理,粉色区域被减了两次,需要加一次回来
在这里插入图片描述
同样,对于差分数组的构造,我们只需将输入的每个a[i][j] 看作 对(i, j) ~ (i, j) 的区域修改即可

模板:

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

例题:

AcWing 798. 差分矩阵

AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 1005;

int n, m, q;
int a[N][N], b[N][N];

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( )
{
    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 ++)
            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 ++)
        for (int j = 1; j <= m; j ++)
            a[i][j] = b[i][j] + a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];
    
    for (int i = 1; i <= n; i ++)
    {
        for (int j = 1; j <= m; j ++)
            cout << a[i][j] << ' ';
        cout << endl;
    }

    return 0;
}




双指针

作用:

优化复杂度为O(N²)的双重 for 循环

算法思想:

对于一般的朴素做法

for (int i = 0; i < n; i ++)
	for (int j = 0; j < n; j ++)
		if (check(i, j))
		{
			// 具体的问题
		}

可以找到一个性质,使得 j 指针不用每次从 0 开始后移,即

for (int i = 0; i < n; i ++)
{
	while (j 可取的范围 && check(i, j))	j ++;
	
	// 具体的问题
}

例题 1 :

AcWing 799. 最长连续不重复子序列

朴素做法:
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 100005;

int a[N], s[N];

int main( )
{
    int n;
    cin >> n;
    for (int i = 0; i < n; i ++)    cin >> a[i];
    
    int res = 0;
    for (int i = 0; i < n; i ++)
        for (int j = 0; j < i; j ++)
            if (check(i, j))
                res = max(res, i - j + 1);
                
    cout << res << endl;
    return 0;
}
双指针AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 100005;

int a[N], s[N];

int main( )
{
    int n;
    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]] ++;
        while (s[a[i]] > 1)
        {
            s[a[j]] --;
            j ++;
        }
        
        res = max(res, i - j + 1);
    }
    
    cout << res << endl;
    return 0;
}

例题 2 :

AcWing 800. 数组元素的目标和

朴素做法:
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5 + 5;

int a[N],b[N];

int main( )
{
    int n, m, x;
    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; i < n; i ++)
        for (int j = 0; j < m; j ++)
            if (a[i] + b[j] == x)
                cout << i << ' ' << j << endl;
    return 0;
}
双指针AC代码:
// 双指针
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5 + 5;

int a[N],b[N];

int main( )
{
    int n, m, x;
    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 (j >= 0 && a[i] + b[j] == x)
            cout << i << ' ' << j << endl;
        
    }
    return 0;
}

例题 3 :

AcWing 2816. 判断子序列

双指针AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 1e5 + 5;

int a[N], b[N];

int main( )
{
    int n, m;
    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;
    
    
    return 0;
}




位运算

1.返回 n 的二进制表示的第k位

作用:

求 n 的 二进制表示中 第 k 位是多少 (个位是第0位)

算法思想:

步骤 1 : 先把 n 右移 k 位

n >> k;   //右移运算

解释: 例如将 1010 右移一位得 101,右移两位得 10,右移三位就得 1,所以右移k位就是把第 k 位放到个位上来

步骤 2 : 看个位是多少

x & 1;    //x为0,返回 0 & 1 即为0;x为1,返回 1 & 1即为1

步骤 3 : 将两步骤合并

n >> k & 1

模板:

n >> k & 1

例题:

求 n = 10 的二进制表示(即依次输出 n 二进制表示的每一位是多少

AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

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

2.lowbit操作

作用:

返回 x 的最后一位 1和它后面的0,此操作会在 树状数组 中运用

算法思想:

若 x = 1010 lowbit(x) = 10
若 x = 1000100 lowbit(x) = 100
若 x = 101010110 lowbit(x) = 10

模板

x & -x

解释: 若 x = 1010,所以 -x 为原数的补码(取反+1)即 -x = 0110,再对每一位进行 & 运算,运算结果为 x & -x = 0010

例题:

AcWing 801. 二进制中1的个数

AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;

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

int main( )
{
    cin >> n;
    int x;
    while(n --)
    {
        cin >> x;
        int cnt = 0;
        
        while(x)
        {
            x -= lowbit(x);    
            cnt ++;
        }
        
        cout << cnt << ' ';
    }

    return 0;
}





离散化

作用:

值域很大,但稀疏,可以采用映射的方式,将其离散化

算法思想:

采用映射的方式,将原数组上散列的每个数值按顺序映射到从 0 或 1 开始的自然数,如图:
在这里插入图片描述
步骤 1 : a数组中可能有重复元素
先利用 sort 进行排序,再利用 unique函数 去重

步骤 2 : 映射,即如何算出原数组中下标 x 离散化后的值
要找出下标 x 离散化后的值,即找出下标 x 在离散化数组中是第几个数,用二分求解即可

模板:

vector<int> alls;       // 存储所有相关的下标

// 排序去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());

// 二分求下标x在离散化数组中是第几个数
int find(int x)
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
  
    return r + 1;
}

例题:

AcWing 802. 区间和

AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>

using namespace std;

const int N = 300005;

typedef pair<int, int> PII;

int a[N], s[N];         // 原数组、前缀和数组

vector<int> alls;       // 所有相关的下标
vector<PII> add, query;

int find(int x)
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = l + r >> 1;
        
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    
    return r + 1;
}

int main()
{
    int n, m;
    cin >> n >> m;
    while(n --)         // 操作
    {
        int x, c;
        cin >> x >> c;
        add.push_back({x, c});
        alls.push_back(x);
    }
    
    while(m --)         // 询问
    {
        int l, r;
        cin >> l >> r;
        query.push_back({l, r});
        
        alls.push_back(l);
        alls.push_back(r);
        
    }
    
    // 排序去重
    sort(alls.begin(), alls.end());
    alls.erase(unique(alls.begin(), alls.end()), alls.end());
    
    for (auto item : alls)
        cout << item << endl;
    
    for (auto item : add)      // 添加
    {
        int x = find(item.first);
        
        cout << item.first << ' ' << x << endl;
        
        int c = item.second;
        a[x] += c;
    }

    // 处理前缀和
    for (int i = 1; i <= alls.size(); i ++)  s[i] = s[i - 1] + a[i];
    
    for (auto item : query)
    {
        int l = find(item.first);
        int r = find(item.second);
    }
        
    return 0;
}

总结:

  • 离散化离散的是下标







区间合并

作用:

将区间进行合并

算法思想:

类似 贪心 + 模拟 的思想
步骤 1 : 对区间左端点进行排序
pair 的 sort 排序,默认先以第一个关键字,再以第二个关键字进行排序
步骤 2 : 维护一个区间,并扫描之后的区间
我们将起点记为st,终点记为ed,分为四种情况(实际三种)
情况一: 后面区间的st 在 维护区间st 的前面,如图
在这里插入图片描述
由于事先进行了排序,所以扫描的后面区间 st 必然是 <= 当前维护区间的 st,此种情况不存在

情况二: 后面区间的st 在 维护区间st 的后面,ed 在 维护区间 ed 的前面,即维护区间包含了后面区间,如图
请添加图片描述
在这种情况下,无需改变区间,继续向后扫描下一个区间

情况三: 后面区间的st 在 维护区间st 的后面,ed 在 维护区间 ed 的后面,即维护区间与后面区间存在交集,如图
请添加图片描述这种情况下,我们需要求两个区间的并集,即 维护区间的 st 不动,将ed 改为后面区间的ed

情况四: 后面区间的st 在 维护区间ed 的后面,即维护区间与后面区间没有交集,如图
在这里插入图片描述
这种情况下,两区间没有交集,由于我们事先进行了排序,所以从该红色区间往后,所有的区间都不会再与当前维护的区间有交集,将当前维护的蓝色区间加到答案方案里,并将红色区间作为新的维护区间,继续向后扫描

对于情况二,三,我们可以合并考虑,只需要令ed = 蓝色的 ed 和 红色的 ed 中的较大值即可

模板:

void merge(vector<PII> &segs)        // 取地址
{
    vector<PII> res;
    
    sort(segs.begin(), segs.end());
    
    int st = segs[0].first, ed = segs[0].second;    // 首区间
    for (auto item : segs)
        if (ed < item.first)        // 没有交集
        {
            if (st != -2e9) res.push_back({st, ed});
            st = item.first;
            ed = item.second;
        }
        else    ed = max(ed, item.second);      // 有交集,右端点取大值
        
    if (segs.size() != 0)   res.push_back({st, ed});    // 最后一个区间未计数
    
    segs = res;
}

例题:

AcWing 803. 区间合并

AC代码:
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

typedef pair<int, int> PII;

const int N = 100005;

int n;
vector<PII> segs;

void merge(vector<PII> &segs)        // 取地址
{
    vector<PII> res;
    
    sort(segs.begin(), segs.end());
    
    int st = segs[0].first, ed = segs[0].second;    // 首区间
    for (auto item : segs)
        if (ed < item.first)        // 没有交集
        {
            if (st != -2e9) res.push_back({st, ed});
            st = item.first;
            ed = item.second;
        }
        else    ed = max(ed, item.second);      // 有交集,右端点取大值
        
    if (segs.size() != 0)   res.push_back({st, ed});    // 最后一个区间未计数
    
    segs = res;
}

int main( )
{
    cin >> n;
    
    for (int i = 0; i < n; i ++)
    {
        int l, r;
        cin >> l >> r;
        segs.push_back({l, r});
    }
    
    merge(segs);
    
    cout << segs.size() << endl;
    
    return 0;
}

总结:

  • 区间合并函数参数需要引用进行取址,对原数组进行修改
  • 维护的最后一个区间不能漏掉
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值