AcWing-算法基础课(第一章、第二章)

本文围绕C++展开,介绍了快速排序、归并排序等算法,以及二分、高精度运算等方法。还涉及前缀和、差分、双指针等技巧,和单链表、双链表等数据结构。同时给出了各知识点对应的习题,帮助理解掌握。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

快速排序

  • 快速排序的主要思路
    • 找一个数作为分界点,取中间位置的数作为分界点,可以避免考虑边界问题
    • 调整区间,使得分界点左边的值都小于等于分界点,分界点右边的值都大于等于分界点
    • 递归处理左右两部分区间

时间复杂度:期望是O(nlogn)。(这里的logn是以2为底的)

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int q[N];
int n;

void quick_sort(int q[], int l, int r)
{
    if (l >= r)// 当区间只剩一个数时则不需要继续了
        return;
        
    int x = q[(l + r) / 2];// 取中间位置的数作为分界点,可以避免考虑边界问题
    int i = l - 1;// 后面的判断中我们是先进行右移,再判断,所以先-1
    int j = r + 1;// 后面的判断中我们是先进行左移,再判断,所以先+1
    
    // 调整区间,使得分界点左边的值都小于等于分界点,分界点右边的值都大于等于分界点
    while (i < j)
    {
        do i++; while (q[i] < x);
        do j--; while (q[j] > x);
        
        if (i < j)// 当i和j还没有相遇时,则交换两边的数值
            swap(q[i], q[j]);
    }
    
    quick_sort(q, l, j);// 递归处理左区间
    quick_sort(q, j + 1, r);// 递归处理右区间
}

int main()
{
    scanf("%d", &n);
    
    for (int i = 0; i < n; i++)
        scanf("%d", &q[i]);
    
    quick_sort(q, 0, n - 1);// 对数组中的数进行快速排序
    
    for (int i = 0; i < n; i++) 
        printf("%d ", q[i]);
    puts("");
    
    return 0;
}

归并排序

  • 归并排序的主要思路
    • 确定分界点,以中间点作为分界点
    • 然后递归排序左边和右边
    • 归并排好序的左边和右边(合二为一)

时间复杂度:O(nlogn),一共有logn层,每层是遍历n个数。(这里的logn是以2为底的)

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int q[N];
int t[N];// 合并左右两部分排好序的区间时,所需要用到的临时数组
int n;

void merge_sort(int q[], int l, int r)
{
    if (l >= r)// 当区间只剩一个数时则不需要继续了
        return;
        
    int mid = (l + r) / 2;// 取中间点作为分界点
    int i = l;// 左区间的开头
    int j = mid + 1;// 右区间的开头
    
    merge_sort(q, l, mid);// 递归处理左区间
    merge_sort(q, mid + 1, r);// 递归处理右区间
    
    int k = 0;// 存储临时数组的下标
    
    // 递归的终点是左区间和右区间中的数都是只有一个数
    // 把排好序的左区间和右区间中的数,按从小到大放到临时数组中,即归并两个区间
    while (i <= mid && j <= r)
    {
        if (q[i] <= q[j]) 
            t[k++] = q[i++];
        else 
            t[k++] = q[j++];
    }
    while (i <= mid) t[k++] = q[i++];// 要么是把左区间中剩下的数放到临时数组中
    while (j <= r) t[k++] = q[j++];// 要么是把右区间中剩下的数放到临时数组中
    
    // 把临时数组中的数放回到原数组中
    for (int i = l, j = 0; i <= r; i++)
        q[i] = t[j++];
}

int main()
{
    scanf("%d", &n);
    
    for (int i = 0; i < n; i++) 
        scanf("%d", &q[i]);
        
    merge_sort(q, 0, n - 1);// 对数组中的数进行归并排序
    
    for (int i = 0; i < n; i++) 
        printf("%d ", q[i]);
        
    return 0;
}

二分

二分主要用来处理如下问题:给定一个区间,然后把区间划分为左边和右边,左边全都满足同一种性质,右边全都满足同一种性质。然后通过二分,我们可以找到左边的右边界或者找到右边的左边界

整数二分

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int q[N];
int n;
int m;

int main()
{
    scanf("%d %d", &n, &m);
    
    for (int i = 0; i < n; i++)// 读入从小到大排好序的整数
        scanf("%d", &q[i]);
    
    while (m--)// m次询问
    {
        int x;
        scanf("%d", &x);// 输入需要找到的数
        
        int l = 0;// 二分区间的左边界
        int r = n - 1;// 二分区间的右边界
        
        while (l < r)// 找到第一个大于或大于等于x的数
        {
            int mid = (l + r) / 2;
            if (q[mid] >= x)
                r = mid;
            else
                l = mid + 1;
        }
        
        if (q[l] != x)// 判断二分找到的边界的值是否为需要找到的数
            printf("-1 -1\n");
        else
        {
            printf("%d ", l);// 输出需要找到的数的起始下标
            
            l = 0;// 二分区间的左边界
            r = n - 1;// 二分区间的右边界
            
            while (l < r)// 找到第一个小于或小于等于x的数
            {
                int mid = (l + r + 1) / 2;
                /*
                    当l与r差1时,(l+r)/2的结果一定会等于l,
                    此时会导致下一行代码执行后,发生死循环,
                    因此上一行代码必须改为(l+r+1)/2
                */
                if (q[mid] <= x) l = mid;
                else 
                    r = mid - 1;
            }
            
            printf("%d\n", l);// 输出需要找到的数的终止下标
        }
    }
    return 0;
}

浮点数二分

开平方

#include <iostream>

using namespace std;

int main()
{
    double x;
    scanf("%lf", &x);// 输入一个大于0的数
    
    double tmp = 1;
    double l = 0;// 二分区间的左边界
    double r = max(tmp, x);// 当x小于1时,二分区间的右边界需要取1
    
    // 当右边界减去左边界小于等于一个极小的数时,则可以视为我们找到了边界值,跳出循环
    while (r - l > 1e-8)// double默认保留6位小数展示,因此这里取极小值为1e-8
    {
        double mid = (l + r) / 2;
        if (mid * mid >= x) r = mid;
        else l = mid;
    }
    
    printf("%lf\n", l);// 边界值即为x开平方的值
    
    return 0;
}

高精度

大整数:大整数的位数长度一般大于1000位

两个大整数的相加

  • 朴素法
#include <iostream>
#include <vector>

using namespace std;

vector<int> add(vector<int>& A, vector<int>& B)// 加上引用是为了提高效率,如果不加&,则会把整个数组拷贝一遍
{
    vector<int> C;
    int t = 0;// 最开始两个最低位的数相加时,由前两位数相加后得出的进位数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)// 当两个大整数长度相同,且最高位相加有进位时,则还需要把该进位放进结果中。该进位一定为1
        C.push_back(t);
    
    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]);
    puts("");
    
    return 0;
}
  • 压位法
#include <iostream>
#include <vector>

using namespace std;

const int BASE = 1e9;// 压9位

vector<int> add(vector<int>& A, vector<int>& B)// 加上引用是为了提高效率,如果不加&,则会把整个数组拷贝一遍
{
    vector<int> C;
    int t = 0;// 最开始两个最低位的数相加时,由前两位数相加后得出的进位数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 % BASE);// 压9位
        t /= BASE;// 压9位
    }
    
    if (t)// 当两个大整数长度相同,且最高位相加有进位时,则还需要把该进位放进结果中。该进位一定为1
        C.push_back(t);
        
    return C;
}

int main()
{
    vector<int> A, B;// 用于存储大整数
    string a, b;
    
    cin >> a >> b;// 以字符串类型读入大整数
    
    for (int i = a.size() - 1, s = 0, t = 1, cnt = 0; i >= 0; i--)// 低位先进入数组
    {
        s += (a[i] - '0') * t;
        t *= 10;
        cnt++;
        if (cnt == 9 || i == 0)// 压9位
        {
            A.push_back(s);
            s = 0, t = 1, cnt = 0;
        }
    }
    for (int i = b.size() - 1, s = 0, t = 1, cnt = 0; i >= 0; i--)// 低位先进入数组
    {
        s += (b[i] - '0') * t;
        t *= 10;
        cnt++;
        if (cnt == 9 || i == 0)// 压9位
        {
            B.push_back(s);
            s = 0, t = 1, cnt = 0;
        }
    }
    
    vector<int> C = add(A, B);
    
    cout << C.back();// 最高位不足9位时前面不需要补0,直接打印输出
    
    for (int i = C.size() - 2; i >= 0; i--)// 高位先打印输出
        printf("%09d", C[i]);// 每一位不足9位时前面需要补0
    puts("");
    
    return 0;
}

两个大整数的相减

#include <iostream>
#include <vector>

using namespace std;

bool cmp(vector<int>& A, vector<int>& B)
{
    if (A.size() != B.size())// 若A和B的长度不一样,则直接比较A和B的长度
        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;// 当A和B相等,则可以返回true
}

vector<int> sub(vector<int>& A, vector<int>& B)
{
    vector<int> C;
    int t = 0;// 最开始两个最低位的数相减时,前面的借位为0
    
    for (int i = 0; i < A.size(); i++)
    {
        t = A[i] - t;// A的数先减去借位
        
        if (i < B.size())// 判断B是否有数可减
            t = t - B[i];
            
        C.push_back((t + 10) % 10);// 同时处理A[i]-t-B[i]小于或者大于等于0的情况
        
        if (t < 0)
            t = 1;// 当A[i]-t-B[i]小于0,则说明需要向高位借一位数
        else 
            t = 0;
    }
    
    for (int i = C.size() - 1; i > 0; i--)
        if (!C.back())
            C.pop_back();// 需要把前导0去掉,但是最少要保留一个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,则直接计算A-B的值,否则就先计算B-A的值,然后在前面加上一个负号
    {
        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]);
    }
    
    puts("");
    return 0;
}

一个大整数与一个整数的相乘

#include <iostream>
#include <vector>

using namespace std;

vector<int> mul(vector<int>& A, int b)
{
    vector<int> C;
    int t = 0;// 最开始A的最低位乘以b时,上一步处理过的t为0
    
    for (int i = 0; i < A.size() || t; i++)// 当t不为0时,则还需要把t的数值存到结果中
    {
        // 把A的每一位数乘以b,再加上上一步处理过的t
        if (i < A.size())
            t = A[i] * b + t;
            
        C.push_back(t % 10);
        
        t /= 10;
    }
    
    while (C.size() > 1 && !C.back())// 需要把前导0去掉,但是最少要保留一个0
        C.pop_back();
        
    return C;
}

int main()
{
    string a;
    vector<int> A;// 用于存储大整数
    int b;
    
    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--)// 高位先打印输出
        printf("%d", C[i]);
        
    puts("");
    return 0;
}

一个大整数除以一个整数

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

using namespace std;

vector<int> div(vector<int> A, int b, int& r)// 这里添加引用是为了把余数的值返回到main()中
{
    vector<int> C;
    r = 0;// 最开始A的最高位除以b时,上一步得到的余数为0
    
    for (int i = A.size() - 1; i >= 0; i--)
    {
        r = r * 10 + A[i];// 用上一步得到的余数乘以10再加上A的这一位,继续求商
        C.push_back(r / b);// 直接把商保存到结果中
        r %= b;// 求出余数
    }
    
    reverse(C.begin(), C.end());// 需要反转一下
    
    while (C.size() > 1 && !C.back())// 需要把前导0去掉,但是最少要保留一个0
        C.pop_back();
        
    return C;
}

int main()
{
    string a;
    vector<int> A;// 用于存储大整数
    int b;
    
    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;
}

前缀和

  • 给定某个数组,然后得到该数组的前缀和数组

前缀和数组的作用:能快速求出某个区间内的数值总和

一维

#include <iostream>

using namespace std;

const int N = 1e5 + 10; 

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

int main()
{
    scanf("%d %d", &n, &m);
    
    // 输入n个数,前缀和问题一般从数组下标为1开始存值,可以避免边界问题
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
        
    // 公式:第i-1个数的前缀和 加上 第i个数的值 等于 第i个数的前缀和
    for (int i = 1; i <= n; i++) 
        s[i] = s[i - 1] + a[i];
    
    while (m--)// m次询问
    {
        int l, r;
        scanf("%d %d", &l, &r);
        printf("%d\n", s[r] - s[l - 1]);// 利用前缀和数组,可直接求出某个区间数值的总和
    }
    
    return 0;
}

