当前位置: 首页 > news >正文

AcWing--数据结构(二)

Trie 树

用来高效的快速存储和查找字符串集合的数据结构

如存储:abcdef,abdef,aced,...

从根节点开始存储,从前往后存储,看是否有a,没有就创建,依次存储。

一般在最后一个字符打个标记,意思就是当前字符结尾有个单词。

对于查找,要找aced

就从根节点看,如果走到结尾且有标记,就是存在,没有就不存在。


 

int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的字符串数量

// 插入一个字符串
void insert(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        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]; 

//Trie树快速存储字符集合和快速查询字符集合
#include <iostream>using namespace std;const int N = 100010;
//son[][]存储子节点的位置,也存储第几个结点(idx的值),分支最多26条;
//cnt[p]存储以p节点结尾的字符串个数(同时也起标记作用)
//idx表示当前要插入的节点是第几个,每创建一个节点值+1
int son[N][26], cnt[N], idx;
char str[N];void insert(char *str)
{int p = 0;  //类似指针,指向当前节点for(int i = 0; str[i]; i++){int u = str[i] - 'a'; //将字母转化为数字if(!son[p][u]) son[p][u] = ++idx;   //该节点不存在,创建节点p = son[p][u];  //使“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]; }return cnt[p];  //返回字符串出现的次数
}int main()
{int m;cin >> m;while(m--){char op[2];cin>>op>>str; if(*op == 'I') insert(str);else cout<<query(str)<<endl;//改成char op; op == 'I'也行}return 0;
}

#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010,M=31*N;int a[N];
int son[M][2],idx;
//M代表一个数字串二进制可以到多长void insert(int x)
{int p=0;  //根节点for(int i=30;i>=0;i--){int u=x>>i&1;   //取x的二进制中的第i位数字if(!son[p][u]) son[p][u]=++idx; ///如果插入中发现没有该子节点,开出这条路p=son[p][u]; //指针指向下一层}
}
//异或运算相同为0不同为1 
int search(int x)
{int p=0;int res=0;for(int i=30;i>=0;i--){                               int u=x>>i&1; //从最大位开始找if(son[p][!u]) //如果当前层有对应的不相同的数,res左移一位并加一 {   p=son[p][!u];res=res*2+1;//*2相当左移一位  然后如果找到对应位上不同的数res+1}                                                       else //如果当前层有对应的相同的数,res左移一位并加零                                                                                                                    //刚开始找0的时候是一样的所以+0    到了0和1的时候原来0右移一位,判断当前位是同还是异,同+0,异+1{p=son[p][u];res=res*2+0;}}return res;
}
int main()
{int n; cin>>n;for(int i=0;i<n;i++){cin>>a[i];insert(a[i]);}int res=0;for(int i=0;i<n;i++){   res=max(res,search(a[i]));  ///search(a[i])查找的是a[i]值的最大与或值}cout<<res<<endl;return 0; 
}

并查集(常考)

常见操作:


1.将两个集合合并
2.询问两个元素是否在一个集合当中

近乎O(1)


基本原理:每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储
它的父节点,p[x]表示x的父节点
问题1:如何判断树根:if(p[x]==x)
问题2:如何求x的集合编号:while(p[x]!=x)x = p[x]
问题3:如何合并两个集合:px是x的集合编号,py是y的集合编号。p[x]=y

(1)朴素并查集:

    int p[N]; //存储每个点的祖宗节点

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);


(2)维护size的并查集:

    int p[N], size[N];
    //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        size[i] = 1;
    }

    // 合并a和b所在的两个集合:
    size[find(b)] += size[find(a)];
    p[find(a)] = find(b);


(3)维护到祖宗节点距离的并查集:

    int p[N], d[N];
    //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x)
        {
            int u = find(p[x]);
            d[x] += d[p[x]];
            p[x] = u;
        }
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        d[i] = 0;
    }

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
    d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量

