Trie树 Trie树是用来快速存储和查找字符串集合的数据结构。 
Trie树的基础知识可以参考:
Trie树例题:Trie字符串统计(Acwing 835)
维护一个字符串集合,支持两种操作:
“I x”向集合中插入一个字符串x;
 
“Q x”询问一个字符串在集合中出现了多少次。
 
 
共有N个操作,输入的字符串总长度不超过 $10^5$,字符串仅包含小写英文字母。
输入格式:第一行包含整数N,表示操作数。接下来N行,每行包含一个操作指令,指令为”I x”或”Q x”中的一种。
输出格式:对于每个询问指令”Q x”,都要输出一个整数作为结果,表示x在集合中出现的次数。每个结果占一行。
数据范围:$1 \le N \le 2 \times 10^4$。
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <iostream>  using  namespace  std ;const  int  N = 100010 ;int  son[N][26 ], cnt[N], idx;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[0 ] == 'I' )  insert(str);         else  printf ("%d\n" , query(str));     }          return  0 ; } 
 
Trie树例题2:最大异或对
在给定的N个整数$A_1,A_2……A_N$中选出两个进行xor(异或)运算,得到的结果最大是多少?
输入格式:第一行输入一个整数N。第二行输入N个整数$A_1$~$A_N$。
输出格式:输出一个整数表示答案。
数据范围:$1 \le N \le 10^5$,$0 \le A_i  \le 2^31$。
 
异或运算是按位运算(二进制),相同得0,不同得1。同样先考虑暴力做法,可以用两重循环来解决。
1 2 3 4 5 6 int  res = 0 ;for (int  i = 0 ; i < n; i++)  { 	for (int  j = 0 ; j < i; j++)            res = max (res, a[i] ^ a[j]); } 
 
接下来考虑用Trie树来优化内层循环,即从$a_0$到$  a_{i-1}$中找到与$a_i$异或最大的数。本题的数据范围是$0 \le a_i \le 2^31$,因此每个数可以看出一个31位的二进制串,假设$a_i=101110…1$,要找到与$a_i$异或最大的数,就要异或结果高位是1比较好,因此按$a_i$的位数从左往右找,在$a_0$到$  a_{i-1}$中,先找第30位是0的数,再从这部分数中找第29位是1的数,依次类推。在Trie树中,对于每个$a_i$,先查找再插入。
Trie树 不光可以存储字符串,也可以存储整数,也可以存储二进制数。
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #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 ;             if (!son[p][u])  son[p][u] = ++ idx;         p = son[p][u];     } } int  query (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  + !u;         }         else          {             p = son[p][u];             res = res * 2  + u;         }     }     return  res; } int  main ()  {    scanf ("%d" , &n);          int  res = 0 ;     for (int  i = 0 ; i < n; i++)     {         scanf ("%d" , &a[i]);                  insert(a[i]);                  int  t = query(a[i]);         res = max (res, a[i] ^ t);     }          printf ("%d\n" , res);          return  0 ; } 
 
并查集 并查集的作用:
将两个集合合并 
询问两个元素是否在一个集合当中 
 
并查集可以在近乎$O(1)$的时间内完成这两个操作。
基本思想:每一个集合用一棵树(不一定是二叉树)来维护,树根的编号就是整个集合的编号,对于每一点都存储其父节点是谁,p[x]表示x的父节点。
问题1:如何判断树根: if(p[x] == x)
问题2:如何求x的集合编号:while(p[x] != x)  x = p[x];
问题3:如何合并两个集合:设p[x]是x的集合编号,p[y]是y的集合编号,p[x]=y
针对问题2的优化:路径压缩 。一旦从一个点往上找找到了根节点,就把这条路径上所有的点都指向根节点。
并查集例题1(Acwing 836):
一共有n个数,编号是1~n,最开始每个数各自在一个集合中。
现在要进行m个操作,操作共有两种:
“M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作 
“Q a b”,询问编号为a和b的两个数是否在同一个集合中; 
 
