【AcWing】基础算法

目录

1、快速排序

[1.1 快速排序](#1.1 快速排序)

[1.2 第k个数](#1.2 第k个数)

2、归并排序

[2.1 归并排序](#2.1 归并排序)

[2.2 逆序对的数量](#2.2 逆序对的数量)

3、二分

[3.1 数的范围](#3.1 数的范围)

[3.2 数的三次方根](#3.2 数的三次方根)

4、高精度

[4.1 高精度加法](#4.1 高精度加法)

[4.2 高精度减法](#4.2 高精度减法)

[4.3 高精度乘法](#4.3 高精度乘法)

[4.4 高精度除法](#4.4 高精度除法)

5、前缀和与差分

[5.1 前缀和](#5.1 前缀和)

[5.2 子矩阵的和](#5.2 子矩阵的和)

[5.3 差分](#5.3 差分)

[5.4 差分矩阵](#5.4 差分矩阵)

6、双指针算法

[6.1 最长连续不重复的子序列](#6.1 最长连续不重复的子序列)

[6.2 数组元素的目标和](#6.2 数组元素的目标和)

[6.3 判断子序列](#6.3 判断子序列)

7、位运算

8、离散化

9、区间合并


1、快速排序

1.1 快速排序

785. 快速排序 - AcWing题库

1. 确定分界点:q[l]、q[r]、q[l + r >> 2]、随机

2. 调整区间

我们要将区间调整为下面这样子

当q[i] < x时,i++,当q[i] >= x时,i停止。当q[j] > x时,j--,当q[j] <= x时,j停止。判断一下此时是否i < j,若i < j则交换,否则就出循环。

此时为什么是正确的呢?

因为在任意时刻,i左边区间内的数都是小于等于x的,j右边区间内的数都是大于等于x的

3. 递归处理左右两段

#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], n;

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

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

讲解代码中几个需要注意的地方:

  1. 为什么while(i < j)里面的两个while循环不需要先判断i < j?

i < j是为了防止越界访问。我们先来看结束时,i和j的分布情况

(1) i和j可能会相遇在某一个x,此时i == j

(2) 因为i左边的数是<=x,j右边的数是>=x,所以i一定会停在j右边的区间的第一个数,j一定会停在i左边的区间的第一个数。最先开始的时候,i的左边没有数,j的右边没有数,会不会出现意外呢?不会的,以此时选取x=q[l]为例,一开始q[i] == x,i是不会动的,j会先动。若q[j] <= x,那么交换完q[i]和q[j]后,i++,j--,i的左边有值,j的右边有值。若q[j] > x,则j--找<=x的元素,找到后,两者一交换,左右就都不为空了。

  1. 为什么一上来直接i++、j--?

因为无论是正常走,还是交换完,都需要让i++、j--。

注意:初始化时i = l - 1,j = r + 1

  1. 递归时的区间选择

上面我们选取的分界点是x = q[l],递归时就是使用j,若选取的分界点是x = q[r],递归时就是使用i,否则就会出现死循环

#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], n;

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

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

所以当选取的分界点是q[l]或q[r]时,要注意迭代区间的更新

当然,选取分界点时也可以直接使用随机数法或区中间值的方法

#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], n;

void quick_sort(int l, int r)
{
    if (l >= r) return ;
    swap(q[l], q[l + rand() % (r - l + 1)]);
    int i = l - 1, j = r + 1, x = q[l];
    while (i < j)
    {
        while (q[ ++ i] < x)    ;
        while (q[ -- j] > x)    ;
        if (i < j)  swap(q[i], q[j]);
    }
    quick_sort(l, j);
    quick_sort(j + 1, r);
    //quick_sort(l, i - 1);
    //quick_sort(i, r); 也行
}

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

此时会有疑问,就是为什么此时分界点选择的是q[l],但是递归的时候可以用i呢?

实际上也是不行的,比如像5 4 1 3 2,若rand() == 2,变为1 4 5 3 2,若用i仍然会死循环,可以是因为rand()不一定是2

这个算法相比较我们之前的快速排序算法是有优势的

void QuickSort(int* a, int left, int right)//Hoare法
{
	if (left >= right)
		return;
	int begin = left, end = right;
	//随机选key
	int randi = rand() % (right - left + 1);
	randi += begin;
	Swap(&a[left], &a[randi]);
	int keyi = left;
	while (begin < end)
	{
		while(begin < end && a[end] >= a[keyi])
			end--;
		while(begin < end && a[begin] <= a[keyi])
			begin++;
		Swap(&a[begin], &a[end]);
	}
	Swap(&a[begin], &a[keyi]);
	QuickSort(a, left, begin - 1);
	QuickSort(a, begin + 1, right);
}

我们之前的算法在遇到全相等的情况时,时间复杂度是O(N^2),因为遇到==基准值时是会跳过去的,而现在的代码不会

1.2 第k个数

786. 第k个数 - AcWing题库

可以直接使用快速排序先将数组排序,然后再输出第k个数,时间复杂度是O(N*logN)

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int q[N],n,k;
void quick_sort(int l, int r)
{
    if(l >= r) return;
    swap(q[l], q[l + rand() % (r - l + 1)]);
    int i = l - 1,j = r + 1,x = q[l];
    while(i < j)
    {
        while(q[++i] < x) ;
        while(q[--j] > x) ;
        if(i < j) swap(q[i], q[j]);
    }
    quick_sort(l, j);
    quick_sort(j + 1, r);
}
int main()
{
    scanf("%d%d", &n, &k);
    for(int i = 0;i < n;i++) scanf("%d", &q[i]);
    quick_sort(0, n - 1);
    printf("%d\n", q[k - 1]);
    return 0;
}

也可以使用快速选择算法

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int q[N],n,k;
int quick_sort(int l, int r,int k)
{
    if(l >= r) return q[l];
    int i = l - 1,j = r + 1,x = q[l];
    while(i < j)
    {
        while(q[++i] < x) ;
        while(q[--j] > x) ;
        if(i < j) swap(q[i], q[j]);
    }
    int sl = j - l + 1;
    if(sl >= k) return quick_sort(l, j, k);
    else return quick_sort(j + 1, r, k - sl);
}
int main()
{
    scanf("%d%d", &n, &k);
    for(int i = 0;i < n;i++) scanf("%d", &q[i]);
    cout<<quick_sort(0, n - 1, k)<<endl;
    return 0;
}

这道题是保证第k个数在区间里面,所以区间里面至少有1个数,可以l==r作为递归结束条件,但是快速排序中一定要是l >= r,因为快速排序中l可能大于r

2、归并排序

2.1 归并排序

787. 归并排序 - AcWing题库

  1. 确定分界点 mid = (l + r) / 2

  2. 递归排序左右区间

  3. 归并 ---- 将两个有序数组合二为一

注意:在这个步骤中,为了保证排序的稳定性,当两指针指向的值相等时,要将第一个数组的值放入临时数组

#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], tmp[N], n;
void merge_sort(int l,int r)
{
    if(l >= r) return;
    int mid = (l + r) / 2;
    merge_sort(l, mid);
    merge_sort(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];
}
int main()
{
    scanf("%d", &n);
    for(int i = 0;i < n;i++) scanf("%d", &q[i]);
    merge_sort(0, n - 1);
    for(int i = 0;i < n;i++) printf("%d ", q[i]);
    return 0;
}

2.2 逆序对的数量

788. 逆序对的数量 - AcWing题库

可以像归并排序时一样,将数组划分为两部分,此时逆序对会有3种情况

我们只需要一直向下递归,直到每个区间都只有一个值,就能都变成第三种情况

并且我们可以在归并的同时就行排序,因为当q[i] > q[j]时,则q[i]到q[mid]都是大于q[j]的

在"并"的过程中,利用两个有序数组计算逆序对的数量

此时q[i] > q[j],那么前半数组剩余的值都和q[j]构成逆序对

注意,逆序对的数量最多会大于int,所以开long long

#include<iostream>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int q[N], tmp[N], n;
ll merge_sort(int q[], int l, int r)
{
    if(l >= r) return 0;
    int mid = l + r >> 1;
    ll res = 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++];
            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()
{
    scanf("%d", &n);
    for(int i = 0;i < n;i++) scanf("%d", &q[i]);
    ll res = merge_sort(q, 0, n - 1);
    printf("%lld\n", res);
    return 0;
}

3、二分

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

作者:yxc
来源:AcWing

模板不需要特意去记什么时候在计算中点时要+1,只需要写出check后,根据更新判断

为什么计算中点时有时候要+1,有时候不需要呢?

因为l + r >> 1是向下取整,当l = r - 1时,若chack为真,则没变,会死循环

3.1 数的范围

789. 数的范围 - AcWing题库

这道题是整数二分,分为找左端点和右端点

#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], n, q, k;
int main()
{
    scanf("%d %d", &n, &q);
    for(int i = 0;i < n;i++) scanf("%d", &a[i]);
    while(q--)
    {
        scanf("%d", &k);
        // 先找左端点
        int l = 0, r = n - 1;
        while(l < r)
        {
            int mid = l + r >> 1;
            if(a[mid] < k) l = mid + 1;
            else r = mid;
        }
        if(a[l] != k) printf("-1 -1\n");
        else
        {
            // 找右端点
            printf("%d ", l);
            l = 0, r = n - 1;
            while(l < r)
            {
                int mid = l + r + 1 >> 1;
                if(a[mid] > k) r = mid - 1;
                else l = mid;
            }
            printf("%d\n", l);
        }
    }
    return 0;
}

3.2 数的三次方根

790. 数的三次方根 - AcWing题库

这道题是浮点数二分,初始时让l等于数的范围的左端点,r等于数的范围的右端点,一直二分,直到l和r之间的距离足够小即可。如何定义足够小呢?如果题目的浮点数保留6位小数,则while的循环条件时r - l > 1e-8

注意:浮点数二分不能直接取l=0,r=x,因为当0<x<1时,可能会出错,如求二次方跟时,x=0.01,而0.01的二次方根是0.1。在这道题中就不需要考虑这个,因为数的范围已经确定了

#include<iostream>
using namespace std;
int main()
{
    double n;
    scanf("%lf", &n);
    double l = -10000,r = 10000;
    while(r - l > 1e-8)
    {
        double mid = (r + l) / 2;
        if(mid * mid * mid < n) l = mid;
        else r = mid;
    }
    printf("%lf\n", l);
    return 0;
}

浮点数二分相对于整数二分更加简单,因为没有+1-1,所以不需要考虑边界问题

4、高精度

这里的高精度是指大数之间的加减乘除运算

4.1 高精度加法

791. 高精度加法 - AcWing题库

高精度加法,A + B,通常A和B是大小在0到10^6的整数

大整数的存储通常使用数组来存储,并且最好是倒过来存储,方便进位

使用数组进行加法的过程就是模拟整数加法,Ci = Ai + Bi + t,t表示进位

#include<iostream>
#include<string>
#include<vector>
using namespace std;
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');
    vector<int> C = add(A, B);
    for(int i = C.size() - 1;i >= 0;i--) printf("%d", C[i]);
    return 0;
}

4.2 高精度减法

792. 高精度减法 - AcWing题库

高精度减法,A - B,通常A和B是大小在0到10^6的整数

因为一道题中结果可能很大,会使用到高精度的加减乘除,所以四种运算的存储方式相同

若Ai - Bi - t大于等于0,Ci = Ai - Bi - t

若Ai - Bi - t小于0,Ci = Ai - Bi - t + 10,t表示借位

#include<iostream>
#include<string>
#include<vector>
using namespace std;
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;
    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);
        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)) // A >= B
    {
        vector<int> C = sub(A, B);
        for(int i = C.size() - 1;i >= 0;i--) printf("%d", C[i]);
    }
    else
    {
        vector<int> C = sub(B, A);
        printf("-");
        for(int i = C.size() - 1;i >= 0;i--) printf("%d", C[i]);
    }
    return 0;
}

sub中是默认A大于等于B的

(t + 10) % 10,当t >= 0时结果是不变的,当t < 0时,这个位置需要借位

还需要处理前导0,如789-786,此时A是9 8 7,B是6 8 7,减完之后C是3 0 0,反转过后也就是003,我们需要的是3,所以需要处理前导0

4.3 高精度乘法

793. 高精度乘法 - AcWing题库

高精度乘法是一个很大的数字乘一个较小的数字,len(A)<=10^6,b<=10000

我们这里是将b当成一个整体,与A的每一位相乘

Ci = (Ai * b + t) % 10,t表示进位

#include<iostream>
#include<vector>
#include<string>
using namespace std;
int b;
vector<int> mul(vector<int>& A, int b)
{
    int t = 0;
    vector<int> C;
    for(int i = 0;i < A.size() || t;i++) // 结束条件是A遍历完,并且t也为0
    {
        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(); // 处理前导0
    return C;
}
int main()
{
    vector<int> A;
    string a; cin>>a>>b;
    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;
}

同样需要处理前导0,如A = [3, 2, 1],b = 0,结果是C = [0, 0, 0]

4.4 高精度除法

高精度除法通常A <= 10^6,b <= 10000

除法是从最高位开始计算的,而加减乘都是从最低位计算,为了保持统一,除法计算完reverse

#include<iostream>
#include<vector>
#include<string>
#include<algorithm>
using namespace std;
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());
    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; // 余数
    vector<int> C = div(A, b, r);
    for(int i = C.size() - 1;i >= 0;i--) printf("%d", C[i]);
    printf("\n");
    printf("%d\n", r);
    return 0;
}

同样需要处理前导0,如1234 / 11,A = [4, 3, 2, 1],b = 11,计算完后C = [0, 1, 1, 2],逆置完后C = [2, 1, 1, 0]

5、前缀和与差分

5.1 前缀和

795. 前缀和 - AcWing题库

#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], dp[N], n, m, l, r;
int main()
{
    scanf("%d %d", &n, &m);
    for(int i = 1;i <= n;i++) 
    {
        scanf("%d", &q[i]);
        dp[i] = dp[i - 1] + q[i];
    }
    while(m--)
    {
        scanf("%d %d", &l, &r);
        printf("%d\n", dp[r] - dp[l - 1]);
    }
    return 0;
}

5.2 子矩阵的和

796. 子矩阵的和 - AcWing题库

#include<iostream>
using namespace std;
const int N = 1010;
int m, n, q, x1, y1, x2, y2;
int a[N][N], dp[N][N];
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]);
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1] + a[i][j];
        }
    }
    while(q--)
    {
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", dp[x2][y2] - dp[x2][y1 - 1] - dp[x1 - 1][y2] + dp[x1 - 1][y1 - 1]);
    }
    return 0;
}

5.3 差分

797. 差分 - AcWing题库

差分就是前缀和的逆过程

给定一个原数组a,我们要求差分数组b,使得a数组是b数组的前缀和,即a[i]=b[1]+b[2]+...+b[i]

  1. 获得差分数组

很容易就能想到b[i] = a[i] - a[i - 1]

  1. 使用差分数组

我们要对给定的区间[l, r]内的每个a数组加上c,按照正常做法,编译a数组的区间[l, r],每个数加上c,时间复杂度是O(m * n)。

此时就可以使用差分数组了。因为a数组是b数组的前缀和,所以我们会发现对a数组的区间[l, r]每个数加上c,实际上只需要对b[l] += c,b[r + 1] -= c

注意,因为我们对b进行了操作,但没对a操作,所以此时a数组名义上是b数组的前缀和,但是数值上已经不是了,所以m次操作完成后,要重新求一遍a

#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];
int m, n, l, r, c;
int main()
{
    scanf("%d %d", &n, &m);
    for(int i = 1;i <= n;i++) scanf("%d", &a[i]);
    for(int i = 1;i <= n;i++) b[i] = a[i] - a[i - 1];
    while(m--)
    {
        scanf("%d %d %d", &l, &r, &c);
        b[l] += c;
        b[r + 1] -= c;
    }
    for(int i = 1;i <= n;i++)
    {
        a[i] = a[i - 1] + b[i];
        printf("%d ", a[i]);
    }
    return 0;
}

我们会发现,两步是可以使用同一个函数的。第二步是在a数组的区间[l, r]上插入值c。我们可以把第一步抽象为,假设a数组全为0,那么b数组也全为0,现在需要往a数组的[i, i]区间插入数值a[i],因为a数组并不是真的全为0,所以此时a数组既是b数组名义上的前缀和,也是数值上的前缀和

#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];
int m, n, l, r, c;
void insert(int l, int r, int c)
{
    b[l] += c;
    b[r + 1] -= c;
}
int main()
{
    scanf("%d %d", &n, &m);
    for(int i = 1;i <= n;i++) 
    {
        scanf("%d", &a[i]);
        insert(i, i, a[i]);
    }
    while(m--)
    {
        scanf("%d %d %d", &l, &r, &c);
        insert(l, r, c);
    }
    for(int i = 1;i <= n;i++)
    {
        a[i] = a[i - 1] + b[i];
        printf("%d ", a[i]);
    }
    return 0;
}

5.4 差分矩阵

798. 差分矩阵 - AcWing题库

这个就是二维差分,与前面的差分是类似的

#include<iostream>
using namespace std;
const int N = 1010;
int n, m, q, x1, y1, x2, y2, c;
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()
{
    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]);
            insert(i, j, i, j, a[i][j]);
        }
    }
    while(q--)
    {
        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++)
        {
            a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i][j];
            printf("%d ", a[i][j]);
        }
        printf("\n");
    }
    return 0;
}

