1. 基础算法(I)

1.1 排序算法

排序算法(英语:Sorting algorithm)是一种将一组特定的数据按某种顺序进行排列的算法。排序算法多种多样,性质也大多不同。我们评价一种排序算法,主要考虑以下 3 个方面:

  • 稳定性:即排序后数组内相同的数的相对顺序是否发生了变化。更为形式化地,若在原本的数组中 a=bab 之前,排序后的数组中 a 仍然在 b 之前,则称该排序算法是稳定的,否则称该排序算法是不稳定的。
  • 时间复杂度:即该排序算法的时间复杂度的上限。基于比较的排序算法的最优时间复杂度是 O(nlogn) 的。
  • 空间复杂度:即该算法所占的空间规模(相对考虑较少)。

下图(图 1-1)展示了各种排序算法的时间复杂度比较:

图1-1 (图源: OI-wiki)

1.1.1 基本排序算法

以下的“排序”皆指将数组从小到大排序,且下标默认从 1 开始。

1.1.1.1 选择排序

选择排序的基本过程:

  1. 遍历整个未排序的数组,找到最小的一个数,将其与数组的首个未排序元素交换。
  2. 重复第 1 步,直至所有数都已经排序。

例如,将数组 a={2,4,3,1} 排序。第 1 次遍历从第 1 个数开始,找到最小的数是 1,将 1 和第 1 个数 2 交换,此时数组变为 {1,4,3,2};第 2 次遍历从第 2 个数开始,找到最小的数是 2,将 2 和第 2 个数 3 交换,此时数组变为 {1,2,4,3}……以此类推,最终得到排序后的 a 数组为 {1,2,3,4}

选择排序是一种不稳定的排序算法,时间复杂度 O(n2)

1.1.1.2 冒泡排序

冒泡排序的基本过程:

  1. 遍历整个数组,若 ai>ai+1,则交换 ai,ai+1
  2. 重复第 1 步,直至 i[1,n),不存在 ai>ai+1

例如,将数组 a={2,4,3,1} 排序。第 1 次遍历,由于 a2>a3,所以交换 a2,a3,得到 {2,3,4,1},此时又有 a3>a4,所以交换 a3,a4,得到 {2,3,1,4};第 2 次遍历,由于 a2>a3,所以交换 a2,a3,得到 {2,1,3,4}……以此类推,最终得到排序后的 a 数组是 {1,2,3,4}

冒泡排序是一种稳定的排序算法,时间复杂度见下表:

最好情况 平均情况 最坏情况
O(n) O(n2) O(n2)

1.1.1.3 插入排序

插入排序的基本过程:

  1. 将所有元素分为“已排序”和“待排序”两组。
  2. 将“待排序”中的第 1 个元素插入至“已排序”中正确的位置。
  3. 重复第 2 步,直到所有元素都位于“已排序”中。

例如,将数组 a={2,4,3,1} 排序。首先“已排序”组中没有数,“未排序”组中的数为 {2,4,3,1}。第 1 次将 2 插入“已排序”组中,此时“已排序”组中的数为 {2},“未排序”组中的数为 {4,3,1};第 2 次将 4 插入“已排序”组中,由于 2<4,所以此时“已排序”组中的数为 {2,4},“未排序”组中的数为 {3.1};第 3 次将 3 插入“已排序”组中,由于 2<3<4,所以此时“已排序”组中的数为 {2,3,4},“未排序”组中的数是 1……以此类推,最终得到排序后的 a 数组是 {1,2,3,4}

插入排序是一种稳定的排序算法,时间复杂度见下表:

最好情况 平均情况 最坏情况
O(n) O(n2) O(n2)

1.1.1.4 其他排序算法

  • 计数排序 稳定,O(n+w)
  • 基数排序 在值域较小时可以使用,稳定,O(kn+i=1kwi)
  • 希尔排序 对插入排序的改进,不稳定,最优 O(n),最优的最坏复杂度为 O(nlog2n)
  • 堆排序 不稳定,O(nlogn)
  • 桶排序 稳定,最优 O(n+n2/k+k)(当 kn 时接近 O(n)),最坏 O(n2)

