数据结构

写在前面

感谢y总。

链表

单链表

#include <iostream>

using namespace std;

const int N = 1e6+10;

//head表示头指针,e[i]表示节点i的值,ne[i]表示节点i的next指针,idx存储当前未使用的那个点
//idx = 0,1,2,3,...
int head = -1,idx = 0, e[N], ne[N];
//将x插入到头节点
void add_to_head(int x){
    e[idx] = x;
    ne[idx] = head;
    head = idx;
    idx++;
}
//将x插入到下标是k的点的后面
void add(int k,int x){
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx++;
}
//将下标是k的点的后面的点删掉
void remove_k(int k){
    ne[k] = ne[ne[k]];
}
//删除头节点
void remove_head(){
    head = ne[head];
}

双链表

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

// 初始化
void init()
{
    //0是左端点,1是右端点
    r[0] = 1, l[1] = 0;
    idx = 2;
}

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx ++ ;
}

// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a];
    r[l[a]] = r[a];
}

// tt表示栈顶
int stk[N], tt = 0;

// 向栈顶插入一个数
stk[ ++ tt] = x;

// 从栈顶弹出一个数
tt -- ;

// 栈顶的值
stk[tt];

// 判断栈是否为空,如果 tt > 0,则表示不为空
if (tt > 0)
{

}

Acwing 3302. 表达式求值

#include <iostream>
#include <unordered_map>

using namespace std;

const int N = 100010;

int st_num[N], st_op[N];
int top1 = 0, top2 = 0;

void eval(){
    auto b = st_num[top1]; top1--;
    auto a = st_num[top1]; top1--;
    auto x = st_op[top2]; top2--;
    if (x == '+') st_num[++top1] = a+b;
    if (x == '-') st_num[++top1] = a-b;
    if (x == '*') st_num[++top1] = a*b;
    if (x == '/') st_num[++top1] = a/b; 
}

int main(){
    unordered_map<char, int> pr{
        {'+' , 1}, { '-' , 1 }, {'*', 2}, {'/', 2}
    };
    string str;
    cin>>str;
    for (int i = 0; i < str.length(); i++){
        auto c = str[i];
        if (isdigit(c)){
            int x = 0, j = i;
            while (j < str.length() && isdigit(str[j])){
                x = x * 10 + str[j++] - '0';
            }
            i = j-1;
            st_num[++top1] = x;
        }
        else if (c == '(') st_op[++top2] = c;
        else if (c == ')') {
            while (st_op[top2] != '(') eval();
            top2--;
        }
        else {
            while (top2 && pr[st_op[top2]] >= pr[c]) eval();
            st_op[++top2] = c;
        }
    }
    while (top2) eval();
    cout<< st_num[top1]<<endl;
}

队列

// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;

// 队头的值
q[hh];

// 判断队列是否为空,如果 hh <= tt,则表示不为空
if (hh <= tt)
{

}

单调栈

常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}

单调队列

常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ;
    q[ ++ tt] = i;
}

KMP算法

时间复杂度为\(O(n + m)\)

精髓是主串的指针i永远不回退,当发现不匹配时,子串指针j按照next数组进行回退。若回退到0,i++。

假设主串和子串都是下标从1开始匹配。

Acwing 831. KMP字符串

#include <iostream>

using namespace std;

const int N = 100010, M = 1000010;

char s[M], p[N];

int m, n, ne[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 = ne[j];
        if (p[i] == p[j+1]) j++;
        ne[i] = j;
    }
    // kmp匹配过程
    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){
            // cout << i - j + 1 << ' '; 
            cout << i - j<< ' '; // 由于在读入时下标为1开始,所以最终将i-j+1改成i-j
            j = ne[j];
        }
    }
}

Trie树

用于高效存储和查找某一字符串是否存在集合中。

常用于解决前缀的快速合并和查找。

节点标记:表示以当前字母结尾的是有单词的。

字符串存储

AcWing 835. Trie字符串统计

#include <iostream>

using namespace std;

const int N = 100010;

int son[N][26], cnt[N], idx = 0; // 存储子节点、以i结尾的字符串个数、节点编号
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] = ++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;
    scanf("%d", &n);
    while (n--){
        char op[2];
        scanf("%s%s", op, str);
        if (*op == 'I') insert(str);
        else cout<<query(str)<<endl;
    }
}

整数存储

AcWing 143. 最大异或对

大致思路:

将每个数的二进制状态由高位到低位保存到Trie树中。对于数x中的第i位u,Trie树中有存储son[p][!u]则向该方向搜索,此时第i位的异或结果为1,保证高位异或结果尽可能大;如若没有,则只能走向son[p][u]。

一些要注意的点:

  • 异或操作:^
  • 取出二进制数的第i位的简单写法:int u = x >> i & 1;
  • 建议边插入边查询(\(C^2_n\)),依旧能够保证结果。
#include <iostream>

using namespace std;

const int N = 100010, M = N * 31;

int son[M][2], idx = 0;

void insert(int x){
    int p = 0; // 根节点
    for (int i = 30; i >= 0; i -- ){ // 插入操作
        int u = x >> i & 1; // 取出x二进制下第i位的数
        if (!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
    }
}

int query(int x){
    int p = 0, ans = 0;
    for (int i = 30; i >= 0; i -- ){
        int u = x >> i & 1;
        if (son[p][!u]){
            p = son[p][!u];
            ans = ans * 2 + !u;
        }
        else{
            p = son[p][u];
            ans = ans * 2 + u;
        }
    }
    return ans;
}

int main(){
    int n, res = 0;
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ){ // 每次读入一个数,和它之前的进行异或比较即可
        int num;
        scanf("%d", &num);
        insert(num);
        res = max(res, num ^ query(num)); // 更新最大异或结果
    }
    cout << res << endl;
}

并查集

近乎O(1)常见操作:

  • 将两个集合合并
  • 询问两个元素是否在同一个集合当中

基本原理:

  • 每个集合用一棵树表示。树根的编号就是集合的编号。
  • 每个节点存储它的父节点p[x]

如何判断树根:if (p[x] == x) 父节点编号和节点编号相同
如何求x的集合编号:while (p[x] != x) x = p[x];
如何将两个集合合并:px是x的集合编号,py是y的集合编号,让p[x] = y。

优化:路经压缩。查询x之后,将x到根节点路径上的所有点全部指向根节点。

Acwing 836. 合并集合

#include <iostream>

using namespace std;

const int N = 100010;

int p[N];

int find(int x){ //返回x的祖宗节点+路经压缩
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main(){
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++) p[i] = i;
    while (m--){
        char op[2];
        int a, b;
        scanf("%s%d%d", op, &a, &b); // scanf读字符串自动忽略空格和回车
        if (op[0] == 'M') p[find(a)] = find(b); // 集合合并
        else {
            if (find(a) == find(b)) puts("Yes");
            else puts("No");
        }
    }
}

AcWing 240. 食物链

如何根据已知元素的两两关系得到任意元素之间的两两关系?

确定并查集树中每个节点和根节点之间的关系,用它到根节点的距离表示:

  • 模三余一:可以吃根节点
  • 模三余二:可以吃一,也可以被根节点吃
  • 模三余零:和根节点同类

d[N]:维护当前节点到父节点之间的距离。路径压缩之后,父节点也就是根节点。

#include <iostream>

using namespace std;

const int N = 100010;

int p[N], d[N];

int find(int x){
    if (p[x] != x){
        int t = find(p[x]); // 找到根节点
        d[x] += d[p[x]]; // x到父节点的距离加上p[x]到根节点的距离
        p[x] = t; // 将x的父节点指向根节点
    }
    return p[x];
}

int main(){
    int n, k, ans = 0;
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; i++) p[i] = i;
    while (k--){
        int l, x, y;
        cin>>l>>x>>y;
        if (x > n || y > n) {
            ans++;
            continue;
        }
        int px = find(x), py = find(y);
        if (l == 1){ // 操作类别为1
            if (px == py && (d[x] - d[y]) % 3) ans++;// x和y在同一集合中,此时判断两两关系
            else if (px != py){ // 此时x和y还不在集合中。对二者合并,并修改到根节点的距离。
                p[px] = py;
                d[px] = d[y] - d[x];
            }
        } 
        else { // 操作类别为2
            if (px == py && (d[x] - d[y] - 1) % 3) ans++;
            else if (px != py){
                p[px] = py;
                d[px] = d[y] - d[x] + 1;
            }
        }
    }
    cout<<ans<<endl;    
}

模版题
Acwing 4420. 连通分量

  • 坐标(x, y)转id
  • set的维护和插入、判断是否含有某个数
  • 坐标是否越界的判断
#include <iostream>
#include <set>
#include <algorithm>

using namespace std;

const int M = 1010;
const int N = 1e6 + 10;