6、双指针算法

6.1 最长连续不重复的子序列

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

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N], b[N];
int main()
{
    scanf("%d", &n);
    for(int i = 0;i < n;i++) scanf("%d", &a[i]);
    int ans = 0;
    for(int i = 0,j = 0;i < n;i++)
    {
        b[a[i]]++;
        while(b[a[i]] > 1)
        {
            b[a[j]]--;
            j++;
        }
        ans = max(ans, i - j + 1);
    }
    printf("%d", ans);
    return 0;
}

6.2 数组元素的目标和

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

#include<iostream>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n, m, x;
ll a[N], b[N];
int main()
{
    scanf("%d %d %d", &n, &m, &x);
    for(int i = 0;i < n;i++) scanf("%lld", &a[i]);
    for(int i = 0;i < m;i++) scanf("%lld", &b[i]);
    int i = 0,j = m - 1;
    while(i < n && j >= 0)
    {
        long long sum = a[i] + b[j];
        if(sum < x) i++;
        else if(sum > x) j--;
        else 
        {
            printf("%d %d\n", i, j);
            i++, j--;
        }
    }
    return 0;
}

6.3 判断子序列

2816. 判断子序列 - AcWing题库

#include<iostream>
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 = 0;i < n;i++) scanf("%d", &a[i]);
    for(int i = 0;i < m;i++) scanf("%d", &b[i]);
    int j = 0;
    for(int i = 0;i < m;i++)
    {
        if(j >= n) break;
        if(a[j] == b[i]) j++;
    }
    if(j == n) printf("Yes");
    else printf("No");
    return 0;
}