1.1.2 快速排序

模板AcWing 785. 快速排序

题目:给你一个长度为 n 的数组 a,将这个数组从小到大排序。1n105,1ai109

思路

运用分治的思想。对区间 [l,r] 进行快速排序的步骤如下:

  1. 依据参照值 x 将数列划分为两部分,小于等于 x 的放在左边,大于 x 的放在右边。
  2. 将左右两边分别进行快速排序。
  3. 直接将左右两边拼接在一起。

那么只要解决了第 1 步,我们就完成了快速排序。x 的取值一般为区间左端点 l,区间右端点 r,区间中点 mid=l+r2 或随机选取。维护两个指针 i,j,如果 ai>x 则将其放至右侧,如果 ajx 则将其放至左侧。

快速排序是一种不稳定的排序算法,最优和平均时间复杂度为 O(nlogn),最坏时间复杂度为 O(n2)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1e5+10;

int n;
int q[N];

void qsort(int q[], int l, int r) {
    if (l >= r) return ; //边界条件
    
    int x = q[(l+r)/2]; //选取界点x
    int i = l-1, j = r+1; 
    while (i < j) { //将数组以x为分界点划分为两部分
        do i ++; while (q[i] < x);
        do j --; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
     
    qsort(q, l, j), qsort(q, j+1, r); //继续递归划分两边
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d", &q[i]);
    
    qsort(q, 1, n);
    
    for (int i = 1; i <= n; ++i) printf("%d ", q[i]);
    return 0;
}

例题AcWing 786. 第k个数

题目:给你一个长度为 n 的数组 a,求出其中第 k 大的数。1kn105,1ai109

思路:对数组 a 进行快速排序后直接输出 ak 即可,时间复杂度 O(nlogn)。 本题实际上还有期望 O(n) 做法:即在划分区间时按照左边元素个数 il+1k 的大小关系来递归求解。

1.1.3 归并排序

模板AcWing 787. 归并排序

题目:给你一个长度为 n 的数组 a,将这个数组从小到大排序。1n105,1ai109

思路

运用分治的思想。对区间 [l,r] 进行归并排序的步骤如下:(其中 mid=l+r2

  1. 递归排序区间 [l,mid][mid+1,r]
  2. 定义数组 tmp,将 [l,r] 之间的数按从小到大的顺序存入 tmp 中。
  3. tmp 中的数覆盖至区间 [l,r] 上。

归并排序的重点在于第 2 步。维护三个指针 i,j,k,若 aiaj,则 tmpk=ai,否则 tmpk=aj

归并排序是稳定的排序算法,时间复杂度 O(nlogn)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1e5+10;

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

void msort(int q[], int l, int r) {
    if (l >= r) return ; //边界条件
    
    int mid = l + r >> 1;
    msort(q, l, mid), msort(q, mid+1, r); //递归排序区间[l,mid]和[mid+1,r]
     
    int i = l, j = mid+1, k = 1; //[l,mid]和[mid+1,r]已经排好序,将它们按从小到大的顺序存入tmp中
    while (i <= mid && j <= r) { //每次将ai,aj中较小的一个存入tmp中
        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 = 1; i <= r; ++i, ++j) q[i] = tmp[j]; //将tmp中的数覆盖至区间[l,r]上
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d", &q[i]);
    
    msort(q, 1, n);
    
    for (int i = 1; i <= n; ++i) printf("%d ", q[i]);
    return 0;
}

例题AcWing 788. 逆序对的数量

题目:我们定义:在数组 a 中若 i<jai>aj,则称 (i,j) 是一个逆序对。给你一个长度为 n 的数组 a,求出 a 中逆序对的数量。1n105,1ai109

思路

定义 f(l,r) 表示区间 [l,r] 中逆序对的数量。 想求出 f(l,r),我们可以分类讨论。

图1-2

如图 1-2,以区间中点 mid=l+r2 为分界线,逆序对 (i,j) 可以分为 3 种情况:

  1. li<jmid:即 i,j 均位于 mid 左边。这种情况的逆序对数量显然是 f(l,mid)
  2. mid<i<jr:即 i,j 均位于 mid 右边。这种情况的逆序对数量显然是 f(mid,r)
  3. limid<jr:即 i 位于 mid 左边,j 位于 mid 右边。不妨先把区间 [l,mid](mid,r] 都从小到大排好序(并不会影响逆序对数量)。那么对于若 ai>aj,则 ai+1amid 也均大于 aj

以上的 3 种情况,可以通过归并排序的过程求出。

时间复杂度 O(nlogn)

主要代码

int msort(int q[], int l, int r) { //归并排序求逆序对
    if (l >= r) return 0; //如果只有1个数,就不可能有逆序对,直接返回0 
    
    int mid = l + r >> 1;
    int ans = msort(q, l, mid) + msort(q, mid+1, r); //第1种情况和第2种情况
    
    int i = l, j = mid+1, k = 1;
    while (i <= mid && j <= r) { //计算第3种情况
        if (q[i] <= q[j]) tmp[k ++] = q[i ++];
        else tmp[k ++] = q[j ++], ans += mid-i+1; //如果ai>aj,则a{i+1}~a{mid}均大于aj,可以与j构成逆序对
    }
    while (i <= mid) tmp[k ++] = q[i ++];
    while (j <= r) tmp[k ++] = q[j ++];
    
    for (int i = l, j = 1; i <= r; ++i, ++j) q[i] = tmp[j]; 
    return ans;
}

1.2 二分

1.2.1 整数二分

例题AcWing 789. 数的范围

题目:给你一个长度为 n 的按升序排列的数组 aq 次询问,每次询问给出一个整数 k,求出 ka 中的起始位置与结束位置(位置从 0 开始计数,若 k 不存在则输出 -1 -1)。1n105,1q104,1k,ai104

思路

图1-3

由于题目已经排好序了,所以我们可以采用二分的方法。如何进行二分呢?如图 1-3,将区间 [l,r] 从中间分为两段。

  1. 寻找 k 的起始位置时,取 mid=l+r2。 由于 a 是不严格单调递增的,当 amidk 时,k 一定在区间 [l,mid] 中;否则,当 amid<k 时,k 一定在区间 (mid,r] 中。
  2. 寻找 k 的结束位置时,取 mid=l+r+12。 当 amidk 时,k 一定在区间 [mid,r] 中;否则,当 amid>k 时,k 一定在区间 [l,mid) 中。

如上,整数二分写法有 2 种:

  1. 缩小范围时,r=midl=mid+1,取中间值时,mid=(l+r)/2
  2. 缩小范围时,l=midr=mid1,取中间值时,mid=(l+r+1)/2

时间复杂度 O(qlogn)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1e5+10;

int n, q;
int a[N];

int findl(int k) { //寻找k的起始位置,注意这里下标从1开始,没找到返回0
    int l = 1, r = n;
    while (l < r) {
        int mid = l + r >> 1;
        if (a[mid] >= k) r = mid;
        else l = mid+1;
    }
    return (a[l] == k) ? l : 0;
}

int findr(int k) { //寻找k的结束位置
    int l = 1, r = n;
    while (l < r) {
        int mid = l + r + 1 >> 1; //注意一定要加上1,不然会出现错误
        if (a[mid] <= k) l = mid;
        else r = mid-1;
    }
    return (a[l] == k) ? l : 0;
}

int main() {
    scanf("%d%d", &n, &q);
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    
    while (q -- ) {
        int k;
        scanf("%d", &k);
        int l = findl(k)-1; //由于题目中说了下标从1开始,所以要减去1
        int r = findr(k)-1;
        printf("%d %d\n", l, r);
    }
    
    return 0;
}

1.2.2 实数二分

例题AcWing 790. 数的三次方根

题目:给你一个整数 n,求出 n3,保留 6 位小数。105n105

思路:类似整数二分,只需确定好所需的精度 eps,以 l+eps<r 为循环条件,每次根据在 mid 上的判定选择 r=midl=mid 的分支之一即可。通常需要保留 k 位小数时,eps=10k3

时间复杂度 O(logn)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const double eps = 1e-9; //实数二分所取的误差值一般为所需求的小数位的1/1000左右

double n;

double cube_root(double n) { //求n的三次方根
    double l = -100, r = 100;
    while (r-l > eps) {
        double mid = (l+r) / 2;
        if (mid*mid*mid > n) r = mid;
        else l = mid;
    }
    return l;
}

int main() {
    cin >> n;
    double ans = cube_root(n);
    printf("%.6f\n", ans);
    return 0;
}

1.3 高精度

1.3.1 高精度加法

模板AcWing 791. 高精度加法

题目:给你两个不含前导 0 的正整数 A,B,计算 A+B 的值。1lgA,lgB105

思路:模仿竖式加法。

比如,计算 149+287 的值,我们通常是采用下图的方法:

图1-4

那如何让计算机模拟加法的计算呢?主要有以下 3 步:

  1. 将输入的 A,B 反过来;
  2. 从最高位(即原来的最低位)开始,一位一位计算加法,同时记录每次的进位;
  3. 倒序输出最后的结果。

时间复杂度 O(max(logA,logB))

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

vector<int> add(vector<int> &A, vector<int> &B) {
    int t = 0; //t记录进位
    vector<int> C; //C存储结果
    
    for (int i = 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() {
    string a, b;
    cin >> a >> b;
    vector<int> A, B;
    for (int i = a.size()-1; i >= 0; --i) //将A,B反过来
        A.push_back(a[i]-'0');
    for (int i = b.size()-1; i >= 0; --i)
        B.push_back(b[i]-'0');
    
    vector<int> C = add(A, B);
    
    for (int i = C.size()-1; i >= 0; --i) //倒序输出A,B之和
        printf("%d", C[i]);
    return 0;
}

1.3.2 高精度减法

模板AcWing 792. 高精度减法

题目:给你两个不含前导 0 的正整数 A,B,计算 AB 的值。1lgA,lgB105

思路:与高精度加法类似,模仿竖式计算即可。时间复杂度 O(max(logA,logB))

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

bool cmp(vector<int> &A, vector<int> &B) { //比较A,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 1;
}

vector<int> sub(vector<int> &A, vector<int> &B) { //高精减
    vector<int> C;
    int t = 0;
    for (int i = 0; i < A.size(); ++i) {
        t = A[i] - t;
        if (i < B.size()) t -= B[i];
        C.push_back((t+10) % 10);
        t = (t >= 0) ? 0 : 1;
    }
    while (C.back() == 0 && C.size() > 1) C.pop_back();
    return C;
}

int main() {
    string a, b;
    cin >> a >> b;
    vector<int> 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');
    
    vector<int> C;
    if (cmp(A, B)) C = sub(A, B);
    else putchar('-'), C = sub(B, A); //如果B>A,说明A-B为负数,先输出负号,再计算B-A
    
    for (int i = C.size()-1; i >= 0; --i) printf("%d", C[i]);
    return 0;
}

1.3.3 高精度乘法

1.3.3.1 高精乘低精

模板AcWing 793. 高精度乘法

题目:给你两个不含前导 0 的非负整数 A,B,计算 A×B 的值。1lgA105,1B105

思路:将 B 视作一个整体,模仿竖式计算即可。时间复杂度 O(logA)

代码

#include <iostream>
#include <cstdio>
#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.back() == 0 && C.size() > 1) 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');
    
    vector<int> C = mul(A, b);
    for (int i = C.size()-1; i >= 0; --i) printf("%d", C[i]);
    return 0;
}

1.3.3.2 高精乘高精

题目:给你两个不含前导 0 的非负整数 A,B,计算 A×B 的值。1lgA,lgB105

思路

与高精加不同的是,高精乘高精的进位在最后统一处理。其余与竖式计算类似:

循环遍历 AB 的每一位,对于 A 的第 i 位和 B 的第 j 位,在不考虑进位的情况下,显然有 Ci+j=Ai×Bj

时间复杂度 O(log(AB))

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

vector<int> mul(vector<int> &A, vector<int> &B) {
    vector<int> C(A.size()+B.size()+10, 0); //开一个大小为A.size()+B.size()+10的vector,初始化全为0
    
    for (int i = 0; i < A.size(); ++i) {
        for (int j = 0; j < B.size(); ++j)
            C[i+j] += A[i] * B[j];
    }
    
    int t = 0; //处理进位
    for (int i = 0; i < C.size(); ++i) { 
        t += C[i];
        C[i] = t % 10;
        t /= 10;
    }
    
    while (C.size() > 1 && C.back() == 0) C.pop_back(); //去前导0
    return C;
}

int main() {
    string a, b;
    cin >> a >> b;
    vector<int> 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');
    
    vector<int> C = mul(A, B);
    
    for (int i = C.size()-1; i >= 0; --i)
        cout << C[i];
    return 0;
}

1.3.4 高精度除法

模板AcWing 794. 高精度除法

题目:给你两个不含前导 0 的非负整数 A,B,计算 A/B 的商和余数。1lgA105,1B105

思路

图1-5

如图 1-5 展示了通过竖式除法计算 237/5 的过程:

  1. 2 除以 5,无法除。余 2
  2. 23 除以 5,商 43
  3. 37 除以 5,商 72

所以 237÷5=452

仿照上述过程,不难写出竖式除法的代码。注意,高精加、减、乘都是从低位到高位计算,而除法是从高位到低位计算。

时间复杂度 O(logA)

代码

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

using namespace std;

vector<int> div(vector<int> &A, int b, int &r) { //高精除
    r = 0; //r为余数
    vector<int> C; //C为商
    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需要额外反转一次
    while (C.size() > 1 && C.back() == 0) C.pop_back(); //去前导0
    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'); //反向读入
    
    int r = 0;
    vector<int> C = div(A, b, r);
    
    for (int i = C.size()-1; i >= 0; --i) printf("%d", C[i]); //倒序输出
    puts("");
    printf("%d", r);
    return 0;
}

1.4 前缀和与差分

1.4.1 前缀和

1.4.1.1 一维前缀和

模板AcWing 795. 前缀和

题目:给你一个包含 n 个整数的数组 am 次询问,每次询问包含 2 个正整数 l,r,求出 lirai(数组下标从 1 开始)。1n,m105,1000ai1000

思路

暴力计算,时间复杂度 O(nm),不能接受,我们需要考虑更加快速的做法。

不妨设 si=k=1iak,定义 s0=0,我们有:

(1)lirai=al+al+1++ar(2)=(a1+a2++ar)(a1+a2++al1)(3)=i=1raii=1l1ai(1.1)=srsl1

接下来思考如何计算 si。 有:

(1.2)si=k=1iak=k=1i1ak+ai=si1+ai

时间复杂度 O(n+m)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1e5+10;

int n, m;
int a[N], s[N]; //s[i]表示前i个数之和

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        s[i] = s[i-1] + a[i]; //预处理前缀和,即1.2
    }
    
    while (m -- ) {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", s[r]-s[l-1]); //式1.1
    }
    return 0;
}

1.4.1.2 二维前缀和

模板AcWing 796. 子矩阵的和

题目:给你一个 n×m 大小的矩阵 aq 次询问,每次询问包含 4 个整数 x1,y1,x2,y2,求出 x1ix2y1jy2ai,j1n,m103,1q2×105,1000ai,j1000

思路

仿照一维前缀和,定义 si,j=x=1iy=1jai,j

图1-6

如上图,由容斥原理,可得:

(1.3)x1ix2y1jy2ai,j=sx2,y2sx11,y2sx2,y11+sx11,y11

类似地,有:

(1.4)si,j=si1,j+si,j1si1,j1+ai,j

时间复杂度 O(nm+q)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1010;

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

int main() {
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            scanf("%d", &a[i][j]);
            s[i][j] = s[i][j-1]+s[i-1][j]-s[i-1][j-1]+a[i][j]; //式1.4
        }
    }
    
    while (q -- ) {
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        int ans = s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]; //式1.3
        printf("%d\n", ans);
    }
    return 0;
}