int n, m, p[N], size_[N];
char g[M][M];

int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, -1, 0, 1};

int find(int x){
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}

int get(int x, int y){
    return (x - 1) * m + y;
}

int main(){
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n * m; i++)
        p[i] = i, size_[i] = 1;
    char ch;
    scanf("%c", &ch);
    for (int i = 1; i <= n; i++){
        for (int j = 1; j <= m; j++){
            scanf("%c", &g[i][j]);
        }
        scanf("%c", &ch);
    }
    for (int i = 1; i <= n; i++){
        for (int j = 1; j <= m; j++){
            if (g[i][j] == '*')
                continue;
            int id1 = get(i, j);
            for (int k = 0; k < 4; k++){
                int x = i + dx[k], y = j + dy[k];
                if (x >= 1 && x <= n && y >= 1 && y <= m && g[x][y] == '.'){
                    int id2 = get(x, y);
                    if (find(id1) != find(id2)){
                        size_[find(id2)] += size_[find(id1)];
                        p[find(id1)] = find(id2);
                    }
                }
            }
        }
    }
    for (int i = 1; i <= n; i++){
        for (int j = 1; j <= m; j++){
            if (g[i][j] == '.'){
                printf("%c", g[i][j]);
                continue;
            }
            int ans = 1;
            set<int> nodes;
            for (int k = 0; k < 4; k++){
                int x = i + dx[k], y = j + dy[k];
                if (x >= 1 && x <= n && y >= 1 && y <= m && g[x][y] == '.'){
                    int id = get(x, y);
                    int father = find(id);
                    if (! nodes.count(father)){
                        nodes.insert(father);
                        ans += size_[father];
                    }
                }
            }
            printf("%d", ans % 10);
        }
        printf("\n");
    }
}

手写一个堆,支持的操作:

  • 插入一个数:heap[++size] = x; up(size);
  • 求集合当中的最小值:heap[1];
  • 删除最小值:heap[1] = heap[size]; size --; down(1);
  • 删除任意一个数(stl不支持):heap[k] = heap[size]; size--; up(k); down(k);
  • 修改任意一个数(stl不支持):heap[k] = x; up(k); down(k);

AcWing 839. 模拟堆

#include <iostream>
#include <string.h>

using namespace std;

const int N = 100010;

int h[N], ph[N], hp[N], size_ = 0;

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 (u * 2 <= size_ && h[u * 2] < h[t])
        t = u * 2;
    if (u * 2 + 1 <= size_ && h[u * 2 + 1] < h[t])
        t = u * 2 + 1;
    if (t != u){
        heap_swap(t, u);
        down(t);
    }
}

void up(int u){
    while (u / 2 && h[u / 2] > h[u]){
        heap_swap(u, u / 2);
        u /= 2;
    }
}

int main(){
    int n, m = 0;
    scanf("%d", &n);
    while (n--){
        char str[2];
        int k, x;
        scanf("%s", str);
        if (!strcmp(str, "I")){
            cin >> x;
            m++;
            h[++size] = x, ph[m] = size_, hp[size_] = m;
            up(size_);
        }
        else if (!strcmp(str, "PM"))
            cout << h[1] << endl;
        else if (!strcmp(str, "DM")){
            heap_swap(1, size_);
            size_--;
            down(1);
        }
        else if (!strcmp(str, "D")){
            cin >> k;
            k = ph[k];
            heap_swap(k, size_);
            size_--;
            down(k), up(k);
        }
        else{
            cin >> k >> x;
            k = ph[k];
            h[k] = x;
            down(k), up(k);
        }
    }
}

哈希表

大规模数据向小规模值域的映射, h[k] = x。

常见操作:添加一个数、查找一个数。

对于删除操作:不是真的删除,开一个bool变量表示。

时间复杂度趋近于O(1)。

哈希冲突:

  • 把两个不同的数映射为了同一个数。

  • 通过开放寻址法和拉链法解决。

  • 存储结构

    • 开放寻址法
    • 拉链法
  • 字符串哈希方式

开放寻址法

只开一个一维数组,没开链表。数组长度为数据规模的2-3倍。

h(x) = k。从第k个坑位开始向后遍历,直到有坑位为止。

int h[N];

int null = 0x3f3f3f3f;

memset(h, 0x3f, sizeof h); // 按字节进行0x3f,int型数据有四个字节

// 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
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 k = find(x);
判断h[k]是否等于null。

拉链法

开一个数组存储所有的哈希值。在第i个哈希值拉一条链,附上原数值。