7、位运算

801. 二进制中1的个数 - AcWing题库

位运算中,主要是两种问题

  1. n的二进制表示中第k位是几 n >> k & 1,例如n=15,二进制是1010,左边是高位,右边是低位,下标是3210

  2. lowbit(x):返回x的最后一位1,例如x=15,二进制是1010,lowbit(x)=10

lowbit是如何求的呢?x&-x = x&(~x+1)

lowbit可用于统计x的二进制表示中1的个数,每次将x中的一位1减掉,知道x变成0

使用方法一解题:

#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int n, a[N], b[N];
int main()
{
    scanf("%d", &n);
    for(int i = 0;i < n;i++) scanf("%d", &a[i]);
    for(int i = 0;i < n;i++)
    {
        int sum = 0;
        for(int k = 0;k <= 31;k++)
            if((a[i] >> k & 1) == 1) sum++;
        b[i] = sum;
    }
    for(int i = 0;i < n;i++) printf("%d ", b[i]);
    return 0;
}

使用方法二解题:

// 方法二
int lowbit(int x)
{
    return x & -x;
}
int main()
{
    int n;
    scanf("%d", &n);
    while(n--)
    {
        int x;
        scanf("%d", &x);
        int res = 0;
        while(x) x -= lowbit(x), res++;
        cout<<res<<" ";
    }
    cout<<endl;
    return 0;
}