二维

#include <iostream>

using namespace std;

const int N = 1e3 + 10;

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

int main()
{
    scanf("%d %d %d", &n, &m, &q);
    
    // 输入n*m个数,前缀和问题一般从数组下标为1开始存值,可以避免边界问题
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            scanf("%d", &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--)// q次询问
    {
        int x1, y1;// 子矩阵的左上角坐标
        int x2, y2;// 子矩阵的右下角坐标
        scanf("%d %d %d %d", &x1, &y1, &x2, &y2);
        
        // 利用二维前缀和数组,可直接求出某个子矩阵数值的总和
        printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
    }
    
    return 0;
}

差分

  • 给定某个数组A,然后得到该数组A的差分数组B,然后可以利用差分数组B来处理对数组A中某个区间内都加上某个常数的问题

一维

差分:

  • 存在数组(数组下标从1开始,方便处理问题):a[1],a[2],a[3],…,a[n-1],a[n]
  • 我们需要构造一个数组:b[1],b[2],b[3],…,b[n-1],b[n]、使得:a[i] = b[1] + b[2] + b[3] + … + b[i-1] + b[i]
  • 则可推出:b[n]=a[n]-a[n - 1]
  • a数组是b数组的前缀和,则我们称b数组为a数组的差分

差分能解决什么问题

  • 假如我们要实现,把数组a中[l,r]区间的值都加上一个常数c,即:a[l]+c、a[l+1]+c、…、a[r-1]+c、a[r]+c。如果循环一遍数组a实现的话,则时间复杂度是O(n)的
  • 则我们直接对数组b进行如下操作即可实现,如下操作的时间复杂度是O(1)的
    • b[l]+c
    • b[r+1]-c
#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int a[N];// 可以看成是前缀和数组
int b[N];// 可以看成是差分数组
int n, m;

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]);
        
    for (int i = 1; i <= n; i++)
        /*
            假设前缀和数组为空,
            利用对差分数组的操作,可以实现对前缀和数组中某个区间内都加上某个常数,
            此时在操作差分数组的时候,就能实现差分数组的初始化
        */
        insert(i, i, a[i]);
    
    while (m--)// m次询问
    {
        int l, r, c;
        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];
        
    for (int i = 1; i <= n; i++) 
        printf("%d ", a[i]);
    puts("");
    
    return 0;
}

二维

#include <iostream>

using namespace std;

const int N = 1e3 + 10;

int a[N][N];// 可以看成是前缀和数组
int b[N][N];// 可以看成是差分数组
int n, m, q;

// 操作差分数组的过程,类比一维的
void insert(int x1, int y1, int x2, int y2, int c)
{
    b[x1][y1] += c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y1] -= 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]);
    
    /*
        假设前缀和数组为空,
		利用对差分数组的操作,可以实现对前缀和数组中某个子矩阵内都加上某个常数,
		此时在操作差分数组的时候,就能实现差分数组的初始化
	*/
   	for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
			insert(i, j, i, j, a[i][j]);
    
	while (q--)// 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++)
            a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i][j];
    
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
            printf("%d ", a[i][j]);
        puts("");
    }
    return 0;
}

双指针算法

双指针算法的核心思想是:让用双重循环实现的时间复杂度为O(n^2)的程序,可变成用双指针实现的时间复杂度为O(n)的程序

#include <iostream>
#include <cstring>

using namespace std;

int main()// 下面代码可以实现:输入包含空格的字符串,然后分别输出被空格隔开的单词
{
	char str[1000];

	cin.getline(str, 100);

	int n = strlen(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;
}

最长连续不重复子序列

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;

int a[N];
int s[N];// 用于记录j到i区间内的数是否存在重复的情况
int n;

int main()
{
    scanf("%d", &n);
    
    for (int i = 0; i < n; i++) 
        scanf("%d", &a[i]);

    int res = 0;
    
    for (int i = 0, j = 0; i < n; i++) 
    {
        // 用a[i]这个数的数值当做s数组的下标,来记录a[i]这个数的个数
        s[a[i]]++;
        
        // 当s[a[i]]>1时,则表示j到i区间内的数存在重复的情况
        while (s[a[i]] > 1)
        {
            s[a[j]]--;
            
            // j往右移动,直至s[a[i]]<=1,表示j到i区间内的数不存在重复的情况
            j++;
        }
        
        res = max(res, i - j + 1);
    }
    
    printf("%d\n", res);
    return 0;
}

位运算

#include <iostream>

using namespace std;

int main()
{
    int n = 14;
    
    // 输出n的二进制表示
    for (int i = 3; i >= 0; i--)
        printf("%d ", (n >> i) & 1);
    
    printf("\n");
    return 0;
}

二进制中1的个数

#include <iostream>

using namespace std;

// lowbit(x)这个函数是我们自定义的,能够实现返回 x 的最后一位1及其后面的所有0
int lowbit(int x)
{
    // x & -x 和 x & (~x + 1) 的作用一样
    return x & (~x + 1);
}

int main()
{
    int n;
    scanf("%d", &n);
    while (n--)
    {
        int x;
        scanf("%d", &x);
        
        int cnt = 0;
        
        while (x)
        {
            x -= lowbit(x);// x每次减去最后一个1
            cnt++;// 统计x二进制表示中1的个数
        }
        
        printf("%d ", cnt);
    }
    
    puts("");
    return 0;
}

离散化

存在一些数,这些数的值域比较大,数的个数比较少,但是需要把这些数当作某个数组的下标来处理问题时,我们就需要用到离散化的方式处理。因为我们不可能把值域很大的数值作为数组的下标

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

using namespace std;

const int N = 3e5 + 10;// 所用到的区间个数最大为30万个

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

vector<int> alls;// 存储所有的坐标值

vector<pair<int, int>> add;// 存储添加操作
vector<pair<int, int>> query;// 存储区间的左右两个边界

int find(int x)
{
    int l = 0;// 二分区间的左边界
    int r = alls.size() - 1;// 二分区间的右边界
    while (l < r)
    {
        int mid = (l + r) / 2;
        if (alls[mid] >= x) 
            r = mid;
        else
            l = mid + 1;
    }
    return l + 1;// 处理前缀和的问题,下标从1开始
}

// 返回类型是一个迭代器,可以看成是指针
vector<int>::iterator unique(vector<int>& alls)
{
    int j = 0;
    for (int i = 0; i < alls.size(); i++)
        if (!i || alls[i] != alls[i - 1])
            alls[j++] = alls[i];
    return alls.begin() + j;
}

int main()
{
    scanf("%d %d", &n, &m);
    while (n--)
    {
        int x, c;
        scanf("%d %d", &x, &c);
        add.push_back({x, c});// 存储添加操作
        
        alls.push_back(x);// 存储坐标值
    }
    while (m--)
    {
        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());
    // alls.erase(unique(alls), alls.end());// 这里使用了自己实现的unique函数
    
    for (auto t : add)
    {
        int x = find(t.first);// 找到坐标值映射到alls数组的下标
        a[x] += t.second;
    }
    
    for (int i = 1; i <= alls.size(); i++)// 得到前缀和数组
        s[i] = s[i - 1] + a[i];
        
    for (auto t : query)
    {
        int l = find(t.first);// 找到坐标值映射到alls数组的下标
        int r = find(t.second);// 找到坐标值映射到alls数组的下标
        printf("%d\n", s[r] - s[l - 1]);
    }
    
    return 0;
}

区间合并

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

using namespace std;

typedef pair<int, int> PII;

vector<PII> seg;// 存储所有区间

void merge(vector<PII>& seg)
{
    vector<PII> res;
    
    // pair是双关键字进行比较,先比较first,再比较second
    // 对所有的区间按照左端点从小到大进行排序
    sort(seg.begin(), seg.end());
    
    int st = -2e9, ed = -2e9;
    for (auto t : seg)
    {
        // 如果当前记录的区间的右端点小于正遍历到的区间的左端点,则说明无法合并区间
        if (ed < t.first)
        {
            if (st != -2e9) 
                res.push_back({st, ed});// 把当前记录的区间添加到结果中
            
            st = t.first;// 更新当前记录的区间的左端点
            ed = t.second;// 更新当前记录的区间的右端点
        }
        else
            ed = max(ed, t.second);// 更新当前记录的区间的右端点
    }
    
    // 需要把最后一个记录的区间添加到结果中
    if (st != -2e9) 
        res.push_back({st, ed});
        
    seg = res;
}

int main()
{
    int n;
    scanf("%d", &n);
    while (n--)
    {
        int l, r;
        scanf("%d %d", &l, &r);
        
        seg.push_back({l, r});// 存储所有区间
    }
    
    merge(seg);
    
    printf("%d\n", seg.size());
    
    return 0;
}

习题

快速排序

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int q[N];
int n;

void quick_sort(int q[], int l, int r)
{
    if (l >= r)// 当区间只剩一个数时则不需要继续了
        return;
        
    int x = q[(l + r) / 2];// 取中间位置的数作为分界点,可以避免考虑边界问题
    int i = l - 1;// 后面的判断中我们是先进行右移,再判断,所以先-1
    int j = r + 1;// 后面的判断中我们是先进行左移,再判断,所以先+1
    
    // 调整区间,使得分界点左边的值都小于等于分界点,分界点右边的值都大于等于分界点
    while (i < j)
    {
        do i++; while (q[i] < x);
        do j--; while (q[j] > x);
        
        if (i < j)// 当i和j还没有相遇时,则交换两边的数值
            swap(q[i], q[j]);
    }
    
    quick_sort(q, l, j);// 递归处理左区间
    quick_sort(q, j + 1, r);// 递归处理右区间
}

int main()
{
    scanf("%d", &n);
    
    for (int i = 0; i < n; i++)
        scanf("%d", &q[i]);
    
    quick_sort(q, 0, n - 1);// 对数组中的数进行快速排序
    
    for (int i = 0; i < n; i++) 
        printf("%d ", q[i]);
    puts("");
    
    return 0;
}

第k个数

  • 如果直接用快速排序解法则时间复杂度是O(nlogn),但是我们用以下解法的时间复杂度是O(n)
#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int q[N];
int n, k;

int quick_sort(int l, int r, int k)
{
    if (l >= r)// 当区间只剩一个数时则证明已经找到了第k个小的数
        return q[l];// 直接返回结果
        
    int x = q[(l + r) / 2];// 取中间位置的数作为分界点,可以避免考虑边界问题
    int i = l - 1;// 后面的判断中我们是先进行右移,再判断,所以先-1
    int j = r + 1;// 后面的判断中我们是先进行左移,再判断,所以先+1
    
    // 调整区间,使得分界点左边的值都小于等于分界点,分界点右边的值都大于等于分界点
    while (i < j)
    {
        do i++; while (q[i] < x);
        do j--; while (q[j] > x);
        
        if (i < j)// 当i和j还没有相遇时,则交换两边的数值
            swap(q[i], q[j]);
    }
    
    int sl = j - l + 1;// 计算左区间中数的个数
    
    if (k <= sl)// 判断我们要找的第k个小的数是否在左区间
        // 递归处理左区间
        return quick_sort(l, j, k);
    else 
        // 递归处理右区间,此时我们要找的第k个小的数转化成要找在右区间中第k-sl个小的数
        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]);
        
    printf("%d\n", quick_sort(0, n - 1, k));
    return 0;
}

归并排序

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int q[N];
int t[N];// 合并左右两部分排好序的区间时,所需要用到的临时数组
int n;

void merge_sort(int q[], int l, int r)
{
    if (l >= r)// 当区间只剩一个数时则不需要继续了
        return;
        
    int mid = (l + r) / 2;// 取中间点作为分界点
    int i = l;// 左区间的开头
    int j = mid + 1;// 右区间的开头
    
    merge_sort(q, l, mid);// 递归处理左区间
    merge_sort(q, mid + 1, r);// 递归处理右区间
    
    int k = 0;// 存储临时数组的下标
    
    // 递归的终点是左区间和右区间中的数都是只有一个数
    // 把排好序的左区间和右区间中的数,按从小到大放到临时数组中,即归并两个区间
    while (i <= mid && j <= r)
    {
        if (q[i] <= q[j]) 
            t[k++] = q[i++];
        else 
            t[k++] = q[j++];
    }
    while (i <= mid) t[k++] = q[i++];// 要么是把左区间中剩下的数放到临时数组中
    while (j <= r) t[k++] = q[j++];// 要么是把右区间中剩下的数放到临时数组中
    
    // 把临时数组中的数放回到原数组中
    for (int i = l, j = 0; i <= r; i++)
        q[i] = t[j++];
}

