Trie,并查集,堆

本文介绍了Trie字典树的存储和查找机制,以及如何用Trie统计字符串和数字。同时详细讲解了并查集的基本原理、路径压缩优化和实际应用,包括如何合并集合及查询元素关系。还涵盖了堆的数据结构,包括大根堆和小根堆的存储、down()和up()操作的实现,以及堆排序和复杂情况下的堆模拟。
摘要由CSDN通过智能技术生成

1. Trie 字典树

高效地存储和查找字符串、数字等集合的数据结构。
在这里插入图片描述
一般在每个词的结尾打一个标记。

  • 实际存储中,图中的每个字符都是被存储在边上的,节点是不存储字符信息的,节点存储的是idx,即节点编号。
  • 查询某个字符串S是否在这个字符串集合中是否存在的话,可以从根开始遍历,当遍历到空节点或者S已经遍历结束但是trie树中对应节点不是字符串的话,说明不存在该字符串。

Trie字符串统计

#include <iostream>

using namespace std;

const int N = 1e5 + 10; //注意这里的N是所有输入的字符串最大总长度,不是指单个字符串的最长长度

// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量
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 
    {
        int u = str[i] - 'a'; //将每个字符映射到0~25
        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]; //字符数组的结尾是 \0 
        scanf("%s%s", op, str); //用scanf读入字符数组时不用加&
        if (op[0] == 'I') insert(str); //注意是 op[0],op[2] = \0
        else printf("%d\n", query(str));
    }
    
    return 0;
}

最大异或对
Trie还可以存储数字,将十进制数字转换为位数相同的二进制数,对于每一位找不同的数异或结果最大(0找1,1找0)。

#include <iostream>

using namespace std;

const int N = 1e5 + 10, M = 31e5 + 10; //每个数转换为二进制最多有31位

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

void insert(int x)
{
    int p = 0; //根节点
    for (int i = 30; i >= 0; i--) //从最高位开始存储,统一位数
    {
        int u = x >> i & 1;
        if (!son[p][u]) son[p][u] = ++idx;
        p = son[p][u];
    }
    //长度都一样就不用cnt数组标记最后一位了
}

int search(int x)
{
    int p = 0, res = 0;
    for (int i = 30; i >= 0; i--)
    {
        int u = x >> i & 1;
        if (son[p][!u]) //取不同的数异或结果最大
        {
            p = son[p][!u];
            res = res * 2 + 1; //异或的结果  左移1位再+1
        }
        else //如果不存在
        {
            p = son[p][u];
            res = res * 2 + 0;
        }
    }
    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]);
        res = max(res, search(a[i]));
    }
    
    printf("%d", res);
    
    return 0;
}

2. 并查集

  1. 将两个集合合并
  2. 询问两个元素是否在一个集合中

基本原理:每个集合用一棵表示。树根的编号就是整个结合的编号。每个节点存储它的父节点,p[x]表是x的父节点。

  1. 如何判断树根:if(p[x]==x)。除了根节点外,p[x]都不等于x
  2. 如何求x的集合编号:while (p[x] != x) x = p[x],只要p[x]不等于x,就一直往上走。
  3. 如何合并两个集合:把一棵树插到另一棵树的根节点上。px是x的集合编号,py是y的集合编号,p[x]=y

优化:路径压缩 find(int x)

通过递归的方法来逐层修改返回时的某个节点的直接前驱(即pre[x]的值)。简单说来就是,当从一个节点一直往上找找到根节点时,即找到了一条路,将x到根节点路径上的所有点的pre(上级)都设为根节点。

例题

合并集合
这里用字符数组(字符串)op[2]不用字符op,是因为scanf读入字符%c时会读入空格或回车等一些字符,太麻烦了。但是scanf读入字符串时会自动忽略空格和回车,所以用scanf读入一个字符时建议还是用字符串的形式。

#include <iostream>

using namespace std;

const int N = 1e5 + 10;
int n, m;
int p[N]; //存的是每个元素的父节点是谁

//核心
int find(int x) //返回x的祖宗节点 + 路径压缩(每个节点的父节点都会指向根节点)
{
    if (p[x] != x) p[x] = find(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[2](字符串)不用字符op是因为scanf读入%c时会读入空格或回车等一些字符,太麻烦了。但是scanf读入字符串时会自动忽略空格和回车,所以用scanf读入一个字符时建议还是用字符串的形式。
        int a, b;
        scanf("%s%d%d", op, &a, &b);
        
        if (op[0] == 'M') p[find(a)] = find(b); //将a所在集合的根的父亲设置为b所在集合的根
        else
        {
            if (find(a) == find(b)) puts("Yes");
            else puts("No");
        }
    }
    
    return 0;
}