输入格式:第一行输入整数n和m。接下来m行,每行包含一个操作指令,指令为“M a b”或“Q a b”中的一种。
数据范围:$1 \le n, m \le 10^5$
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <iostream>  using  namespace  std ;const  int  N = 100010 ;int  n, m;int  p[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 = 0 ; i < n; i++)  p[i] = i;            while (m--)     {         char  op[2 ];           int  a, b;         scanf ("%s%d%d" , op, &a, &b);                  if (op[0 ] == 'M' )  p[find (a)] = find (b);           else          {             if (find (a) == find (b))  puts ("Yes" );                else  puts ("No" );         }     }          return  0 ; } 
 
并查集例题2:连通块中点的数量(Acwing 837)
给定一个包含n个点(编号为1~n)的无向图,初始时图中没有边。现在要进行m个操作,操作共有三种:
“C a b”,在点a和点b之间连一条边,a和b可能相等; 
“Q1 a b”,询问点a和点b是否在同一个连通块中,a和b可能相等; 
“Q2 a”,询问点a所在连通块中点的数量。 
 
输入格式:第一行输入整数n和m。接下来m行,每行包含一个操作指令,指令为“C a b”,“Q1 a b”或“Q2 a”中的一种。
数据范围:$1 \le n, m \le 10^5$
 
可以发现这道题的前两个操作和并查集是一样的,我们可以用一个集合维护连通块,一个连通块中的点就在一个集合当中。当在两个连通块之间连一条边时,起到的作用就是把两个集合合并。额外的操作是统计一个集合中点的数量。
用cnt[N]表示每个集合中点的数量,我们只保证根节点的cnt[]是有意义的。将数a和b所在的两个集合合并时,即让find(a)的父节点是find(b),然后让find(b)的cnt等于find(a)的cnt加上find(b)的cnt。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include <iostream>  using  namespace  std ;const  int  N = 100010 ;int  n, m;int  p[N], cnt[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 = 0 ; i < n; i++)        {         p[i] = i;          cnt[i] = 1 ;     }     while (m--)     {         char  op[4 ];           int  a, b;         scanf ("%s" , op);                  if (op[0 ] == 'C' )         {             scanf ("%d%d" , &a, &b);             if (find (a) == find (b))  continue ;                cnt[find (b)] += cnt[find (a)];               p[find (a)] = find (b);           }         else  if (op[1 ] == '1' )         {             scanf ("%d%d" , &a, &b);             if (find (a) == find (b))  puts ("Yes" );                else  puts ("No" );         }         else           {             scanf ("%d" , &a);             printf ("%d\n" , cnt[find (a)]);           }     }          return  0 ; } 
 
并查集例题3:食物链(Acwing 240)
动物王国中有三类动物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 \le N \le 50000, 0 \le K \le100000$。
 
通过确定每个点与根节点之间的关系,来确定任意两个点之间的关系。由于三种动物的关系是循环被吃,就用每个点到根节点之间的距离来表示它与根节点之间的关系。如果一个点到根节点的距离是1,表示它可以吃根节点;如果一个点到根节点的距离是2,表示它被根节点吃(1吃根,2吃1,根吃2 );如果一个点到根节点的距离是3,表示它和根节点是同类…….如此类推,每3个 一循环。因此可以把集合中所有的点归为三类,用并查集维护每个点到根节点的距离 ,按点到根节点的距离对3取模 :
余1——可以吃根节点; 
余2——可以被根节点吃; 
余0——与根节点是同类。 
 
在用并查集维护时,每个点存的是其到父节点的距离,做路径压缩时,就更新为其到根节点的距离。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #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] - d[x] + 1 ;                 }             }         }              }     printf ("%d\n" , res);              return  0 ; } 
 
堆 堆,要支持的操作:
插入一个数 
求集合当中的最小值 
删除最小值 
删除任意一个元素 
修改任意一个元素 
 
堆是一棵完全二叉树。小根堆:每个点的值都小于等于左右儿子的。
堆的基础知识可以参考:数据结构与算法(19)优先级队列
完全二叉树的基础知识可以参考:
 
