数据结构二
trie树
类似于数据结构中的树,但不是二叉树,一个节点可以有多于两个的子节点
其完成功能主要是存储和查找,(可以通过维护特殊的变量解决特定的题目,以下例题中有讲)
-
存储:
从根节点开始idx==0
;根节点不存储数据,利用二位数组son[p][u]
存储数据
p代表当前节点的深度(p从0开始),u代表当前的深度中叶子的个数(种类)
最后在最后结尾字母处,应当使计数数组cnt[idx]++
,即图中星标,代表当前节点中有字符串的结尾 -
查找:
从根结点处向下遍历,若没有找到p深度下的u那么返回
找到则执行下一层,直到最后找到元素返回cnt[idx]
实例插入结构
先后插入abc,abcf,efg三个字符串
例题1:trie字符串统计
#include <iostream>
#include <string>
using namespace std;
const int N=1e5+10;
int son[N][26]; //N记录的是树的深度,本题也就是单词的长度,26是每层最多出现字母的可能性,就是26个小写字母
int cnt[N]; // 以“abc”字符串为例,最后一个字符---'c'对应的idx作为cnt数组的下标。数组的值是该idx对应的个数,由于idx不重复,所以每个单词对应的下标都是唯一的
int idx; //记录开辟空间的下标,计数从1开始
char str[N];
void insert(char str[]){
int p=0; //定义数的根节点是0
for(int i=0;str[i];i++){
int u=str[i]-'a'; //把每个字母转换成0-25的数字
if(!son[p][u]) son[p][u]=++idx; //如果该单词不存在,给他开辟一块空间,让他加入树
p=son[p][u]; //把son[p][u]同时赋值给p ,son[p][u]的值idx可以指示下一层的深度在什么位置
}
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;
cin>>n;
char op;
while(n--){
cin>>op>>str;
if(op=='I')insert(str);
else cout<<query(str)<<endl;
}
}
例题2:最大异或对
思路:
利用trie树求解
首先每个十进制数都可以通过x>>i&1
的方式转化为二进制数
首先明确一点在n个数中寻找两个异或值最大的数,两两比较只需要比较
C
n
(
2
)
Cn(2)
Cn(2)
即
n
∗
(
n
−
1
)
/
2
n*(n-1)/2
n∗(n−1)/2次 就不需要全部拿来比较
我们采用边向trie树中插入,一边查找的方式,每一个插入的数只需要同他前面的所有数比较即可
在查找时,尽量向着不同于当前数的位置前进
例如如果想找到5(101)
异或最大的值,我们期望能够去找到2(010)
如果当前位没有,只能被迫前进
最后返回最大数计数res
#include <iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10,M=31*N;
int n,idx;
int son[M][2]; //记录trie树,总计最多有N个数输入,每个数最多31位,所以树的深度最多是31*M
int q[N]; //存放整数
//插入操作
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];
}
}
//查找与x异或最大的数操作
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(){
cin>>n;
int res=0;
for(int i=0;i<n;i++)scanf("%d",&q[i]);
for(int i=0;i<n;i++){ //该步骤是边插入边查询,因为共有n个数,两两比较共需比较Cn(2)次,即n(n-1)/2次
insert(q[i]);
int t=query(q[i]);
res=max(res,t^q[i]); //更新答案的最大值
}
cout<<res;
return 0;
}
并查集
例题1:合并集合
- 注意只有根节点的
p[x]==x
,所以路径压缩这一步if(p[x]!=x)p[x]=find(p[x])
翻译成中文就是如果当前节点的父节点不是等于他本身(即他不是根节点),那么就递归的找寻他的根节点,
并将所有点都指向了根节点,最后返回的p[x]
代表的是该集合的编号
,而不是根节点的父节点
#include <iostream>
using namespace std;
const int N=1e5+10;
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; //初始状态每个节点都作为根节点
while(m--){
char op[2];
int i,j;
scanf("%s%d%d",op,&i,&j); //记得用%s来读取字符串,如果用%c读取单独的字符,可能会读取末尾的空格或者回车
if(op[0]=='M')p[find(i)]=find(j); //让j的祖宗节点成为i节点祖宗节点的父亲节点
else
{
if(find(i)==find(j))puts("Yes");
else puts("No");
}
}
return 0;
}
例题2:连通块中点的数量
合并集合的变形:前两个操作和插入与查询操作一致
只需要在根节点加入一个cnt[N]去维护树中元素的数量即可
#include <iostream>
using namespace std;
const int N =1e5+10;
int p[N],cnt[N];
int n,m;
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; //只有根节点数据有效,记录当前树共有多少元素
}
char op[5];
int a,b;
while(m--){
scanf("%s",op);
if(op[0]=='C'){
scanf("%d%d",&a,&b);
if(find(a)==find(b))continue; //防止a,b是同一个节点时,树的元素翻倍
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))cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
else{
scanf("%d",&a);
int res =cnt[find(a)]; //找到该节点的根节点,并返回根节点的计数cnt
cout<<res<<endl;
}
}
return 0;
}
例题3. 食物链**
- 明确:前面说的话一定是真话,一旦后面的话与前面的话发生冲突,那么他就是假话
- 这道题的关键在于利用trie树解题的同时,维护一个怎么样的变量,可以去表达A,B,C
之间的食物链关系- 本题通过利用每个
子节点与根节点的距离d[N]
表达他们之间的关系,无论如何都将所有点插入到一个树内,比较他们与根节点的关系
,也可以得到各点之间的关系- 假定根节点是0号生物距离他“1”的生物吃他,距离他“2”的生物吃1,距离他“3”的生物去吃2,不难推断“3”和根节点0是同一种生物,如何表示他们是同一种生物?
只需要使其距离%3
即可…也就是说距离为3,6,9,12...的生物均和0同类
,,距离为1,4,7,11…的生物均吃0,2,5,8的生物均吃1以此类推
理清这个思路本题得解
#include<iostream>
using namespace std;
const int N=50010;
int p[N],d[N]; //d[N]维护的是该节点到祖宗节点之间的距离
int n,k;
int find(int x){
if(p[x]!=x){
int t=find(p[x]);//先将找到的祖宗节点的坐标保存到t,如果直接写p[x]=find(p[x]),d[x]+=d[p[x]]此时p[x]是根节点那么d[p[x]]等于0,就导致这步没有意义
d[x]+=d[p[x]]; //这一步不仅将当前节点到父亲的距离更新成到根节点的距离,路径上所有的点都更新成了到根节点的距离,,,,**注意这一步千万不能写在int t=find(p[x])的前面否则在递归的时候,就会把每个节点的d[x]更新成到他父亲节点前一个节点的距离,而不是到根节点的距离** find递归操作的本质不仅是返回祖宗节点,同样把每个节点的指向祖宗节点
p[x]=t;
}
return p[x];
}
int main(){
int op,res=0;
cin>>n>>k;
for(int i=0;i<=n;i++)p[i]=i;
while(k--){
int x,y;
cin>>op>>x>>y;
if(x>n||y>n)res++;
else{
int px=find(x),py=find(y);
if(op==1){
if(px==py&&(d[x]-d[y])%3)res++; //px==py表示之前说过有关x,y关系的话(即x,y已经在关系树内),判断他们是否为同一物种,注意不能写成d[x]%3!=d[y]%3(d[x]和d[y]都有可能是负数)
//如果这样写,应保证其均为正数,应写成(d[x]%3+3)%3!=(d[y]%3+3)%3
else if(px!=py){
p[px]=py;
d[px]=d[y]-d[x]; //如果x和y不在同一树内(即之前没有说过关于x和y关系的话),将他们插入到同一树内,,因为d[px]的值默认为0,所以d[px]的值由人为规定,见图三,,因为(d[x]+d[px]-d[y])%3==0所以 d[px]=d[y]-d[x]
}
}
else{
if(px==py&&(d[x]-d[y]-1)%3)res++; //此处同理不能写成(d[x]-d[y])%3!=1应写成((d[x]+d[y])%3+3)%3!=1
else if(px!=py){
p[px]=py;
d[px]=d[y]+1-d[x]; //同理(d[x]+d[px]-d[y]-1)%3==0
}
}
}
}
cout<<res;
return 0;
}
堆
底层结构:完全二叉树
例题1:堆排序
down
和up
时间复杂度均为log(2)n
- 堆的性质:
小根堆
(根节点永远是当前根的最小值)
并且每一个节点的值都要小于其子节点的值- 根节点坐标应当从1开始,如果从0开始左右儿子都是0,不便于计算下标
小根堆的down操作
- 每一层想下执行down操作时 ,时间复杂度是当前节点所在层数(
log2n
)级别,如果倒数第二层的节点down一层,时间复杂度是1
(最多执行一次swap操作),倒数第三层的节点向下down两层
,时间复杂度是2(最多执行两次down操作)以此类推,最后计算出整个堆的时间复杂度应当是O(n)
以上图为例
初始化堆操作:使其从n/2元素位置即倒数第二层
向下执行down操作 倒数第二层有n/4个元素向下down1层
时间复杂度为O(n)
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1e5+10;
int h[N],size; //用一维数组h[n]实现堆,size为数组当前大小
int n,m;
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找到三个数中的最小值,并保存节点下标
if(t!=u){
swap(h[u],h[t]);
down(t);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)scanf("%d",&h[i]);
size=n;
for(int i=n/2;i>=1;i--)down(i); //初始化堆使其从n/2层逐个向下沉
for(int i=1;i<=m;i++){ //输出根节点,用最后的数覆盖根节点,然后down操作重新排序
cout<<h[1]<<" ";
h[1]=h[size];
size--;
down(1);
}
return 0;
}
例题2 模拟堆
包含上题所写的堆的五个基本操作
本题难点在于要动态的删除和修改第k个插入的数
所以我们新开两个数组用于动态的跟踪第k个插入的数的位置
ph[N](POINT HEAP)该数组的下标表示第k个插入的数,内容是对应的堆中元素的下标
(该数组的作用是给定一个k,可以迅速找到堆中的数)
hp[N](HEAP POINT)该数组的下标与h下标相同,代表元素在堆中的位置,内容是对应的ph数组的下标
(该数组的作用是在交换操作时,可以根据当前两元素下标找到对应的ph)
所以在交换两元素时就不能是简单的交换值,同时也要交换其对应的ph和hp
交换操作的具体过程,可以不区分先后顺序
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N=1e5+10;
int n;
int h[N],ph[N],hp[N],size;//ph存放第k个插入的数对应的堆中的数的下标,,hp存放的是堆中的数对应的ph的下标(你是第几个插入堆的)
void heap_swap(int x ,int y){ //传入需要交换数值的数的下标,,此步骤需要逐个交换ph,hp和h即可
swap(h[x],h[y]);
swap(ph[hp[x]],ph[hp[y]]); //交换x,y对应的ph
swap(hp[x],hp[y]);
}
void up(int u){
while(u/2&&h[u/2]>h[u]){
heap_swap(u/2,u); //注意传值是下标
u/=2;
}
}
void down(int u){
int t=u;
if(u*2<=size&&h[t]>=h[2*u])t=2*u;
if(u*2+1<=size&&h[t]>=h[2*u+1])t=2*u+1;
if(t!=u){
heap_swap(t,u);
down(t);
}
}
int main(){
int m=0; //m代表当前插入的数是第几个
scanf("%d",&n);
while(n--){
char op[5];
int x,k;
scanf("%s",op);
if(!strcmp(op,"I")){
cin>>x;
m++,size++;
ph[m]=size;
hp[size]=m;
h[size]=x;
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]; //通过ph,,找到要删除的数在堆里对应的下标
heap_swap(k,size);
size--;
down(k);
up(k);
}
else{
scanf("%d%d",&k,&x);
k=ph[k];
h[k]=x;
down(k);
up(k);
}
}
return 0;
}