数据结构

单链表

用数组模拟单链表

邻接表

  • 存储树和图

单链表

  • 样式:
    • head -> $\phi$
    • head -> element -> element -> $\phi$
  • 定义
    • int e[N]:表示某个点的值是多少
    • int ne[N]:表示下一个点的指针
    • 两个数组用下标关联起来
    • 空节点的下标用-1表示
    • ![[Pasted image 20241216084447.png]]

一些函数操作

  • 初始化
    • 请注意,head不是一个结点,而是一个“指针”,head指向的是头结点的下标
void init(){
    head = -1;
    idx = 0;
}
  • 在链表头部,插入一个新结点
    • 操作步骤:
      1. 将新结点的next指针指向头结点的下一个结点
      2. 将新插入的结点的next指针指向头结点
void add_to_head(int x){
    e[idx] = x;
    ne[idx] = head;
    head = idx ++;

}

代码实现

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;


// head表示头结点的下标
// e[N]存储每个结点的值,表示结点i的值
// ne[N]表示结点i的next指针,即为下一个结点的下标值
// idx存储当前已经用到了哪个点
int head, e[N], ne[N], idx;

void init(){
    head = -1;
    idx = 0;
}

void add_to_head(int x){
    e[idx] = x;
    ne[idx] = head;
    head = idx ++;

}

void add(int k, int x){
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx ++;

}


void remove(int k){
    ne[k] = ne[ne[k]];
}

int main(){

    int m;
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin >> m;

    init();

    while(m --){
        char op;

        int x, k;

        cin >> op;
        if(op == 'H'){
            cin >> x;
            add_to_head(x);
        }
        else if(op == 'D'){
            cin >> k;
            if(!k)
                head = ne[head];
            remove(k - 1);
        }
        else{
            cin >> k >> x;
            add(k - 1, x);
        }
    }

    for(int i = head; i != -1; i = ne[i]){
        cout << e[i] << ' ';
    }

    cout << endl;

    return 0;
}

双链表

用数组模拟双链表

  • 每个节点有两个指针
  • int l[N]存储左侧指针指向的下标,int r[N]存储右侧指针指向的下标

主要功能

初始化函数

  • 0表示左节点,用1表示右节点
  • 那么最开始双链表的情况就为r[0] = 1, l[1] = 0

代码实现

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 100;

int m;
int e[N], l[N], r[N], idx;

void init(){
    r[0] = 1, l[1] = 0;
    idx = 2;

}



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

}

void remove(int k){
    r[l[k]] = r[k];
    l[r[k]] = l[k];

}

int main(){

    cin >> m;
    init();

    while(m --){
        string op;
        int x, k;
        cin >> op;
        if(op == "R"){
            cin >> x;
            add(l[1], x);
            // 这里的1表示的是右边界,在右边界左侧节点的右侧插入新结点
        }
        else if(op == "L"){
            cin >> x;
            add(0, x);
            // 最开始在这里犯了错误,应该直接在0节点(头结点)的右侧进行插入的
            // 0, 1都没有实际值,只是一个头尾指针
        }
        else if(op == "D"){
            cin >> k;
            remove(k + 1);
            // 数组中的idx实际是从2开始算的,所以这里是k + 1
        }
        else if(op == "IL"){
            cin >> k >> x;
            add(l[k + 1], x);
        }
        else{
            cin >> k >> x;
            add(k + 1, x);
        }
    }

    for(int i = r[0]; i != 1; i = r[i]){
        cout << e[i] << " ";
    }

    cout << endl;

    return 0;
}

模拟栈

实现思想

  • 先进后出

代码实现

#include <bits/stdc++.h>

using namespace std;

const int N = 100010;

int m;
int stk[N], tt;

int main(){

    cin >> m;
    while(m --){
        string op;
        int x;
        cin >> op;
        if(op == "push"){
            cin >> x;
            stk[++ tt] = x;
        }
        else if(op == "pop"){
            tt --;
        }
        else if(op == "query"){
            cout << stk[tt] << endl;
        }
        else{
            cout << (tt ? "NO" : "YES") << endl;
        }
    }
    return 0;
}

表达式求值

算法思想

  • 利用中缀表达式模拟递归求解
  • 如何判断一颗子树已经被遍历完?当前运算符的优先级小于等于上一运算符的优先级

实现代码

#include <bits/stdc++.h>

using namespace std;

stack<int> num;
stack<char> op;

unordered_map<char, int> h{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};