8、离散化

802. 区间和 - AcWing题库

这道题数轴上的数字是[-10^9, 10^9],一共是2*10^9个数,会进行n次插入和m次查询,每次查询会输入一个区间,n和m最大都是10^5,也就是说最多会用到3*10^5个数,这与2*10^9比起来是十分小的,如果我们要开一个2*10^9的数组,显然有很多空间是会浪费掉的。于是,这道题使用了离散化

因为值域太大,所以将用到的下标映射到从0开始的自然数(从1开始也行),映射的过程就称为离散化

我们先开一个数组,将用到的下标全部放进这个数组中,然后再进行离散化

离散化时要注意两个问题:

(1)用到的下标可能是存在重复的,所以要先去重

(2)如何将下标映射到从0开始的自然数?此时需要用到二分,利用其在存放数组中的下标,一个数映射后的值就是其在数组中的下标

具体步骤就是

将{x,c},{l,r}键值对分别放入add,query中,将用到的坐标x、l、r放入alls中

此时alls是一个无序、有重复的数组,并且里面的值大小差异可能是很大的

然后对alls进行排序+去重,这样就可以将alls中的值映射为下标(排序后,x、l、r的相对位置不会发生变化,所以可以进行映射)

数组a存储映射后的值,s存储a的前缀和,因为需要求前缀和,所以a从1开始,只需要修改一下二分的返回值即可