1.4.2 差分

1.4.2.1 一维差分

模板AcWing 797. 差分

题目:给你一个长度为 n 的整数序列 am 次操作,每次操作包含 3 个整数 l,r,c,表示将区间 [l,r] 间的数都加上 c。 求出最后得到的序列。1n,m105,1000ai1000,1000c1000

思路

暴力模拟,时间复杂度 O(nm),无法通过。

定义 a 的差分数组 p,其中:

  • p1=a1
  • pi=aiai1(i>1)

对于每次修改操作 l r c,只需将 pl 加上 cpr+1 减去 c 即可。最后只需要对 p 数组求一遍前缀和,即可得到操作后的 a 数组。

首先我们来证明将 p 求一遍前缀和就能得到 a。 根据 p 的定义,可知:

(4)i=1kpi=p1+p2++pk(5)=a1+(a2a1)++(akak1)(1.5)=ak

得证。

然后我们来证明修改操作。同样由 p 的定义,将 pl 加上 c 后,有

  • al 的值变为 i=1lpi+c=al+c
  • al+1 的值变为 i=1l+1pi+c=al+1+c

以此类推,直到 an 的值变为 i=1npi+c=an+c

但我们给 ar+1an 多加了一个 c,所以要再给 pr+1 减去 c

时间复杂度 O(n+m)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1e5+10;

