数据结构(二)__习题——Trie、并查集、堆、栈

前言

重学算法第6天,希望能坚持打卡不间断,从基础课开始直到学完提高课。

预计时长三个月内,明天再来!肝就完了
2月18日,day06 打卡

今日已学完y总的
算法基础课-2.4-Week3 习题课
共3题,知识点如下
Trie(单词查找树):最大异或对
并查集:食物链

复习了
堆:模拟堆

额外补充1题合计4题
栈:表达式求值

Trie树

AcWing 143. 最大异或对

思路:

异或:相同为0,不同为1,俗称不进位加法

在这里插入图片描述

暴力做法

在这里插入图片描述
优化

每次从高位往前找,每次尽量往与当前位不同的分支走(相同为0,不同为1)
如果有0和1都可选,就选高位开始不同的(异或后更大),相同的划掉
如果只有1个可以走那就没得选,

在这里插入图片描述

我们当前走的分支每次都比划去的大,所以直达走到最低位时,当前走的分支就是
异或后得到最大值的分支

可以用 trie 来存储每个数
把每个整数看成31位的二进制串
在这里插入图片描述
从根节点开始,每次尽量从与当前位不同的上面走,走到叶结点就找到了

trie 不光能存字符串,还能存整数,二进制数,
计算机内所有信息都是二进制表示,因此 trie 能存储所有信息

样例
在这里插入图片描述

可以边插入边查找

每次写代码前最好先把思路梳理一下

从前往后枚举,插入一个数,查询一个数
每次查询的是a[i]前面a[i] 异或 值最大的是啥

#include <iostream>
#include <algorithm>

using namespace std;

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

int n;
int a[N];
int son[M][2], idx;

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]; // p走到儿子上去
        
    }
}

int query(int x) {
    int p = 0, res = 0;
    for (int i = 30; i >= 0; i--) {
        int u = x >> i & 1; // 0或1 
        if (son[p][!u]) {
            p = son[p][!u]; // 另一个方向(与该位不等的)如果存在就走上去
            res = res * 2 + !u;
        } else {
            p = son[p][u];
            res = res * 2 + u; // *2 相当于左移一位,最后1位 置0,如果u为0加0,为1就加1
        }
    }
    return res;
}

int main() {
    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]); // t为与a[i] 异或 值最大的
        res = max(res, a[i] ^ t);
    }
    
    printf("%d\n", res);
    
    return 0;
}

res

在这里插入图片描述

并查集

AcWing 240. 食物链

样例

环形,3被2吃,2被1吃,1被3吃

在这里插入图片描述

只要知道两个动物的关系,就放到一个集合里面,
一定可以推出同一集合中所有对应的关系(只有三种动物)

如:xy是同类,y被z吃,则x也被z吃
在这里插入图片描述
x被y吃,y被z吃,则z被x吃
在这里插入图片描述

如何确定关系?

记录每个点与根节点的关系
用每个点到根节点的距离来记录关系

可以分为3大类

在这里插入图片描述
在这里插入图片描述
距离的含义:表示第几代
y是0代,x吃y的就是1代,z吃x就是2代,如果k吃z就是3代

该题只有ABC三种,是循环的
A是0代,B吃A是1代,C吃B是2代,
第三代吃C,而A吃C,所以第三代就是第0代,即A
第四代吃A,而B吃A,所以第四代就是第2代,即B
所以用模表示即可 ABC
余1吃A —第1代,是B
余2吃B —第2代,是C
余0吃C —同类,是A
在这里插入图片描述

本来是存的到父节点的距离,路径压缩的时候更新成到根节点的距离即可
在这里插入图片描述
而只要知道到根节点的距离,就能根据余数判断出关系
在这里插入图片描述

#include <iostream>

using namespace std;

const int N = 50010;

int n, m; // m表示说话的次数
int p[N], d[N]; // p: 父节点, d: 距离

int find(int x) {
    if (p[x] != x) {
        // 用t存储的原因:find之后d[p[x]里面存的就不是p[x]这个点了,而是根节点
        int t = find(p[x]); //定义1个变量存一下p[x]的根节点是谁
        d[x] += d[p[x]]; // 两段相加就是总距离
        p[x] = t; // 加完再让p[x] 指向根节点
    }
    return p[x];
}