对每一次插入,先找一下x映射后的值,也就是x在alls中的下标,a[k] += c

全部放入a数组后,再对a数组求前缀和

对于每一次询问,先找一下l、r映射后的值,也就是l、r在alls中的下标,s[r] - s[l - 1]

以下面这个样例为例

add {1,2} {3,6} {7,5}

query {1,3} {4,6} {7,8}

alls 1 3 7 1 3 4 6 7 8

排序 + 去重

alls 1 3 4 6 7 8

下标 0 1 2 3 4 5

对插入操作用到的值1 3 7映射后的下标进行插入操作

a 2 6 0 0 5 0

下标 1 2 3 4 5 6

对a求前缀和

s 2 8 8 8 13 13

下标 1 2 3 4 5 6

对于第一次询问{1,3},s[2] - s[0] = 8

对于第二次询问{4,6},s[4] - s[2] = 0

对于第三次询问{7,8},s[6] - s[4] = 5

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int n, m;
int a[N], s[N]; // a是存离散化后的值的数组,s是存a的前缀和
vector<int> alls; // 存需要离散化的元素,x、l、r需要离散化,去重就是对alls去重
vector<PII> add, query; 
// add存要在x位置增加c的键值对
// query存每次询问的左右区间l和r
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()
{
    cin>>n>>m;
    for(int i = 0;i < n;i++)
    {
        int x, c;
        cin>>x>>c;
        add.push_back({x, c});
        alls.push_back(x);
    }
    for(int i = 0;i < m;i++)
    {
        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 : add)
    {
        int x = find(item.first);
        a[x] += item.second;
    }
    
    // 预处理前缀和
    for(int i = 1;i <= alls.size();i++) s[i] = s[i - 1] + a[i];
    
    // 处理询问
    for(auto item : query)
    {
        int l = find(item.first), r = find(item.second);
        cout<<s[r] - s[l - 1]<<endl;
    }
    return 0;
}