int main()
{
    scanf("%d", &n);
    
    for (int i = 0; i < n; i++) 
        scanf("%d", &q[i]);
        
    merge_sort(q, 0, n - 1);// 对数组中的数进行归并排序
    
    for (int i = 0; i < n; i++) 
        printf("%d ", q[i]);
        
    return 0;
}

逆序对的数量

#include <iostream>

using namespace std;

typedef long long LL;

const int N = 1e5 + 10;

int q[N];
int t[N];// 合并左右两部分排好序的区间时,所需要用到的临时数组
int n;
LL cnt;// 结果值最大会超过int类型,所以要用long long类型

void merge_sort(int l, int r)
{
    if (l >= r)// 当区间只剩一个数时则不需要继续了
        return;
    
    int mid = (l + r) / 2;// 取中间点作为分界点
    int i = l;// 左区间的开头
    int j = mid + 1;// 右区间的开头
    
    merge_sort(l, mid);// 递归处理左区间
    merge_sort(mid + 1, r);// 递归处理右区间
    
    int k = 0;// 存储临时数组的下标
    
    // 递归的终点是左区间和右区间中的数都是只有一个数
    // 把排好序的左区间和右区间中的数,按从小到大放到临时数组中,即归并两个区间
    while (i <= mid && j <= r)
    {
        if (q[i] <= q[j]) 
            t[k++] = q[i++];
        // 当q[i]>q[j]时,说明q[i~mid]的这些数都与当前的q[j]形成逆序对
        else
        {
            t[k++] = q[j++];
            cnt += mid - i + 1;// 计算逆序对数量
        }
    }
    while (i <= mid) t[k++] = q[i++];// 要么是把左区间中剩下的数放到临时数组中
    while (j <= r) t[k++] = q[j++];// 要么是把右区间中剩下的数放到临时数组中
    
    // 把临时数组中的数放回到原数组中
    for (int i = l, j = 0; i <= r; i++)
        q[i] = t[j++];
}

int main()
{
    scanf("%d", &n);
    
    for (int i = 0; i < n; i++) 
        scanf("%d", &q[i]);
        
    // 对数组中的数进行归并排序,在该过程中可以计算逆序对的数量
    merge_sort(0, n - 1);
    
    printf("%lld\n", cnt);
    return 0;
}

数的范围

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int q[N];
int n;
int m;

int main()
{
    scanf("%d %d", &n, &m);
    
    for (int i = 0; i < n; i++)// 读入从小到大排好序的整数
        scanf("%d", &q[i]);
    
    while (m--)// m次询问
    {
        int x;
        scanf("%d", &x);// 输入需要找到的数
        
        int l = 0;// 二分区间的左边界
        int r = n - 1;// 二分区间的右边界
        
        while (l < r)// 找到第一个大于或大于等于x的数
        {
            int mid = (l + r) / 2;
            if (q[mid] >= x)
                r = mid;
            else
                l = mid + 1;
        }
        
        if (q[l] != x)// 判断二分找到的边界的值是否为需要找到的数
            printf("-1 -1\n");
        else
        {
            printf("%d ", l);// 输出需要找到的数的起始下标
            
            l = 0;// 二分区间的左边界
            r = n - 1;// 二分区间的右边界
            
            while (l < r)// 找到第一个小于或小于等于x的数
            {
                int mid = (l + r + 1) / 2;
                /*
                    当l与r差1时,(l+r)/2的结果一定会等于l,
                    此时会导致下一行代码执行后,发生死循环,
                    因此上一行代码必须改为(l+r+1)/2
                */
                if (q[mid] <= x) l = mid;
                else 
                    r = mid - 1;
            }
            
            printf("%d\n", l);// 输出需要找到的数的终止下标
        }
    }
    return 0;
}

数的三次方根

#include <iostream>

using namespace std;

int main()
{
    double x;
    scanf("%lf", &x);
    
    double l = -10000;// 二分区间的左边界
    double r = 10000;// 二分区间的右边界
    
    // 当右边界减去左边界小于等于一个极小的数时,则可以视为我们找到了边界值,跳出循环
    while (r - l > 1e-8)// double默认保留6位小数展示,因此这里取极小值为1e-8
    {
        double mid = (l + r) / 2;
        if (mid * mid * mid >= x) r = mid;
        else l = mid;
    }
    
    printf("%lf\n", l);// 边界值即为x开3次方的值
    
    return 0;
}

高精度加法

  • 朴素法
#include <iostream>
#include <vector>

using namespace std;

vector<int> add(vector<int>& A, vector<int>& B)// 加上引用是为了提高效率,如果不加&,则会把整个数组拷贝一遍
{
    vector<int> C;
    int t = 0;// 最开始两个最低位的数相加时,由前两位数相加后得出的进位数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)// 当两个大整数长度相同,且最高位相加有进位时,则还需要把该进位放进结果中。该进位一定为1
        C.push_back(t);
    
    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]);
    puts("");
    
    return 0;
}

高精度减法

#include <iostream>
#include <vector>

using namespace std;

bool cmp(vector<int>& A, vector<int>& B)
{
    if (A.size() != B.size())// 若A和B的长度不一样,则直接比较A和B的长度
        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;// 当A和B相等,则可以返回true
}

vector<int> sub(vector<int>& A, vector<int>& B)
{
    vector<int> C;
    int t = 0;// 最开始两个最低位的数相减时,前面的借位为0
    
    for (int i = 0; i < A.size(); i++)
    {
        t = A[i] - t;// A的数先减去借位
        
        if (i < B.size())// 判断B是否有数可减
            t = t - B[i];
            
        C.push_back((t + 10) % 10);// 同时处理A[i]-t-B[i]小于或者大于等于0的情况
        
        if (t < 0)
            t = 1;// 当A[i]-t-B[i]小于0,则说明需要向高位借一位数
        else 
            t = 0;
    }
    
    for (int i = C.size() - 1; i > 0; i--)
        if (!C.back())
            C.pop_back();// 需要把前导0去掉,但是最少要保留一个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,则直接计算A-B的值,否则就先计算B-A的值,然后在前面加上一个负号
    {
        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]);
    }
    
    puts("");
    return 0;
}

高精度乘法

#include <iostream>
#include <vector>

using namespace std;

vector<int> mul(vector<int>& A, int b)
{
    vector<int> C;
    int t = 0;// 最开始A的最低位乘以b时,上一步处理过的t为0
    
    for (int i = 0; i < A.size() || t; i++)// 当t不为0时,则还需要把t的数值存到结果中
    {
        // 把A的每一位数乘以b,再加上上一步处理过的t
        if (i < A.size())
            t = A[i] * b + t;
            
        C.push_back(t % 10);
        
        t /= 10;
    }
    
    while (C.size() > 1 && !C.back())// 需要把前导0去掉,但是最少要保留一个0
        C.pop_back();
        
    return C;
}

int main()
{
    string a;
    vector<int> A;// 用于存储大整数
    int b;
    
    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--)// 高位先打印输出
        printf("%d", C[i]);
        
    puts("");
    return 0;
}

高精度除法

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

using namespace std;

vector<int> div(vector<int> A, int b, int& r)// 这里添加引用是为了把余数的值返回到main()中
{
    vector<int> C;
    r = 0;// 最开始A的最高位除以b时,上一步得到的余数为0
    
    for (int i = A.size() - 1; i >= 0; i--)
    {
        r = r * 10 + A[i];// 用上一步得到的余数乘以10再加上A的这一位,继续求商
        C.push_back(r / b);// 直接把商保存到结果中
        r %= b;// 求出余数
    }
    
    reverse(C.begin(), C.end());// 需要反转一下
    
    while (C.size() > 1 && !C.back())// 需要把前导0去掉,但是最少要保留一个0
        C.pop_back();
        
    return C;
}

int main()
{
    string a;
    vector<int> A;// 用于存储大整数
    int b;
    
    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;
}

前缀和

#include <iostream>

using namespace std;

const int N = 1e5 + 10; 

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

int main()
{
    scanf("%d %d", &n, &m);
    
    // 输入n个数,前缀和问题一般从数组下标为1开始存值,可以避免边界问题
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
        
    // 公式:第i-1个数的前缀和 加上 第i个数的值 等于 第i个数的前缀和
    for (int i = 1; i <= n; i++) 
        s[i] = s[i - 1] + a[i];
    
    while (m--)// m次询问
    {
        int l, r;
        scanf("%d %d", &l, &r);
        printf("%d\n", s[r] - s[l - 1]);// 利用前缀和数组,可直接求出某个区间数值的总和
    }
    
    return 0;
}

子矩阵的和

#include <iostream>

using namespace std;

const int N = 1e3 + 10;

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

int main()
{
    scanf("%d %d %d", &n, &m, &q);
    
    // 输入n*m个数,前缀和问题一般从数组下标为1开始存值,可以避免边界问题
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            scanf("%d", &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--)// q次询问
    {
        int x1, y1;// 子矩阵的左上角坐标
        int x2, y2;// 子矩阵的右下角坐标
        scanf("%d %d %d %d", &x1, &y1, &x2, &y2);
        
        // 利用二维前缀和数组,可直接求出某个子矩阵数值的总和
        printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
    }
    
    return 0;
}

差分

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int a[N];// 可以看成是前缀和数组
int b[N];// 可以看成是差分数组
int n, m;

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]);
        
    for (int i = 1; i <= n; i++)
        /*
            假设前缀和数组为空,
            利用对差分数组的操作,可以实现对前缀和数组中某个区间内都加上某个常数,
            此时在操作差分数组的时候,就能实现差分数组的初始化
        */
        insert(i, i, a[i]);
    
    while (m--)// m次询问
    {
        int l, r, c;
        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];
        
    for (int i = 1; i <= n; i++) 
        printf("%d ", a[i]);
    puts("");
    
    return 0;
}

差分矩阵

#include <iostream>

using namespace std;

const int N = 1e3 + 10;

int a[N][N];// 可以看成是前缀和数组
int b[N][N];// 可以看成是差分数组
int n, m, q;

// 操作差分数组的过程,类比一维的
void insert(int x1, int y1, int x2, int y2, int c)
{
    b[x1][y1] += c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y1] -= 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]);
    
    /*
        假设前缀和数组为空,
		利用对差分数组的操作,可以实现对前缀和数组中某个子矩阵内都加上某个常数,
		此时在操作差分数组的时候,就能实现差分数组的初始化
	*/
   	for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
			insert(i, j, i, j, a[i][j]);
    
	while (q--)// 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++)
            a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i][j];
    
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
            printf("%d ", a[i][j]);
        puts("");
    }
    return 0;
}

最长连续不重复子序列

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;

int a[N];
int s[N];// 用于记录j到i区间内的数是否存在重复的情况
int n;

int main()
{
    scanf("%d", &n);
    
    for (int i = 0; i < n; i++) 
        scanf("%d", &a[i]);

    int res = 0;
    
    for (int i = 0, j = 0; i < n; i++) 
    {
        // 用a[i]这个数的数值当做s数组的下标,来记录a[i]这个数的个数
        s[a[i]]++;
        
        // 当s[a[i]]>1时,则表示j到i区间内的数存在重复的情况
        while (s[a[i]] > 1)
        {
            s[a[j]]--;
            
            // j往右移动,直至s[a[i]]<=1,表示j到i区间内的数不存在重复的情况
            j++;
        }
        
        res = max(res, i - j + 1);
    }
    
    printf("%d\n", res);
    return 0;
}

数组元素的目标和

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int a[N], b[N];
int n, m;
int x;// 目标和

int main()
{
    scanf("%d %d %d", &n, &m, &x);
    
    for (int i = 0; i < n; i++)
        scanf("%d", &a[i]);
    for (int i = 0; i < m; i++) 
        scanf("%d", &b[i]);
    
    // 题目保证存在唯一解,所以不用担心会越界
    for (int i = 0, j = m - 1; ; i++)
    {
        while (a[i] + b[j] > x) 
            j--;
            
        if (a[i] + b[j] == x)
        {
            printf("%d %d\n", i, j);
            break;
        }
        
    }
    return 0;
}