void eval(){
    int b = num.top();
    num.pop();

    int a = num.top();
    num.pop();

    int c = op.top();
    op.pop();

    int r = 0;

    if(c == '+')    r = a + b;
    else if (c == '-')  r = a - b;
    else if (c == '*')  r = a * b;
    else r = a / b;

    num.push(r);

}

int main(){
    string s;
    // 读入表达式
    cin >> s;

    for(int i = 0; i < s.size(); i ++){
        // 开始遍历表达式
        if(isdigit(s[i])){
            int x = 0, j = i;
            while(j < s.size() && isdigit(s[j])){
                x = x * 10 + s[j] - '0';
                j ++;
            }

            i = j - 1;
            num.push(x);
        }
        else if(s[i] == '('){
            op.push(s[i]);
        }
        else if(s[i] == ')'){
            // 干脆就没让右括号进栈
            while(op.top() != '('){
                eval();
            }
            op.pop();
        }
        else{
            while(op.size() && h[op.top()] >= h[s[i]]){
                eval();
            }
            op.push(s[i]);
        }
    }
    while(op.size())    eval();
    cout << num.top() << endl;
    return 0;
}

队列

模拟队列

解题思路

  • 用数组模拟队列
  • 初始化数组,hh = 0表示对头,tt = -1表示队尾
  • 在队尾插入元素,在队头弹出元素
  • 插入元素:q[++ tt] = x
  • 弹出元素:hh ++
  • 取出队头元素:q[hh]
  • 取出队尾元素:q[tt]

实现代码


#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 100;
int q[N], hh, tt = -1;

int main(){
    int m;
    scanf("%d", &m);

    while(m --){
        string s;
        int x;
        cin >> s;
        if(s == "push"){
            scanf("%d", &x);
            q[++ tt] = x;
        }else if(s == "pop"){
            hh ++;
        }else if(s == "empty"){
            if(hh <= tt)
                printf("NO\n");
            else
                printf("YES\n");
        }else{
            printf("%d\n", q[hh]);
        }

        // cout << endl;
    }

    return 0;
}

单调栈

单调栈

知识点

解题思路

  • 先想一下,如果使用暴力做法,该怎么做
    • 第一层循环:首先遍历数组中的每个元素
    • 第二层循环:从遍历到的元素开始向前遍历,寻找第一个大于当前遍历到的元素的元素
  • 使用单调栈:
    • 如果$a[i] > a[j]$,那么元素$a[i]$在后续的求解过程中,一定不会被使用到,所以我们考虑将其删去
    • 经过调整之后,栈序列中元素一定是单调递增的

实现代码

#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 100;
int stk[N], tt;

int main(){
    int n;
    scanf("%d", &n);

    for(int i = 0; i < n; i ++){
        int x;
        cin >> x;
        while(tt && stk[tt] >= x)
            tt --;


        if(tt)
            printf("%d ", stk[tt]);
        else
            printf("-1 ");

        stk[++ tt] = x;
    }

    return 0;
}

单调队列

滑动窗口

解题思路

  • 如果使用普通队列进行解题的话,时间复杂度需要$\Omega(nk)$
  • 构建单调队列
    • 求窗口中最小值时,将单调队列构建为从队头开始的到队尾的单调递增队列
    • 求窗口中最大值时,将单调队列构建为从队头开始的到队尾的单调递减队列
  • 单次遍历数组后,每移动一次窗口输出一次最大/最小元素

实现代码


#include <bits/stdc++.h>

using namespace std;
const int N = 1e6 + 100;
int a[N], q[N];

int n, k;

int main(){
    scanf("%d%d", &n, &k);

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

    int hh = 0, tt = -1;
    for(int i = 0 ; i < n; i ++){
        // 如果队头元素已经超出窗口范围
        if(hh <= tt && i - k + 1 > q[hh])
            hh ++;

        while(hh <= tt && a[q[tt]] >= a[i])
            tt --;

        q[++ tt] = i;
        if(i >= k - 1)
            printf("%d ", a[q[hh]]);
    }
    cout << endl;

    hh = 0, tt = -1;

    for(int i = 0; i < n; i ++){
        if(hh <= tt && i - k + 1 > q[hh])
            hh ++;

        while(hh <= tt && a[q[tt]] <= a[i])
            tt --;

        q[++ tt] = i;

        if(i >= k - 1)
            printf("%d ", a[q[hh]]);
    }

    cout << endl;
    return 0;

}