#include<iostream>using namespace std;const int N=100010;
int p[N];//存储父节点的数组,p[x]=y表示x的父节点为y //找根节点(集合编号)的函数 ,查找过一遍后所有经过的x的父节点的父节点都会变为集合编号以降低时间复杂度 
int find(int x)
{if(p[x]!=x) p[x]=find(p[x]);//只有根节点的p[x]=x,所以要一直找直到找到根节点 return p[x];//找到了便返回根节点的值
}int main()
{int n,m;cin>>n>>m;for(int i=1;i<=n;i++) p[i]=i;//刚开始每个数自成一个集合,每个数自己为根节点 while(m--){char op;int a,b;cin>>op>>a>>b;if(op=='M') p[find(a)]=find(b);//集合合并操作(把a合并到b集合)else{if(find(a)==find(b)) cout<<"Yes"<<endl;//如果根节点一样,就输出yeselse cout<<"No"<<endl;}}return 0;
}

 2.连通块中的点的数量

#include <iostream>using namespace std;const int N = 100010;//只有根节点的size能代表集合的节点数量,所以size中括号里一般都要有find函数先找出根节点 
int p[N],size[N];int find(int x){if(p[x] != x) p[x] = find(p[x]);return p[x];
}int main(){int n,m;cin>>n>>m;for(int i = 1; i <= n;i++){p[i] = i;size[i] = 1;}while(m--){string op;int a,b;cin>>op;if(op == "C"){cin>>a>>b;if(find(a) == find(b)) continue;size[find(b)]+=size[find(a)];//先加size p[find(a)] = find(b);//再连	}else if(op == "Q1"){cin>>a>>b;if(find(a) == find(b)) cout<<"Yes"<<endl;else cout<<"No"<<endl;}else{cin>>a;cout<<size[find(a)]<<endl;}}return 0;
}

 

#include <iostream>

using namespace std;

const int N = 50010;

//d[x]表示x到父节点的距离,p[x]表示x的父节点 
int p[N],d[N];

/*int find(int x)函数两个作作用:
1.使根节点成为x的父节点,并返回x的根节点
2.将d[x]即x到父节点的距离变为x到根节点的距离*/
int find(int x){
    if(p[x]!=x){
        int t = find(p[x]);
        //find完之后,p[x]直接连接根节点了,d[p[x]]是x的父节点p[x]到根节点的距离,即:
        /*x到根节点的距离 = x到父节点的距离 + 父节点到根节点的距离
        d[x] = d[x] + d[p[x]];*/ 
        //如果不先find那么d[p[x]]就是x的父节点p[x]到父节点的距离,就不一定是p[x]到根节点的距离,即: 
        /*x到根节点的距离 = x到父节点的距离 + 父节点到父节点的距离
        d[x] = d[x] + d[p[x]];*/ 
        //因为find完之后p[x]的子节点就是x,父节点就是根节点0 
        d[x] += d[p[x]];//这步进行完d[x]是没错的了,就是x到根节点的总距离
        //这步再把x的父节点改为根节点,取代之前x和根节点之间的父节点,这样x和根节点就是距离也没错也是直接连接的了  
        p[x] = t; 
    }
    return p[x];
}

int main(){
    int n,m;
    cin>>n>>m;
    for(int i = 1; i<= n; i++) p[i] = i;
    int res = 0;
    while(m--){
        int t,x,y;
        cin>>t>>x>>y;
        //1.X或Y比N大,假话
        if( x>n || y>n) res++;
        else{
            //为什么px一定要等于py也就是x和y为什么一定要在一个集合内呢
            //如果不在同一个集合内,就无法正确地模拟食物链的关系。因为在同一个集合内,才能通过距离数组 d 来维护元素之间的食物链关系,并查集核心思想也是要在一个集合内 
            int px = find(x),py = find(y);
            if(t == 1){//此时t == 1,表示已经告诉我们x和y是同类 
                //在一个树上并且x和y不是同类即(d[x] - d[y])%3余1或余2,假话+1 
                if(px == py && (d[x] - d[y])%3) res++;//注意此时已经经过了find函数所以d[x]是x到根节点的距离 
                else if(px != py){//当x和y不在一个集合内时把他们合并,注意if条件不要漏,不要直接else,是只有x和y不在一个集合时才要合并
                    p[px] = py;
                    d[px] = d[y] - d[x];//1式 
                }
            }
            else{//此时告诉我们x吃y
                //在一个树上并且x不吃y即(d[x] - d[y] - 1)%3不等于0,假话+1
                if(px == py && (d[x] - d[y] - 1)%3) res++;
                else if(px != py){//当x和y不在一个集合内时把他们合并,注意if条件不要漏,不要直接else,是只有x和y不在一个集合时才要合并
                    p[px] = py;
                    d[px] = d[y] +1-d[x];//2式,1式和2式具体思路看图 
                }
            }
        }
    }
    cout<<res<<endl;
    return 0;
 

//简洁版
#include <iostream>using namespace std;const int N = 50010;int p[N],d[N];int find(int x){if(p[x] != x){int t= find(p[x]);d[x]+=d[p[x]];p[x] = t;}return p[x];
}int main(){int n,m;cin>>n>>m;for(int i = 1; i <= n ; i++) p[i] = i;int res = 0;while(m--){int op,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++;else if(px != py){p[px] = py;d[px] = d[y]-d[x]; }}else{if( px == py && (d[x]-d[y]-1)%3) res++;else if(px != py){p[px] = py;d[px] = d[y]-d[x] + 1; }}}}cout<<res<<endl;return 0;
}