判断子序列

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

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

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 i = 0, j = 0;
    while (i < n && j < m)
    {
        if (a[i] == b[j]) i++;
        j++;
    }
    
    // 当i==n时,表示a数组是b数组的子序列
    printf("%s\n", i == n ? "Yes" : "No");
    
    return 0;
}

二进制中1的个数

#include <iostream>

using namespace std;

// lowbit(x)这个函数是我们自定义的,能够实现返回 x 的最后一位1及其后面的所有0
int lowbit(int x)
{
    // x & -x 和 x & (~x + 1) 的作用一样
    return x & (~x + 1);
}

int main()
{
    int n;
    scanf("%d", &n);
    while (n--)
    {
        int x;
        scanf("%d", &x);
        
        int cnt = 0;
        
        while (x)
        {
            x -= lowbit(x);// x每次减去最后一个1
            cnt++;// 统计x二进制表示中1的个数
        }
        
        printf("%d ", cnt);
    }
    
    puts("");
    return 0;
}

区间和

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

using namespace std;

const int N = 3e5 + 10;// 所用到的区间个数最大为30万个

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

vector<int> alls;// 存储所有的坐标值

vector<pair<int, int>> add;// 存储添加操作
vector<pair<int, int>> query;// 存储区间的左右两个边界

int find(int x)
{
    int l = 0;// 二分区间的左边界
    int r = alls.size() - 1;// 二分区间的右边界
    while (l < r)
    {
        int mid = (l + r) / 2;
        if (alls[mid] >= x) 
            r = mid;
        else
            l = mid + 1;
    }
    return l + 1;// 处理前缀和的问题,下标从1开始
}

// 返回类型是一个迭代器,可以看成是指针
vector<int>::iterator unique(vector<int>& alls)
{
    int j = 0;
    for (int i = 0; i < alls.size(); i++)
        if (!i || alls[i] != alls[i - 1])
            alls[j++] = alls[i];
    return alls.begin() + j;
}

int main()
{
    scanf("%d %d", &n, &m);
    while (n--)
    {
        int x, c;
        scanf("%d %d", &x, &c);
        add.push_back({x, c});// 存储添加操作
        
        alls.push_back(x);// 存储坐标值
    }
    while (m--)
    {
        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());
    // alls.erase(unique(alls), alls.end());// 这里使用了自己实现的unique函数
    
    for (auto t : add)
    {
        int x = find(t.first);// 找到坐标值映射到alls数组的下标
        a[x] += t.second;
    }
    
    for (int i = 1; i <= alls.size(); i++)// 得到前缀和数组
        s[i] = s[i - 1] + a[i];
        
    for (auto t : query)
    {
        int l = find(t.first);// 找到坐标值映射到alls数组的下标
        int r = find(t.second);// 找到坐标值映射到alls数组的下标
        printf("%d\n", s[r] - s[l - 1]);
    }
    
    return 0;
}

区间合并

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

using namespace std;

typedef pair<int, int> PII;

vector<PII> seg;// 存储所有区间

void merge(vector<PII>& seg)
{
    vector<PII> res;
    
    // pair是双关键字进行比较,先比较first,再比较second
    // 对所有的区间按照左端点从小到大进行排序
    sort(seg.begin(), seg.end());
    
    int st = -2e9, ed = -2e9;
    for (auto t : seg)
    {
        // 如果当前记录的区间的右端点小于正遍历到的区间的左端点,则说明无法合并区间
        if (ed < t.first)
        {
            if (st != -2e9) 
                res.push_back({st, ed});// 把当前记录的区间添加到结果中
            
            st = t.first;// 更新当前记录的区间的左端点
            ed = t.second;// 更新当前记录的区间的右端点
        }
        else
            ed = max(ed, t.second);// 更新当前记录的区间的右端点
    }
    
    // 需要把最后一个记录的区间添加到结果中
    if (st != -2e9) 
        res.push_back({st, ed});
        
    seg = res;
}

int main()
{
    int n;
    scanf("%d", &n);
    while (n--)
    {
        int l, r;
        scanf("%d %d", &l, &r);
        
        seg.push_back({l, r});// 存储所有区间
    }
    
    merge(seg);
    
    printf("%d\n", seg.size());
    
    return 0;
}

单链表

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int e[N];// 表示节点i的值
int ne[N];// 表示节点i的next指针是多少
int idx;// 存储当前已经用到了哪个节点
int head;// 表示头节点的下标

// 初始化单链表
void init()
{
    head = -1;
    idx = 0;
}

// 将新节点插到头节点
void add_to_head(int x)
{
    e[idx] = x;
    ne[idx] = head;
    head = idx++;
}

// 将新节点插到下标为k的这个点的后面
void add_to_k(int k, int x)
{
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx++;
}

// 将下标为k的点的下一个点删掉
void remove_to_k(int k)
{
    ne[k] = ne[ne[k]];
}

int main()
{
    int n;
    scanf("%d", &n);
    
    init();// 初始化单链表
    
    while (n--) 
    {
        char op;
        int k, x;
        scanf(" %c", &op);// 这里使用scanf读入一定要注意
        if (op == 'H') 
        {
            scanf("%d", &x);
            add_to_head(x);// 将新节点插到头节点
        }
        else if (op == 'I')
        {
            scanf("%d %d", &k, &x);
            add_to_k(k - 1, x);// 将新节点插到下标为k的这个点的后面
        }
        else 
        {
            scanf("%d", &k);
            if (!k)// 当k为0时,则删除头节点
                head = ne[head];
            else 
                remove_to_k(k - 1);// 将下标为k的点的下一个点删掉
        }
    }
    
    // 遍历单链表
    for (int i = head; i != -1; i = ne[i])
        printf("%d ", e[i]);
        
    return 0;
}

双链表

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int e[N];// 表示节点i的值
int l[N];// 表示节点i的左指针
int r[N];// 表示节点i的右指针
int idx;// 存储当前已经用到了哪个节点

// 初始化双链表
void init()
{
    // 下标0为左端点,下标1为右端点    
    r[0] = 1;
    l[1] = 0;
    idx = 2;
}

void add_kr(int k, int x)// 在下标为k的节点的右边插入一个节点
{
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx;
    r[k] = idx++;
}

void remove_k(int k)// 删除下标为k的这个节点
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

int main()
{
    init();// 初始化双链表
    
    int m;
    scanf("%d", &m);
    
    while (m--)// m次询问
    {
        int k, x;
        
        string op;
        cin >> op;
        
        if (op == "L")
        {
            scanf("%d", &x);
            add_kr(0, x);// 在双链表的最左端插入一个节点
        }
        else if (op == "R")
        {
            scanf("%d", &x);
            add_kr(l[1], x);// 在双链表的最右端插入一个节点
        }
        else if (op == "D")
        {
            scanf("%d", &k);
            remove_k(k + 1);// 将第k个插入的节点删除,对应的下标为k+1
        }
        else if (op == "IL")
        {
            scanf("%d %d", &k, &x);
            add_kr(l[k + 1], x);// 在第k个插入的节点的左边插入一个节点,对应的下标为k+1
        }
        else 
        {
            scanf("%d %d", &k, &x);
            add_kr(k + 1, x);// 在第k个插入的节点的右边插入一个节点,对应的下标为k+1
        }
    }
    
    // 从左端点开始进行遍历
    for (int i = r[0]; i != 1; i = r[i])
        printf("%d ", e[i]);
        
    puts("");
    return 0;
}

模拟栈

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int stk[N];// 栈
int t;// 栈顶指针

int main()
{
    int m;
    scanf("%d", &m);
    
    while (m--)
    {
        int x;
        string op;
        cin >> op;
        
        if (op == "push")
        {
            scanf("%d", &x);
            stk[t++] = x;// 向栈顶插入一个数
        }
        else if (op == "pop")
            stk[--t] = 0;// 从栈顶弹出一个数
        else if (op == "empty")
            printf("%s\n", t ? "NO" : "YES");// 判断栈是否为空
        else 
            printf("%d\n", stk[t - 1]);// 查询栈顶元素
    }
    
    return 0;
}

模拟队列

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int queue[N];// 队列
int hh;// 队头
int tt;// 队尾

int main()
{
    // 初始时,队头下标为0,队尾下标为-1
    hh = 0;
    tt = -1;
    
    int m;
    scanf("%d", &m);
    
    while (m--)// m次询问
    {
        int x;
        string op;
        cin >> op;
        if (op == "push")
        {
            scanf("%d", &x);
            queue[++tt] = x;// 在队尾插入一个数
        }
        else if (op == "pop")
            hh++;// 从队头弹出一个数
        else if (op == "empty")
            printf("%s\n", hh > tt ? "YES" : "NO");// 判断队列是否为空
        else 
            printf("%d\n", queue[hh]);// 查询队头元素的值
    }
    
    return 0;
}

单调栈

时间复杂度:O(n)

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int stk[N];// 栈
int tt;// 可以看成是栈顶指针,初值为0

int main()
{
    int n;
    scanf("%d", &n);
    while (n--)
    {
        int x;
        scanf("%d", &x);
        
        // 当栈不为空且栈顶元素大于等于当前的数值时,则弹出栈顶元素
        // 保持栈的单调性
        while (tt && stk[tt] >= x)
            tt--;
            
        printf("%d ", tt ? stk[tt] : -1);
        
        stk[++tt] = x;// 把当前数值加入到栈中
    }
    puts("");
    return 0;
}

单调队列

#include <iostream>

using namespace std;

const int N = 1e6 + 10;

int a[N];
int q[N];// 滑动窗口的数组(用数组模拟队列),这里的q数组记录的值是a数组的下标
int n;
int k;// 滑动窗口的大小

int main()
{
    scanf("%d %d", &n, &k);
    
    for (int i = 0; i < n; i++) 
        scanf("%d", &a[i]);
        
    int hh = 0, tt = -1;// 一开始设定,队头为0,队尾为-1
    
    for (int i = 0; i < n; i++)// 输出滑动窗口中的最小值
    {
        if (q[hh] < i - k + 1)// 判断队头对应的a数组的下标是否已经超出窗口的范围
            hh++;
        
        // 当队列不为空,且队尾对应的a数组的下标的值大于等于当前遍历到的a[i],则弹出队尾元素
        // 保证队列的单调性
        while (hh <= tt && a[q[tt]] >= a[i])
            tt--;
            
        q[++tt] = i;// 这里的q数组记录的值是a数组的下标
        
        // 输出结果时,必须保证已经遍历了前k个数
        if (i >= k - 1) 
            // 当前队列队头对应的a数组的下标的值即为滑动窗口中的最小值
            printf("%d ", a[q[hh]]);
    }
    puts("");
    
    hh = 0, tt = -1;
    
    for (int i = 0; i < n; i++)// 输出滑动窗口中的最大值
    {
        if (q[hh] < i - k + 1) // 判断队头对应的a数组的下标是否已经超出窗口的范围
            hh++;
            
        // 当队列不为空,且队尾对应的a数组的下标的值小于等于当前遍历到的a[i],则弹出队尾元素
        // 保证队列的单调性
        while (hh <= tt && a[q[tt]] <= a[i]) 
            tt--;
            
        q[++tt] = i;// 这里的q数组记录的值是a数组的下标
        
        // 输出结果时,必须保证已经遍历了前k个数
        if (i >= k - 1) 
            // 当前队列队头对应的a数组的下标的值即为滑动窗口中的最大值
            printf("%d ", a[q[hh]]);
    }
    puts("");
    return 0;
}

KMP

时间复杂度:O(n)

#include <iostream>

using namespace std;

const int N = 1e5 + 10, M = 1e6 + 10;

char p[N];// 模式串
int ne[N];// 模式串的next数组
char s[M];// 字符串
int n, m;