用数组模拟堆 ,堆的存储方式:用一个一维数组,1号点是根节点(下标从1开始),节点x的左儿子是2x,右儿子是2x+1。有两个基本操作 :down(x),把一个节点往下移,如果把一个节点的值变大了,它有可能就要往下移;up(x),把一个节点往上移,如果把一个数变小了,它有可能就要往上移 。用这两个基本操作就可以实现上面的五个操作:
插入一个数:heap[++ size] = x; up[x]; 
求集合当中的最小值:heap[1]; 
删除最小值:(用堆的最后一个元素覆盖掉堆顶元素)heap[1] = heap[size]; size--; down(1); 
删除任意一个元素:heap[k] = heap[size]; size --; down(k); up(k);(每次donw和up只会执行一个) 
修改任意一个元素:heap[k] = x; down(k); up(x); 
 
堆例题1:堆排序(Acwing  838)
输入一个长度为n的整数数列,从小到大输出前m小的数。
输入格式:第一行包含整数n和m。第二行包含n个整数,表示整数数列。
输出格式:共一行,包含m个整数,表示整数数列中前m小的数。
数据范围:$1 \le m, n \le 10^5$,$1 \le 数列中元素 \le 10^9$
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <iostream>  #include <algorithm>  using  namespace  std ;const  int  N = 100010 ;int  n, m;int  h[N], cnt;void  down (int  u)  {    int  t = u;       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);     } } void  up (int  u)  {    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]);        cnt = n;                    for (int  i = n / 2 ; i ; i--)  down(i);          while (m--)     {         printf ("%d " , h[1 ]);                     h[1 ] = h[cnt];         cnt --;         down(1 );     }          return  0 ; } 
 
堆例题2:模拟堆(Acwing 839)
维护一个集合,初始时集合为空,支持如下几种操作:
“I x”,插入一个数x;
 
“PM”,输出当前集合中的最小值;
 
“DM”,删除当前集合中的最小值(数据保证此时的最小值唯一);
 
“D k”,删除第k个插入的数;
 
“C k x”,修改第k个插入的数,将其变为x;
 
 
输入格式:第一行包含整数N。接下来N行,每行包含一个操作指令,操作指令为”I x”,”PM”,”DM”,”D k”或”C k x”中的一种。
输出格式:对于每个输出指令“PM”,输出一个结果,表示当前集合中的最小值。每个结果占一行。
数据范围:$1 \le N \le 10^5$,$-10^9 \le x \le 10^9$
 
这道题的难点在于操作4和5,怎么快速地找到第k个插入的数。这就需要额外维护两个数组,用ph[k]存第k个插入的点在堆中的位置(下标),hp[k]表示堆中的位置为k的点是第几个插入的点;如ph[j] = k, hp[k] = j; 可以理解为映射。这样例题1代码中普通的swap()操作就要改成这里的堆的特有的交换操作heap_swap()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 #include <iostream>  #include <algorithm>  #include <string.h>  using  namespace  std ;const  int  N = 100010 ;int  h[N], cnt;  int  ph[N], hp[N]; 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  <= 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)        {         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[5 ];         int  k, x;                  scanf ("%s" , op);         if (!strcmp (op, "I" ))           {             scanf ("%d" , &x);             cnt ++;             m ++;             ph[m] = cnt, hp[cnt] = m;             h[cnt] = x;             up(cnt);         }         else  if (!strcmp (op, "PM" )) printf ("%d\n" , h[1 ]);           else  if (!strcmp (op, "DM" ))           {             heap_swap(1 , cnt);             cnt --;             down(1 );         }         else  if (!strcmp (op, "D" ))            {             scanf ("%d" , &k);             k = ph[k];             heap_swap(k, cnt);             cnt --;             down(k);  up(k);         }         else              {             scanf ("%d%d" , &k, &x);             k = ph[k];             h[k] = x;             down(k);  up(k);         }     }          return  0 ; }