KMP

KMP 字符串

知识点

解题思路

  • 先考虑暴力算法怎么做,然后想如何去优化
  • KMP习惯上从1开始
//朴素做法

for(int i = 1; i <= n; i ++){
    bool flag = true;
    for(int j = 1; j <= n; j ++){
        if(s[i + j - 1] != s[j]){
            flag = false;
            break;
        }
    }
}
  • 上方图片中,如果原字符串在与模版字符串进行匹配时,遇到了某一个字符不相等,那么我们可以进行将模版字符串进行后移的操作;后移的目的为使得原字符串和模版字符串的匹配能够继续进行
  • 这时,我们需要探究上图中绿色子串与模版字符串的开头匹配的最大值;若获取了最大值,我们就可以获取向后移动的最小距离
  • 所以,我们需要对模版串进行预处理:
    • 计算以某个点为后缀,从模版字符串的前缀字符串(这种字符串的第一个字符是模版字符串的字符)能够相等的最大长度是多少
    • 每个点对应的长度,存储在Next[]数组中,上面这一点也就是这个Next[]数组的含义
    • 例:
      • 给定Next[i] = j,那么模版字符串p[1 ~ j] = p[i - j + 1, i](第一个字符到第j个字符构成的字符串与第i - j + 1个字符到第i个字符构成的字符串是相同的)
  • 继续讨论
  • 在上图中,设定原字符串为S,模版字符串为P,那么可得:
    • S[1 ~ i - 1] = P[1 ~ j]
    • S[1 ~ i] != P[1 ~ j + 1]
  • 遇到了不匹配的情况,所以我们要继续将模版字符串进行后移
  • 就将P的第Next[j]位对准S的第i - 1位,继续检查P的第Next[j] + 1位是否与S的第i位相等
    • 如果相等,则继续检查
    • 如果不相等,则继续后移
  • 那么该如何求Next[]数组呢?
    • 我们假定存在以字符P[i - 1]为结尾的字符串可以与P[j]为结尾的前缀字符串进行匹配
    • 接下来判断P[i]P[j + 1]是否匹配
      • 如果匹配成功,则j ++
      • 如果匹配失败,则j = ne[j]继续进行匹配

实现代码


#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 100, M = 1e6 + 100;
// const int N = 10010, M = 100010;
int ne[N];
int n, m;
// char s[M], p[M];
char p[N], s[M];

int main(){

    cin >> n >> p + 1 >> m >> s + 1;

    // 求解ne数组
    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;
    }

    // 匹配过程
    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);
            j = ne[j];
        }


    }
    return 0;
}


Trie

Trie字符串统计

知识点

  • Trie:高效地存储和查找字符串的数据结构

解题思路

  • 插入字符串:从头结点root开始遍历,
    • 如果首字母存在则继续遍历,直至遍历到Trie树中不存在的字符,将该字符串后面的字符作为孩子节点加入到Trie
    • 如果首字母不存在,直接将整个字符串作为孩子节点加入到头结点root
  • 对于字符串结尾的单词节点,我们使用特殊标记将其标记,已表明该节点为一个字符串的结尾

实现代码


#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 100;
// son数组用来表示节点N最多有26个孩子节点,以此类推
int son[N][26], cnt[N], idx;
char str[N];

void insert(char str[]){
    int p = 0;
    for(int i = 0; str[i]; i ++){
        // 首先将字符转化为0~25表示
        int u = str[i] - 'a';
        // 当前节点的孩子节点有没有当前字符
        // 如果没有,则加入对应字符
        if(!son[p][u])
            son[p][u] = ++ idx;
        // 如果有,则继续遍历
        p = son[p][u];
    }

    cnt[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 cnt[p];
}

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

    return 0;
}

最大异或对