int main()
{
    scanf("%d", &n);
    scanf("%s", p + 1);// 从下标为1开始读入
    
    scanf("%d", &m);
    scanf("%s", s + 1);// 从下标为1开始读入
    
    // 求模式串的next数组的过程
    // i从2开始,是因为ne[1]=0,相当于规定了模式串相等部分的前缀和后缀的长度不能相同
    for (int i = 2, j = 0; i <= n; i++)
    {
        while (j && p[i] != p[j + 1]) 
            j = ne[j];
            
        if (p[i] == p[j + 1]) 
            j++;
            
        ne[i] = j;// 记录next数组的值
    }
    
    // kmp匹配的过程
    // j从0开始,是因为每次要和s[i]测试匹配的是p[j+1]
    for (int i = 1, j = 0; i <= m; i++)
    {
        // 当模式串还能往前退时并且当前进行测试匹配的两个字符不相同时,就往前退
        while (j && s[i] != p[j + 1]) 
            j = ne[j];
        
        // 当当前测试匹配的两个字符相同时,模式串往后继续比较下一个字符
        if (s[i] == p[j + 1]) 
            j++;
            
        if (j == n)// 如果匹配成功
        {
            printf("%d ", i - n + 1 - 1);// 输出结果
            
            j = ne[j];
        }
    }
    puts("");
    return 0;
}

Trie

  • Trie树:用来高效地存储和查找字符串集合的数据结构

Trie字符串统计

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int son[N][26];// 每个节点最多只会向外连26条边,这个son数组中值为0的点是空节点
int idx;// 表示当前用到哪个下标,根节点的下标为0
int cnt[N];// 这个数组存的是以当前这个节点为结尾的单词有多少个
char str[N];

void insert(char str[])
{
    int p = 0;// 从根节点开始遍历,根节点的下标为0
    
    for (int i = 0; str[i]; i++)// 字符串结尾为'\0',可以用来判断退出循环
    {
        int u = str[i] - 'a';
        if (!son[p][u])
            son[p][u] = ++idx;// 每插入一个新单词的字母就分配一个下标
        p = son[p][u];// 移动到下一个下标
    }
    
    cnt[p]++;// 以当前这个节点为结尾的单词数量+1
}

int query(char str[])
{
    int p = 0;// 从根节点开始遍历,根节点的下标为0
    
    for (int i = 0; str[i]; i++)
    {
        int u = str[i] - 'a';
        if (!son[p][u]) 
            return 0;// 如不存在这个单词则数量为0
        p = son[p][u];// 移动到下一个下标
    }
    
    return cnt[p];// 返回搜索到的这个单词的数量
}

int main()
{
    int m;
    scanf("%d", &m);
    
    while (m--)// m次询问
    {
        char op[2];
        scanf("%s", op);
        scanf("%s", str);
        if (op[0] == 'I')
            insert(str);
        else 
            printf("%d\n", query(str));
    }
    
    return 0;
}

并查集

  • 并查集的两个操作,时间复杂度近乎为O(1)

    • 快速地将两个集合合并
    • 询问两个元素是否在同一个集合中
  • 核心思想:

    • 每个集合用一棵树来表示
    • 树的根节点的编号就是该集合的编号
    • 每个节点存储它的父节点

合并集合

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int p[N];// 存储的值为父节点的下标

int find(int x)// 返回x所在集合的根节点的编号,同时做了路径压缩的优化
{
    // 如果x不是所在集合的根节点,那么我们进行递归处理,对x的父节点进行查找判断其是否为根节点
    if (p[x] != x)
        p[x] = find(p[x]);// 同时做了路径压缩的优化
        
    return p[x];// 递归的结束条件是p[x]==x,即找到了x所在集合的根节点
}

int main()
{
    int n, m;
    scanf("%d %d", &n, &m);
    
    // 初始时,每个节点对应的父节点(也是根节点)就是自己
    for (int i = 1; i <= n; i++) 
        p[i] = i;
        
    while (m--)// m次询问
    {
        int a, b;
        char op[2];
        scanf("%s", op);// 用字符数组来代替字符读入,能避免踩坑
        scanf("%d %d", &a, &b);
        
        if (op[0] == 'M') 
            p[find(a)] = find(b);// 让a的祖宗节点的父节点等于b的祖宗节点
        else 
            printf("%s\n", find(a) == find(b) ? "Yes" : "No");// 判断a和b是否在同一个集合
    }
    return 0;
}

堆是一颗二叉树

小根堆:每个点都小于等于它左右两个子节点

大根堆:每个点都大于等于它左右两个子节点

用一维数组(下标从1开始)可以存储一颗二叉树:下标为x的节点的左儿子下标为2x、右儿子下标为2x+1

模拟堆

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

using namespace std;

const int N = 1e5 + 10;

int h[N];

int hsize;// 记录堆中数的个数
int k;// 记录当前是第几个插入堆中的数

int kh[N];// 这个数组存的是第k个插入的点在堆中的下标值
int hk[N];// 这个数组存的是堆中的某个下标的点是第几个插入的点

// 这是堆中带了双向索引的交换操作
void heap_swap(int a, int b)
{
    // 交换值
    swap(h[a], h[b]);
    
    // 对应的索引也要进行交换
    swap(kh[hk[a]], kh[hk[b]]);
    swap(hk[a], hk[b]);
}

// 这是小根堆的down操作
void down(int u)
{
    int t = u;// 记录最小值的下标
    
    // 判断左儿子是否存在并且是否小于父节点(也为最小值)
    if (u * 2 <= hsize && h[u * 2] < h[t])
        t = u * 2;
    // 判断右儿子是否存在并且是否小于最小值
    if (u * 2 + 1 <= hsize && h[u * 2 + 1] < h[t])
        t = u * 2 + 1;
        
    // 如果最终发现最小值不是父节点,则需要把最小值交换到父节点,保证堆为小根堆
    if (t != u)
    {
        heap_swap(t, u);
        down(t);// 对原来父节点的值继续进行down操作
    }
}

// 这是小根堆的up操作
void up(int u)
{
    // 判断父节点是否存在并且是否大于当前儿子节点的值
    while (u / 2 > 0 && h[u / 2] > h[u])
    {
        // 保证堆为小根堆
        heap_swap(u / 2, u);
        u /= 2;
    }
}

int main()
{
    int m;
    scanf("%d", &m);
    while (m--)// m次询问
    {
        char op[5];
        scanf("%s", op);
        
        if (!strcmp(op, "I"))// 向堆中插入一个数
        {
            int x;
            scanf("%d", &x);
            
            hsize++;// 堆中数的个数加1
            k++;
            
            h[hsize] = x;// 向堆中的最后一个下标位置进行插入
            
            kh[k] = hsize;// 记录第k个插入的数在堆中的下标值
            hk[hsize] = k;// 记录堆中的该下标的点是第k个插入的数
            
            up(hsize);// 对堆中尾部插入的数进行up操作
        }
        else if (!strcmp(op, "PM"))// 输出堆中最小值
            printf("%d\n", h[1]);
        else if (!strcmp(op, "DM"))// 删除堆中最小值
        {
            heap_swap(1, hsize);
            hsize--;
            down(1);
        }
        else if (!strcmp(op, "D"))// 删除第k个插入堆中的数
        {
            int k;
            scanf("%d", &k);
            
            int temp = kh[k];// 找到第k个插入的数在堆中的下标值
            
            heap_swap(temp, hsize);
            hsize--;
            
            // 要么进行down操作,要么进行up操作,二选一
            down(temp);
            up(temp);
        }
        else// 修改第k个插入堆中的数的值
        {
            int k, x;
            scanf("%d %d", &k, &x);
            
            int temp = kh[k];// 找到第k个插入的数在堆中的下标值
            
            h[temp] = x;
            
            // 要么进行down操作,要么进行up操作,二选一
            down(temp);
            up(temp);
        }
    }
    return 0;
}

Hash表

Hash表的主要作用:把一个比较庞大的值域映射到一个比较小的值域

Hash表处理在映射过程中发生的哈希冲突的两种方法(存储结构)

  • 拉链法(一维数组+链表)
  • 开放寻址法(只用一个一维数组,但是这个一维数组的长度一般为数据范围的2~3倍,这样能有效降低哈希冲突的概率)

模拟散列表

  • 拉链法
#include <iostream>
#include <cstring>

using namespace std;

/*
    在进行映射的过程中,负责取模的这个数一般要为质数,并且这个质数要离2的整数次幂尽可能的远
    因为在数学上可以证明,这么取的话发生哈希冲突的概率是最小的
*/
/*
void find_N()// 找到大于100000的第一个质数
{
    for (int i = 1e5; ; i++)
    {
        bool flag = true;
        for (int j = 2; j * j <= i; j++)
        {
            if (i % j == 0) 
            {
                flag = false;
                break;
            }
        }
        if (flag)
        {
            printf("%d\n", i);
            break;
        }
    }
}
*/

const int N = 100003;

int h[N];// 相当于存储N个单链表的头节点指针
int e[N];// 表示节点i的值
int ne[N];// 表示节点i的next指针是多少
int idx;// 存储当前已经用到了哪个节点

void insert(int x)
{
    /*
        在C++中如果x为负数则余数为负数,x为正数则余数为正数
        (x % N + N) % N的目的就是为了让x % N的结果一定变为正数
    */
    int k = (x % N + N) % N;// k就是哈希值
    
    // 可以看成是在单链表的头节点处插入一个新节点
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx++;
}

bool find(int x)
{
    /*
        在C++中如果x为负数则余数为负数,x为正数则余数为正数
        (x % N + N) % N的目的就是为了让x % N的结果一定变为正数
    */
    int k = (x % N + N) % N;// k就是哈希值
    
    // 下面就是单链表查询某个元素是否存在的操作
    for (int i = h[k]; i != -1; i = ne[i])
        if (e[i] == x)
            return true;
    return false;
}

int main()
{
    // find_N();
    
    int m;
    scanf("%d", &m);
    
    // 先把数组的值全部赋值为-1,单链表中的空指针一般用-1表示
    memset(h, -1, sizeof h);
    
    while (m--)// m次询问
    {
        char op[2];
        int x;
        scanf("%s %d", op, &x);
        
        if (op[0] == 'I') 
            insert(x);// 插入一个数x
        else 
            printf("%s\n", find(x) ? "Yes" : "No");// 查询x这个数是否存在集合中
    }
    return 0;
}
  • 开放寻址法
#include <iostream>
#include <cstring>

using namespace std;

/*
    在进行映射的过程中,负责取模的这个数一般要为质数,并且这个质数要离2的整数次幂尽可能的远
    因为在数学上可以证明,这么取的话发生哈希冲突的概率是最小的
*/
/*
void find_N()
{
    for (int i = 2e5; ; i++)// 找到大于200000的第一个质数
    {
        bool flag = true;
        for (int j = 2; j * j <= i; j++)
            if (i % j == 0) 
            {
                flag = false;
                break;
            }
        if (flag)
        {
            printf("%d\n", i);
            break;
        }
    }
}
*/

const int N = 200003;
const int _NULL = 0x3f3f3f3f;// 这是一个大于1e9的数,用于标记是否为空位置
// 在算法竞赛中,我们常采用0x3f3f3f3f来作为无穷大
// 十六进制:3F 3F 3F 3F
// 二进制:0011 1111 0011 1111 0011 1111 0011 1111

int h[N];

int find(int x)
{
    /*
        在C++中如果x为负数则余数为负数,x为正数则余数为正数
        (x % N + N) % N的目的就是为了让x % N的结果一定变为正数
    */
    int k = (x % N + N) % N;
    
    // 这里一定不会发生死循环,因为h[k]不会被填满,一定会出现h[k] == _NULL的情况
    while (h[k] != _NULL && h[k] != x)
        if (k == N)// 如果寻址到尽头了,则从头开始继续寻址
            k = 0;
        else k++;

    /*
        这里返回的k有两种含义:
        1、如果x在哈希表中存在,就是返回所在的位置
        2、如果x不存在哈希表中,就是返回它应该存储的位置
    */
    return k;
}

int main()
{
    // find_N();
    int m;
    scanf("%d", &m);
    
    // 先把数组的值全部赋值为0x3f3f3f3f,标记为空位置
    memset(h, 0x3f, sizeof h);
    
    while (m--)// m次询问
    {
        char op[2];
        int x;
        scanf("%s %d", op, &x);
        
        int k = find(x);// 开放寻址法
        
        if (op[0] == 'I')
            h[k] = x;// 插入一个数x
        else 
            printf("%s\n", h[k] != _NULL ? "Yes" : "No");// 查询x这个数是否存在集合中
    }
    return 0;
}

字符串哈希

字符串哈希的主要作用:可以用于快速判断两个字符串是否相等

字符串哈希的核心思想:从一个K进制的角度来把一个字符串看成是一个数字

#include <iostream>

using namespace std;

typedef unsigned long long ULL;// unsigned long long 为8个字节

