算法基础2.2Trie树(字典树),并查集,堆

Trie树

基本用法:首先是用来高效的存储和查找字符串集合的这样一个数据结构,然后来看一下它是如何来存储字符串的,首先它长的是一个字典的形式,举一个例子,比方说想在里面存储这样的一堆单词:abcde,abdef,aced,bcdf,bcff,cdaa,bcdc。凡是用到的题目,字符串一般来说,都是要么全是小写字母,要么全是大写字母,要么就是数字,要么是0和1,字母的类型不会很多。

首先是有一个根结点root,然后先把第一个单词存进来,存的时候从前往后依次遍历每一个字符,当前的单词是abcdef,然后首先从根结点开始,看一下根结点有没有a这个点作为子节点,然后没有的话,就把a节点创建出来,以此类推,下一个字母是b,c,d,e,f.然后来看第二个单词abdef,还是一样的道理,从根结点开始,首先看有没有a这个字母,有a走到a,然后看b,有b走到b,然后下一个是d,没有d的话创建一个,下一个是e,d是没有e的,再把e创建出来,下一个是f。然后再看第三个单词aced ,然后还是从根往后看,首先看第一个字母是a,然后下一个字母是c,没有的话给它创建一个,然后下一个字母是e,然后下一个字母是d。然后以此类推,bcdf,bcff,cdaa,bcdc。

一般来说在存的时候,会在每一个单词结尾的地方打上一个标记,表示说以这个字母结尾的这个结点,是有一个单词的,为了最后把它检测出来,这是串数的一个存储。

来看一下它是如何查找的,呃,比方说想查找一下aced,从根结点开始走,先走第一个字母a,然后走第二个字母c,走第三个字母e,走第四个字母d,而且d上有一个标记,表示说存在一个单词以d结尾,那么就可以说在前面这个集合里面是存在一个aced的,比方说再查找一个不存在的单词,比方说abcf,那么从第一个字母开始,先走到a,再走到b,再走到c,然后下一步走到f,发现f不存在了,那就说明整个数里边是不存在这个单词的,比方说查找abcdabcd那么我们来看一下,先走到a,然后走到b,然后走到c,然后走到d,虽然当这个单词结尾结束的时候,整个路径上的点都存在,但是d上是没有标记的。那就说明不存在一个单词以d结尾,那么它也是不存在的。

int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量

// 插入一个字符串
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];
}

作者:yxc
链接:https://www.acwing.com/blog/content/404/
来源:AcWing

模板题:

维护一个字符串集合,支持两种操作:

  1. I x 向集合中插入一个字符串 x;
  2. Q x 询问一个字符串在集合中出现了多少次。

共有 N 个操作,所有输入的字符串总长度不超过 105105,字符串仅包含小写英文字母。

输入格式

第一行包含整数 N,表示操作数。

接下来 N 行,每行包含一个操作指令,指令为 I x 或 Q x 中的一种。

输出格式

对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x在集合中出现的次数。

每个结果占一行。

数据范围

1≤N≤2∗10的4次方

输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1

#include <iostream>

using namespace std;

const int N = 100010;

int son[N][26], cnt[N], idx;  //son表示每个节点的子节点,小写字母,最多向外连26条边共26个,

//cnt存的是以当前这个字母结尾的单词有多少个,idx和单链表中相同,表示当前用到了哪个下标。

//下标为0的点,既是根节点,又是空结点

char str[N];

