内容:trie、并查集和堆
手写堆
可以删除任意一个数 和 修改任意一个元素的操作(主要用在 迪杰斯特拉里)
一、 tries树 (字典树)
用来高效地存储 查找 字符串集合的数据结构
(一般字符串的 类型 不会很多)
如题:
AC代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int son[N][26]; //存子节点
int cnt[N];//记录 以i位 结尾 的 串数目
int idx;//此时新加结点 的总位数
//插入
void insert(string s){
int p = 0;//当前结点的指针
int len = s.size();
for(int i = 0;i < len;i++){
int u = s[i] - '0';
if(!son[p][u]) son[p][u] = ++idx; //加入结点
p = son[p][u];
}
cnt[p] ++;
}
//查询
int query(string s){
int p = 0;
int len = s.size();
for(int i = 0; i < len; i++){
int u = s[i] - '0';
if(!son[p][u]) return 0; //中间但凡有一个结点找不到 就返回
p = son[p][u];//继续深入查找
}
return cnt[p];
}
int main(){
// 75 25 打字
ios::sync_with_stdio(false);
int n;
cin>>n;
string s;
char ch;
while(n--){
//ch = getchar(); 会错!!
cin>>ch>>s;
if(ch=='Q'){
printf("%d\n",query(s));
}
if(ch=='I'){
insert(s);
}
}
return 0;
}
图形助解:
二、并查集
1.基础用法
(面试 常考 因为考思维 且 代码短)
思维强 代码短
思路: 用树来维护两个集合
用处:
1.将两个集合合并
2.询问两个元素 是否在一个集合中
最重要优化:路径压缩
通过让这条路径上的每一个点都指向根节点实现的
如图:
题目:
AC代码:
#include<iostream>
using namespace std;
const int N = 100010;
int p[N];//父节点数组;
int n,m;
//返回祖宗节点 路径压缩
int find (int x){
if(p[x]!=x) p[x] = find(p[x]);
//如果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];
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;
}
提示: C++的字符 如果是一个字符也用字符串儿来读入
因为如果是scanf来读的话 读入字符 会多读入空格
小技巧 一个字符也用 %s来读入 用的时候用首位就好。
2.顺便维护一些其他 信息
做法:用一个size数组来记,
size : 每一个集合里面 点的数量
AC代码:
#include<iostream>
using namespace std;
const int N = 100010;
int n,m;
int p[N];
int si[N]; //只保证根结点的size有意义
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[2];
int a,b;
scanf("%s",op);
if(op[0]=='C'){
scanf("%d%d",&a,&b);
if(find(a)==find(b)) continue;
//在同一个两通块中就不用
si[find(b)] += si[find(a)]; //a接到b的上面
//把集合a的数目加到 b(新的根结点)上
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 if(op[1]=='2'){
scanf("%d",&a);
printf("%d\n",si[find(a)]);
}
//找到根结点 输出它的size 就可以了
}
return 0;
}
三、堆
1.存储与表示方法
堆是一个完全二叉树
背景:
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,
并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。分别是小根堆和大根堆
一层一层的存
由于堆是一个完全二叉树,所以它可以用非常简便的数组来进行存储。
数组存储的元素就是节点的值,而数组下标位置就代表着该结点在堆中的位置。
如果某一个结点存储时在数组中下标为i
- 它的父节点位置在 i/2
- 它的左子结点位置在 2i
- 它的右子结点位置在 2i+1
堆状数据结构 完全二叉树 都是这样 用一维数组存
2.应用
要求:
- 插入一个数
- 求集合中的最小值
- 删除最小值
- 删除任意元素
- 修改任意元素
求解方法:
1.插入一个数:在堆的最后一个位置 加入一个x 不断上移
2.删除最小的一个数删除 heap【1】(删除堆顶元素) 用整个堆的最后一个元素 去替代对顶元素,再对堆 进行堆化
图,展示的大根堆 道理是一样的
3.删除和修改任意元素:
不管三七二十一 不判断是down还是up 反正都只会做一个
建堆:
//从堆的倒数第二层,开始向下 堆化调整
for(int i=n/2;i;i--) down(i);
堆化下调:
void down(int u){
int t = u;
if(2*u<=n&&h[2*u]<h[u]) t = 2*u;
if(2*u+1<=n&&h[2*u+1]<h[u]) t = 2*u+1;
if(u!=t){ //如果小标变了 那就该交换了
swap(h[t],h[u]);
down(1);
}
}
堆化上调:
void up(int u){
while(u/2&&h[u/2]>h[u]){
swap(h[u/2],h[u]);
u /= 2;
}
}
3.例题
1)堆排序
题目链接
解析: 前m个最小的数 就是 前m个小根堆 堆顶
AC代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int h[N],size; //h[]堆数组,size 最后一个元素
int n,m;
void down(int u){
int t = u;
if(2*u<=n&&h[2*u]<h[u]) t = 2*u;
if(2*u+1<=n&&h[2*u+1]<h[u]) t = 2*u+1;
if(u!=t){ //如果小标变了 那就该交换了
swap(h[t],h[u]);
down(1);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1; i <= n;i++) scanf("%d",&h[i]);
int size = n;
//建堆
for(int i=n/2;i;i--) down(i);
//操做
while(m--){
printf("%d ",h[1]);
h[1] = h[size];
size--;
down(1);
}
return 0;
}
2)模拟堆
关键: 第k个插入的点
解决办法和难点: 存和维护两个数组
解析:
但是为什么会用ph和hp这两个数组呢?
1.原因在于在删除第k个插入元素的操作中,我们首先得知道第k个插入元素在堆数组中的什么位置,即堆数组下标是啥。
很显然,用一个ph数组来存储会方便查找。
2.似乎有了ph数组就可以完成所有操作了,但为什么还要有一个hp数组呢?
原因就在于在swap操作中我们输入是堆数组的下标,无法知道每个堆数组的下标对应的是第几个插入的元素。
所以需要hp数组方便查找
AC代码:
记住 递归的时候 变量一定要理清楚!!!! 我的天啊
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100005;
int ph[N],hp[N],h[N];
int si = 0;
void s_swap(int a,int b){
swap(h[a],h[b]);
swap(hp[a],hp[b]);
swap(ph[hp[a]],ph[hp[b]]);
}
void down(int u){
int t = u;
//t不断变换的值
if(2*u<=si&&h[2*u]<h[t]) t = 2*u;
if(2*u+1<=si&&h[2*u+1]<h[t]) t = 2*u + 1;
if(t!=u){
s_swap(t,u); down(t);
} //t的值变了 那么就要和原来的交换了
}
void up(int u){
if(u/2>0&&h[u]<h[u/2]){
s_swap(u,u/2);
up(u>>1);
}
}
int main(){
int n,m=0;
cin>>n;
while(n--){
string op;
int k,x;
cin>>op;
if(op=="I"){ //在末尾插入
scanf("%d",&x);
m++;//第几个 插入
h[++si] = x; //目前堆里面的节点数
ph[m] = si;
hp[si] = m;
up(si);
}else if(op=="PM") cout<<h[1]<<endl;
else if(op=="DM"){
s_swap(si,1);
si--;
down(1);
}
else if(op=="D"){
cin>>k;
int u = ph[k]; //找到第k个的坐标位置
s_swap(u,si);
si--;
up(u);
down(u);
}
else if(op=="C"){//修改第k个插入的数 改为x
cin>>k>>x;
h[ph[k]] =x;
down(ph[k]);
up(ph[k]);
}
}
return 0;
}