int main() {
    scanf("%d%d", &n, &m);
    //把每个点先初始化
    for (int i = 1; i <= n; i++) p[i] = i; //d=0,不需要初始化了,全局变量默认值为0

    int res = 0; 
    while (m--) {
        int t, x, y; // t表示询问的种类(D)
        scanf("%d%d%d", &t, &x, &y);
        
        if (x > n || y > n) res++; // 当前的话中x或y比n大,是假话
        else { 
            int px = find(x), py = find(y); // x, y的根节点
            if (t == 1) {// 若X和Y是同类
                
                // 若 x 与 y 在同一个集合中
                if (px == py && (d[x] - d[y]) % 3) res++; //两数到根节点距离之差 %3不为 0,说明不是同一类,是假话
                
                // 若 x 与 y 不在同一个集合中
                else if (px != py) {
                    p[px] = py;  // 让py成为px的父节点,即把x所在集合的根节点插入到y所在集合根节点上
                    d[px] = d[y] - d[x];
                }
            }
            else { // 若X吃Y , 即%3后,x要比y多1
                if (px == py && (d[x] - d[y] - 1) % 3) res++; // 若距离之差-1 %3不为 0,说明吃不掉,是假话
                else if (px != py) {
                    p[px] = py;
                    d[px] = d[y] + 1 - d[x];
                } 
            }
        }
    }

    printf("%d\n", res);
    
    return 0;
}

距离图

在这里插入图片描述

不在一个集合中时

xy为同类,不在一个集合中时,(d[p(x)] + d(x) - d(y) ) %3 = 0 需要成立
d[p(x)] = d(y) - d(x)

在这里插入图片描述

凡是涉及集合合并的都可以用并查集
考虑什么时候用啥数据结构时,
先看题目中的哪些操作可以进行优化,操作有什么样的特点
如:
寻找一堆数里的最小值或最大值-----用堆来做
维护有序链表—用平衡树来做
维护区间最大值,区间和等—可能要用树状数组或者线段树来做
并查集的题目不太容易看出来,需要多做题

AcWing 839. 模拟堆

该题难在需要支持随便的修改和插入,修改堆里的某一个元素
就需要存映射,即难在映射
交换的时候每个点的映射也需要交换

ph,左边指向右边
hp,右边指向左边

在这里插入图片描述
hp[a]hp[b]就是a b指向左边点的,绿线
在这里插入图片描述
ph[hp[a]]就是左边点hp[a]指向 a 的
ph[hp[b]]就是左边点hp[b]指向 b 的
交换两条蓝色的线的指向 swap(ph[hp[a]], ph[hp[b]]);,变为虚线
在这里插入图片描述
交换绿色线的指向swap(hp[a],hp[b]);,变成虚线
在这里插入图片描述

最后交换ab的值swap(h[a],h[b]);
最难的就是交换操作,其他的与堆没有区别

down操作和up操作略,看上一篇即可

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

const int N = 100010;

int n, m;
 
int h[N], sizes; 
// ph[k]存的是第k个插入的数下标是多少
// hp[k]堆里的某个点是第几个插入点
// 既要从第几个插入点找堆内到元素,又要堆里的元素找回来,所以要一一对应
int ph[N], hp[N];

// 存储映射,不常用
void heap_swap(int a, int b) {
    // 需要把所有指向都交换,继续一一对应
    swap(ph[hp[a]], ph[hp[b]]);  // ph:从下标映射到堆
    swap(hp[a],hp[b]);  //hp:从堆里映射回下标
    swap(h[a],h[b]);     //交换堆内元素
}

void down(int u) {
    int t = u;
    if (u * 2 <= sizes && h[u * 2] < h[t]) t= u * 2; 
    if (u * 2 + 1 <= sizes && h[u * 2 + 1] < h[t]) t= u * 2 + 1; 
    if (u != t) { 
        heap_swap(u, t);
        down(t);
    }
}

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