void insert(char *str)   //插入操作
{
    int p = 0;  //根节点开始
    for (int i = 0; str[i]  //字符串结尾是/0,拿这个判断是否走到了结尾  ; i ++ )
    {
        int u = str[i] - 'a';  //把小写字母a-z映射到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];  //返回以p结尾的单词数量
}

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

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/45282/
来源:AcWing

例题:最大异或对

在给定的 N 个整数 A1,A2……AN中选出两个进行 xor(异或)运算,得到的结果最大是多少?

输入格式

第一行输入一个整数 N。

第二行输入 N个整数 A1~AN。

输出格式

输出一个整数表示答案。

数据范围

1≤N≤10的5次方
0≤Ai<2的31次方

输入样例:
3
1 2 3
输出样例:
3

这道题的启示是:字典树不单单可以高效存储和查找字符串集合,还可以存储二进制数字
思路:将每个数以二进制方式存入字典树,找的时候从最高位去找有无该位的异.

#include<iostream>
#include<algorithm>
using namespace std;
int const N=100010,M=31*N;

int n;
int a[N];
int son[M][2],idx;
//M代表一个数字串二进制可以到多长

void insert(int x)
{
    int p=0;  //根节点
    for(int i=30;i>=0;i--)
    {
        int u=x>>i&1;   /取X的第i位的二进制数是什么  x>>k&1(前面的模板)
        if(!son[p][u]) son[p][u]=++idx; ///如果插入中发现没有该子节点,开出这条路
        p=son[p][u]; //指针指向下一层
    }
}
int search(int x)  //找最大异或值
{
    int p=0;int res=0;
    for(int i=30;i>=0;i--)
    {                               ///从最大位开始找
        int u=x>>i&1;
        if(son[p][!u]) 如果当前层有对应的不相同的数
        {   ///p指针就指到不同数的地址

          p=son[p][!u];
          res=res*2+1;
             ///*2相当左移一位  然后如果找到对应位上不同的数res+1 例如    001
        }                                                       ///       010 
        else                                                      --->011                                                                           //刚开始找0的时候是一样的所以+0    到了0和1的时候原来0右移一位,判断当前位是同还是异,同+0,异+1
        {
            p=son[p][u];
            res=res*2+0;
        }
    }
    return res;
}
int main(void)
{
    cin.tie(0);
    cin>>n;
    idx=0;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
        insert(a[i]);
    }
    int res=0;
    for(int i=0;i<n;i++)
    {   
        res=max(res,search(a[i]));  ///search(a[i])查找的是a[i]值的最大与或值
    }
    cout<<res<<endl;
}


作者:小菜鸡UP
链接:https://www.acwing.com/solution/content/9587/
来源:AcWing

并查集

并查集是一个面试和比赛的时候非常容易出的一个数据结构,因为它代码很短但是思路都比较精巧。

一般来说,这个数据结构都是可以快速的支持一些操作,并查集支持操作是:第一个操作,将两个集合合并,第二个操作,询问两个元素是否在一个集合当中。

基本思想就是首先用树的形式来维护所有的集合,每一个集合用一个树的形式来维护,每一个集合的编号是它根结点的编号,每一个集合的根结点元素是它的代表元素。对于每一个点,都存储一下它的父结点,p[x]表示x的父节点,当想求某一个点属于哪一个集合的时候,就可以根据这个点的父节点。然后看一下父节点是不是树根,如果不是树根的话,就再往上找,直到找到树根为止。那么当前这个元素属于的集合的编号就是树根的编号。因此可以用这样的方式来快速的找到每一个元素是属于哪一个集合的。

首先第一个问题,如何判断一个点是不是树根?它如果是树根的话,其实等价于px=x,除了根结点之外,p[x]都不等于x,

问题二,如何求x的集合编号,就是从x一路往上走,走到树根就可以了,while(p[x]!=x) x=p[x];这相当于第二个应用,求一下x和y的集合编号是否相等

问题三:如何合并两个集合。可以把一个集合当成另一个集合的一个儿子,假设px是x的集合编号,py是y的集合编号,那么合并的话,就是让px=y就可以了。就是把x直接插到y上去,就是把这个这个点直接插到我们这个点上去就可以了。

如果仅仅是这样的话,时间复杂度其实还是蛮高的,主要是在求x编号这一步上,每一次都需要从当前这个点遍历到根结点,遍历的次数和树的高度是成正比的,这里有一个优化,假设x费了千辛万苦,终于找到根节点了。找到之后的话,直接把这个路径上所有的点都直接指向根节点。只会搜一遍,并查集加完这个优化之后,基本上就已经可以看成O1的时间复杂度了,这个优化叫路径压缩。并查集可能要维护一些额外信息。

模板题:

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m个操作,操作共有两种:

  1. M a b,将编号为 a 和 b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;
输入格式

第一行输入整数 n和 m。

接下来 m 行,每行包含一个操作指令,指令为 M a b 或 Q a b 中的一种。

输出格式

对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1≤n,m≤10的5次方

输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes

小模板(1)朴素并查集:

    int p[N]; //存储每个点的祖宗节点

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

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
 

#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]);  //如果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 == 'M') p[find(a)] = find(b);  //合并让a的祖宗节点的父亲等于b的祖宗节点,把a的祖宗节点直接插到b祖宗节点的下面
        else
        {
            if (find(a) == find(b)) puts("Yes");  //判断是否在同一集合中
            else puts("No");
        }
    }

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/45287/
来源:AcWing