这里使用任何一个二分模板均可

9、区间合并

803. 区间合并 - AcWing题库

这道题就是首先将区间全部读入到一个vector<pair<int, int>>中,然后根据区间的左端点对区间进行排序,然后对所有区间进行遍历,遍历过程中将有交集的区间进行合并,合并好的区间放入一个新的数组中

遍历:维护当前区间,遍历后面的区间,看后面区间与当前区间的关系。因为已经按区间左端点进行排序了,所以关系只有3种

#include<iostream>
#include<algorithm>
using namespace std;
typedef pair<int, int> PII;
int n;
vector<PII> segs, res; // segs读入所有区间,res存放合并好的区间
void merge(vector<PII>& segs)
{
    sort(segs.begin(), segs.end());
    int st = -2e9, ed = -2e9; // 最先开始让维护的区间位于最左边,并且第一次更新区间不能放入res
    for(int i = 0;i < n;i++)
    {
        if(ed < segs[i].first)
        {
            if(i != 0) res.push_back({st, ed});
            st = segs[i].first;
            ed = segs[i].second;
        }
        else ed = max(ed, segs[i].second);
    }
    if(st != -2e9) res.push_back({st, ed}); 
    // 最后还需要将最后维护的区间放入,此时要判断一下,因为有可能segs中没有任何区间
    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;
}
相关推荐
Swxctx18 分钟前
Go版数据结构 -【1.2 数据结构的时间与空间复杂度】
数据结构·算法·golang·go版数据结构
应茶茶24 分钟前
动态规划算法:10.路径问题_地下城游戏_C++
c++·算法·动态规划
穷人小水滴29 分钟前
修复 blender 中文输入 BUG (linux/wayland/GNOME/ibus)
linux·c++·开源·bug·blender·wayland·ibus
蟹堡王首席大厨29 分钟前
数模方法论-蒙特卡洛法
python·算法·数学建模·matlab·蒙特卡洛
xqhoj34 分钟前
C++学习指南(六)----list
c语言·开发语言·数据结构·c++·学习·算法·list
西柚与蓝莓38 分钟前
662. 二叉树最大宽度 BFS 力扣
开发语言·python·算法
Minuw43 分钟前
SQLiteHelper
c++
怀九日1 小时前
C++(学习)2024.9.23
开发语言·c++·学习·算法·运算符·重载
么耶咩_5151 小时前
双向链表
数据结构·链表
程序猿阿伟1 小时前
《C++编程魔法:构建绿色主题的奇幻游戏世界》
开发语言·c++·游戏