• <数据结构> - 数据结构在算法比赛中的应用(下)


    目录

    Trie树 

    Trie字符串统计

    最大异或对

    并查集

    合并集合

    连通块中点的数量

    食物链

    堆排序

    模拟堆

     哈希表

    模拟散列表

    字符串哈希


    Trie树 

    Trie字符串统计

    思路:

    设 idx索引用于构建树, 结点son[节点位置][节点分支指针],cnt[]记录单词个数

    1. #include
    2. using namespace std;
    3. const int N = 100010;
    4. int son[N][26], cnt[N], idx; //因为只包含小写字母,所以每个节点最多有26个子节点
    5. char str[N];
    6. void insert(char *str)
    7. {
    8. int p = 0; //下标是0的点即是根节点,又是空节点,如son[0][0]为根节点的分支'a'
    9. for (int i = 0; str[i]; i ++ ) //字符串末尾有'\0'
    10. {
    11. int u = str[i] - 'a';
    12. if (!son[p][u]) son[p][u] = ++ idx;
    13. p = son[p][u];
    14. }
    15. cnt[p] ++ ; //记录这个单词
    16. }
    17. int query(char *str)
    18. {
    19. int p = 0;
    20. for (int i = 0; str[i]; i ++ )
    21. {
    22. int u = str[i] - 'a';
    23. if (!son[p][u]) return 0;
    24. p = son[p][u];
    25. }
    26. return cnt[p];
    27. }
    28. int main()
    29. {
    30. int n;
    31. scanf("%d", &n);
    32. while (n -- )
    33. {
    34. char op[2];
    35. scanf("%s%s", op, str);
    36. if (*op == 'I') insert(str);
    37. else printf("%d\n", query(str));
    38. }
    39. return 0;
    40. }

    最大异或

    思路:

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

     

     取x的第i位二进制数作为结点

    1. #include
    2. #include
    3. using namespace std;
    4. int const N=100010,M=31*N; //M表示trie树的结点个数,即:31个二进制长度*总数
    5. int n,idx;
    6. int a[N];
    7. int son[M][2]; //因为只需要存二进制1和0,所以2即可
    8. void insert(int x)
    9. {
    10. int p=0;
    11. for(int i=30;i>=0;i--)
    12. {
    13. int u=x>>i&1; //取x的第i位二进制数是什么
    14. if(!son[p][u]) son[p][u]=++idx;
    15. p=son[p][u];
    16. }
    17. }
    18. int search(int x)
    19. {
    20. int p=0,res=0;
    21. for(int i=30;i>=0;i--) //遍历31个二进制位
    22. {
    23. int u=x>>i&1;
    24. if(son[p][!u]) //为了取异或后最大值,如果有不同的就先走
    25. {
    26. p=son[p][!u];
    27. res=res*2+1; //异或相异为1,res整体向前挪一位+1
    28. }
    29. else
    30. {
    31. p=son[p][u];
    32. res=res*2+0; //相同为0
    33. }
    34. }
    35. return res;
    36. }
    37. int main()
    38. {
    39. cin>>n;
    40. for(int i=0;i
    41. {
    42. cin>>a[i];
    43. insert(a[i]);
    44. }
    45. int res=0;
    46. for(int i=0;i
    47. {
    48. res=max(res,search(a[i]));
    49. }
    50. cout<
    51. return 0;
    52. }

    并查集

    合并集合

    思路:

    路径压缩具体操作: 

    如图

    find(1) p[1] = 2  p[1] = find(2)
    find(2) p[2] = 3  p[2] = find(3)
    find(3) p[3] = 4  p[3] = find(4)
    find(4) p[4] = 4  将p[4]返回 

    于是一路回溯;4=p[4]=p[3]=p[2]=p[1];
     

    1. #include
    2. using namespace std;
    3. int p[100010];
    4. int find(int x)
    5. {
    6. if(p[x]!=x) p[x]=find(p[x]);
    7. return p[x];
    8. }
    9. int main()
    10. {
    11. int n,m;
    12. cin>>n>>m;
    13. for(int i=1;i<=n;i++) p[i]=i;
    14. while(m--)
    15. {
    16. char op;
    17. int a,b;
    18. cin>>op>>a>>b;
    19. if(op=='M') p[find(a)]=find(b); //将a的根插到b的根下,成为b分支
    20. else
    21. {
    22. if(find(a)==find(b))
    23. printf("Yes\n");
    24. else
    25. printf("No\n");
    26. }
    27. }
    28. return 0;
    29. }

    连通块中点的数量

    算法 - 并查集详解:

    http://t.csdn.cn/SsIY9

    1. #include
    2. using namespace std;
    3. int n,m;
    4. int p[100010],cnt[100010];
    5. int find(int x)
    6. {
    7. if(p[x]!=x) p[x]=find(p[x]);
    8. return p[x];
    9. }
    10. int main()
    11. {
    12. cin>>n>>m;
    13. for(int i=1;i<=n;i++)
    14. {
    15. p[i]=i;
    16. cnt[i]=1;
    17. }
    18. while(m--)
    19. {
    20. int a,b;
    21. string s;
    22. cin>>s;
    23. if(s=="C")
    24. {
    25. cin>>a>>b;
    26. a=find(a),b=find(b);
    27. p[a]=b;
    28. if(a!=b) cnt[b]+=cnt[a];
    29. }
    30. else if(s=="Q1")
    31. {
    32. cin>>a>>b;
    33. a=find(a),b=find(b);
    34. if(a==b) puts("Yes");
    35. else puts("No");
    36. }
    37. else
    38. {
    39. cin>>a;
    40. cout<find(a)]<
    41. }
    42. }
    43. return 0;
    44. }

    食物链

    思路

     

    如果不是同一颗树,就把x树插到y树下,成为分支;由前面的合并操作,我们已经算出x到根px的距离d[x],y到根py的距离d[y];那么如何算px到py的距离呢?

    我们设距离为

    由于需要满足是同一种类,即最终的x到根距离%3==y到根距离%3

    公式为(d[x]+?)%3==(d[y])%3;

    简化得  ?=d[y]-d[x];

    1. else if (px != py) //如果是不同的树
    2. {
    3. p[px] = py; //把x树插到y树下,成为分支
    4. d[px] = d[y] - d[x]; //
    5. }

    1. #include
    2. using namespace std;
    3. const int N = 50010;
    4. int n, m;
    5. int p[N], d[N];
    6. int find(int x)
    7. {
    8. if (p[x] != x)
    9. {
    10. int t = find(p[x]);
    11. d[x] += d[p[x]]; //d[p[x]]就指p[x]到上一个节点的距离,最终d[x]为该节点到宗结点距离
    12. p[x] = t;
    13. }
    14. return p[x];
    15. }
    16. int main()
    17. {
    18. scanf("%d%d", &n, &m);
    19. for (int i = 1; i <= n; i ++ ) p[i] = i; //有n种动物
    20. int res = 0;
    21. while (m -- )
    22. {
    23. int t, x, y;
    24. scanf("%d%d%d", &t, &x, &y);
    25. if (x > n || y > n) res ++ ; //大于范围,直接假
    26. else
    27. {
    28. int px = find(x), py = find(y); //找x和y的根节点
    29. if (t == 1) //判断同类,顺手记录
    30. {
    31. if (px == py && (d[x] - d[y]) % 3) res ++ ; //x和y在同一颗树上,
    32. //两个值到根节点的距离%3不同,说明不是同一类
    33. else if (px != py) //如果是不同的树
    34. {
    35. p[px] = py; //把x树插到y树下,成为分支
    36. d[px] = d[y] - d[x]; //根px到根py的距离
    37. }
    38. }
    39. else //判断是否x吃y
    40. {
    41. if (px == py && (d[x] - d[y] - 1) % 3) res ++ ;//x和y在同一颗树上,
    42. //x值到根节点的距离没有比y距离多1,说明吃不掉
    43. else if (px != py)
    44. {
    45. p[px] = py;
    46. d[px] = d[y] + 1 - d[x]; //同理
    47. }
    48. }
    49. }
    50. }
    51. printf("%d\n", res);
    52. return 0;
    53. }


     

    堆排序

     思路:

    1、向上调整算法:

    1. void up(int u)
    2. {
    3. while(u/2&&h[u/2]>h[u])
    4. {
    5. swap(h[u/2],h[u]);
    6. u/=2;
    7. }
    8. }

    2、向下调整算法 :

    1. void down(int u)
    2. {
    3. int t = u;
    4. if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;
    5. if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    6. if (u != t)
    7. {
    8. swap(h[u], h[t]);
    9. down(t);
    10. }
    11. }

    如何手写一个堆?完全二叉树 5个操作

    1. 插入一个数         heap[ ++ size] = x; up(size);
    2. 求集合中的最小值   heap[1]
    3. 删除最小值         heap[1] = heap[size]; size -- ;down(1);
    4. 删除任意一个元素   heap[k] = heap[size]; size -- ;up(k); down(k);
    5. 修改任意一个元素   heap[k] = x; up(k); down(k);

    h[i] 表示第i个结点存储的值,i从1开始,2*i是左子节点,2*i + 1是右子节点
    size 既表示堆里存储的元素个数,又表示最后一个结点的下标

    i为什么从n/2开始down?

            因为n是最大值,n/2是n的父节点,因为n是最大,所以n/2是最大的有子节点的父节点,所以从n/2往前遍历,就可以把整个数组的父节点遍历一遍

     如图,我们找到最大父节点5,然后向上遍历4321都是父节点,而5就是n/2

    1. #include
    2. using namespace std;
    3. int const N = 100010;
    4. int h[N], siz; //堆有两个变量h[N],size; 怎么这里的size和文件里有冲突,只能改成siz了
    5. void down(int u)
    6. {
    7. int t = u;//t存储三个结点中存在的最小的结点的下标,初始化为当前结点u
    8. if (u * 2 <= siz && h[u * 2] < h[t]) t = u * 2; // 左子节点存在并且小于当前结点,更新t的下标
    9. if (u * 2 + 1 <= siz && h[u * 2 + 1] < h[t]) t = u * 2 + 1;//右子节点存在并且小于当前结点,更新t的下标
    10. if (t != u)//如果t==u意味着不用变动,u就是三个结点中拥有最小值的结点下标,否则交换数值
    11. {
    12. swap(h[t], h[u]);
    13. down(t); //交换数值后,t这个结点存储原本u的值,u存储存储t的值(三个数中的最小值)。u不用调整了,但t情况不明,可能需要调整。直到它比左右子节点都小
    14. }
    15. }
    16. int main()
    17. {
    18. int n, m;
    19. cin >> n >> m;
    20. for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
    21. siz = n; //初始化size,表示堆里有n 个元素
    22. for (int i = n / 2; i; i --) down(i); //把堆初始化成小根堆,从二叉树的倒数第二行开始,把数字大的下沉
    23. while (m -- )
    24. {
    25. printf("%d ", h[1]);
    26. h[1] = h[siz];
    27. siz --;
    28. down(1);
    29. }
    30. return 0;
    31. }

    模拟堆

    思路:

    操作与堆排序一样,但由于需要插入和删除第k个数,要用额外数组作为指针存储位置,以便快速找到k,于是交换位置的同时也要交换指针

    因为需要插入和删除第k个数,所以需要用hp[]记录idx值,然后用ph[]记录hp[]对应的结点 

     

    理解hp与ph数组,以及为什么需要它们

    • 堆h[i]只能存放数据,不能存放是第几个数字,所以需要ph[k] = i来指明,第k个数字在h[]中对应的i是多少
    • 在执行交换操作的时候,可以直接交换数字,swap(h[a],h[b])
    • 但是对于ph[k_1] = a和ph[k_2] = b来说,a和b是它们存放的值,不 能通过值来找下标,也就是找不k_1,k_2是多少
    • 于是引入hp[a] = k_2,hp[b] = k_2,则可以实现反向的操作

    例如: 

    h[a] = 10, h[b] = 20 swap: h[a] = 20,h [b] = 10
    hp[a] = 1 ,hp[b] = 2 swap:hp[a] = 2 ,hp[b] = 1
    ph[1] = a ,ph[2] = b swap:ph[1] = b ,ph[2] = a

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. const int N = 100010;
    6. int h[N], ph[N], hp[N], cnt;
    7. void heap_swap(int a, int b)
    8. {
    9. swap(ph[hp[a]],ph[hp[b]]);
    10. swap(hp[a], hp[b]);
    11. swap(h[a], h[b]);
    12. }
    13. void down(int u)
    14. {
    15. int t = u;
    16. if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;
    17. if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    18. if (u != t)
    19. {
    20. heap_swap(u, t);
    21. down(t);
    22. }
    23. }
    24. void up(int u)
    25. {
    26. while (u / 2 && h[u] < h[u / 2])
    27. {
    28. heap_swap(u, u / 2);
    29. u >>= 1;
    30. }
    31. }
    32. int main()
    33. {
    34. int n, m = 0;
    35. scanf("%d", &n);
    36. while (n -- )
    37. {
    38. char op[5];
    39. int k, x;
    40. scanf("%s", op);
    41. if (!strcmp(op, "I"))
    42. {
    43. scanf("%d", &x);
    44. cnt ++ ;
    45. m ++ ;
    46. ph[m] = cnt, hp[cnt] = m;
    47. h[cnt] = x;
    48. up(cnt);
    49. }
    50. else if (!strcmp(op, "PM")) printf("%d\n", h[1]);
    51. else if (!strcmp(op, "DM"))
    52. {
    53. heap_swap(1, cnt);
    54. cnt -- ;
    55. down(1);
    56. }
    57. else if (!strcmp(op, "D"))
    58. {
    59. scanf("%d", &k);
    60. k = ph[k];
    61. heap_swap(k, cnt);
    62. cnt -- ;
    63. up(k);
    64. down(k);
    65. }
    66. else
    67. {
    68. scanf("%d%d", &k, &x);
    69. k = ph[k];
    70. h[k] = x;
    71. up(k);
    72. down(k);
    73. }
    74. }
    75. return 0;
    76. }

     哈希表

    模拟散列表

    算法 - 哈希表_NO.-LL的博客-CSDN博客

    拉链法:

    1. //拉链法
    2. #include
    3. #include
    4. using namespace std;
    5. const int N = 100003;//取大于1e5的第一个质数
    6. int h[N],e[N],ne[N],idx;//开一个h槽,邻接表,h[]表示每个链表头节点,e[]表示x值,ne[]下一个节点下标
    7. int n;
    8. void insert(int x)
    9. {
    10. int k = (x % N + N) % N;//c++中如果是负数,那他取模也是负的,加N再%N就一定是一个正数
    11. e[idx] = x;
    12. ne[idx] = h[k]; //相当于创建单链表
    13. h[k] = idx++;
    14. }
    15. bool find(int x)
    16. {
    17. int k = (x % N + N) % N;
    18. for(int i = h[k];i != -1;i = ne[i]) //遍历链表
    19. {
    20. if(e[i] == x) return true;
    21. }
    22. return false;
    23. }
    24. int main()
    25. {
    26. cin >> n;
    27. memset(h,-1,sizeof h);//先将槽清空,用-1表示
    28. while(n--)
    29. {
    30. string op;
    31. int x;
    32. cin >> op >> x;
    33. if(op == "I")
    34. {
    35. insert(x);
    36. }
    37. else
    38. {
    39. if(find(x))
    40. {
    41. puts("Yes");
    42. }
    43. else
    44. {
    45. puts("No");
    46. }
    47. }
    48. }
    49. return 0;
    50. }

    开放选址法

    1. #include
    2. #include
    3. using namespace std;
    4. const int N=1e6+9; //开放寻址法一般开 数据范围的 2~3倍, 这样大概率就没有冲突了(我开了10倍)
    5. //开成质数取模时减小冲突概率
    6. const int null=0x3f3f3f3f; //比1e9大的数(在数据范围找不到的数)
    7. int h[N],n;
    8. int find(int x)
    9. {
    10. int t=(x%N+N)%N;
    11. while(h[t]!=null&&h[t]!=x) //如果位置不空并且不是x(不是自己)
    12. {
    13. t++; //就找下一个位置
    14. if(t==N) t=0; //找到最后一个时再从0开找
    15. }
    16. return t; //如果这个位置是空的, 则返回的是他应该存储的位置
    17. }
    18. int main()
    19. {
    20. cin>>n;
    21. memset(h,0x3f,sizeof h); //将每个值变为0x3f3f3f3f(以字节赋值 int4字节)
    22. while(n--)
    23. {
    24. string op;
    25. int x;
    26. cin>>op>>x;
    27. if(op=="I")
    28. {
    29. h[find(x)]=x;
    30. }
    31. else
    32. {
    33. if(h[find(x)]==null) puts("No");
    34. else puts("Yes");
    35. }
    36. }
    37. return 0;
    38. }

    字符串哈希

    设 h[i]前i个字符的hash值;

     为什么是h[l-1]? 由题意得,在abcdef中2 4为bcde,那么就是h[4]-h[2-1];
    为什么是P^(r-l+1)?  ABCDE 与 ABC 的前三个字符值是一样,只差两位,乘上 P^2 把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。而P^2==p[3]==p[r-l+1](p从0次幂开始)

    1. #include
    2. using namespace std;
    3. typedef unsigned long long ull;
    4. const int N=1e5+10,P=131; //或者P=13331
    5. ull h[N],p[N]; //非常刚好的免去了取模的操作
    6. ull find(int l,int r)
    7. {
    8. return h[r]-h[l-1]*p[r-l+1]; //部分求和
    9. }
    10. int main()
    11. {
    12. int n,m;
    13. cin>>n>>m;
    14. string x;
    15. cin>>x;
    16. p[0]=1; //p^0==1
    17. h[0]=0; //hash表从1开始有值,处理边界
    18. for(int i=0;i
    19. {
    20. p[i+1]=p[i]*P;
    21. h[i+1]=h[i]*P+x[i]; //前缀和
    22. }
    23. while(m--)
    24. {
    25. int l1,r1,l2,r2;
    26. cin>>l1>>r1>>l2>>r2;
    27. if(find(l1,r1)==find(l2,r2)) puts("Yes");
    28. else puts("No");
    29. }
    30. return 0;
    31. }


     

  • 相关阅读:
    链动2+1模式系统,是如何成为人、货、场资源流动加速器?
    算法设计与分析 | 输油管道
    Linux进程优先级与环境变量初识
    《canvas》之第7章 变形操作
    Llama 2 70B 问答 - 由人工神经网络训练的程序,与使用编程语言和数学算法编写的程序之间有何区别?
    微前端 - micro-app
    数据库连接池的概念和原理
    AI推介-大语言模型LLMs论文速览(arXiv方向):2024.01.10-2024.01.20
    热忱与专业齐飞 | 微软最有价值专家项目,广纳微软技术贡献者!
    Linux 多线程多进程
  • 原文地址:https://blog.csdn.net/qq_61386381/article/details/126545392