连通块中点的数量
注意顺序

#include <iostream>

using namespace std;

const int N = 1e5 + 10;
int n, m;
int p[N], si[N]; //记录每个节点所在连通块中点的数量,只在根节点的值有意义

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

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
    {
        p[i] = i;
        si[i] = 1;
    }
    
    while (m--)
    {
        char op[3];
        int a, b;
        scanf("%s", op);
        if (op[0] == 'C')
        {
            scanf("%d%d", &a, &b);
            if (a == b || find(a) == find(b)) continue;
            
            //注意下面两句顺序不能换,必须要先加连通块大小再操作集合,
            //否则操作完集合后,a和b的根结点将会重叠,导致连通块大小计算错误
            si[find(b)] += si[find(a)];
            p[find(a)] = find(b);
        }
        else if (op[1] == '1')
        {
            scanf("%d%d", &a, &b);
            if (a == b || find(a) == find(b)) printf("%s\n", "Yes");
            else printf("%s\n", "No");
        }
        else
        {
            scanf("%d", &a);
            printf("%d\n", si[find(a)]);
        }
    }
    
    return 0;
}

食物链
用并查集维护额外信息d[x]:每个节点到根节点的距离。
依题意知,总共ABC三类动物,A吃B,B吃C,C吃A成一个循环关系,由此来定义节点到根节点的距离:将每个节点到根节点的距离模3余1:可以吃跟根节点,余2:可以被根节点吃,余3:与根节点同类。
根节点到自己的距离为0,所以第1层吃第0层,第2层吃第1层,第3层(是第0层的同类)吃第2层,第4层(是第1层的同类)吃第3层…以此类推。

注意:

  1. find函数和数组d的含义:在这里插入图片描述
  2. 判断的时候不能用d[px]%3 != d[py]%3,因为在更新d[px]时有可能会出现负数的情况,-1与2是同一类,但是如果这样写就判断错误;如果用减法再取模就正确(if条件中,负数不为0也成立)。
#include <iostream>

using namespace std;

const int N = 50010;
int p[N], d[N]; //parent数组和辅助数组(维护i节点到根节点的距离,因为初始时每个节点都是根节点,所以初始距离都为0)
int n, k;

int find(int x)
{
    if (p[x] != x)
    {
        int t = find(p[x]); //t暂存p[x]的根节点
        d[x] += d[p[x]]; //d[x]更新为x到根节点的距离
        p[x] = t; //更新父节点为根节点
    }
    return p[x];
}

int main()
{
    cin >> n >> k;
    for (int i = 1; i <= n; i++) p[i] = i;
    
    int res = 0;
    while (k--)
    {
        int r, x, y; //r是说法的种类D,不用d为了不和d数组命名重复
        cin >> r >> x >> y;
        
        if (x > n || y > n) res++;
        else
        {
            int px = find(x), py = find(y); //px和py都是根节点了
            if (r == 1)
            {
                //如果在同一棵树上,且到根节点的距离不相等,说明不是同一种类
                if (px == py && (d[x] - d[y]) % 3) res++;
                else if (px != py)
                {
                    p[px] = py; //让x的根节点的父节点指向y的根节点
                    
                    //更新距离,定义px到py之间的距离:因为x和y是同类,
                    //所以x合并到y的集合中后,(d[x]+?)%3==d[y]%3 => d[y]-d[x]=?,
                    //其中?为x的祖宗节点px到y的祖宗节点的距离
                    d[px] = d[y] - d[x];
                }
            }
            else if (r == 2)
            {
                //x到根节点的距离比y到根节点的距离多1 =>(d[x]-d[y]-1)%3==0
                if (px == py && (d[x] - d[y] - 1) % 3) res++;
                else if (px != py)
                {
                    p[px] = py;
                    //因为x吃y,所以d[x]+?-d[y]-1==0 => ?=d[y]+1-d[x],其中?为d[px]
                    d[px] = d[y] + 1 - d[x];
                }
            }
        }
    }
    cout << res << endl;
    
    return 0;
}

3. 堆

堆是一棵完全二叉树

大根堆:

每个节点的值都大于或等于其左右孩子节点的值。

小根堆:

每个结点的值都小于或等于其左右孩子结点的值。