int n, m;
int a[N], p[N]; //p为差分数组

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        p[i] = a[i] - a[i-1];
    }
    
    while (m -- ) {
        int l, r, c;
        scanf("%d%d%d", &l, &r, &c);
        p[l] += c, p[r+1] -= c; //修改操作
    }
    
    for (int i = 1; i <= n; ++i) {
        a[i] = a[i-1]+p[i]; //求出修改后的a数组
        printf("%d ", a[i]);
    }
    return 0;
}

1.4.2.2 二维差分

模板AcWing 798. 差分矩阵

题目:给你一个 n×m 大小的矩阵 aq 次操作,每次操作包含 5 个整数 x1,y1,x2,y2,c,表示将左上角坐标为 (x1,y1),右下角坐标为 (x2,y2) 的子矩阵中的数全部加上 c。 输出最终的矩阵。1n,m1000,1q105,1000aij1000,1000c1000

思路

仿照一维差分,定义 a 的差分矩阵为 p。 不同的是,此时我们不需要考虑如何初始化(可以通过插入操作完成)。

对于每次操作,将 px1,y1 添加 cpx2+1,y1 减少 cpx1,y2+1 减少 cpx2+1,y2+1 增加 c 即可。

最后再对 p 求一遍二维前缀和就可以得到更新后的矩阵。