如果想维护一些额外的信息,比方说在合并两个集合的时候动态的知道每一个集合当前有多少个元素,并查集也是可以做的,就是可以在并查集的这些基本操作过程当中,维护一些额外的变量,就可以让整个的过程维护一些额外信息了。

例题:

给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。

现在要进行 m个操作,操作共有三种:

  1. C a b,在点 a和点 b之间连一条边,a和 b可能相等;
  2. Q1 a b,询问点 a和点 b是否在同一个连通块(a可以走到b,b也可以走到a)中,a和 b可能相等;
  3. Q2 a,询问点 a 所在连通块中点的数量;
输入格式

第一行输入整数 n和 m。

接下来 m行,每行包含一个操作指令,指令为 C a bQ1 a b 或 Q2 a 中的一种。

输出格式

对于每个询问指令 Q1 a b,如果 a和 b在同一个连通块中,则输出 Yes,否则输出 No

对于每个询问指令 Q2 a,输出一个整数表示点 a所在连通块中点的数量

每个结果占一行。

数据范围

1≤n,m≤10的5次方

输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3

可以发现,这个题除了第三问之外,前两问和上一个题的前两问是一模一样的,可以用一个集合来维护两个连通块,用集合来维护连通块啊,一个连通块当中的点就在一个集合当中,当在两个集合之间连一条边的时候,起到的作用就是把两个集合合并,因此前两个操作和上一个题是一模一样的。然后只是多了一个额外的操作,就是去统计一下每个集合里面点的数量,然后这需要额外记一个变量叫size,表示的是每一个集合里边点的数量,那么最开始的时候size应该等于一,最开始的时候每个集合里面只有一个点,所以它的size=1。

小模板(2)维护size的并查集:

    int p[N], size[N];
    //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

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

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        size[i] = 1;
    }

    // 合并a和b所在的两个集合:
    size[find(b)] += size[find(a)];
    p[find(a)] = find(b);


作者:yxc
链接:https://www.acwing.com/blog/content/404/
来源:AcWing

#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
int p[N], cnt[N];   //cnt就是size数组

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);  //如果当前这个结点不是根节点,就返回父节点的祖宗节点,相当                                                      于向上走了一层
    return p[x];
}

int main()
{
    cin >> n >> m;

    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        cnt[i] = 1;   //初始化集合元素数量为1
    }

    while (m -- )
    {
        string op;
        int a, b;
        cin >> op;

        if (op == "C")
        {
            cin >> a >> b;
            a = find(a), b = find(b);  //提前一步处理根节点,只保证根节点的size是有意义的
            if (a != b)
            {
                p[a] = b;   //把a接到b下
                cnt[b] += cnt[a]; //再把a的连通块大小加到b上,还是先把a的连通块大小加到b上再操作集合都是可以的,如果没有提前一步的处理,就必须要先加连通块大小再操作集合,否则操作完集合后,a和b的根结点将会重叠,导致输出错误!
            }
        }
        else if (op == "Q1")
        {
            cin >> a >> b;
            if (find(a) == find(b)) puts("Yes");  //特判一下a和b相等的情况,不需要任何操作
            else puts("No");
        }
        else   //第三个操作,求集合中点的数量
        {
            cin >> a;
            cout << cnt[find(a)] << endl;  //直接返回find(a)的size
        }
    }

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/45295/
来源:AcWing