</div ### 知识点 ### 解题思路 ### 实现代码 ```cpp #include <bits/stdc++.h> using namespace std; const int N = 1e5 + 100, M = 3100010; int a[N], son[M][2], idx; int n; void insert(int x){ int p = 0; for(int i = 30; i >= 0; i --){ int &s = son[p][x >> i & 1]; if(!s) s = ++ idx; p = s; } } int search(int x){ int p = 0, res = 0; for(int i = 30; i >= 0; i --){ int s = x >> i & 1; if(son[p][!s]){ res += 1 << i; p = son[p][!s]; }else{ p = son[p][s]; } } return res; } int main(){ cin >> n; for(int i = 0 ; i < n; i ++){ scanf("%d", &a[i]); insert(a[i]); } int res = 0; for(int i = 0; i < n; i ++){ res = max(res, search(a[i])); } cout << res << endl; return 0; } ``` # 并查集 ## 食物链
### 知识点 ### 解题思路 - 用并查集来表示食物链 - 通过与根节点的距离,来判断某个节点上的物种和根节点是什么关系 - **回来还需要重新理解!** ### 实现代码 ```cpp #include <bits/stdc++.h> using namespace std; const int N = 50010; int n, m; int p[N], d[N]; int find(int x){ if(p[x] != x){ int t = find(p[x]); d[x] += d[p[x]]; p[x] = t; } return p[x]; } int main(){ cin >> n >> m; for(int i = 1; i <= n; i ++) p[i] = i; int res = 0; while(m --){ int t, x, y; cin >> t >> x >> y; if(x > n || y > n) res ++; else{ int px = find(x), py = find(y); if(t == 1){ if(px == py && (d[x] - d[y]) % 3){ res ++; }else if(px != py){ p[px] = py; d[px] = d[y] - d[x]; } }else{ if(px == py && (d[x] - d[y] - 1) % 3){ res ++; }else if(px != py){ p[px] = py; d[px] = d[y] + 1 - d[x]; } } } } cout << res << endl; return 0; } ``` # 堆 ## 堆排序
### 知识点 - `strcmp` 是 C/C++ 标准库中的一个函数,用于比较两个字符串。 - `strcmp` 按字典序(lexicographical order)比较两个字符串 `str1` 和 `str2`,并返回一个整数值表示比较结果。 - **`< 0`**:`str1` 小于 `str2`。 - **`== 0`**:`str1` 等于 `str2`。 - **`> 0`**:`str1` 大于 `str2`。 ### 解题思路 - 如何手写一个堆? - 插入一个数 - 求集合中的最小值 - 删除最小值 - 删除任意一个元素 - 修改任意一个元素 - 堆是一个完全二叉树 - 除了最后一层的节点,上面所有的节点都是非空的 - 以小根堆为例 - 小根堆:任意节点都是小于等于它的孩子节点的 - 头节点是整个堆中的最小值
- 用一维数组就可以存储一个堆(下标从1开始),假设节点`1`存入数组中,那么 - 它的左儿子节点为`2` - 它的右儿子节点为`3` - 对于节点`x`,左儿子存入`2x`,右儿子存入`2x + 1`
- 堆中有两个操作,分别为`down`和`up` - `down`操作:用来下调一个数,直至这个数的位置稳定 - 在下调的过程中,需要将这个数与它的两个孩子节点进行比较 - 将三者中的最小值作为父亲节点 - `up`操作:用来上移一个数,直至这个数的位置稳定 - 如果当前节点比父节点小,那么就让该节点与父节点进行交换 - 实现操作: - 插入一个数:`heap[++ size] = x; up(size)` - 求集合中的最小值:`heap[1]` - 删除最小值:`heap[1] = heap[size]; size --; down(1);` - 删除任意一个元素:`heap[k] = heap[size]; down(k); up(k)` - 如果最后一个元素大于原来的元素,那么就要将这个新节点执行`down`操作,因为原来都无法往上走了,现在变大了更不能往上走了 - 如果最后一个元素小于原来的元素,那么就要将这个新节点执行`up`操作,因为原来都无法往下走了,那么现在变小了更不能往下走了 - 鉴于`up`和`down`操作只能执行一个,所以我们索性都写上就行 - 修改任意一个元素:`heap[k] = x; down(k); up(k);` ### 实现代码 ```cpp #include <bits/stdc++.h> using namespace std; const int N = 1e5 + 100; int n, m; int heap[N], length; void down(int u){ int t = u; // 注意下面两个判断条件都有等于号! if(2 * u <= length && heap[u * 2] < heap[t]) t = 2 * u; if(2 * u + 1 <= length && heap[u * 2 + 1] < heap[t]) t = 2 * u + 1; if(t != u){ swap(heap[u], heap[t]); down(t); } } int main(){ cin >> n >> m; for(int i = 1; i <= n; i ++) cin >> heap[i]; length = n; for(int i = n / 2; i; i --) down(i); while(m --){ printf("%d ", heap[1]); heap[1] = heap[length --]; down(1); } return 0; } ``` ## 模拟堆
### 知识点 ### 解题思路 - 该题的主要难点为**第`k`个插入的数字**,所以我们还要记录数字插入的次序 - 因为`h[]`数组中下标为`k`的数字不一定是第`k`个插入的数字,所以我们需要一个额外的值来存储某个数是第几个插入的 - `h[]`数组中存储的是该节点的值,该数组的下标用来标记该节点在堆的位置是什么 - `hp[]`数组中存储值的是该节点是第几个插入的,该数组的下标也是该节点在堆的位置是什么 - `hp[a] = m`表示位于堆中第`a`号位置的元素是第`m`个插入的 - `ph[]`数组中存储的是该节点在数组中的位置是什么,该数组的下表是该节点是第几个插入的 - `ph[m] = a`表示第`m`个插入的元素位于堆中的第`a`号位置 ### 实现代码 ```cpp #include <bits/stdc++.h> using namespace std; const int N = 1e5 + 100; int h[N], hp[N], ph[N], cnt; void heap_swap(int a, int b){ swap(ph[hp[a]], ph[hp[b]]); swap(hp[a], hp[b]); swap(h[a], h[b]); } void down(int u){ int t = u; if(2 * u <= cnt && h[2 * u] < h[t]) t = 2 * u; if(2 * u + 1 <= cnt && h[2 * u + 1] < h[t]) t = 2 * u + 1; // 必须使用h[2 * u + 1] < h[t],这样才能比较出来最小值 if(t != u){ heap_swap(t, u); down(t); } } void up(int u){ // int t = u; while(u / 2 && h[u] < h[u / 2]){ heap_swap(u, u / 2); u = u / 2; } } int main(){ int n, m = 0; cin >> n; while(n --){ char op[5]; int k, x; cin >> op; if(!strcmp(op, "I")){ cin >> x; cnt ++; m ++; // 不能自作聪明,一定要明确为什么要先cnt++和m++ // 否则会导致映射不对 ph[m] = cnt; hp[cnt] = m; h[cnt] = x; up(cnt); } else if(!strcmp(op, "PM")) cout << h[1] << endl; else if(!strcmp(op, "DM")){ heap_swap(1, cnt); cnt --; down(1); } else if(!strcmp(op, "D")){ cin >> k; k = ph[k]; heap_swap(cnt, k); cnt --; down(k); up(k); }else{ cin >> k >> x; h[ph[k]] = x; up(ph[k]); down(ph[k]); } } return 0; } ``` ## 前K个高频元素-LeetCode
### 知识点 - 模拟堆 - `unordered_map` ### 解题思路 - 构建大根堆 - 将数字出现的频率作为堆节点中的值 - 建立`unordered_map`用来记录每个堆节点对应的数字是谁 ### 实现代码 ```cpp class Solution { public: // 修改参数类型,ph现在是unordered_map void heapSwap(vector& h, vector& hp, unordered_map<int, int>& ph, int a, int b) { // 交换数字在堆中的位置 swap(ph[hp[a]], ph[hp[b]]); // 交换堆位置对应的数字 swap(hp[a], hp[b]); // 交换频率 swap(h[a], h[b]); } // 修改参数类型,ph现在是unordered_map void down(vector& h, vector& hp, unordered_map<int, int>& ph, int u, int cnt) { int t = u; if (2 * u <= cnt && h[t] < h[2 * u]) t = 2 * u; if (2 * u + 1 <= cnt && h[t] < h[2 * u + 1]) t = 2 * u + 1; if (t != u) { heapSwap(h, hp, ph, t, u); down(h, hp, ph, t, cnt); } } vector topKFrequent(vector& nums, int k) { vector ans; unordered_map<int, int> freq; // 使用哈希表统计频率 // 统计每个数字的频率 for (int num : nums) { freq[num]++; } int cnt = 0; vector h(freq.size() + 1, 0); // 频率 vector hp(freq.size() + 1, 0); // 堆位置->数字 unordered_map<int, int> ph; // 数字->堆位置 // 将频率数据放入堆中 for (auto& [num, count] : freq) { cnt++; h[cnt] = count; // 频率 hp[cnt] = num; // 数字 ph[num] = cnt; // 位置 } // 建立大根堆 for (int i = cnt / 2; i > 0; i--) { down(h, hp, ph, i, cnt); } // 取出前k个高频元素 for (int i = 0; i < k && cnt > 0; i++) { ans.push_back(hp[1]); // 堆顶元素 heapSwap(h, hp, ph, 1, cnt); cnt--; down(h, hp, ph, 1, cnt); } return ans; } }; ``` # 哈希表 ## 模拟散列表
### 知识点
- 哈希表:将一个大范围的数缩小到小范围内 - 例如:将`-1e9 ~ 1e9`的数字缩小到`1e5`之内 - 我们可以使用`x mod 1e5`进行,这个`mod`的数,一般要取成质数 - 对于冲突的值,我们要解决冲突;按照解决冲突的方式,我们将哈希表分为: - 开放寻址法 - 拉链法 ### 解题思路 #### 拉链法
- 在算法题中,哈希表一般不删除元素,如果要删除元素,我们使用一个额外的`bool`变量来记录该元素的状态 #### 开放寻址法
### 实现代码 #### 拉链法 ```cpp #include <bits/stdc++.h> using namespace std; const int N = 1e5 + 3; int h[N], e[N], ne[N], idx; void insert(int x){ int k = (x % N + N) % N; e[idx] = x; ne[idx] = h[k]; h[k] = idx ++; } bool find(int x){ int k = (x % N + N) % N; for(int i = h[k]; i != -1; i = ne[i]){ if(e[i] == x) return true; } return false; } int n; int main(){ cin >> n; memset(h, -1, sizeof h); while(n --){ int x; char op[2]; cin >> op >> x; if(op[0] == 'I') insert(x); else{ if(find(x)) cout << "Yes" << endl; else cout << "No" << endl; } } return 0; } ``` #### 开放寻址法 ```cpp #include <bits/stdc++.h> using namespace std; const int N = 2e5 + 3; const int null = 0x3f3f3f3f; int h[N]; int find(int x){ int t = (x % N + N) % N; while(h[t] != null && h[t] != x){ t ++; if(t == N) t = 0; } return t; } int main(){ int n; cin >> n; memset(h, 0x3f, sizeof h); while(n --){ char op[2]; int x; cin >> op >> x; if(op[0] == 'I'){ h[find(x)] = x; } else{ if(h[find(x)] == null) cout << "No" << endl; else cout << "Yes" << endl; } } } ``` ## 字符串哈希
### 知识点 ### 解题思路 - 将一个字符串转换为P进制的数进行表示 - 将这个P进制的数转化为十进制的数之后`mod`一个数字,最后的结果作为这个字符串的哈希值 - 假设字符串`ABCD`对应的P进制数为`1234` - $(1234)_p=1\times p^3+2\times p^2+3\times p^1+4\times p^0$ - $\text{哈希值}=(1\times p^3+2\times p^2+3\times p^1+4\times p^0)\text{ mod }Q$ - $\text{经验值:}p=131\text{或}13331,Q=2^{64}$ - 在这个过程中,我们要求: - 某一个字符不能被映射成0 - 我们的人品足够好,在转换哈希值时,不会遇到冲突
- 将字符串的从左数第一个字符作为高位(和数字一样),计算字符串`[1 ~ R]`的哈希值,记为$h[R] = a\times p^{R-1} + ...$ - 同样,计算字符串`[1 ~ L - 1]`的哈希值,记为$h[L - 1] = b\times p^{L - 2} + ...$ - 为了计算出字符串`[L ~ R]`的哈希值,我们将`h[L - 1]`与`h[R]`对齐,也就是进行补位。将补位后的值作为减数,`h[R]`作为被减数,二者的差即为待求的哈希值$h[R] - h[L - 1] * p ^{R - L + 1}$ ### 实现代码 ```cpp #include <bits/stdc++.h> using namespace std; typedef unsigned long long ULL; const int N = 1e5 + 100, P = 131; int n, m; char str[N]; ULL h[N], p[N]; ULL get(int l, int r){ return h[r] - h[l - 1] * p[r - l + 1]; } int main(){ cin >> n >> m >> str + 1; p[0] = 1; for(int i = 1; i <= n; i ++){ p[i] = p[i - 1] * P; h[i] = h[i - 1] * P + str[i]; } while(m --){ int l1, r1, l2, r2; cin >> l1 >> r1 >> l2 >> r2; if(get(l1, r1) == get(l2, r2)) cout << "Yes" << endl; else cout << "No" << endl; } return 0; } ```



Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Google Gemini updates: Flash 1.5, Gemma 2 and Project Astra
  • Displaying External Posts on Your al-folio Blog
  • 强化学习导论
  • 企业项目实训
  • 面试总结