时间复杂度 O(nm+q)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m, q;
int p[N][N]; //p即为差分矩阵

void insert(int x1, int y1, int x2, int y2, int c) { //插入操作
    p[x1][y1] += c, p[x1][y2+1] -= c, p[x2+1][y1] -= c, p[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) {
            int x;
            scanf("%d", &x);
            insert(i, j, i, j, x); //相当于将左上角坐标为(i,j),右下角坐标为(i,j)的矩阵增加x
        }
    }
    
    while (q -- ) {
        int x1, y1, x2, y2, c;
        scanf("%d%d%d%d%d", &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) {
            p[i][j] = p[i][j] + p[i-1][j] + p[i][j-1] - p[i-1][j-1]; //通过前缀和求出更新后的矩阵
            printf("%d ", p[i][j]);
        }
        puts("");
    }
    
    return 0;
}

1.5 双指针算法

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

题目:给你一个包含 n 个元素的数组 a,求出其最长不重复子序列。1n105,1ai105

思路:运用双指针算法。

先考虑朴素 O(n2) 算法:枚举终点 i,再遍历区间 [1,i] 找到起点 j。 我们可以用双指针算法将其优化至 O(n)

维护两个指针 i,j,分别指向连续不重复子序列的终点和起点。当 i 每向右移一个数时,我们需要判断此时 [j,i] 是不是连续不重复子序列,如果不是,j 就向右移动直到 [j,i] 为连续不重复子序列。