例题:食物链

动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。

A 吃 B,B 吃 C,C吃 A。

现有 N 个动物,以 1∼N 编号。

每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N 个动物所构成的食物链关系进行描述:

第一种说法是 1 X Y,表示 X 和 Y 是同类。

第二种说法是 2 X Y,表示 X 吃 Y。

此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。

当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  1. 当前的话与前面的某些真的话冲突,就是假话;
  2. 当前的话中 X 或 Y 比 N 大,就是假话;
  3. 当前的话表示 X 吃 X,就是假话。

你的任务是根据给定的 N 和 K句话,输出假话的总数。

输入格式

第一行是两个整数 N 和 K,以一个空格分隔。

以下 K 行每行是三个正整数 D,X,Y两数之间用一个空格隔开,其中 D 表示说法的种类。

若 D=1,则表示 X 和 Y 是同类。

若 D=2,则表示 X 吃 Y。

输出格式

只有一个整数,表示假话的数目。

数据范围

1≤N≤50000
0≤K≤1000000

输入样例:
100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5
输出样例:
3

小模板:

(3)维护到祖宗节点距离的并查集:

    int p[N], d[N];
    //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离

    // 返回x的祖宗节点
    int find(int x)  //核心代码
    {
        if (p[x] != x)
        {
            int u = find(p[x]);
            d[x] += d[p[x]];
            p[x] = u;
        }
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        d[i] = 0;
    }

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
    d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量

作者:yxc
链接:https://www.acwing.com/blog/content/404/
来源:AcWing

#include <iostream>

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()
{
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; i ++ ) p[i] = i;

    int res = 0;
    while (m -- )
    {
        int t, x, y;
        scanf("%d%d%d", &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];
                }
            }
        }
    }

    printf("%d\n", res);

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/45325/
来源:AcWing

主要是讲如何去手写一个堆,堆是维护一个数据集合,第一个是插入一个数,第二个是求集合当中的最小值,第三个是删除最小值,删前面三个是最基本的三个操作。stl里面的堆也可以支持,后面还有两个操作也是可以支持的,第四个是删除任意一个元素,第五个是修改任意一个元素。后面两个操作stl里面的堆是实现不了的。

堆的基本结构是一个二叉树或者是一个完全二叉树,完全二叉树是说这个树长得非常平衡,除了最后一层节点之外,上面所有节点都是满的,最后一层节点的话是从左到右排列。以小根堆为例,小根堆有一个性质,每一个点都是小于等于左右儿子的,这是一个递归定义,可以发现根节点就是整个数据结构里面的最小值。

注意堆的存储和前面讲的链表存储是不太一样的,是一种全新的存储方式。用一个一维数组来存的。

堆有两个基本操作,第一个操作是down x,第二个操作是up x ,堆的所有操作完全可以用这两个操作组合起来,down是把一个节点往下移,up是把一个节点往上移。然后来看一下down的一个基本的逻辑是什么?down操作,其实对应的就是如果把某一个点的值变大了。就要把它往下移,因为堆是一个小根堆,变大之后就要往下沉,越往上的数越小,越大的数应该越往下压。因此down操作,就是说如果把一个值变大了,就把它往下移,6之所以不跟4交换因为与4交换后3仍小于4

然后up操作是如果说把一个数变小了,那么它就有可能可以往上走,因为上面数是小的,下面数是大的。

因为根结点一定是小于等于右边这个点的,因此这个点是不用动的,它一定是不会参与进来的,所以每次往上走的时候,只需要跟父节点来比较就可以了。up和down操作的时间复杂度都和偶数的高度成正比,因此是log n的。

然后来看一下如何用这两个操作来拼出来五种操作。