堆是一颗完全二叉树(除了最后一层节点,其他都是满的,最后一层也是从左到右排的)

根节点是最小值,其他父节点小于等于左右两节点

//h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
//ph存储堆中的下标,ph是插入顺序数组 
//hp存储元素的插入顺序,h存储的是元素的值,h和hp都是堆数组
//se表示size,插入点在堆中的下标,因为size是关键字所以用se代替
int h[N], ph[N], hp[N], se;

void heap_swap(int a, int b){
    //下面三个交换只要保证 swap(hp[a], hp[b])在swap(ph[hp[a]], ph[hp[b]])后面就行,其他无所谓 
    swap(ph[hp[a]], ph[hp[b]]);//交换堆中的下标
    /*上面这一步是交换堆中的下标,为什么不能直接交换a和b:首先,是因为交换a和b会影响后面两个交换。
    但是这么交换的实际意思是:只是交换了ph数组中对应a和b的那个值,所以这样既正确修改了映射,又没影响到a和b*/ 
    swap(hp[a], hp[b]);//交换插入顺序 
    swap(h[a], h[b]);//交换值 
}

void down(int r){
    int t=r;
    if (r * 2 <= se && h[r * 2]< h[t]) t = r * 2;
    if (r *2 + 1 <= se && h[r * 2 + 1]< h[t]) t = r * 2 + 1;
    if (r != t){
        heap_swap(r, t);
        down(t);
    }
}
void up(int u){
    //只要保证比根结点的值大就行了 
    while (u / 2 && h[u] < h[u / 2]){//此节点索引为u,根节点为u/2 
        heap_swap(u /2, u);
        u /= 2;//下次循环再跟根节点的根节点比...以此类推 
    }
}

// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);

 1.堆排序

 


//小顶堆:1.根节点小于等于两个子节点;2.假如根节点的索引为x,则左子节点索引为2x,右为2x+1;3.堆在几何意义上是一颗完全二叉树 #include<iostream> 
#include<algorithm>using namespace std;const int N = 100010;int h[N], se;//se是数组长度size int n, m;void down(int r)
{int t = r;//t是最小值//一定要用h[t]比较,如上图if (2 * r <= se && h[2 * r] < h[t])//先跟左子节点比t = 2 * r;if (2 * r + 1 <= se && h[2 * r + 1] < h[t])//再跟右子节点比t = 2 * r + 1;if (r != t){swap(h[r], h[t]);//跟最后比出来的最小的那个结点换值/*为什么要在if (r != t)里面down(t),而不是这个函数的最后或其他位置:因为当r == t时,根节点就是最小值,也就是之前t没被赋索引;如果t被赋索引了,此时t是两个子节点中最小的那一个,因为这个最小的值被这个小子树的根节点换走了,所以此时这个子节点的值就是根结点的值,是比原来的值更大的,所以以这个子节点为根节点的子树可能就不满足小顶堆了,所以要再down一下,同理,到了以这个子节点为根节点的小子树如果也是这种情况也要继续再down,以此类推。*/down(t);}
}int main()
{cin >> n >> m;se = n;for (int i = 1; i <= n; i++) cin>>h[i];//使数组成为堆,即初始化堆for (int i = n / 2; i > 0; i--) down(i);while (m--){cout << h[1] << " ";h[1] = h[se--];//删除down(1);}return 0;
}