通过反证法,不难证明 j 不可能向左移动:若 [j,i] 是中包含重复元素,则 [j1,i] 中也必定包含重复元素,矛盾。

然后我们来思考如何判断 [j,i] 合法。不妨设 [j,i1] 已经为一个合法序列,可以开一个桶 s,动态记录 [j,i1] 中每个数的出现数量。若 si>1,说明 [j,i] 不合法,j 需要向右移动,同时 sj:=sj1

每次取 ij+1 的最大值即为答案。

时间复杂度 O(n)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1e5+10;

int n;
int a[N], s[N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    
    int ans = 0;
    for (int i = 1, j = 1; i <= n; ++i) {
        s[a[i]] ++; //将ai加入桶中
        while (j <= i && s[a[i]] > 1) s[a[j]] --, j ++; //向右寻找j使得[j,i]合法
        ans = max(ans, i-j+1);
    }
    cout << ans << endl;
    return 0;
}

例题AcWing 800. 数组元素的目标和

题目:给你两个长度分别为 n,m升序数组 A,B,求出一个数对 (i,j) 使得 ai+bj=x(下标从 0 开始)。1n,m1051ai,bj109,保证有唯一解。

思路

由于 A,B 均为升序,所以若 ai+bj>x,那么 ai+1+bj 一定也大于 x。 我们可以维护两个指针 i,j,初始时 i 指向 a0j 指向 bm。 若 ai+bj>xj 向左移动,若 ai+bj<xi 向右移动。最后输出 i,j 即可。

