一、trie树
主要思想:在存储、查找多个字符串问题中,常规遍历算法耗时太长,将多个字符串构建成一个trie字符串树能够大大减小算法的时间开销。trie树从一个空的根节点开始,遍历要存入字符串的每个字母,若当前节点没有该字母的子节点,则添加子节点,若存在则移动到该子节点储存下一个字母。此外,为了识别一些短字符串在树的中途,还未遍历到叶子结点就结束的情况,增加一个字符串结束标记。
例题:给出n个操作,“I string”代表插入字符串string,“Q string”代表查询以string结尾的字符串的个数。
#include<iostream>
using namespace std;
const int N=10010;
int son[N][26],cnt[N];//son存储子结点,cnt存储结尾字符串个数,本题中只涉及26个字母
int idx;//当前可用结点
char str[N];
void insert(char str[]){
int p=0;
for(int i=0;str[i];i++){//从头结点开始遍历
int u=str[i]-'a';//将字母的ASCLL码转为数字
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;
char op[2];
scanf("%d",&n);
while(n--){
scanf("%s %s",op,str);
if(op[0]=='I') insert(str);
else printf("%d\n",query(str));
}
return 0;
}
二、并查集
主要思想:主要运用在集合合并、集合查找等问题中。将集合中的元素以树的形式连接起来,根节点元素即为代表该集合的标记元素,是整个集合的祖宗结点。这样存储后,集合合并问题变成为集合a的祖宗结点添加一条边,指向集合b的祖宗结点。查询两个元素是否在同一个集合变成查询两个元素的祖宗结点是否相同。但沿着父结点查询祖宗结点还是会消耗很多时间,使用路径压缩将查询祖宗结点路上遇到的所有结点的父结点都指向祖宗结点,就可以大大减小树的高度,使得问题能在几乎O(1)的时间内解决。
例题:给出n个数,它们一开始都是独立的集合,再给出m个操作,“M a b”表示将a,b所在的集合进行合并,“Q a b”表示查询a,b是否在同一个集合,如果在同一个集合则返回“YES”,否则返回“NO”。
#include<iostream>
using namespace std;
const int N=10010;
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=1;i<=n;i++) p[i]=i;
char op[2];
int a,b;
while(m--){
scanf("%s%d%d",op,&a,&b);
//让b所在集合的祖宗结点成为a所在集合的祖宗结点的父结点
if(op[0]=='M') p[find(a)]=find(b);
else{
if(find(a)==find(b)) printf("YES\n");
else printf("NO\n");
}
}
return 0;
}
三、最小堆
主要思想:最小堆是一个左右孩子都小于结点的完全二叉树(除最后一层外,其余结点都有左右两个孩子),用一维数组存储最小堆时,h[i]为父结点,h[2*i]为它的左孩子,h[2*i+1]为它的右孩子。通过up(i)和down(i)两个基础操作,能够实现堆中结点的移动,保证最小堆的数据大小关系。在需要修改第k个插入的结点(注意,第k个插入的结点不是堆中的k结点)问题中,还需要构建ph[]数组,存储插入顺序到堆中结点的映射,即通过插入结点顺序k找到堆中的结点i,hp[]数组,存储堆中的结点到插入顺序的映射,即通过堆中的结点i找到插入该结点的顺序k。
例题:给出n行操作,“I x”表示在堆中插入x,“PM”表示输出堆中的最小值,“DM”表示删除堆中的最小值,“D k”表示删除第k个插入的元素,“C k x”表示把第k个插入的元素换成x。
#include<iostream>
#include<algorithm>
#include<string.h>//比较操作字符
using namespace std;
const int N=10010;
int n,m=0;
int h[N],ph[N],hp[N],size;//size作为界限判断堆的结点值是否有效
char op[2];
void heap_swap(int a,int b){//【1】
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(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/2]>h[u]){//根节点存在且根节点不是最小
heap_swap(u/2,u);
u/=2;
}
}
int main(){
scanf("%d",&n);
/*
for(int i=1;i<=n;i++) scanf("%d",&h[i]);//在不需要插入顺序的问题中的初始化方式
size=n;
for(int i=n/2;i;i--) down(i);//从一半的地方开始移动就足够
*/
int x,k;
while(n--){
scanf("%s",&op);
if(!(strcmp(op,"I"))){
scanf("%d",&x);
m++;//m为插入顺序
size++;
h[size]=x;//为方便操作,在堆中进行插入删除都是在尾端进行
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",&k);
k=ph[k];//找到第k个插入的结点是堆中的几号结点
heap_swap(k,size);
size--;
up(k),down(k);
}
else if(!(strcmp(op,"C"))){
scanf("%d %d",&k,&x);
k=ph[k];
h[k]=x;
up(k),down(k);//up和down事实上只执行一个,都写上不用判定
}
}
return 0;
}
【1】:在需要插入顺序的问题中,通过自定义交换函数保证堆中结点交换时ph[]和hp[]的对应关系。