首先第一个,当我们想往堆里插入一个数的时候,是把这个数插到最后一个元素。假设用size来表示当前堆的大小,用heap来表示堆,如果想插入x,heap[++size]=x;up(size),就是在整个堆的最后一个位置插上x,然后把这个数不断往上移,第二个如果想求当前对象的最小值,heap[1],就是第一个数就一定是最小的,第三个删除一个最小值,基本原理因为要把第一个元素删掉,那么就把堆的最后一个元素覆盖堆顶元素,然后让size--,就把所有元素删掉就可以了。然后再把堆顶down 1遍就可以了,为什么要这么做呢?因为这个是一个一维数组,所以说当想删除头结点的时候,非常的困难,但是当想删除尾结点的时候,非常方便,想删除最后一个数的时候。只要让size--就可以了,首先第一步,先用最后一个点来覆盖掉第一个点,heap[1]=heap[size],第二步,把最后一个点干掉,size--,第三步,让一号点往下走,down(1),第四个是删除任意一个元素,和删除根结点是类似的,假设想删除第k个点,heap[k]=head[size],size--,然后这里要分情况来判断了,如果heap[k]的值变大了,那么就应该down一遍,变大就应该往下走,如果它变小了就应该往上走,或者也可以不用去判断,因为无外乎只有三种情况了,要么是不变,那就不用变,要么变大了就往下走,要么变小了就往上走,因此每一次这三种情况,只会选一个,那么可以直接down 1遍再up 1遍就可以了。这个up和down的话,只会执行一个,因为只有变大的时候才会往下走,只有变小的时候才会往上走。同理修改一个元素也是一样的一个道理,假设想把第k的元素修改成x,heap[k]=x,down(k),up(k),

注意下标是从一开始的,因为假设下标从零开始,那么零的左儿子是两倍的零,还是零就冲突了.因此下标从一开始比较方便,

先来讲一下down操作如何做,down操作是一个递归的过程,就是从根结点开始,如果当前这个点比某一个子结点要大的话就把那个子节点换上来,然后换完之后再递归处理.

模板题:堆排序

输入一个长度为 n的整数数列,从小到大输出前 m 小的数。

输入格式

第一行包含整数 n 和 m。

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

输出格式

共一行,包含 m 个整数,表示整数数列中前 m 小的数。

数据范围

1≤m≤n≤10的5次方
1≤数列中元素≤10的9次方

输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3

先来看一下里面需要用到什么操作,首先需要建堆,其次每一次需要先把堆顶输出来,因此需要用到求当前堆的最小值,把堆顶输出出来,然后第二次不是把堆顶删掉,还需要用到第二个操作就是删除最小值,其实只需要用到这两个操作,可以发现,只需要用到这两个操作的话,就只需要用down操作就可以了,所以说这个题的话,只需要实现down就可以了,

小模板// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;

// 交换两个点,及其映射关系
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 (u != t)
    {
        heap_swap(u, t);
        down(t);
    }
}

void up(int u)   //up操作
{
    while (u / 2 && h[u] < h[u / 2])  //父节点比当前这个节点要大
    {
        heap_swap(u, u / 2);  //越小越优秀,就换上去
        u >>= 1;   //u/2
    }
}

// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);


作者:yxc
链接:https://www.acwing.com/blog/content/404/
来源:AcWing

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m;
int h[N], cnt;  //h就是heap,cnt就是size存的就是当前heap里有多少个元素