2.模拟堆

 

#include<iostream>
#include<algorithm> using namespace std;const int N = 100010;//ph存储堆中的下标,ph是插入顺序数组 
//hp存储元素的插入顺序,h存储的是元素的值,h和hp都是堆数组 
//se表示size,插入点在堆中的下标 
int h[N], ph[N], hp[N], se;//参数是堆中的下标
void heap_swap(int a, int b){swap(ph[hp[a]], ph[hp[b]]);//交换堆中的下标/*上面这一步是交换堆中的下标,为什么不能直接交换a和b:首先,是因为交换a和b会影响后面两个交换。但是这么交换的实际意思是:只是交换了ph数组中对应a和b的那个值,所以这样既正确修改了映射,又没影响到a和b*/ swap(hp[a], hp[b]);//交换插入顺序 swap(h[a], h[b]);//交换值 
}void down(int r){int t=r;if (r * 2 <= se && h[r * 2]< h[t]) t = r * 2;if (r *2 + 1 <= se && h[r * 2 + 1]< h[t]) t = r * 2 + 1;if (r != t){heap_swap(r, t);down(t);}
}
void up(int u){//只要保证比根结点的值大就行了 while (u / 2 && h[u] < h[u / 2]){//此节点索引为u,根节点为u/2 heap_swap(u /2, u);u /= 2;//下次循环再跟根节点的根节点比...以此类推 }
}int main(){int m;cin>>m;int n = 0;//n表示插入的第几个数     while(m--){string op;int k,x;cin>>op;//1.插入一个数x if(op=="I"){cin>>x;n++;se++;//se表示在堆中的下标 h[se]=x;hp[se]=n;ph[hp[se]]=se;up(se);//新插入的值浮上去,注意这点易忘 }//2.输出当前集合中的最小值else if(op=="PM") cout<<h[1]<<endl;//3.删除当前集合中的最小值else if(op=="DM"){heap_swap(1,se);//删除操作都是通过交换实现,删除完要进行沉或者浮se--;down(1);}//4.删除第k个插入的数 else if(op=="D"){cin>>k;int u = ph[k];heap_swap(u,se);//删除操作都是通过交换实现,删除完要进行沉或者浮          se--;                    up(u);down(u);}//5.修改第 k个插入的数,将其变为 xelse if(op=="C"){//ph[k]表示第k个数在堆中的下标 cin>>k>>x;h[ph[k]]=x;                down(ph[k]);                up(ph[k]);}}return 0;
}

相关文章:

  • Redis 单线程的“天花板”与集群的必要性
  • 学习记录:DAY34
  • Windows家庭版安装docker
  • SOCKSv5 协议通信的完整阶段与报文格式详解
  • Linux常用指令大全
  • 【编译原理】第九章 习题
  • 医院信息化建设的要点
  • 东方通密码重置指南:快速解决
  • 【C++】atoi和std::stoi
  • 大数据在UI前端的应用深化研究:用户情感分析的探索与实践
  • Go中interface接口的设计理念
  • 【雅思播客013】what do you do
  • 黑马JVM解析笔记(五):深入理解Java字节码执行机制
  • 1 Studying《Is Parallel Programming Hard》6-9
  • 飞算科技依托 JavaAI 核心技术,打造企业级智能开发全场景方案
  • 杭州市长姚高员带队调研景联文科技,听取高质量数据集建设情况
  • 论基于架构的软件设计方法(ABSD)及应用
  • oracle集合一 关联数组(索引表)学习
  • 秘窟燃战.纷魄凌霄(第二集)
  • 关于前端npm install安装依赖和打包的一些问题记录