时间复杂度 O(n)

代码:

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1e5+10;

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

int main() {
    scanf("%d%d%d", &n, &m, &x);
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    for (int i = 1; i <= m; ++i) scanf("%d", &b[i]);
    
    int i = 1, j = m;
    while (true) {
        while (a[i]+b[j] > x) j --;
        if (a[i]+b[j] == x) break;
        i ++;
    }
    printf("%d %d\n", i-1, j-1); //由于这里的下标从1开始,所以要额外减去1
    return 0;
}

例题AcWing 2816. 判断子序列

题目:给你两个长度分别为 n,m 的数组 a,b,判断 a 是否是 b 的子序列。1n,m105,109ai,bj109

思路

ab 的子序列,则 a 中的元素一定在 b 按相同顺序出现。通过双指针维护即可。

初始时,i 指向 a1j 指向 b1j 向右移动直到 bj=ai,随后 i 继续向右移动。最终若 i 未遍历完 a 说明 a 不是 b 的子序列。

时间复杂度 O(n+m)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1e5+10;

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

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    for (int i = 1; i <= m; ++i) scanf("%d", &b[i]);
    
    int i = 1, j = 1;
    while (i <= n && j <= m) { //双指针遍历
        while (b[j] != a[i] && j < m) j ++;
        i ++, j ++;
    }
    i --, j --;
    if (i == n && a[i] == b[j]) puts("Yes");
    else puts("No");
    return 0;
}

1.6 位运算

模板AcWing 801. 二进制中1的个数

题目:给你 n 个数,求出每个数 x 的二进制表示中 1 的个数。

思路:我们可以使用 lowbit 函数。

首先我们要知道 lowbit 函数的定义:lowbit(x)=x&x。 其中的 & 为按位与,x 为正整数。

例如,lowbit(10)=(00001010)&(11110110)=(00000010)=2。 不难发现,若正整数 x 的补码表示为 01000n0,那么 lowbit(x)=1000n0

我们来证明这个结论。由于 x 的补码为 x+1,所以 x 的补码即为 10111n1+1=11000n0。 所以 lowbit(x)=x&x 即为 1000n0