模的数尽量取质数,降低哈希冲突几率。

Acwing 840. 模拟散列表

#include <iostream>
#include <cstring>

using namespace std;

const int N = 100003;

int h[N], e[N], ne[N], idx = 0;

void insert(int x){
    int k = (x % N + N) % N; //考虑x取负的情况,要将值域全映射为正数。
    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 main(){
    memset(h, -1, sizeof h);
    int n;
    cin>>n;
    while (n--){
        char op[2];
        int x;
        scanf("%s %d",op, &x);
        if (*op == 'I') insert(x);
        else {
            if (find(x)) cout<<"Yes"<<endl;
            else cout<<"No"<<endl;
        }
    }
}

字符串前缀哈希法

快速判断两个区间的字符子串是否相同。

前缀哈希值的定义:将n位字符串映射为n位p进制的数,再模上Q。这样就将字符串映射到了0~Q-1的数字。

  • 在单个字母映射规则中,不能映射为0。(如'A', 'AA', ..., 都是0)
  • 通常情况下,完全不考虑冲突情况。
  • 经验值:P取131 or 13331, Q取\(2^{64}\)
  • 好处:可以由求得的前缀哈希得到某一指定的子串哈希。
  • 用unsighed long long来存储所有的h,这样不用取模。

Acwing 841. 字符串哈希

#include <iostream>

using namespace std;

typedef unsigned long long ULL;

const int N = 100010, P = 131;

char str[N];
ULL h[N], p[N];

ULL get(int l, int r){ // 获得子串区间l-r的哈希值,由于使用了ULL,不用再模上Q
    return h[r] - h[l-1] * p[r - l + 1];
}

int main(){
    int n, m;
    cin>>n>>m;
    scanf("%s", str+1);
    p[0] = 1;
    for (int i = 1; i <= n; i ++){
        h[i] = h[i-1]*P + str[i];
        p[i] = p[i-1] * P;
    }
    while (m--){
        int l1, r1, l2, r2;
        cin>>l1>>r1>>l2>>r2;
        if (get(l1, r1) == get(l2, r2)) puts("Yes");
        else puts("No");
    }
}

C++ STL

string

  • size()/length()
  • clear()
  • empty()
  • string a = "zzy"; a += "cs"
  • a.substr(i, len): 从第i个位置开始,长度为n的子串。省略第二个参数,一直到串尾。

pair

pair<type1, type2>p用于存储二元组。前后两个数据类型任意。(如pair<int, string>p)

  • 取得第一个元素:p.first
  • 取得第二个元素:p.second
  • 支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
  • 定义:p =
  • 三种属性的存储:pair<int, pair<int, int>>p

stack

  • st.size()
  • st.empty()
  • st.top()
  • st.push()
  • st.pop()

queue

  • q.size()
  • q.empty()
  • q.push()
  • q.pop()
  • q.front()
  • q.back()

deque

  • q.size()
  • q.empty()
  • q.push_front(), q.pop_front()
  • q.push_back(), q.pop_back()
  • q.front(), q.back()
  • q.begin(), q.end()
  • 支持按索引寻址[]

优先队列

priority_queue:默认是大根堆。

小根堆的定义:

priority_queue<int, vector<int>, greater<int>> heap;

常见方法:

priority_queue<int> heap;
heap.size()
heap.empty()
heap.push()  插入一个元素
heap.top()  返回堆顶元素
heap.pop()  弹出堆顶元素

set, map

set<int> s
multi_set<int> ms

set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
    size()
    empty()
    clear()
    begin()/end()
    ++, -- 返回前驱和后继,时间复杂度 O(logn)

    set/multiset
        insert()  插入一个数
        find()  查找一个数
        count()  返回某一个数的个数
        erase()
            (1) 输入是一个数x,删除所有x   O(k + logn)
            (2) 输入一个迭代器,删除这个迭代器
        lower_bound()/upper_bound()
            lower_bound(x)  返回大于等于x的最小的数的迭代器
            upper_bound(x)  返回大于x的最小的数的迭代器
    map/multimap
        insert()  插入的数是一个pair
        erase()  输入的参数是pair或者迭代器
        find()
        []用作赋值和查找,可以像数组一样操作!
        注意multimap不支持此操作。 时间复杂度是 O(logn)
        lower_bound()/upper_bound()
posted @ 2024-11-04 15:32  周哲宇Ghost  阅读(31)  评论(0)    收藏  举报