const int N = 1e5 + 10;
const int P = 131;// P表示进制数,值为131

/*
	h[0]表示前0个字符的哈希值,默认为0
    h[r]表示前r个字符的哈希值
    哈希值的范围:0 ~ 2的64次方-1
    在C++中溢出相当于取模
    
*/
ULL h[N];
ULL p[N];// 用数组p存储进制数P的几次方
char str[N];

ULL get(int l, int r)
{
    // h[l - 1] * p[r - l + 1] 相当于把前l-1个字符的哈希值进行左移r-l+1位,与前r个字符的哈希值对齐,然后进行相减
    return h[r] - h[l - 1] * p[r - l + 1];// 返回l到r的哈希值
}

int main()
{
    int n, m;
    scanf("%d %d", &n, &m);
    scanf("%s", str + 1);// 这里是从str+1开始输入
    
    p[0] = 1;// 表示进制数P的0次方等于1
    
    // 预处理进制数P的几次方
    for (int i = 1; i <= n; i++)
        p[i] = p[i - 1] * P;
    
    // 字符串哈希的核心思想:从一个K进制的角度来把一个字符串看成是一个数字
    for (int i = 1; i <= n; i++)
        // 当进制数P的值为131或者13331,且h[i]是对2的64次方进行取模后的结果,那么这种情况下发生的哈希冲突概率极低!
        h[i] = h[i - 1] * P + str[i];// 把原字符串中所有前缀的哈希值求出来

    while (m--)// m次询问
    {
        int l1, r1, l2, r2;
        scanf("%d %d %d %d", &l1, &r1, &l2, &r2);
        printf("%s\n", get(l1, r1) == get(l2, r2) ? "Yes" : "No");
    }
    return 0;
}

知识点补充

在C++中溢出相当于取模

#include <iostream>

using namespace std;

int main()
{
    // unsigned long long 8个字节,范围是 0~18446744073709551615
    unsigned long long aa = -1;
    unsigned long long bb = -2;
    unsigned long long cc = 18446744073709551616;// 溢出,相当于模上了一个2的64次方
    unsigned long long dd = 18446744073709551617;// 溢出,相当于模上了一个2的64次方

    cout << "aa: " << aa << endl;// aa: 18446744073709551615
    cout << "bb: " << bb << endl;// bb: 18446744073709551614
	cout << "cc: " << cc << endl;// cc: 0
	cout << "dd: " << dd << endl;// dd: 1
    
    return 0;
}

STL

可参考语法基础课的内容

操作系统为某一个程序分配空间时,所需的时间与申请空间大小无关,与申请空间次数有关

/*
    vector  变长数组,倍增的思想(宁愿浪费空间,也要减少申请的次数)
        front() 返回第一个数
        back()  返回最后一个数
        push_back() 向最后插入一个数
        pop_back()  把最后一个数删掉
        begin()	返回值是第一个元素的地址
        end()	返回值是最后一个元素的下一个位置的地址
        size()  返回元素的个数,其它容器都有这个方法,时间复杂度:O(1)
		empty() 如果为空则返回true,其它容器都有这个方法,时间复杂度:O(1)
        clear() 清空,queue、priority_queue、stack容器没有这个方法
        支持比较运算,按字典序
        
    string  字符串
        substr()	返回某一个子串
        c_str()		返回string对应的字符数组的头指针
        size()/length()	返回字符串长度
        empty()		如果为空则返回true
        clear()		清空
        
    queue   队列
        push()	向队尾插入一个元素
        back() 	返回队尾元素
        front()	返回队头元素
        pop()	弹出队头元素
        size()	
        empty()
        
    priority_queue  优先队列,也称堆,默认定义为大根堆
        push()	向堆中插入一个元素
        top()   返回堆顶元素
        pop()   弹出堆顶元素
        
    stack   栈
        push()	向栈顶插入一个元素
        top()	返回栈顶元素    
        pop()	弹出栈顶元素
        size()	
        empty()
        
    deque   双端队列
        front()	返回第一个元素
        back()	返回最后一个元素
        push_back()	向队尾插入一个元素
        pop_back()	弹出队尾元素
        push_front()	向队首插入一个元素
        pop_front()		弹出队首元素
        begin()	
        end()
        size()	
        empty()
        clear()
        
    set,map,multiset,multimap    基于平衡二叉树,动态维护有序序列
        begin()
        end()
        begin()++,begin()-- 返回前驱或后继
        end()++,end()-- 返回前驱或后继
        size()
        empty()
        clear()
        
        set(不可以有重复元素),multiset(可以有重复元素)
            insert()	插入一个数
            find()		查找一个数,如果不存在则返回end()返回值,是一个迭代器
            count()		返回某一个数的个数
            erase(某个数)   删除所有等于某个数的元素
            erase(迭代器)   删除这个迭代器所指向的元素
            lower_bound(某个数)	返回 大于等于 某个数的最小的元素的迭代器
            upper_bound(某个数)	返回 大于 某个数的最小的元素的迭代器
            
        map,multimap
            insert()    该方法的参数是一个pair
            erase()     该方法的参数是一个pair或迭代器
            find()      
            lower_bound()
            upper_bound()
            
    unordered_set,unordered_map,unordered_multiset,unordered_multimap	基于哈希表,无序
        和上面类似,增删改查的时间复杂度是O(1)
		因为是无序,没有lower_bound(),upper_bound()方法
        因为是无序,没有迭代器的++,--
        
    bitset  压位
        ~   取反
        &   与
        |	或
        ^	异或
        <<	左移
        >>	右移
        ==	
        !=
        count() 返回有多少个1
        any()   判断是否至少有一个1
        none()  判断是否全为0
        set()   把所有位置变成1
        set(k, v)   将第k位变成v
        reset() 把所有位置变成0
        flip()  把所有位置取反
        flip(k) 把第k位取反
        
    pair 可以存储一个二元组
    	first	取得第一个元素
    	second	取得第二个元素
        支持比较运算,按字典序
*/

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

#include <vector>
#include <queue>
#include <set>
#include <bitset>

using namespace std;

int main()
{
    vector<int> a;
    vector<int> b(10);
    vector<int> c(10, 3);// 定义了一个长度为10的vector,里面的每个数都是3
    vector<int> d[10];// 定义了10个vector
    for (auto x : c) cout << x << ' ';
    cout << endl;
    for (int i = 0; i < 10; i++) a.push_back(i);
    for (int i = 0; i < a.size(); i++) cout << a[i] << ' ';
    cout << endl;
    for (vector<int>::iterator i = a.begin(); i != a.end(); i++) cout << *i << ' ';
    cout << endl;
    for (auto t : a) cout << t << ' ';
    cout << endl;
    vector<int> e(4, 3);
    vector<int> f(3, 4);
    if (e < f) cout << "e < f" << endl;
    
    pair<int, string> g;
    g = make_pair(10, "yxc");
    g = {20, "abc"};
    pair<int, pair<int, int>> h;
    
    string i = "yxc";
    i += "def";
    i += 'c';
    cout << i << endl;// yxcdefc
    cout << i.substr(1, 2) << endl;// xc
    cout << i.substr(1) << endl;// xcdefc
    printf("%s\n", i.c_str());// yxcdefc
    
    queue<int> j;
    j = queue<int>();// 重新构造一个queue,这样可以实现清空一个队列
    
    priority_queue<int> k;// 默认为大根堆
    priority_queue<int, vector<int>, greater<int>> l;// 这样定义为小根堆
    
    set<int> m;// 不能有重复元素
    multiset<int> n;// 可以有重复元素
    
    bitset<10000> o;// 定义了一个长度为10000的bitset
    
    return 0;
}

习题

单链表

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int e[N];// 表示节点i的值
int ne[N];// 表示节点i的next指针是多少
int idx;// 存储当前已经用到了哪个节点
int head;// 表示头节点的下标

// 初始化单链表
void init()
{
    head = -1;
    idx = 0;
}

// 将新节点插到头节点
void add_to_head(int x)
{
    e[idx] = x;
    ne[idx] = head;
    head = idx++;
}

// 将新节点插到下标为k的这个点的后面
void add_to_k(int k, int x)
{
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx++;
}

// 将下标为k的点的下一个点删掉
void remove_to_k(int k)
{
    ne[k] = ne[ne[k]];
}

int main()
{
    int n;
    scanf("%d", &n);
    
    init();// 初始化单链表
    
    while (n--) 
    {
        char op;
        int k, x;
        scanf(" %c", &op);// 这里使用scanf读入一定要注意
        if (op == 'H') 
        {
            scanf("%d", &x);
            add_to_head(x);// 将新节点插到头节点
        }
        else if (op == 'I')
        {
            scanf("%d %d", &k, &x);
            add_to_k(k - 1, x);// 将新节点插到下标为k的这个点的后面
        }
        else 
        {
            scanf("%d", &k);
            if (!k)// 当k为0时,则删除头节点
                head = ne[head];
            else 
                remove_to_k(k - 1);// 将下标为k的点的下一个点删掉
        }
    }
    
    // 遍历单链表
    for (int i = head; i != -1; i = ne[i])
        printf("%d ", e[i]);
        
    return 0;
}

双链表

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int e[N];// 表示节点i的值
int l[N];// 表示节点i的左指针
int r[N];// 表示节点i的右指针
int idx;// 存储当前已经用到了哪个节点

// 初始化双链表
void init()
{
    // 下标0为左端点,下标1为右端点    
    r[0] = 1;
    l[1] = 0;
    idx = 2;
}

void add_kr(int k, int x)// 在下标为k的节点的右边插入一个节点
{
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx;
    r[k] = idx++;
}

void remove_k(int k)// 删除下标为k的这个节点
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

int main()
{
    init();// 初始化双链表
    
    int m;
    scanf("%d", &m);
    
    while (m--)// m次询问
    {
        int k, x;
        
        string op;
        cin >> op;
        
        if (op == "L")
        {
            scanf("%d", &x);
            add_kr(0, x);// 在双链表的最左端插入一个节点
        }
        else if (op == "R")
        {
            scanf("%d", &x);
            add_kr(l[1], x);// 在双链表的最右端插入一个节点
        }
        else if (op == "D")
        {
            scanf("%d", &k);
            remove_k(k + 1);// 将第k个插入的节点删除,对应的下标为k+1
        }
        else if (op == "IL")
        {
            scanf("%d %d", &k, &x);
            add_kr(l[k + 1], x);// 在第k个插入的节点的左边插入一个节点,对应的下标为k+1
        }
        else 
        {
            scanf("%d %d", &k, &x);
            add_kr(k + 1, x);// 在第k个插入的节点的右边插入一个节点,对应的下标为k+1
        }
    }
    
    // 从左端点开始进行遍历
    for (int i = r[0]; i != 1; i = r[i])
        printf("%d ", e[i]);
        
    puts("");
    return 0;
}

模拟栈

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int stk[N];// 栈
int t;// 栈顶指针

int main()
{
    int m;
    scanf("%d", &m);
    
    while (m--)
    {
        int x;
        string op;
        cin >> op;
        
        if (op == "push")
        {
            scanf("%d", &x);
            stk[t++] = x;// 向栈顶插入一个数
        }
        else if (op == "pop")
            stk[--t] = 0;// 从栈顶弹出一个数
        else if (op == "empty")
            printf("%s\n", t ? "NO" : "YES");// 判断栈是否为空
        else 
            printf("%d\n", stk[t - 1]);// 查询栈顶元素
    }
    
    return 0;
}

表达式求值

#include <iostream>
#include <stack>
#include <unordered_map>

using namespace std;

stack<int> num;// 记录数字的栈
stack<char> op;// 记录运算符的栈

void eval()
{
    // 从记录数字的栈中取出两个数,注意运算时是b在后,a在前
    int b = num.top(); 
    num.pop();
    int a = num.top(); 
    num.pop();
    
    // 从记录运算符的栈中取出一个运算符
    char c = op.top(); 
    op.pop();
    
    // 把运算结果读入到记录数字的栈中
    if (c == '+')
        num.push(a + b);
    else if (c == '-') 
        num.push(a - b);
    else if (c == '*') 
        num.push(a * b);
    else 
        num.push(a / b);
}