int main() {
    int n, m = 0;
    scanf("%d", &n);
    while (n --) {
        char op[10];
        int k, x;
        scanf("%s",op);
        if (!strcmp(op, "I")) { // 插入一个数 x;
            scanf("%d", &x);
            sizes++; //堆里要多一个元素
            m++; // m表示第几个插入的数
            
            ph[m] = sizes; // 第m个插入的数在堆中的sizes位置
            hp[sizes] = m; // 堆里面sizes这个数对应的是m
            
            h[sizes] = x;
            up(sizes); // 插入到堆尾,需要向上走
        }
        // strcmp(str1,str2),若str1=str2,则返回零;若str1>str2,则返回正, <返回负数
        else if (!strcmp(op, "PM")) printf("%d\n", h[1]);//输出当前集合中的最小值
        else if (!strcmp(op, "DM")) {
            heap_swap(1, sizes); // 将最后一个的值与最小值交换
            sizes--; // 删除最后一个
            down(1); // 下沉
        }
        else if (!strcmp(op, "D")) { //删除第 k 个插入的数;
            scanf("%d", &k);
            k = ph[k]; // 找到k在堆里的位置
            heap_swap(k, sizes);
            sizes--;
            down(k), up(k); // 最多只会执行一个
        } else { // 修改第 k 个插入的数,将其变为 x;
            scanf("%d%d", &k, &x);
            k = ph[k]; // 先找个第k个插入的数 在堆里的位置
            h[k] = x;  // 直接修改成x
            down(k), up(k);
        }
    } 
    
    return 0;
}

3302. 表达式求值

在这里插入图片描述
中缀表达式需存运算符

可以用栈来达到用递归求解的效果

一开始所有父节点都存的是运算符,算完之后将值存在运算符处
左中右将整棵树遍历完之后,就可以从下往上算了
用下面两个的值算出父节点处的值
在这里插入图片描述
默认根节点的操作符优先级最低,越往下优先级越高

如何判断某颗子树被遍历完?

由于先操作优先级高的,低的后操作

往上走,如果前运算符的优先级低于上一个运算符
说明当前节点的左右子树都操作完了

在这里插入图片描述
如果在括号内,必然是往下走的,需要从右往左算

运算符可以任意定,所以该题是个模板题

#include <iostream>
#include <cstring>
#include <algorithm>
#include <stack>
#include <unordered_map>

using namespace std;

// 需要存数字和运算符
stack<int> num;
stack<char> op;

//eval() 即操作最后一个运算符(末尾运算符操作一下末尾两个数)
void eval() { 
    auto b = num.top(); num.pop();// b是倒数第一个数,右子树 
    auto a = num.top(); num.pop();// a是倒数第二个数,左子树
    auto c = op.top(); op.pop(); //取出运算符(a b的父节点)
    int x;
    if (c == '+') x = a + b;
    else if (c == '-') x = a - b;
    else if (c == '*') x = a * b;
    else x = a / b;
    num.push(x); // 将数存到栈内(原来运算符位置) 
}

int main() {
    // 定义优先级,数越大优先级越大
    unordered_map<char, int> pr{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
    string str;
    cin >> str;
    for (int i = 0; i < str.size(); i++) {
        auto c = str[i];
        if (isdigit(c)) { // 如果当前字符为数字
            int x = 0, j = i;
            while (j < str.size() && isdigit(str[j])) // 括号别漏了或者写错了
                x = x * 10 + str[j ++] - '0';
            i = j - 1;
            num.push(x); // 当前数为x
        }
        else if (c == '(') op.push(c); // 左括号直接入栈
        else if (c == ')') {
            // 将栈内所有运算符,从右往左操作一遍直到遇到(
            while (op.top() != '(') eval();
            op.pop(); // 左括号弹出
        }
        else { // 一般运算符
            // 栈不为空且栈顶元素优先级大于等于当前元素优先级,操作栈顶元素
            while (op.size() && pr[op.top()] >= pr[c]) eval(); 
            // 否则将当前元素插入栈里
            op.push(c);
        }
    }
    // 将所有没有操作的运算符,从右往左操作一遍
    while (op.size()) eval();
    // 最后栈顶元素就是答案
    cout << num.top() << endl;
    
    return 0;
}

评论区

在这里插入图片描述
优质题解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值