存储:下标从1开始

用一个一维数组,下标为1的点是根节点,x的左儿子:2x,x的右儿子:2x+1。

down() 和 up() 操作

如何手写一个堆(以小根堆为例)

  1. 插入一个数 heap[++size] = x; up(size);
  2. 求集合中的最小值 heap[1];
  3. 删除最小值:用整个堆的最后一个元素覆盖掉堆顶元素(因为一维数组删除头结点很困难,删除尾结点很方便) heap[1] = heap[size]; size--; down(1);
  4. 删除任意一个元素 heap[k] = heap[size]; size--; down(k); up(k);虽然写了down和up,只会执行一个,大了就down,小了就up。
  5. 修改任意一个元素 heap[k] = x; down(k); up(k);

例题

堆排序

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int n, m;
int h[N], siz;

void down(int u)
{
    int t = u;
    // 如果u的左子节点存在且小于u,替换
    if (u * 2 <= siz && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= siz && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t) //不相等说明根节点不是最小的
    {
        swap(h[u], h[t]); //交换
        down(t); //递归处理
    }
}

void up(int u) //up操作的话只用跟父节点比,往上走
{
    while (u / 2 && h[u / 2] > h[u]) //父节点存在且父节点大于它
    {
        swap(h[u / 2], h[u]);
        u /= 2;
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
    siz = n;
    
    // 从 n/2 开始down
    // 因为n是最大值,n/2是n的父节点,所以n/2是最大的有子节点的父节点,
    // 所以从n/2往前遍历,就可以把整个数组遍历一遍
    for (int i = n / 2; i; i--) down(i);
    
    while (m--)
    {
        printf("%d ", h[1]); //h[1]为最小值
        h[1] = h[siz]; //删除最小值,用整个堆的最后一个元素覆盖掉堆顶元素
        siz--;
        down(1);
    }
    
    return 0;
}

堆模拟
复杂情况:因为第4、5个操作涉及到第k个插入的数,需要引入两个数组ph[]hp[]
ph[k]=i 表示第k个插入的点在堆(一维数组)里的下标为i,(pointer->heap),
hp[i]=k 表示在堆里下标为i的数是第k个插入的,(heap->pointer)。

#include <iostream>

using namespace std;

const int N = 1e5 + 10;
//h代表heap(堆),ph(point->heap)可以获得第几个插入的元素现在在堆的那个位置
//hp(heap->point)可以获得在堆的第n个元素存的是第几个插入的元素
//siz是大小
int h[N], ph[N], hp[N], siz;
int n, idx = 0; //idx-每个元素的插入次序

// 堆的全新的交换方式
void heap_swap(int a, int b)
{
    //先由hp找到对应的插入次序,然后交换ph数组中记录的两个元素的下标
    swap(ph[hp[a]], ph[hp[b]]);
    swap(hp[a], hp[b]); //交换hp数组中记录的两个元素的插入次序
    swap(h[a], h[b]); // 最后交换堆中的两个元素
}

void down(int u)
{
    int t = u; //让t代指u以及其两个儿子(三个点)中的最大值,先初始化为u
    if (u * 2 <= siz && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= siz && 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, u / 2);
        u /= 2;
    }
}

int main()
{
    scanf("%d", &n);
    while (n--)
    {
        string op;
        int k, x;
        cin >> op;
        
        //插入一个数 x
        if (op == "I")
        {
            scanf("%d", &x);
            siz++;
            idx++;
            ph[idx] = siz; //堆尾插入,故第idx次插入的元素下标为siz
            hp[siz] = idx; //当前下标为siz的元素为第idx次插入
            h[siz] = x; //当前插入的值,即h[ph[idx]=x
            up(siz); //从堆尾向上调整
        }
        //输出当前集合中的最小值
        else if (op == "PM") printf("%d\n", h[1]);
        //删除当前集合中的最小值
        else if (op == "DM")
        {
            heap_swap(1, siz); //用堆尾元素覆盖头元素
            siz--;
            down(1);
        }
        //删除第 k 个插入的数
        else if (op == "D")
        {
            scanf("%d", &k);
            k = ph[k];
            heap_swap(k, siz);
            siz--;
            down(k), up(k); //只会执行一个
        }
        //修改第 k 个插入的数,将其变为 x
        else if (op == "C")
        {
            scanf("%d%d", &k, &x);
            k = ph[k];
            h[k] = x;
            down(k), up(k);
        }
    }
    
    return 0;
    
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值