int main()
{
    // 设定运算符的优先级
    unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};

    string str;
    cin >> str;
    
    // 根据读取到的字符进行对应的操作
    for (int i = 0; i < str.size(); i++)
    {
        char c = str[i];
        
        if (isdigit(c))// isdigit()函数能判断字符是否为数字
        {
            int res = 0;
            int j = i;
            
            while (j < str.size() && isdigit(str[j]))
                res = res * 10 + (str[j++] - '0');// 记录完整的一个数
                
            i = j - 1;// 重新定位i指针的位置
            num.push(res);// 把完整的一个数读入到记录数字的栈中
        }
        else if (c == '(')
            op.push(c);
        else if (c == ')')
        {
            while (op.top() != '(')// 要保证括号中的式子优先进行运算完毕
                eval();
            op.pop();// 最后将左括号弹出栈
        }
        else 
        {
            /*
                如果不使用while循环,只用if判断,那么这个例子就会报错:2-(1-3)*2-3
                
                所以这里一定要使用while循环,
                因为要保证在新的运算符读入到记录运算符的栈中前,
                记录运算符的栈中的运算符的优先级大于等于当前新的运算符的优先级时,要先对式子进行运算完毕,
                保证式子从左往右进行运算
            */
            while (op.size() && pr[op.top()] >= pr[c]) 
                eval();
            op.push(c);// 把新的运算符读入到记录运算符的栈中
        }
    }
    
    /*
        最后需要清空记录运算符的栈,得到最终结果,
        此时式子是从右往左进行运算的,
        说明此时式子中的运算符优先级,右边一定是大于等于左边,
        并且到这一步时,式子中最多只会有两个运算符
    */
    while (op.size())
        eval();
        
    printf("%d\n", num.top());
    return 0;
}

模拟队列

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int queue[N];// 队列
int hh;// 队头
int tt;// 队尾

int main()
{
    // 初始时,队头下标为0,队尾下标为-1
    hh = 0;
    tt = -1;
    
    int m;
    scanf("%d", &m);
    
    while (m--)// m次询问
    {
        int x;
        string op;
        cin >> op;
        if (op == "push")
        {
            scanf("%d", &x);
            queue[++tt] = x;// 在队尾插入一个数
        }
        else if (op == "pop")
            hh++;// 从队头弹出一个数
        else if (op == "empty")
            printf("%s\n", hh > tt ? "YES" : "NO");// 判断队列是否为空
        else 
            printf("%d\n", queue[hh]);// 查询队头元素的值
    }
    
    return 0;
}

单调栈

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int stk[N];// 栈
int tt;// 可以看成是栈顶指针,初值为0

int main()
{
    int n;
    scanf("%d", &n);
    while (n--)
    {
        int x;
        scanf("%d", &x);
        
        // 当栈不为空且栈顶元素大于等于当前的数值时,则弹出栈顶元素
        // 保持栈的单调性
        while (tt && stk[tt] >= x)
            tt--;
            
        printf("%d ", tt ? stk[tt] : -1);
        
        stk[++tt] = x;// 把当前数值加入到栈中
    }
    puts("");
    return 0;
}

滑动窗口

#include <iostream>

using namespace std;

const int N = 1e6 + 10;

int a[N];
int q[N];// 滑动窗口的数组(用数组模拟队列),这里的q数组记录的值是a数组的下标
int n;
int k;// 滑动窗口的大小

int main()
{
    scanf("%d %d", &n, &k);
    
    for (int i = 0; i < n; i++) 
        scanf("%d", &a[i]);
        
    int hh = 0, tt = -1;// 一开始设定,队头为0,队尾为-1
    
    for (int i = 0; i < n; i++)// 输出滑动窗口中的最小值
    {
        if (q[hh] < i - k + 1)// 判断队头对应的a数组的下标是否已经超出窗口的范围
            hh++;
        
        // 当队列不为空,且队尾对应的a数组的下标的值大于等于当前遍历到的a[i],则弹出队尾元素
        // 保证队列的单调性
        while (hh <= tt && a[q[tt]] >= a[i])
            tt--;
            
        q[++tt] = i;// 这里的q数组记录的值是a数组的下标
        
        // 输出结果时,必须保证已经遍历了前k个数
        if (i >= k - 1) 
            // 当前队列队头对应的a数组的下标的值即为滑动窗口中的最小值
            printf("%d ", a[q[hh]]);
    }
    puts("");
    
    hh = 0, tt = -1;
    
    for (int i = 0; i < n; i++)// 输出滑动窗口中的最大值
    {
        if (q[hh] < i - k + 1) // 判断队头对应的a数组的下标是否已经超出窗口的范围
            hh++;
            
        // 当队列不为空,且队尾对应的a数组的下标的值小于等于当前遍历到的a[i],则弹出队尾元素
        // 保证队列的单调性
        while (hh <= tt && a[q[tt]] <= a[i]) 
            tt--;
            
        q[++tt] = i;// 这里的q数组记录的值是a数组的下标
        
        // 输出结果时,必须保证已经遍历了前k个数
        if (i >= k - 1) 
            // 当前队列队头对应的a数组的下标的值即为滑动窗口中的最大值
            printf("%d ", a[q[hh]]);
    }
    puts("");
    return 0;
}

KMP字符串

#include <iostream>

using namespace std;

const int N = 1e5 + 10, M = 1e6 + 10;

char p[N];// 模式串
int ne[N];// 模式串的next数组
char s[M];// 字符串
int n, m;

int main()
{
    scanf("%d", &n);
    scanf("%s", p + 1);// 从下标为1开始读入
    
    scanf("%d", &m);
    scanf("%s", s + 1);// 从下标为1开始读入
    
    // 求模式串的next数组的过程
    // i从2开始,是因为ne[1]=0,相当于规定了模式串相等部分的前缀和后缀的长度不能相同
    for (int i = 2, j = 0; i <= n; i++)
    {
        while (j && p[i] != p[j + 1]) 
            j = ne[j];
            
        if (p[i] == p[j + 1]) 
            j++;
            
        ne[i] = j;// 记录next数组的值
    }
    
    // kmp匹配的过程
    // j从0开始,是因为每次要和s[i]测试匹配的是p[j+1]
    for (int i = 1, j = 0; i <= m; i++)
    {
        // 当模式串还能往前退时并且当前进行测试匹配的两个字符不相同时,就往前退
        while (j && s[i] != p[j + 1]) 
            j = ne[j];
        
        // 当当前测试匹配的两个字符相同时,模式串往后继续比较下一个字符
        if (s[i] == p[j + 1]) 
            j++;
            
        if (j == n)// 如果匹配成功
        {
            printf("%d ", i - n + 1 - 1);// 输出结果
            
            j = ne[j];
        }
    }
    puts("");
    return 0;
}

Trie字符串统计

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int son[N][26];// 每个节点最多只会向外连26条边,这个son数组中值为0的点是空节点
int idx;// 表示当前用到哪个下标,根节点的下标为0
int cnt[N];// 这个数组存的是以当前这个节点为结尾的单词有多少个
char str[N];

void insert(char str[])
{
    int p = 0;// 从根节点开始遍历,根节点的下标为0
    
    for (int i = 0; str[i]; i++)// 字符串结尾为'\0',可以用来判断退出循环
    {
        int u = str[i] - 'a';
        if (!son[p][u])
            son[p][u] = ++idx;// 每插入一个新单词的字母就分配一个下标
        p = son[p][u];// 移动到下一个下标
    }
    
    cnt[p]++;// 以当前这个节点为结尾的单词数量+1
}

int query(char str[])
{
    int p = 0;// 从根节点开始遍历,根节点的下标为0
    
    for (int i = 0; str[i]; i++)
    {
        int u = str[i] - 'a';
        if (!son[p][u]) 
            return 0;// 如不存在这个单词则数量为0
        p = son[p][u];// 移动到下一个下标
    }
    
    return cnt[p];// 返回搜索到的这个单词的数量
}

int main()
{
    int m;
    scanf("%d", &m);
    
    while (m--)// m次询问
    {
        char op[2];
        scanf("%s", op);
        scanf("%s", str);
        if (op[0] == 'I')
            insert(str);
        else 
            printf("%d\n", query(str));
    }
    
    return 0;
}

最大异或对

#include <iostream>

using namespace std;

const int N = 1e5 + 10;
const int M = N * 31;// 每个数的范围对应二进制的位数最多有31位

int a[N];
int son[M][2];// 最多有N*31个节点,每个节点最多只会向外连2条边,这个son数组中值为0的点是空节点
int idx;// 表示当前用到哪个下标,根节点的下标为0

void insert(int x)
{
    int p = 0;// 从根节点开始遍历,根节点的下标为0
    
    // 从x的二进制表示的数的最高位开始,进行插入操作
    for (int i = 30; i >= 0; i--)
    {
        // 循环取出x的第i+1位二进制数,然后对每一位二进制数进行插入操作
        int u = x >> i & 1;
        
        if (!son[p][u])
            son[p][u] = ++idx;// 每插入一位新的二进制数就分配一个下标
            
        p = son[p][u];// 移动到下一个下标
    }
}

int query(int x)
{
    int p = 0;// 从根节点开始遍历,根节点的下标为0
    int res = 0;
    
    // 从x的二进制表示的数的最高位开始,优先查询每一位与其异或为1的二进制数
    for (int i = 30; i >= 0; i--)
    {
        // 循环取出x的第i+1位二进制数
        int u = x >> i & 1;
        
        if (son[p][!u])// 优先查询与其相反的二进制数,进行异或,即异或为1
        {
            p = son[p][!u];// 移动到下一个下标
            
            // 相当于把二进制数转化为十进制数
            res = res * 2 + !u;// 记录查询到的异或对的十进制表示的数值
        }
        else
        {
            p = son[p][u];// 移动到下一个下标
            
            // 相当于把二进制数转化为十进制数
            res = res * 2 + u;// 记录查询到的异或对的十进制表示的数值
        }
    }
    
    return res;
}

int main()
{
    int n;
    scanf("%d", &n);
    
    for (int i = 0; i < n; i++)
        scanf("%d", &a[i]);
        
    int res = 0;
    for (int i = 0; i < n; i++)
    {
        insert(a[i]);// 每个数先进行插入操作
        int t = query(a[i]);// 再查询每个数对应与之异或为最大值的数
        res = max(res, t ^ a[i]);// 更新最大异或对
    }
    printf("%d\n", res);
    return 0;
}

合并集合

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int p[N];// 存储的值为父节点的下标

int find(int x)// 返回x所在集合的根节点的编号,同时做了路径压缩的优化
{
    // 如果x不是所在集合的根节点,那么我们进行递归处理,对x的父节点进行查找判断其是否为根节点
    if (p[x] != x)
        p[x] = find(p[x]);// 同时做了路径压缩的优化
        
    return p[x];// 递归的结束条件是p[x]==x,即找到了x所在集合的根节点
}

int main()
{
    int n, m;
    scanf("%d %d", &n, &m);
    
    // 初始时,每个节点对应的父节点(也是根节点)就是自己
    for (int i = 1; i <= n; i++) 
        p[i] = i;
        
    while (m--)// m次询问
    {
        int a, b;
        char op[2];
        scanf("%s", op);// 用字符数组来代替字符读入,能避免踩坑
        scanf("%d %d", &a, &b);
        
        if (op[0] == 'M') 
            p[find(a)] = find(b);// 让a的祖宗节点的父节点等于b的祖宗节点
        else 
            printf("%s\n", find(a) == find(b) ? "Yes" : "No");// 判断a和b是否在同一个集合
    }
    return 0;
}

连通块中点的数量

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int p[N];// 存储的值为父节点的下标
int cnt[N];// 每个集合中点的数量以根节点统一计算的为准
int n, m;

int find(int x)// 返回x所在集合的根节点的编号,同时做了路径压缩的优化
{
    // 如果x不是所在集合的根节点,那么我们进行递归处理,对x的父节点进行查找判断其是否为根节点
    if (p[x] != x)
        p[x] = find(p[x]);// 同时做了路径压缩的优化
        
    return p[x];// 递归的结束条件是p[x]==x,即找到了x所在集合的根节点
}