回到正题,如何求出 x 的二进制表示中有多少个 1 呢?显然,每次 x 减去 lowbit(x) 后,x 的二进制表示中 1 的数量就会减少 1。 循环减去 lowbit(x),减的次数即为 1 的个数。

时间复杂度 O(nlogx)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

int n;

int lowbit(int x) { //lowbit函数
    return x & -x;
}

int main() {
    scanf("%d", &n);
    while (n -- ) {
        int x;
        scanf("%d", &x);
        
        int cnt = 0;
        while (x) {
            x -= lowbit(x); //每次减去lowbit(x),1的个数减少1
            cnt ++;
        }
        printf("%d ", cnt);
    }
    return 0;
}

1.7 离散化

例题AcWing 802. 区间和

题目

有一根无限长的数轴,初始时每个点的值都为 0。 有 n 次增加操作和 m 次询问操作:

  • 对于每次增加操作,输入两个整数 x,c,表示给数轴上表示点 x 的点的值增加 c
  • 对于每次询问操作,输入两个整数 l,r,输出区间 [l,r] 内所有点的值之和。

其中,109x109,1n,m105,109l,r109,104c104

思路:观察到值域很大但实际使用的数很少,考虑运用离散化的思想。

先将每次操作的 x,l,r 存下来,先排序后去重。通过二分查找每个数在去重后的离散化数组内出现的位置,用前缀和优化即可。

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

typedef pair<int, int> pii;

const int N = 5e5+10;

int n, m;
int a[N], s[N]; //离散化后的前缀和数组
vector<pii> nums, query; //nums存储每次的增加操作,query存储每次询问操作
vector<int> alls; //alls存储所有用到的数

int find(int x) { //在alls中找到等于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 l+1; //由于数组a,s的下标是由1开始的,而alls的下标是从0开始的,所以要额外+1
}

int main() {
    scanf("%d%d", &n, &m);
    
    for (int i = 1; i <= n; ++i) {
        int x, c;
        scanf("%d%d", &x, &c);
        nums.push_back({x, c}); //存储增加操作
        alls.push_back(x);
    }
    for (int i = 1; i <= m; ++i) {
        int l, r;
        scanf("%d%d", &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 op : nums) { //处理每次增加操作
        int x = op.first, c = op.second;
        a[find(x)] += c;
    }
    
    for (int i = 1; i <= alls.size(); ++i) s[i] = s[i-1] + a[i]; //预处理前缀和
    
    for (auto op : query) { //处理每次询问操作
        int l = op.first, r = op.second;
        int ans = s[find(r)] - s[find(l)-1];
        printf("%d\n", ans);
    }
    return 0;
}

1.8 区间合并

例题AcWing 803. 区间合并

题目:给你 n 个区间 [li,ri],求出合并有交集的区间(在端点处相交也视作交集)后所有区间的数量。1n105,109l,r109

思路

先将 n 个区间根据左端点排序。然后维护两个指针 l,r,分别指向合并后区间的左端点和右端点。那么如何判断区间 [li,ri][l,r] 是否有交集呢?

  • li>ri,则两个区间没有交点,区间数量增加 1
  • 否则,将右端点更新为 max(ri,r)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

typedef pair<int, int> pii;

int n;
vector<pii> segs; //存储每个区间

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        int l, r;
        scanf("%d%d", &l, &r);
        segs.push_back({l, r});
    }
    
    sort(segs.begin(), segs.end()); //排序
    
    int cnt = 0;
    int l = segs[0].first, r = segs[0].second;
    for (int i = 1; i < segs.size(); ++i) {
        if (segs[i].first > r) l = segs[i].first, r = segs[i].second, cnt ++; //无法合并,区间数量+1
        else r = max(segs[i].second, r); //可以合并,r更新为max(ri,r)
    }
    cnt ++; //注意还需要统计最后一个区间
    
    printf("%d\n", cnt);
    return 0;
}

本文作者:Jasper08

本文链接:https://www.cnblogs.com/Jasper08/p/17461495.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Jasper08  阅读(20)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
🔑