算法笔记记录篇章一 算法篇

384 阅读30分钟

算法基础课

零.学习方法

(一)理解思想

(二)理解默写代码模板

1.背诵方法

(1)先看主要思想,在背过(不是一个字母一个字母背,而是能把代码写出来并调试通过即可),再找到对应题目练习

(2)提高熟练度:一个题目写完,写完后删掉,再重新写一遍,重复3-5次

一.基础算法

(一)排序算法

1.快排(不稳定)(O(nlogn))

Tips:让快排稳定方法:可以把元素ai变成双关键字<ai,i>

(1)原理:基于分治

①确定分界点:
左边界q[l]或右边界q[r]或中间界q[(l+r)/2]或随机确定

\color{#FF3030}{★}②调整区间:
将区间一分为二,调整后使得第一个区间中数都<=x,第二个区间中数都>=x,注意分界点上数不一定为x

③递归处理左右两段

(2)例子解释:

最终实现得第一个区间中数都<=x,第二个区间中数都>=x

(3)模板:

#include<iostream>
using namespace std;

const int N = 1e6 + 10;

int n;
int q[N];

// 快速排序
void quick_sort(int q[],int l,int r) {
    if(l >= r) return; // 判边界只有一个数时返回空
    int x = q[l + r >> 1], i = l - 1, j = r + 1; // 取分界点x,两头指针i,j
    while(i < j) { // 循环迭代,只要i,j未相遇,i往右走,j往左走
        do i++; while(q[i] < x);
        do j--; while(q[j] > x);
        if(i < j)  swap(q[i],q[j]);
        // 若所用语言没有swap
        // {
        //     int t = q[i];
        //     q[i] = q[j];
        //     q[j] = t;
        // }
        
    }
    /*这种情况只能int x = q[l]去左边界,若int x = q[r]取右边界也会造成无限递归死循环
    */
    quick_sort(q,l,j); // 左边界排序
    quick_sort(q,j + 1,r); // 右边界排序
    /*此处换成i也可以,但要注意对称
    且上面int x = q[l]处要改为int x = q[(l + r +1) / 2]
    (原因是若保持int x = q[l]会造成无限递归死循环)
    quick_sort(q,l,i - 1); // 左边界排序
    quick_sort(q,i,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]);
    }
    return 0;
}

4.区别:快速选择算法(O(n))

#include<iostream>
using namespace std;

const int N = 1e6 + 10;

int n,k;
int q[N];

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


2.归并排序(稳定)(O(nlogn))--基于分治

(1)原理:基于分治

①确定分界点:mid=(l+r)/2

②先递归排序左边和右边

\color{#FF3030}{★}③归并:把两个有序的数组合并为一个有序数组(O(n))

(2)例子解释

注:排序算法中稳定指原序列中两数值相同,排完序后位置不变-->稳定

(3)模板

#include<iostream>
using namespace std;

const int N = 1e6 + 10;

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

// 归并排序
void merge_sort(int q[],int l,int r) {
    if(l >= r) return;
    int mid = l + r >> 1; // >>相当于/2
    merge_sort(q,l,mid),merge_sort(q,mid + 1, r); //递归排序,分别递归左右边
    int k = 0,i = l,j = mid + 1;
    // 归并
    
    while(i <= mid && j <= r) { // 先在中点两边分别比较并升序
        if(q[i] <= q[j]) temp[k++] = q[i++];
        else temp[k++] = q[j++];
    }
    // 防止一边已经排序完成,另一边还没排序完,所以把另一边继续排序
    while(i <= mid) temp[k++] = q[i++];
    while(j <= r) temp[k++] = q[j++];
    // 将排好序的值再赋给q[]
    for(i = l, j = 0; i <= r; i++,j++) q[i] = temp[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;
}

4.例题:逆序对数量求解

逆序对:当i < j时,q[i] > q[j]

题目:
输入格式
第一行包含整数n,表示数列的长度。

第二行包含 n 个整数,表示整个数列。

输出格式
输出一个整数,表示逆序对的个数。

数据范围
1≤n≤100000
输入样例:
6
2 3 4 5 6 1
输出样例:
5
思路:

注意:此处用long long形式,是因为当倒序时逆序对数量最多,逆序对数量为(n-1)+(n-2)+...+1 = n(n-1)/2,约等于n*n/2,又因为n<=1e6,故最大为 5 * 10^9>int范围的最大数值

#include<iostream>
using namespace std;

const int N = 1e6 + 10;
typedef long long  LL;

int n;
int q[N],temp[N];
LL count;

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


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

(二)二分查找

1 整数二分

\color{#FF3030}{★} 有单调性的题目一定可以二分,可以二分的题目不一定需要单调性
单调性是二分的充分不必要条件
\color{#FF3030}{★} 二分的本质并不是单调性

(1)原理

mid = l + r >> 1时

①寻找中间值

(2)模板

给定一个按照升序排列的长度为n的整数数组,以及 q 个查询。

对于每个查询,返回一个元素k的起始位置和终止位置(位置从0开始计数)。

如果数组中不存在该元素,则返回“-1 -1”。

输入格式 第一行包含整数n和q,表示数组长度和询问个数。

第二行包含n个整数(均在1~10000范围内),表示完整数组。

接下来q行,每行包含一个整数k,表示一个询问元素。

输出格式 共q行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回“-1 -1”。

数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1

#include<iostream>
using namespace std;

const int N = 1e6 + 10;
int n,m,x;
int q[N];

int main() {
    scanf("%d%d",&n,&m);
    // ★注意:数组进行二分查找前题是已升序排好序了
    for(int i = 0; i < n; i ++ ) {
        scanf("%d",&q[i]);
    }
    while(m--) {
    scanf("%d",&x);
    // ★取左区间方法,取左边界值
    int l = 0, r = n - 1;
    while(l < r) {
        // 定义mid值在左侧,取左区间
        int mid = l + r >> 1;
        // 判断要查找值x是否在左区间内,并改变边界值
            // 在左区间,改变右边界值,不在左区间,改变左边界值,到右区间查找
        if(q[mid] >= x) r = mid;
        else l = mid + 1;
    }
    // 如果查找不到x,则退出
    if(q[l] != x) cout<<"-1 -1"<<endl;
    else {
        cout<< l <<" ";
        // ★取右区间方法,取右边界值
        int l = 0, r = n - 1;
        while(l < r ) {
            // 定义mid,取右区间
            int mid = l + r + 1 >> 1;
            // 判断要查找值x是否在右区间内,并改变边界值
                // 在右区间,改变左边界值,不在右区间,改变右边界值,到左区间查找
            if(q[mid] <= x) l = mid;
            else r = mid - 1;
        }
        cout<< l <<endl;
        
        }
        
    }    
    return 0;
}

2 浮点数二分

(1)模板

给定一个浮点数n,求它的三次方根。

输入格式 共一行,包含一个浮点数n。

输出格式 共一行,包含一个浮点数,表示问题的解。

注意,结果保留6位小数。

数据范围
−10000≤n≤10000
输入样例:
1000.00
输出样例:
10.000000

#include<iostream>
#include<cmath>
using namespace std;

// ★注意:题目要求保留n位小数,那么N就要取1e-(n+2)
// ★N的类型要视题目而定,浮点数则为double
const double N = 1e-8;

int main() {
    double x;
    cin>>x;
    // l与r分别为题目提供的x的范围
    double l = -10000, r = 10000;
    while(r - l > N) {
        double mid = (l + r) / 2 ;
        // 3次方根
        if(pow(mid,3) >= x) r = mid;
        else l = mid;
    }
    printf("%lf",l);
    return 0;
}

(三)高精度

1.高精度加法

(1)原理

①大整数存储

②运算

2.模板

给定两个正整数,计算它们的和。

输入格式 共两行,每行包含一个整数。

输出格式 共一行,包含所求的和。

数据范围
1≤整数长度≤100000
输入样例:
12
23
输出样例:
35

#include<iostream>
#include<vector>
using namespace std;

const int N = 1e6 + 10;

// C = A + B
vector<int> add(vector<int> &A, vector<int> &B) {
    vector<int> C;
    int t = 0;
    // A + B
    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
        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');
    // 计算好的结果放入C中
    auto C = add(A,B);
    for(int i =C.size() - 1; i >= 0; i--) printf("%d",C[i]);
    
    return 0;
}

2.高精度减法

(1)原理

(2)模板

给定两个正整数,计算它们的差,计算结果可能为负数。

输入格式 共两行,每行包含一个整数。

输出格式 共一行,包含所求的差。

数据范围
1≤整数长度≤105
输入样例:
32
11
输出样例:
21

#include<iostream>
#include<vector>
using namespace std;

const int N = 1e6 + 10;

// 判断是否A >= B
bool cmp(vector<int> &A, vector<int> &B) {
    // 判断A,B长度来比较A,B谁大
        // return A.size() > B.size()是依据判断结果决定返回true/false
    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]是依据判断结果决定返回true/false
            return A[i] > B[i];
        }
    return true; 
    }
}

// C = A - B
vector<int> sub(vector<int> &A, vector<int> &B) {
    vector<int> C;
    for(int i = 0,t = 0; i <= A.size() - 1; i ++ ) {
        // t代表借位
        t = A[i] - t;
        // 保证B还有时进行减法操作
        if(i < B.size()) t -= B[i]; 
        // 把余数放入C,此种情况对t >= 0或t < 0皆成立
        C.push_back((t + 10) % 10);
        // 此时t表示借位
        if(t < 0) t = 1;
        else t = 0;
    }
    // 清除数字前多余的0,类似于0001,把1前的0清掉
    while(C.size() > 1 && C.back() == 0)  {
        C.pop_back();
    }
    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');
    // 计算好的结果放入C中
    if(cmp(A,B)) {
        auto C = sub(A,B);
        for(int i =C.size() - 1; i >= 0; i--) printf("%d",C[i]);
    }
    else {
        auto C = sub(B,A);
        printf("-");
        for(int i =C.size() - 1; i >= 0; i--) printf("%d",C[i]);
    }
    
    
    return 0;
}

3.高精度乘法

(1)原理

(2)模板

#include<iostream>
#include<vector>
using namespace std;

// C = A * B
vector<int> mul(vector<int> &A,int b) {
    vector<int> C;
    // t为进位值
    int t = 0;
    for(int i = 0; i < A.size()  || t; i ++ ) {
        if(i < A.size()) {
            t += A[i] * b;
        }
        C.push_back(t % 10);
        t /= 10;
    }
    // 清除数字前多余的0,类似于0001,把1前的0清掉 
    while(C.size() > 1 && C.back() == 0)  {
        C.pop_back();
    }
    return C;
}

int main() {
    string a;
    int b;
    cin>> a >> b;
    vector<int> A;
    for(int i = a.size() - 1; i >= 0; i -- ) {
        A.push_back(a[i] - '0');
    }
    auto C = mul(A,b);
    for(int i = C.size() - 1; i >= 0; i -- ) {
        printf("%d",C[i]);
    }
    
    return 0;
}

4.高精度除法

(1)原理

(2)模板

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std; 

// C = A / b
vector<int> div(vector<int> &A,int b,int &r) { // r通过引用传递到main函数
    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就是商
        r %= b;
    }
    // 因为除法C存储商本应该从前往后,但是为了和前面几种统一,上面没有变,reverse是把它重新反转过来,符合除法的商从前往后排的规律
    reverse(C.begin(),C.end());
    while(C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}

int main() {
    string a;
    int b;
    vector<int> A;
    cin >> a >> b;
    for(int i = a.size() - 1; i >= 0; i -- ) {
        A.push_back(a[i] - '0');
    }
    int r;
    auto C = div(A,b,r);
    for(int i = C.size() - 1; i >= 0; i -- ) {
        printf("%d",C[i]);
    }
    cout<<endl;
    printf("%d",r);
    return 0;
}

(四)前缀与差分

1.前缀和:子序列求和

(1)原理

①一维前缀和

②二维前缀和:子矩阵的和

2.差分:操作序列某一部分加减操作

①一维差分

②二维差分

(2)模板

1.前缀和

①一维前缀和

#include<iostream>
using namespace std;

const int N = 1e6 + 10;
int s[N];
int a[N];

int main() {
    // ios::sync_with_stdio(false); // 让cin与scanf()异步,提高cin访问速度,用了后速度还是慢于scanf(),但用了之后scanf()无效
    
    int n,m;
    scanf("%d%d",&n,&m);
    
    for(int i = 1; i <= n; i ++ ) scanf("%d",&a[i]);
    // 前缀和初始化
    for(int i = 1; i <= n; i ++ ) s[i] = s[i - 1] + a[i]; 
    while(m -- ) {
        int l,r;
        scanf("%d%d",&l,&r);
        // 区间和计算
        printf("%d\n",s[r] - s[l - 1]);
    }
    
    return 0;
}

②二维前缀和:子矩阵的和

#include<iostream>
using namespace std;

// 注意N要在题目规定范围内,不要超过
const int N =1000 + 10;
int a[N][N];
int s[N][N];
int n,m,q;

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 ++) 
            s[i][j] = s[i - 1][j] + s[i][j - 1] -s[i - 1][j - 1] + a[i][j];
          
            
    while(q -- ) {
        int x1,y1,x2,y2;
        scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
        // 求子矩阵和
        printf("%d\n",s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1-1][y1-1]);
        
    }
    return 0;
}

2.差分

①一维差分

#include<iostream>
using namespace std;

const int N = 1e6+10;

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

// 将序列中[l, r]之间的每个数加上c
int 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]);
    // 把a[i]输入到b[i]
    for(int i = 1; i <= n; i++) insert(i,i,a[i]);
    while(m--) {
        int l,r,c;
        scanf("%d%d%d",&l,&r,&c);
        insert(l,r,c);
    }
    // 将b数组变成a数组的前缀和
    for(int i = 1; i <= n; i++ ) b[i] += b[i - 1];
    for(int i = 1; i <= n; i++) printf("%d ",b[i]);
    return 0;
}

②二维差分

#include<iostream>
using namespace std;

const int N = 1010;

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

void insert(int x1,int y1,int x2,int y2,int c) {
    b[x1][y1] += c;
    b[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--) {
        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]);
            printf("\n");
    } 
    return 0;
    
}

(五)双指针算法:优化朴素算法时间复杂度位o(n)

1.原理

2.模板

(1)输出含空格单词问题

输入一个字符串把其中每个单词输出来,单词间只有一个空格

// 输入一个字符串把其中每个单词输出来,单词间只有一个空格
#include<iostream>
#include<cstring>
using namespace std;

int main() {
    char str[1000];
    gets(str);
    int n = strlen(str);
    for(int i = 0; i < n; i ++ ) {
        // 让i指向单词开头,j指向单词间空格处
        int j = i;
        while(j < n && str[i] != ' ') j++;
        // 输出每个单词且每个单词占一行
        for(int k = i; k < j; k++) cout<<str[k];
        cout<<endl;
        // 把i指向第二个单词开头
        i = j;
    }
    
    return 0;
}

(2)最长连续不重复子序列

#include<iostream>
using namespace std;

const int N = 1e6 + 10;

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

int main() {
    scanf("%d",&n);
    for(int  i = 0; i < n; i ++ ) {
        cin >> a[i];
    } 
    int res = 0;
    for(int i = 0,j = 0; i < n; i ++ ) {
        // i向后寻找是否有重复元素,若无,继续向前,若有,j向前到i处并把之前存储的元素删除
        s[a[i]]++;
        while(j <= i && s[a[i]] > 1) { // s[a[i]] > 1判断是否有重复元素,也就是如果有重复元素s[a[i]]==1
            s[a[j]]--;
            j++;
        }
        res = max(res,i - j + 1);
    }
    cout << res;
    
    return 0;
}

(六)位运算

1.原理

(1)lowbit运算:返回x中最后一个1,统计x中1的个数

(2)n >> k & 1:求n的第k位数字

2.模板

(1)lowbit运算:返回x中最后一个1,统计x中1的个数

#include<iostream>
using namespace std;

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

int main() {
    int n;
    cin >> n;
    while(n -- ) {
        int x;
        cin >> x;
        int res = 0;
        // 每次减去x的最后一位1
        while(x) x -= lowbit(x), res++;
        cout << res << " ";
    }
    return 0;
}

(2)n >> k & 1:求n的第k位数字

返回输入的数的负数的补码:

#include<iostream>

using namespace std;

int main() {
    int n = 10;
    unsigned int x = -n;
    for(int i = 30; i >= 0; i -- ) {
        cout << (x >> i & 1) << " ";
    }
    
    return 0;
}

(七)离散化(特指整数保序离散化):个数有限但值域很大,有时需要把值域当作下标使用,则需要将值域各个元素分别映射到0....n-1

1.原理

2.模板

(八)区间合并

1.原理

(1)两个区间端点相交->合并为一个区间

(2)一个区间包含另一个区间-->采用较大的那个区间

(3)一个区间与其他区间无交集:无法合并,独成一个区间

2.模板:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

typedef pair<int,int> PII; // 分别存储左右端点
const int N = 1e6 + 10;

int n; // n个区间
vector<PII> segs; 

void merge(vector<PII> &segs) {
    vector<PII> res;
    // 对区间排序
    sort(segs.begin(),segs.end());
    // 区间为-1e9~1e9,故端点都从-2e9开始
    int start = -2e9, end = -2e9;
    for(auto seg:segs) {
        // 区间之间无交集时
        if(end < seg.first) {
            if(start != -2e9) {
                res.push_back({start,end});
            }
                start = seg.first,end = seg.second;
        }
        // 区间之间有交集时
        else {
            end = max(end,seg.second);
        }
        
    }
        // 防止区间为空
        if(start != -2e9) {
            res.push_back({start,end});
        }
        segs = res;
    }
    

int main() {
    cin >> n;
    for(int i = 0; i < n; i ++ ) {
        int l,r; // 每个区间左右端点
        cin >> l >> r;
        segs.push_back({l,r}); // 将左右端点存储
    }
    merge(segs); // 合并区间
    cout << segs.size() << endl;
    return 0;
}

二.数据结构

(一)链表与邻接表:树与图的存储:用数组模拟链表

1.原理

(1)单链表

单链表-->邻接表-->存储图和树

①插入头结点:算法题中大多数情况都是把新结点插入到头结点

②插入结点到k结点后面

③删除k后面的结点

(2)双链表:优化某些问题

①在第k个点的右边插入一个点

②在第k个点的左边插入一个点

void add(int l[k],x) {
    // 给插入的结点赋值
    e[idx] = x;
    // 让插入节点的右端点连接k节点的右边一个点
    r[idx] = r[k];
    // 让插入节点的左端点连接k节点
    l[idx] = k;
    // 让k结点的右边那个点的左端点连接插入节点
    l[r[k]] = idx;
    // 让k结点的右右端点连接插入节点
    r[k] = idx;
}

③删除第k个节点

2.模板

(1)单链表

#include<iostream>
using namespace std;

const int N = 1e6+10;

// 定义
// head头指针
// val[N]存储结点具体值
// next_one[N]存储下一个节点地址
// index存储当前用到哪个结点
int head,val[N],next_one[N],index;

// 初始化
void init() {
    head = -1;
    index = 0;
}

// 将x插入到头结点
void insert_to_head(int x) {
    // 把值存入当前节点
    val[index] = x,
    // 把当前结点与头结点下一个结点相连
    next_one[index] = head,
    // 把头结点与当前结点相连
    head = index,
    // 当前节点往后移动一格
    index++;
}

// 将x插入到下标为k的结点后面
void insert(int k, int x) {
    // 把值存入当前节点
    val[index] = x,
    // 把当前结点与第k个结点的下一个结点相连
    next_one[index] = next_one[k],
    // 把第k个结点的下一个结点与当前结点相连
    next_one[k] = index,
    // 当前节点往后移动一格
    index++;
}

// 将下标为k的点的后面的点删掉
void remove(int k) {
    // 用k的下一个结点的下一个结点覆盖k的下一个节点
    next_one[k] = next_one[next_one[k]];
}



int main() {
    int m;
    cin >> m;
    init();
    int x,k;
    while(m --) {
        char option;
        cin >> option;
        if(option == 'H') {
            cin >> x;
            insert_to_head(x);
        } else if(option == 'D') {
            cin >> k;
            // 鲁棒性:注明k=0时删除头结点
            if(!k) head = next_one[head];
            // k - 1是因为题目中k是从0开始的
            remove(k - 1);
        } else {
            cin >> k >> x;
            insert(k - 1,x);
        }
    }
    for(int i = head; i != -1; i = next_one[i]) {
        cout << val[i] << ' ';
    }
    
    return 0;
}

(2)双链表


(二)栈与队列:单调队列、单调栈

1.原理

(1)栈的模拟与相关操作

// ****** ********栈
// stake:栈本身,top:栈顶指针:永远指向新进入栈的元素,即栈顶元素
int stake[N], top = 0;
// 入栈
stake[ ++top ] = x;
// 出栈
top -- ;

// 判断栈是否为空
if( top >= 0) not empty
else empty

// 栈顶元素
stake[top];

(2)队列的模拟与相关操作

// *************模拟队列
//  seq[N]:模拟队列,队列元素从队尾进入,队首出去
//  top队列尾指针:永远指向新进队列元素,即队尾元素
//  pop_one:队列首指针:永远指向最先进队列的元素即队首元素
int seq[N], top = -1, pop_one = 0;

// 插入元素
seq[ ++top ] = x;

// 出队列
pop_one++; // 队列尾指针往队尾移动,代表队首元素被释放

// 判断队列是否为空
if(pop_one <= top) not empty
else empty

// 取出队首元素
seq[pop_one];

// 取出队尾元素
seq[top];

(3)单调栈(用的情况较少):,给定序列中每一个数左边离他最近的数在什么地方,降低一个问题的时间复杂度(O(n))

暴力做法

单调栈优化

(4)单调队列(用的情况较少)

思路:先用暴力,在删去无用元素,在考虑有没有单调性,若有单调性,在思考如何优化

2.模板

(1)栈的模拟与相关操作


(2)队列的模拟与相关操作


(3)单调栈(用的情况较少)

#include<iostream>
using namespace std;

const int N = 1e6 + 10;
int n;
int stk[N];
int tt = 0;

int main() {
    scanf("%d",&n);
    for(int i = 0; i < n; i ++ ) {
        int x;
        scanf("%d",&x);
        while(tt && stk[tt] >= x) tt -- ;
        if(tt) printf("%d ",stk[tt]);
        else printf("-1 ");
        stk[ ++ tt ] = x;
        
    }
    return 0;
}

(4)单调队列(用的情况较少)

#include<iostream>
using namespace std;

const int N = 1e6 + 10;

int n,k;
// a[N]输入的数组,q[N]滑动窗口所在的数组
int a[N],q[N];

int main() {
    scanf("%d%d",&n,&k);
    for(int i = 0; i < n; i ++ ) scanf("%d",&a[i]);
    //定义队头队尾
    int front = 0,rear = -1;
    // 每个滑动窗口最小值
    for(int i = 0; i < n; i ++ ) {
        // 判断队头是否划出窗口
        while(front <= rear && i - k + 1 > q[front]) front ++ ;
        while(front <= rear && a[q[rear]] >= a[i]) rear -- ;
        q[ ++ rear] = i;
        if(i >= k - 1) printf("%d ", a[q[front]]);
    }
    puts("");
    
    // 每个滑动窗口最大值
    front = 0,rear = -1;
    for(int i = 0; i < n; i ++ ) {
        // 判断队头是否划出窗口
        while(front <= rear && i - k + 1 > q[front]) front ++ ;
        while(front <= rear && a[q[rear]] <= a[i]) rear -- ;
        q[ ++ rear] = i;
        if(i >= k - 1) printf("%d ", a[q[front]]);
    }
    puts("");
    
    return 0;
}

(三)kmp:除了循环节其余的建议使用字符串哈希法

1.原理

思路:先思考暴力怎么做,在思考如何去优化

暴力做法

kmp:
next[j]与i是否匹配
例子:

(1)next数组构造

(2)kmp匹配过程

2.模板(此版本O(n))

#include<iostream>
using namespace std;

const int N = 100000 + 10, M = 1000000 + 10;

int n,m;
char p[N],s[M];
int next_one[N];

int main() {
   
    cin >> n >> p + 1 >> m >> s + 1;
    
    //求next过程
    for(int i = 2, j = 0; i <= n; i ++ ) {
        while(j && p[i] != p[j + 1]) j = next_one[j];
        if(p[i] == p[j + 1]) j ++ ;
        next_one[i] = j;
    }
    
    // kmp匹配过程
    for(int i = 1, j = 0; i <= m; i ++ ) {
        while(j && s[i] != p[j + 1]) j = next_one[j];
        if(s[i] == p[j + 1]) j++;
        if(j == n) {
            printf("%d ", i - n);
            j = next_one[j];
        }
        
    }
    return 0;
}

(四)Trie树:高效的快速存储和查找字符串集合。

类型:全部小写字母/全部大写字母/全部数字,长度不会太长

1.原理

trie树存储

trie树查找

2.模板

#include<iostream>
using namespace std;

const int N = 1e5 + 10;

// 下标为0的点既是根节点也是空结点
// 下标为x的点,x结点所有的儿子存到了son[x]中,son[x][i]就是x的第i个儿子
// count[x]存储以x结尾的单词数量多少个
// index存储当前下标
int son[N][26],count[N],index;
char str[N];

// 插入
void insert(char str[]) {
    int p = 0;
    for(int i = 0; str[i]; i ++ ) {
        int  u = str[i] - 'a';
        if(!son[p][u]) son[p][u] = ++ index;
        p = son[p][u];
    }
    count[p] ++ ;
}

//查找
int query(char str[]) {
    int p = 0;
    for(int i = 0; str[i]; i ++ ) {
        int  u = str[i] - 'a';
        if(!son[p][u]) return 0;
        p = son[p][u];
    }
    return count[p];
}

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

(五)并查集:容易考,代码短,思路精巧

功能:快速的处理栈问题,合并两个集合/询问两个元素是否在一个集合当中(近乎O(1)时间完成操作)

1.原理

初版:

优化:路径压缩

2.模板

#include<iostream>
using namespace std;

const int N = 1e5 + 10;

int p[N];
int n,m;

// 返回x的祖宗节点,路径压缩法
int find(int x) {
    // 利用递归的方法寻找根节点
    if(p[x] != x) p[x] = find(p[x]); // p[x] == x =>p[x]为根节点
    return p[x];
}

int main() {
    scanf("%d%d",&n,&m);
    // 初始化:生成集合中数
    for(int i = 1; i <= n; i ++ ) p[i] = i;
    
    while(m -- ) {
        char op[2]; // op:操作类型
        int a,b;
        scanf("%s%d%d",op,&a,&b);
        if(op[0] == 'M') {
        // 把a连接到b上,所以所有连通块都在b上
            p[find(a)] = find(b); // 合并a,b集合
        } else {
            // 查询a,b是否在同一集合中
            if(find(a) == find(b)) puts("Yes");
            else puts("No");
            
        } 
        
    }
    
    return 0;
}

(六)堆(一般O(nlogn),特殊操作可以O(n))

1.原理

功能

注意:需要删除修改堆中任意一个元素需要映射操作(见下面模拟堆)

小根堆

小根堆存储方式

时间复杂度O(n)方法

2.模板

(1)堆排序

#include<iostream>
#include<algorithm>
using namespace std;

const int N = 1e5 + 10;

int heap[N],length;
int n,m;
// 往下走(较大的值)
void down(int u) {
    int t = u; // u为根节点,放入t中,t表示三个点中最小值
    if(u * 2 <= length && heap[u * 2] < heap[t]) t = u * 2; // 左儿子存在且左儿子的值小于根节点,最小值变为左儿子
    if(u * 2 + 1 <= length && heap[u * 2 + 1] < heap[t]) t = u * 2 + 1; // 右儿子存在且右儿子的值小于根节点,最小值变为右儿子
    if(u != t) { // 若根节点不为最小值,则交换根节点与最小值使得根节点变为最小值
        swap(heap[u],heap[t]);
        down(t);
    }
}
/*// 向上走(较小的值)
void up(int u) {
    // 如果父亲结点比当前结点大,交换二者位置
    while(u / 2 && heap[u / 2] > heap[u]) {
        swap[heap[u / 2],heap[u]];
        u /= 2;
    }
}
*/
int main() {
    scanf("%d%d",&n,&m);
    // 构造二叉树
    for(int i = 1; i <= n; i ++ ) scanf("%d",&heap[i]);
    length = n;
    // 优化步骤,时间复杂度由O(nlogn)变为O(n),从n/2开始是因为最下面一列的n/2个不可能在down,所以上面的n/2个才能down
    for(int i = n / 2; i; i -- ) down(i);
    
    while(m -- ) {
        printf("%d ",heap[1]);
        // 删除最小值
        heap[1] = heap[length];
        length -- ;
        down(1);
    }
    return 0;
}

(2)模拟堆:需要删除修改堆中任意一个元素,需要映射操作

原理

题目

#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;

const int N = 1e5 + 10;

int heap[N],length;
int n,m;
// hp[N]从堆到下标的映射,ph[N]从下标映射到堆
int hp[N],ph[N];

void heap_swap(int a,int b) {
    // 相互交换堆与下标的映射关系,变为交叉对称关系
    swap(ph[hp[a]],ph[hp[b]]); 
    swap(hp[a],hp[b]); 
    // 交换a,b两点
    swap(heap[a],heap[b]);
}


// 往下走(较大的值)
void down(int u) {
    int t = u; // u为根节点,放入t中,t表示三个点中最小值
    if(u * 2 <= length && heap[u * 2] < heap[t]) t = u * 2; // 左儿子存在且左儿子的值小于根节点,最小值变为左儿子
    if(u * 2 + 1 <= length && heap[u * 2 + 1] < heap[t]) t = u * 2 + 1; // 右儿子存在且右儿子的值小于根节点,最小值变为右儿子
    if(u != t) { // 若根节点不为最小值,则交换根节点与最小值使得根节点变为最小值
        heap_swap(u,t);
        down(t);
    }
}
// 向上走(较小的值)
void up(int u) {
    // 如果父亲结点比当前结点大,交换二者位置
    while(u / 2 && heap[u / 2] > heap[u]) {
        heap_swap(u / 2, u);
        u /= 2;
    }
}

int main() {
    scanf("%d",&n);
    while(n -- ) {
        char op[10];
        int k,x;
        scanf("%s",op);
        if(!strcmp(op,"I")) {
            scanf("%d",&x);
            length ++;
            m ++ ;
            ph[m] = length, hp[length] = m;
            heap[length] = x;
            up(length);
        }
        else if(!strcmp(op,"PM")) printf("%d\n", heap[1]);
        else if(!strcmp(op,"DM")) {
            heap_swap(1,length);
            length --;
            down(1);
        }
        else if(!strcmp(op,"D")) {
            scanf("%d",&k);
            k = ph[k];
            heap_swap(k,length);
            length -- ;
            down(k),up(k);
        }
        else {
            scanf("%d%d",&k,&x);
            k = ph[k];
            heap[k] = x;
            down(k),up(k);
        }
    }
    return 0;
}

(七)Hash表:将-1e9~1e9的数映射到0~1e5/1e6的序列中

1.原理

拉链法

开放寻址法

2.模板

(1)模拟散列表

拉链法

#include<iostream>
#include<cstring>
using namespace std;

// 开放寻址法
/*
// 求质数方法
for(int  i = 200000; ;i ++ ) {
    bool flag = true;
    for(int j = 2; j * j <= i; j ++ ) {
        if(i % j == 0) {
            flag = false;
            break;
        }
    }
    if(flag) {
         cout << i << endl;
         break;
    }
}
*/
// 开放寻址法N需要2倍于链地址法再求质数
const int N =200003, null = 0x3f3f3f3f; // null为<-1e9,>1e9的地址,防止与内部值冲突

int h[N];
int find(int x) {
    int k = (x % N + N) % N;
    //找的位置一定可以找到
    while(h[k] != null && h[k] != x) { // k位置不为空并且k位置中的值不是要查找的x
        k ++ ; // 找下一个位置
        if(k == N) k = 0; // 如果找到最后了,返回第一个继续找
    }
    return k; // 找到了,k为x所在下标值,没找到,k就是x在哈希表中存储的位置
}

/*链地址法
// 1e5 + 3是因为1e5 + 3是1e5这个等级的最小质数,散列表的N最好取同等级最小质数
// 求质数方法
for(int  i = 100000; ;i ++ ) {
    bool flag = true;
    for(int j = 2; j * j <= i; j ++ ) {
        if(i % j == 0) {
            flag = false;
            break;
        }
    }
    if(flag) {
         cout << i << endl;
         break;
    }
}

const int N = 1e5 + 3;


// h[k]存放链表第一个结点的下标,e[i]当前结点的值为多少,下一个点的下标ne[i],
int h[N],e[N],ne[N],idx;


void insert(int x) {
    int k = (x % N + N) %N; //除留余数法,+N目的是把负数转换为正数
    // 单链表插入
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx ++ ;
}

bool find(int x) {
    int  k = (x % N + N) % N;
// 当前这个点的下标变到下一个点的坐标i = ne[i],空指针下标-1时停止
    for(int  i = h[k]; i != -1; i = ne[i]) {
        if(e[i] == x) {
            return true;
        }
    }
    return false;
}
*/

int main() {
    int n;
    scanf("%d",&n);
    int x;
    // 开放地址法
    memset(h,0x3f,sizeof h); //死记吧,按字节memset,一个字节8位
    /*// 链地址法
    memset(h,-1,sizeof h);
    */
    while(n -- ) {
        char op[10];
        scanf("%s%d",op,&x);
        //开放寻址法
        /*// 优化代码
        int k = find(x);
        if(op[0] == 'I') {
            h[k] = x; //把x插入到k处
        }
        else {
             if(h[k] != null) puts("Yes");
             else puts("No");
        }
        */
        if(op[0] == 'I') {
            int k = find(x);
            h[k] = x; //把x插入到k处
        }
        else {
             int k = find(x);
             if(h[k] != null) puts("Yes");
             else puts("No");
        }
        
        
        /*链地址法
        if(op[0] == 'I') insert(x);
        else {
             if(find(x)) puts("Yes");
             else puts("No");
        }
        */
       
    }
    
    return 0;
}

(2)字符串前缀哈希法:★★★处理字符串利器,可以解决很多KMP无法解决的问题,快速判断两个字符串是否相等,除了循环节外,其余建议使用该种方法

原理:把任一字符串映射成从0-Q-1之间的数,核心:用k进制角度把字符串看成数字

注意点:

题目

#include<iostream>
using namespace std;
typedef unsigned long long ULL; //由于前缀值的值会很大 所以应该将数组中的数据定义为ULL型

// 核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
// 小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果


const int N = 1e5 + 10;
const int P = 13331;  //P为权重
                    //131为经验值 即P=131或13331时 哈希冲突的可能性最小

int n,m;
char str[N];
ULL h[N];  //h[]存放字符串的前缀值
ULL p[N];  //p[]存放各个位数的相应权值

ULL get(int l, int r) {
    return h[r] - h[l - 1] * p[r- l + 1]; // 这步其实是将h[l-1]左移
     //其目的事实上是为了将h[l-1]的高位与h[r]相对齐从而才可以未完成计算
}

int main() {
    scanf("%d%d%s",&n,&m,str + 1);
    p[0] = 1;  //注意这步千万不要忘了 最开始的权值必须赋值为1 否则接下来就会出错
    for(int i = 1; i <= n; i ++) {
        p[i] = p[i - 1] * P; //计算每个位上的相应权值
        h[i] = h[i - 1] * P + str[i];      //计算字符串前缀值
        //最新加入的数的权值为p的0次 所以直接加上str[i]即可
        
    }
    while(m -- ) {
        int l1,r1,l2,r2;
        scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
        
        if(get(l1,r1) == get(l2,r2)) puts("Yes");
        else puts("No");
        
    }
    
    
    return 0;
}

(八)C++ STL使用技巧

1.原理

vector

pair

string

string方法

queue

队列方法

清空队列

priority_queue:默认大根堆

大根堆方法

小根堆的构造

stack

栈方法

deque:双端队列(用的不是太多,效率太低)

双端队列方法

set/map/multimap,multiset

bitset:省8倍空间

2.模板