Trie树
Trie树 :用来高效存储和查找字符串集合的数据结构
并查集
堆 (#include含有堆,但是不支持删除元素)
835. Trie字符串统计
维护一个字符串集合,支持两种操作:
“I x”向集合中插入一个字符串x;
“Q x”询问一个字符串在集合中出现了多少次。
共有N个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。
输入格式
第一行包含整数N,表示操作数。
接下来N行,每行包含一个操作指令,指令为”I x”或”Q x”中的一种。
输出格式
对于每个询问指令”Q x”,都要输出一个整数作为结果,表示x在集合中出现的次数。
每个结果占一行。
数据范围
1≤N≤2*104
输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1
题目一定限制字母的种类,如26个(较单一)
类似哈夫曼存储前缀编码
每一个分支打标记表示结尾 (查找的时候最后一位单词对应标记,若相等才可能找到)
【Ait Tab快速切换窗口
记:【p=0沿着结点走下去,for(int i = 0;str[i];i++) //字母结尾下一位为0
{ int u = str[i] - ‘a’;//映射
if(!son[p][u]) son[p][u] = ++ idx; //下一个先加
p= son[p][u];
} 循环完单词插入cnt[p]++;
【p=0沿结点走下去,每轮插入完,p指向单词结尾当前结点idx,cnt[p]++】
【查询与插入神似,不用创造结点idx不变,p沿着寻找,找到return cnt[p]】
#include <iostream>
using namespace std;
const int N = 100010;
char str[N]; //单词
//下标0的点,既是根结点,又是空结点
//【比如】以x结尾的单词 ,son[x][儿子节点编号(单词)] 存结点下标
int son[N][26],cnt[N],idx; //son二维存放字典树(存每个结点的儿子), idx每个结点下标(每个字母一个下标)
// cnt为标记 【对应查找单词结尾有被标记才存在】
//题目给出只有小写字母,即每个节点最多向外连接26条边 , N为存放单词数量
void insert(char str[])
{
int p = 0;
for(int i = 0;str[i];i++) //用str[i]判断是否到结尾 == 0 时结束 ,遍历字符串,存入单词
{
int u = str[i] - 'a';//【相对位移量】 (a~z 映射成 0~25)
if(!son[p][u]) son[p][u] = ++ idx; //节点p的儿子没有str[i] ,没有路就建路,p继续往下走 (创建每个单词编号下标)
p = son[p][u]; //p=idx临时节点标号;cnt[p]++; ,同时
}
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]; //p沿着连续下标寻找,若是str[i]遍历完退出就找到,否则return 0
}
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;
}
并查集【近乎O(1)】
1.将两个集合合并
2.询问两个集合是否在一个集合当中
基本原理:每个集合用一颗树表示。树根的编号就是整个集合的编号
对于每一个点都存储它的父节点编号(p[x]表示x的父节点),判断一个点是否属于集合,往父节点向上找,到树根的编号即为集合所在的编号
查询复杂度近乎O(1)
1.路径压缩【走过一遍,直接记录终点坐标】:find()递归查询后直接把节点值赋值为根节点值,直接对应集合
问题1.如何判断树根 if(p[x] == x)
问题2. 如何求x的集合编号 : while(p[x] != x) x = p[x]; //沿着父节点找
问题3.如何合并两个集合:px是x的集合编号,py是y的集合编号,p[x] = y
例如:【p[find(a)] = find(b)】
*常用面试题:代码短,思路
合并集合
一共有n个数,编号是1~n,最开始每个数各自在一个集合中。
现在要进行m个操作,操作共有两种:
“M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
“Q a b”,询问编号为a和b的两个数是否在同一个集合中;
输入格式
第一行输入整数n和m。
接下来m行,每行包含一个操作指令,指令为“M a b”或“Q a b”中的一种。
输出格式
对于每个询问指令”Q a b”,都要输出一个结果,如果a和b在同一集合内,则输出“Yes”,否则输出“No”。
每个结果占一行。
数据范围
1≤n,m≤105
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
暴力法遍历两个集合O(n) 用类似merge_sort写法
优化原理:同一个集合,某个元素找到根节点,其余元素就直接一步到根节点,
不用再沿着父节点遍历【路径压缩】
记:
每个点存储父节点,沿着父节点找根(一个根结点集合)while(p[x] != x) x = p[x];
合并【把一个集合的根节点当做另一个根节点的儿子】px是x的集合编号,py是y的集合编号,p[x] = y
p[find(x)] = find(y);
const int N = 100010;
int p[N];
int find(int x) //返回祖宗节点 + 路径压缩
{
if(p[x] != x) p[x] = find(p[x]); //递归找根节点 (集合的编号)
return p[x];
}
int test_02()
{
scanf("%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); //读字符串用%s可以忽略空格(如出题人在最后多打了一个空格)
if(op[0] = 'M') p[find(a)] = find(b); //合并集合,让一个集合的根节点的父节点为另一个集合的根节点
else
{
if(find(a) == find(b)) puts("Yes"); //在同一个集合里
else puts("No");
}
}
return 0;
}
837.连通块的数量 【并查集变型】
还有变型题:240.食物链(维护每个节点到根节点的距离)
题目描述:
给定一个包含 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 中的一种。
输出格式:
对于每个询问指令 Q1 a b,如果 aa 和 bb 在同一个连通块中,则输出 Yes,否则输出 No。
对于每个询问指令 Q2 a,输出一个整数表示点 a 所在连通块中点的数量
每个结果占一行。
数据范围:
1≤n,m≤105
连通块 :若A可以走到B,且B可以走到A,那么A、B连通
在不同的连通块之间连接一条边的时候 == 等效把两个集合合并
不同的是:多了一个操作,size表示每一个集合的大小(点的数量)
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
int n,m;
const int N = 100010;
int p[N],size[N]; //只保证根节点的size是有意义的就可以!! (主要用来记录集合元素个数)
//如a,b集合合并, 则 ①size[b] += size[a] ,②p[find(a)] = find(b);
int find(int x) //返回祖宗节点 + 路径压缩
{
if(p[x] != x) p[x] = find(p[x]); //递归找根节点 (集合的编号)
return p[x];
}
int main(){
scanf("%d%d%d",&n,&m);
for(int i = 1;i <= n;i++) //依题意初始化
{
p[i] = i; //父节点编号(初始为每个节点自身编号)
size[i] = 1; //初始没有边,每个集合一个元素
//维护(统计)集合数量
}
while(m--)
{
char op[5];
int a,b;
scanf("%s",op);//【c++读入技巧,会忽略末尾空格】
if(op[0] == 'C')
{
scanf("%d%d",&a,&b); //这里要判断一下,若a,b已经在同一个集合里,就continue ,不用做任何操作
if(find(a) == find(b)) continue;
size[find(b)] += size[a]; //合并为b所在集合,维护的数量为a+b
p[find(a)] = find(b); //把一个集合的根节点编号的父节点设置为另一个卷积核的根节点编号
}
else if(op[1] == '1')
{
scanf("%d%d",&a,&b);
if(find(a) == find(b)) printf("Yes");
else puts("No");
}
else //合并集合
{
scanf("%d",&a);
printf("%d\n",size[find(a)]);
}
}
return 0;
}
堆排序
题目内容
输入一个长度为n的整数数列,从小到大输出前m小的数。 (堆的高度:logn)
输入格式
第一行包含整数n和m。
第二行包含n个整数,表示整数数列。
输出格式
共一行,包含m个整数,表示整数数列中前m小的数。
数据范围
1≤m≤n≤105 , 1≤数列中元素≤109
输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3
堆的基本应用: (待强化)
【手写堆基本操作,STL中的堆支持的操作】
1.插入一个数
2.求集合当中的最小值
3.删除最小值
(还可以手写实现删除或修改任意一个元素,STL不能直接实现)
小根堆:父节点都小于等于 子节点的值(小于等于左、右子树)
按照层次遍历存储(完全二叉树的下标 --> 转换成一维存储堆)
性质;x的左儿子 == 2 * x , x的右儿子 == 2 * x + 1;
如何手写一个堆
1.插入一个数 heap[ ++size] = x ;up(size); //往上走,重新变成满足小(或大)根堆
2.求集合当中的最小值 heap[1] ;
3.删除最小值 heap[1] = heap[size] ; size- -; down(1); //①把头和尾交换 ②删去尾部 ③ 让1号节点往下走,重新变成堆
4.删除任意一个元素 head[k] = heap[size]; size- -; down(k); up(k);
5.修改任意一个元素 head[k]; down(k) ; up(k) ;
总结: 用up()和dowm()维护堆的性质,每做一次增删改,都要重新变成堆 为了up和down实际只会执行一个,但为了省代码,不做判断,down和up一起做一遍
#include<algorithm>
const int N = 100010;
int n,m;
int h[N],size;
void down(int u) //核心思想:比较节点u与节点u的左儿子(u*2)和右儿子(u*2+1) , 三个当中找出最小值
{
int t = u;//用t存放最小值的编号
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)
{
swap(h[u],h[t]);//最小值与节点u交换(若本身最小就不变了)
down(t);//递归维护堆
}
}
void up(int u) //u节点与父节点u/2比较,大的放下面(交换)
{
while(u / 2 && h[u / 2] > h[u])
{
swap(h[u / 2],h[u]);
u /= 2;//u节点的编号变成父亲节点编号,再次循环判断直到根节点 u == 1 / 2 --> 0结束
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++) scanf("%d",&h[i]);//从1开始存
size = n;
for(int i = n / 2;i;i--) down(i); //可以证明这个循环是O(n)【上界】 ,真实时间看给的层数,带入更精确
while(m--)
{
printf("%d ",h[1]);//输出头部,再把尾部移到头部down(1)维护小根堆, 删除size--头部,
h[1] = h[size];
size--;
down(1);
}
return 0;
}
模拟堆
维护一个集合,初始时集合为空,支持如下几种操作
1."I x"插入一个数 heap[ ++size] = x ;up(size); //往上走,重新变成满足小(或大)根堆
2."PM"求集合当中的最小值 heap[1] ;
3."DM"删除最小值 heap[1] = heap[size] ; size–; down(1); //①把头和尾交换 ②删去尾部 ③ 让1号节点往下走,重新变成堆
4."D k"删除任意一个元素 head[k] = heap[size]; size–; down(k); up(k);
5."C k x"修改任意一个元素
现在要进行N次操作,对于所有的第二个操作,输出当前集合的最小值
第一行输入N
接下来N行输入指令
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int hp[N], ph[N]; //ph[k] 存第k个插入点的位置 , hp[k] , 第k个点是第几个插入点 【point heap】
int h[N];
int size;
void heap_swap(int u, int v) //映射(不常用)
{
swap(ph[hp[u]], ph[hp[v]]);
swap(hp[u], hp[v]);
swap(h[u], h[v]);
}
void down(int u)
{
int t = u;
if (2*u <= size && h[2*u] < h[t]) t = 2*u;
if (2*u+1 <= size && h[2*u+1] < h[t]) t = 2*u+1;
if (t != u)
{
heap_swap(t, u);
down(t);
}
}
void up(int u)
{
while (u/2 && h[u] < h[u/2])
{
heap_swap(u, u/2);
u >>= 1;
}
}
int main()
{
int n;
scanf("%d", &n);
char op[3];
int a, b;
int m = 0;
while (n--)
{
scanf("%s", op);
if (!strcmp(op, "I"))
{
scanf("%d", &a);
m++;
h[++size] = a, ph[m] = size, hp[size] = m;
up(size);
}
else if (!strcmp(op, "PM")) printf("%d\n", h[1]);
else if (!strcmp(op, "DM"))
{
heap_swap(1, size);
size--;
down(1);
}
else if (!strcmp(op, "D"))
{
scanf("%d", &a);
int u = ph[a];
heap_swap(u, size);
size--;
up(u), down(u);
}
else
{
scanf("%d%d", &a, &b);
int u = ph[a];
h[u] = b;
up(u), down(u);
}
}
return 0;
}