int main()
{
    scanf("%d %d", &n, &m);
    
    for (int i = 1; i <= n; i++)
    {
        p[i] = i;// 初始时,每个节点对应的父节点(也是根节点)就是自己
        cnt[i] = 1;// 初始时,每个集合只有一个点
    }
    
    while (m--)// m次询问
    {
        int a, b;
        char op[5];
        
        scanf("%s", op);// 用字符数组来代替字符读入,能避免踩坑
        
        if (op[0] == 'C')
        {
            scanf("%d %d", &a, &b);
            
            // 如果a和b已经是属于同一个集合,则直接进入下一次循环
            if (find(a) == find(b))
                continue;
                
            cnt[find(b)] += cnt[find(a)];// 计算合并后集合的节点数量
            p[find(a)] = find(b);// 让a的祖宗节点的父节点等于b的祖宗节点
        }
        else if (op[1] == '1')
        {
            scanf("%d %d", &a, &b);
            printf("%s\n", find(a) == find(b) ? "Yes" : "No");// 判断a和b是否在同一个集合
        }
        else
        {
            scanf("%d", &a);
            printf("%d\n", cnt[find(a)]);// 输出a所在集合中点的数量
        }
    }
    return 0;
}

食物链

#include <iostream>

using namespace std;

const int N = 5e4 + 10;

int p[N];// 存储的值为父节点的下标
int d[N];// 存储的值为当前节点到其根节点的距离

int find(int x)
{
    if (p[x] != x)
    {
		int t = find(p[x]);
		
		// x到根节点的距离 = x到父节点的距离 + x的父节点到其父节点的距离
        // 由于先进行递归,所以x的父节点到其父节点的距离相当于x的父节点到其根节点的距离
		d[x] = d[x] + d[p[x]];
		
		p[x] = t;
    }
    return p[x];
}

int main()
{
    int n, m;
    scanf("%d %d", &n, &m);
    
    // 初始时,每个节点对应的父节点(也是根节点)就是自己
    for (int i = 1; i <= n; i++) 
        p[i] = i;
    
    int res = 0;// 记录假话数
    
    while (m--)// m次询问
    {
        int t, a, b;
        scanf("%d %d %d", &t, &a, &b);
        
        if (a > n || b > n)// 当节点编号大于N时即为假话
            res++;
        else
        {
            /*
				每执行一次find()函数,
				就进行一次路径压缩,路径上每个点都直接指向根节点,
				同时求出路径上每个点到根节点的距离
			*/
            int pa = find(a);
            int pb = find(b);
            
            /*
                把并查集中每个节点到根节点的距离分成3类(模拟食物链):
                1、节点到根节点的距离 % 3 等于 0 的节点,为根节点
                2、节点到根节点的距离 % 3 等于 1 的节点,为吃根节点的节点
                3、节点到根节点的距离 % 3 等于 2 的节点,为被根节点吃的节点
            */
            if (t == 1)
            {
                if (pa == pb && (d[a] - d[b]) % 3 != 0)// 如果a和b不是同类,则为假话
                    res++;
                    
                if (pa != pb)// 如果不是同一个集合,则表示a和b的关系还没有确定
                {
                    p[pa] = pb;// 合并为同一个集合,则a和b就会存在关系
                    
                    // d[pa]表示a的根节点到b的根节点的距离
                    d[pa] = d[b] - d[a];// 把a和b的关系确定为同类
                }
            }
            else
            {
                if (pa == pb && (d[a] - d[b] - 1) % 3 != 0)// 如果a不吃b,则为假话
                    res++;
                
                if (pa != pb)// 如果不是同一个集合,则表示a和b的关系还没有确定
                {
                    p[pa] = pb;// 合并为同一个集合,则a和b就会存在关系
                    
                    // d[pa]表示a的根节点到b的根节点的距离
                    d[pa] = d[b] + 1 - d[a];// 把a和b的关系确定为a吃b
                }
            }
        }
    }
    printf("%d\n", res);
    return 0;
}

堆排序

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;

int heap[N]; 
int n, m;
int cnt;// 记录堆中数的个数

// 这是小根堆的down操作
void down(int u)
{
    int t = u;// 记录最小值的下标
    
    // 判断左儿子是否存在并且是否小于父节点(也为最小值)
    if (u * 2 <= cnt && heap[u * 2] < heap[t])
        t = u * 2;
    // 判断右儿子是否存在并且是否小于最小值
    if (u * 2 + 1 <= cnt && heap[u * 2 + 1] < heap[t])
        t = u * 2 + 1;
        
    if (t != u)// 如果最终发现最小值不是父节点,则需要把最小值交换到父节点,保证堆为小根堆
    {
        swap(heap[t], heap[u]);
        down(t);// 对原来父节点的值继续进行down操作
    }
}

int main()
{
    scanf("%d %d", &n, &m);
    
    cnt = n;
    
    // 下标从1开始存储
    for (int i = 1; i <= n; i++)
        scanf("%d", &heap[i]);
    // n / 2 即从树的倒数第二层开始进行down操作,进行堆的初始化,时间复杂度为O(n)
    for (int i = n / 2; i; i--)
        down(i);
        
    while (m--)// m次询问
    {
        printf("%d ", heap[1]);
        heap[1] = heap[cnt];// 把堆中最后一个位置上的数覆盖到第一个位置上的数
        cnt--;// 堆中数的个数减1
        down(1);// 进行down操作
    }
    puts("");
    
    return 0;
}

模拟堆

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

using namespace std;

const int N = 1e5 + 10;

int h[N];

int hsize;// 记录堆中数的个数
int k;// 记录当前是第几个插入堆中的数

int kh[N];// 这个数组存的是第k个插入的点在堆中的下标值
int hk[N];// 这个数组存的是堆中的某个下标的点是第几个插入的点

// 这是堆中带了双向索引的交换操作
void heap_swap(int a, int b)
{
    // 交换值
    swap(h[a], h[b]);
    
    // 对应的索引也要进行交换
    swap(kh[hk[a]], kh[hk[b]]);
    swap(hk[a], hk[b]);
}

// 这是小根堆的down操作
void down(int u)
{
    int t = u;// 记录最小值的下标
    
    // 判断左儿子是否存在并且是否小于父节点(也为最小值)
    if (u * 2 <= hsize && h[u * 2] < h[t])
        t = u * 2;
    // 判断右儿子是否存在并且是否小于最小值
    if (u * 2 + 1 <= hsize && h[u * 2 + 1] < h[t])
        t = u * 2 + 1;
        
    // 如果最终发现最小值不是父节点,则需要把最小值交换到父节点,保证堆为小根堆
    if (t != u)
    {
        heap_swap(t, u);
        down(t);// 对原来父节点的值继续进行down操作
    }
}

// 这是小根堆的up操作
void up(int u)
{
    // 判断父节点是否存在并且是否大于当前儿子节点的值
    while (u / 2 > 0 && h[u / 2] > h[u])
    {
        // 保证堆为小根堆
        heap_swap(u / 2, u);
        u /= 2;
    }
}

int main()
{
    int m;
    scanf("%d", &m);
    while (m--)// m次询问
    {
        char op[5];
        scanf("%s", op);
        
        if (!strcmp(op, "I"))// 向堆中插入一个数
        {
            int x;
            scanf("%d", &x);
            
            hsize++;// 堆中数的个数加1
            k++;
            
            h[hsize] = x;// 向堆中的最后一个下标位置进行插入
            
            kh[k] = hsize;// 记录第k个插入的数在堆中的下标值
            hk[hsize] = k;// 记录堆中的该下标的点是第k个插入的数
            
            up(hsize);// 对堆中尾部插入的数进行up操作
        }
        else if (!strcmp(op, "PM"))// 输出堆中最小值
            printf("%d\n", h[1]);
        else if (!strcmp(op, "DM"))// 删除堆中最小值
        {
            heap_swap(1, hsize);
            hsize--;
            down(1);
        }
        else if (!strcmp(op, "D"))// 删除第k个插入堆中的数
        {
            int k;
            scanf("%d", &k);
            
            int temp = kh[k];// 找到第k个插入的数在堆中的下标值
            
            heap_swap(temp, hsize);
            hsize--;
            
            // 要么进行down操作,要么进行up操作,二选一
            down(temp);
            up(temp);
        }
        else// 修改第k个插入堆中的数的值
        {
            int k, x;
            scanf("%d %d", &k, &x);
            
            int temp = kh[k];// 找到第k个插入的数在堆中的下标值
            
            h[temp] = x;
            
            // 要么进行down操作,要么进行up操作,二选一
            down(temp);
            up(temp);
        }
    }
    return 0;
}

模拟散列表

一般使用开放寻址法

  • 开放寻址法
#include <iostream>
#include <cstring>

using namespace std;

/*
    在进行映射的过程中,负责取模的这个数一般要为质数,并且这个质数要离2的整数次幂尽可能的远
    因为在数学上可以证明,这么取的话发生哈希冲突的概率是最小的
*/
/*
void find_N()
{
    for (int i = 2e5; ; i++)// 找到大于200000的第一个质数
    {
        bool flag = true;
        for (int j = 2; j * j <= i; j++)
            if (i % j == 0) 
            {
                flag = false;
                break;
            }
        if (flag)
        {
            printf("%d\n", i);
            break;
        }
    }
}
*/

const int N = 200003;
const int _NULL = 0x3f3f3f3f;// 这是一个大于1e9的数,用于标记是否为空位置
// 在算法竞赛中,我们常采用0x3f3f3f3f来作为无穷大
// 十六进制:3F 3F 3F 3F
// 二进制:0011 1111 0011 1111 0011 1111 0011 1111

int h[N];

int find(int x)
{
    /*
        在C++中如果x为负数则余数为负数,x为正数则余数为正数
        (x % N + N) % N的目的就是为了让x % N的结果一定变为正数
    */
    int k = (x % N + N) % N;
    
    // 这里一定不会发生死循环,因为h[k]不会被填满,一定会出现h[k] == _NULL的情况
    while (h[k] != _NULL && h[k] != x)
        if (k == N)// 如果寻址到尽头了,则从头开始继续寻址
            k = 0;
        else k++;

    /*
        这里返回的k有两种含义:
        1、如果x在哈希表中存在,就是返回所在的位置
        2、如果x不存在哈希表中,就是返回它应该存储的位置
    */
    return k;
}

int main()
{
    // find_N();
    int m;
    scanf("%d", &m);
    
    // 先把数组的值全部赋值为0x3f3f3f3f,标记为空位置
    memset(h, 0x3f, sizeof h);
    
    while (m--)// m次询问
    {
        char op[2];
        int x;
        scanf("%s %d", op, &x);
        
        int k = find(x);// 开放寻址法
        
        if (op[0] == 'I')
            h[k] = x;// 插入一个数x
        else 
            printf("%s\n", h[k] != _NULL ? "Yes" : "No");// 查询x这个数是否存在集合中
    }
    return 0;
}

字符串哈希

#include <iostream>

using namespace std;

typedef unsigned long long ULL;// unsigned long long 为8个字节

const int N = 1e5 + 10;
const int P = 131;// P表示进制数,值为131

/*
	h[0]表示前0个字符的哈希值,默认为0
    h[r]表示前r个字符的哈希值
    哈希值的范围:0 ~ 2的64次方-1
    在C++中溢出相当于取模
    
*/
ULL h[N];
ULL p[N];// 用数组p存储进制数P的几次方
char str[N];

ULL get(int l, int r)
{
    // h[l - 1] * p[r - l + 1] 相当于把前l-1个字符的哈希值进行左移r-l+1位,与前r个字符的哈希值对齐,然后进行相减
    return h[r] - h[l - 1] * p[r - l + 1];// 返回l到r的哈希值
}

int main()
{
    int n, m;
    scanf("%d %d", &n, &m);
    scanf("%s", str + 1);// 这里是从str+1开始输入
    
    p[0] = 1;// 表示进制数P的0次方等于1
    
    // 预处理进制数P的几次方
    for (int i = 1; i <= n; i++)
        p[i] = p[i - 1] * P;
    
    // 字符串哈希的核心思想:从一个K进制的角度来把一个字符串看成是一个数字
    for (int i = 1; i <= n; i++)
        // 当进制数P的值为131或者13331,且h[i]是对2的64次方进行取模后的结果,那么这种情况下发生的哈希冲突概率极低!
        h[i] = h[i - 1] * P + str[i];// 把原字符串中所有前缀的哈希值求出来

    while (m--)// m次询问
    {
        int l1, r1, l2, r2;
        scanf("%d %d %d %d", &l1, &r1, &l2, &r2);
        printf("%s\n", get(l1, r1) == get(l2, r2) ? "Yes" : "No");
    }
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值