void down(int u)  //down操作,每次看一下父节点是不是三个值中的最小值
{
    int t = u;  //t表示三个点里面最小节点的编号
    if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;    //左儿子
    if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;  //右儿子
    if (u != t)    //说明根节点不是最小值
    {
        swap(h[u], h[t]);  //根节点与最小值交换一下
        down(t);  //递归处理
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
    cnt = n;

    for (int i = n / 2; i; i -- ) down(i);   //建堆,n/2可以用归纳证明。首先最后一半元素一定是堆,因为只有一个数,然后可以归纳证明,假设底下的元素都是堆。那么把这一层down一遍,这一层往下的元素都是堆。堆就是满足每个点都是小于等于两个儿子就可以了,显然就是成立的,因此down到堆顶的话,整个就是一个堆了。

    while (m -- )
    {
        printf("%d ", h[1]);   //把堆顶输出
        h[1] = h[cnt];   //输出完堆顶要把堆顶删掉,让最后一个元素等于第一个元素

        size--;
        down(1);
    }

    puts("");

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/45296/
来源:AcWing

模板题:

维护一个集合,初始时集合为空,支持如下几种操作:

  1. I x,插入一个数 x;
  2. PM,输出当前集合中的最小值;
  3. DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
  4. D k,删除第 k 个插入的数 //需要快速地找到第k个数是啥,要开两个数组,存一个映射关系
  5. C k x,修改第 k个插入的数,将其变为 x;

现在要进行 N 次操作,对于所有第 2 个操作,输出当前集合的最小值。

输入格式

第一行包含整数 N。

接下来 N行,每行包含一个操作指令,操作指令为 I xPMDMD k 或 C k x 中的一种。

输出格式

对于每个输出指令 PM,输出一个结果,表示当前集合中的最小值。

每个结果占一行。

数据范围

1≤N≤10的5次方
−10的9次方≤x≤10的9次方
数据保证合法。

输入样例:
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出样例:
-10
6

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10;
int n,m=0;//m表示当前是第几个要插入的数
int h[N],ph[N],hp[N],cnt;//h代表heap(堆),ph(point->heap)存的是第几个插入的元素的下标,需要能知道第k个插入的点在哪,从p下标映射到堆
//hp(heap->point)可以获得在堆的第n个元素存的是第几个插入的元素,与ph互为反函数,从堆里面映射回下标

//既要从第几个插入点去找到堆里面的元素,又要从堆里的元素找回来,两者要一一对应
void heap_swap(int a,int b){//交换在heap中位置分别为a,b的两个元素
    swap(ph[hp[a]],ph[hp[b]]);//根据a和b的位置找到它们分别是第几个插入的元素,然后将其(在h数组中的)下标转换
    swap(hp[a],hp[b]);//将两个位置存的是第几号元素转换
    swap(h[a],h[b]);//最后再转换值(这三个语句位置可以换,但是从上到下逐渐变短的话比较美观)
}
void down(int u){//当前堆的元素下沉
    int t=u;//让t代指u以及其两个儿子(三个点)中的最大值
    if(u*2<=cnt and h[u*2]<h[t])t=u*2;
    if(u*2+1<=cnt and h[u*2+1]<h[t])t=u*2+1;//注意此处为d[t]
    if(u!=t){//最小值不是t,那么下沉,并且继续down操作
        heap_swap(u,t);
        down(t);
    }
}
void up(int u){
    while(u/2 and h[u/2]>h[u]){//第一个u/2是防止当u冲到顶然后陷入死循环
        heap_swap(u/2,u);
        u/=2;
    }
}
int main(){
    cin>>n;
    while(n--){
        string op;//option(选项)的缩写
        int k,x;
        cin>>op;
        if(op=="I"){//insert(插入)的缩写
            cin>>x;
            cnt++,m++;//初始化
            ph[m]=cnt;//m代表是第几个插入的元素(point)->cnt指向的是插入的位置(heap)
            hp[cnt]=m;//原理同上
            h[cnt]=x;//这里忘记写了,WA一次
            up(cnt);
        }else if(op=="PM"){//Print Min 打印最小值
            cout<<h[1]<<endl;
        }else if(op=="DM")//删除最小值{
            heap_swap(1,cnt);//将第一个数和最后一个元素交换
            cnt--;//所有元素数量减一
            down(1);//将放上来的元素沉下去
        }else if(op=="D") //删除第k个插入的数{
            cin>>k;//k存储拿到第几个输入的数字
            k=ph[k];//k从储存第几个输入的数字变换为储存那个数字存放在堆的哪个位置
            heap_swap(k,cnt);//把最后一个元素换过来
            cnt--;//所有元素数量减一
            down(k);//其可能大,可能小,都操作一遍准没错
            up(k);
        }else{//剩下来还没有操作的就是C(change)将第k个元素修改了,不必多谢一个if判断
            cin>>k>>x;
            k=ph[k];//k从储存第几个输入的数字变换为储存那个数字存放在h的哪个位置
            h[k]=x;
            down(k);//这里又忘记写了,WA两次
            up(k);
        }
    }
    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/45305/
来源:AcWing

end_________________________________________________________________________

  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值