zoukankan      html  css  js  c++  java
  • 数据结构笔记

    数据结构

    二叉搜索树

    递归定义

    它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉搜索树。

    •维护一个集合,支持操作:插入、删除、查找

    •方法:建立一棵有序的二叉树,每个结点对应集合中一个元素。

    •满足性质:结点u的左子树的结点权值都比u小,右子树的结点权值都比u大。

    •插入、删除、查找只需在二叉树中按照权值往下走即可。

    可能的问题:
    可能退化成一条链(例如如果按升序插入),那么每次操作平均复杂度O(n)需要一些技巧让二叉搜索树保持平衡。STL中的set和map都是平衡的二叉搜索树。noip一般情况下够用。

    线段树

    护区间信息的数据结构

    每个结点对应一个序列的区间

    完全二叉树,左子树为左一半区间,右子树为右一半

    单点/区间修改。

    动态开点

    例题

    维护一个数据结构,支持以下两种操作:

    1.插入一个数x

    2.查询[l,r]区间中有多少个数

    操作数<=10^5 x,l,r<=10^9

    强制在线

    用线段树做

    做法

    注意虽然inf=10^9很大,log(inf)还是很小

    操作数很少

    每个结点的值表示对应区间内的数的个数

    每次操作至多增加(O(logn))个非零结点

    我们只需要将这(logn)个非零结点开出来即可,空间复杂度(O(nlog(inf)))

    查询时,如果碰到没有开出的结点,说明这个结点值为(0),返回(0)即可

    注意此时不能使用数组模拟二叉树的方法(和堆类似,(1)为根,(2n-1)(2n)分别为n的左右儿子) 因为得开(10^9)的数组,爆空间

    得加上左右儿子指针的数组。

    为新开的结点赋予大于零的编号 (o=++tot);

    查询时遇到结点编号为(0),就说明这个结点为空,对应区间中没有数

    线段树的合并

    考虑都是动态开点的两棵权值线段树(结点权值为对应区间的数的个数),如何将这两棵线段树合并?(即将他们对应的两个数集合并)

    tree *merge(int l, int r, tree *A, tree *B){
    	if(A == NULL) return B; 
    	if(B == NULL) return A; 
    	if(l == r) return new tree(NULL, NULL, A -> data + B ->data); 
    	int mid = (l + r) >> 1; 
    	return new tree(merge(l, mid, A -> ls, B -> ls), merge(mid + 1, r, A -> rs, B -> rs), A -> data + B -> data); 
    }
    
    

    复杂度:两棵线段树中重合结点(对应位置都开出来的结点)的个数。

    一棵权值线段树表示一个数集。

    如果每棵权值线段树对应的数集都只有一个数,那么这棵权值线段树只有(O(log(inf)))个开出的结点
    (n)个这样的权值线段树合并,复杂度?

    将n个这样表示单元素集合的权值线段树合并,复杂度不会高于将n个元素插入权值线段树。

    因此复杂度(O(nlog(inf)))

    线段树优化建图

    以如下方式给定一个有向图:

    给出结点量n与参数m

    接下来m行,每行三个整数(l_i,r_i,w_i)

    表示点xi向([l_i,r_i])的每个点连出一条长度为(w_i)的边

    求1到n点的最短路

    (n,m<=10^5)

    做法

    对n个顶点建立一棵线段树。

    对于(l_i,r_i,w_i)

    只需将([l_i,r_i])分解成线段树上(log(n))个结点

    (x_i)分别向这(log(n))个结点连一条长度为(w_i)的有向边即可

    边数降为(O(mlogn))

    例题

    CF786B Legacy

    感觉线段树的这些操作听得一脸懵逼

    并查集

    按秩合并

    记录出(size[i])表示这个集合的元素个数,合并时把元素个数小的合并到大的上

    为什么要按秩合并?

    众所周知,并查集如果深度过深了,会导致查询时复杂度退化所以就可以通过记录(size)来减少深度

    void join(int x,int y)
    {
      	x=find(x),y=find(y);
        if(size[x]<size[y]) f[x]=y,xize[y]+=size[x];
        else f[y]=x,xize[x]+=size[y]
    }
    

    二分图判定

    黑白染色法

    大致思路就是先找到一个没被染色的节点u,把它染上一种颜色,之后遍历所有与它相连的节点v,如果节点v已被染色并且颜色和节点u一样,那么就失败了。如果这个节点v没有被染色,先把它染成与节点u不同颜色的颜色,然后遍历所有与节点v相连的节点............就这样循环下去,直到结束为止。

    vector<int> q[N];
    int col[N];//col[u]表示u的颜色,1为白色,-1为黑色,0为未染色
    bool flag=true;
    void dfs(int u,int c)
    {
        col[u]=c;
        for(int i=0;i<e[u].size();++i)
        {
            int v=e[u][i];
            if(!col[v]) dfs(v,-c);
            else if(col[v]==c) flag=false;
        }
    }
    for(int i=1;i<=n;++i)
    	if(!col[i]) dfs(i,1); 
    
    

    字典树

    之前写的笔记链接

    长成这个样子

    图源百度百科

    设字母集为(sum),则每个结点有至多(|sum|)个儿子

    例:给定(n)个字符串的集合,(q)个询问,每个询问给出一个字符串,询问集合中有没有这个字符串
    (trie)做,设字符串总长度为L,空间复杂度(O(L*|sum|)),时间复杂度(O(L))

    int rt=1,cnt=1;
    int ch[N][27],sizes[N];//N是最长串
    void ins(string s)
    {
        int o=rt;
        for(int i=0;i<s.size();++i)
        {
            k=s[i]-'a';
            if(ch[o][k]) o=ch[o][k];
            else ch[o][k]=++cnt,o=ch[o][k];
        }
        sizes[o]++;
    }
    int query(string s)//查询有没有出现过
    {
        int o=rt;
        for(int i=0;i<s.size();++i)
        {
            int k=s[i]-'a';
            if(ch[o][k]) o=ch[o][k];
            else return 0;
    	}
       	return 1;
    }
    

    用字典树解决异或问题(01Trie)

    给定n个正整数,求这(n)个整数中两两异或的最大值?

    (n<=10^5),每个整数(a_i<=10^9)

    把每个正整数转换为一个(30)位的二进制串,分别插入一棵(trie)

    对于每一个整数X,我们想找到它与其它整数异或得到的最大值

    沿着(trie)从上往下走,采取贪心策略。

    每一步设X的当前位为(b),则优先往(b^1)方向走(这样可以在结果的这一位得到一个1);如果(b^1)方向没有儿子,则往(b)方向走

    int ch[N*30][2];
    int rt=1,cnt=1;
    void ins(int x)
    {
        int o=rt;
        for(int i=30;i>=0;--i)
        {
            int k=x>>i&1;
            if(ch[o][k]) o=ch[o][k];
            else ch[o][k]=++cnt,o=cnt;
    	}
    }
    int query(int x)
    {
        int ans=0;
        int o=rt;
        for(int i=30;i>=0;--i)
        {
            int k=x>>i & 1;
            if(ch[o][k^1]) ans|=1<<i,o=ch[o][k^1];
            else o=ch[o][k];
        }
        return ans;
    }
    int a[N],n;
    for(int i=0;i<n;++i) scanf("%d",a+i);
    for(int i=0;i<n;++i) ins(a[i]);
    int ans=0;
    for(int i=0;i<n;++i) ans=max(ans,query(a[i]));
    

    字符串hash

    string s;
    usigned long long v=0;//v是s的hash值
    usigned long long p=1;
    for(int i=0;i<s.size();++i)
    {
    	v+=p*(s[i]-'a');
        p*=26ull;
    }
    
    

    双hash

    取两个模数之后两个模数的hash都出现过才行

    进制hash

    把每位看成进制,有效避免了(ab,ba)判断不出来的情况

    如何得到一个字符串所有子串的hash

    给定一个字符串,每次询问给定四个数(l_1,r_1,l_2,r_2),满足(r_1-l_1=r_2-l_2)

    st表

    (f[i][j])表示从i开始,(2^j)个数的最小值

    (f[i][j])总共有(nlogn)个状态

    可以由(j)从小到大递推求出

    (f[i][j]=min(f[i][j-1],f[i+(1<<(j-1))][j-1]);)

    感觉这东西和倍增差不多

    int a[N],f[N][20];
    int lg[N];
    void init()
    {
        for(int i=1;i<=n;++i) f[i][0]=a[i];
        for(int j=1;(1<<j)<=n;++j)
            for(int i=1;i+(1<<j)-1<=n;++i)
                f[i][j]=min(f[i][j-1],f[i+(1<<j)][j-1]);
        for(int j=1;(1<<j)<=n;++j) lg[(1<<j)]=j;
        for(int j=1;j<=n;+j)
            if(!lg[j]) lg[j]=lg[j-1];
    }
    int query(int l,int r)
    {
        int k=lg[r-l+1];
        return min(f[l][k],f[l-(1<<k)+1][k]);
    }
    

    倍增

    (f[i][j])表示(i)的第(2^i)祖先

    inr lca(int u,int v)
    {
        if(dep[u]>dep[v]) swap(u,v);
        int k=dep[v]-dep[u];
        for(int i=0;i<=lg[k];++i)
       		if(k>>1&1) v=f[i][k];
        if(u==v) return u;
       	for(int i=lg[n];i>-0;--i)
            if(f[u][i]!=f[v][i])
                u=f[u][i],v=f[v][i];
    	return f[u][0];
    }
    

    lca转换为RMQ问题

    感觉比较难

    int seq[N<<1],cnt,pos[N]; //dfs序 
    int f[N<<1][20],dep[N]; //f[i][j]表示dfs序中从i位置开始2^j个结点中 深度最小的结点 
    void init() {
    	for(int i=1;i<=cnt;++i) f[i][0]=seq[i];
    	for(int j=1;j<=LG[cnt];++j)
    		for(int i=1;i+(1<<j)-1<=cnt;++i) 
    			f[i][j] = dep[f[i][j-1]]<dep[f[i+(1<<j-1)][j-1]]?f[i][j-1]:f[i+(1<<j-1)][j-1];
    }
    int lca(int u,int v) {
    	int a=pos[u],b=pos[v];
    	if(a>b) swap(a,b);
    	int k=LG[b-a+1];
    	return dep[f[a][k]]<dep[f[b-(1<<k)+1][k]]?f[a][k]:f[b-(1<<k)+1][k];
    }
    

    tarjan算法离线求lca

    ector<int> e[N],Q[N],id[N]; //e是边表,Q[i]中保存的是与i点相关的查询
    int f[N],n; //f并查集 已初始化 
    int ans[N];
    bool vis[N]; //dfs时是否经过 
    int find(int x){return x==f[x]?x:f[x]=find(f[x]);}
    void ins_query(int u,int v,int o) {
    	//插入一个编号为o的询问,询问(u,v)两点的lca
    	Q[u].push_back(v),id[u].push_back(o);
    	Q[v].push_back(u),id[v].push_back(o); 
    }
    void dfs(int u) {
    	vis[u]=true;
    	for(int i=0;i<Q[u].size();++i) { 
    		int v=Q[u][i];
    		//id[u][i]表示lca查询(u,v)的编号 
    		if(vis[v]) ans[id[u][i]]=find(v); 
    	}
    	for(int i=0;i<e[u].size();++i) {
    		int v=e[u][i];
    		if(vis[v]) continue;
    		dfs(v);
    		f[find(v)]=u;
    	}
    }
    

    一个复杂度的证明

    (n+frac{n}{2}+frac{n}{3}+frac{n}{4}+......+frac{n}{n}=n(1+frac{1}{2}+frac{1}{3}+......+frac{1}{n})=n imes log(n))

    非旋转treap即可持久treap

    学习资料

    那天没事的时候来看看

  • 相关阅读:
    docker的基本操作
    docker和虚拟化技术的区别
    项目命名规则
    Javascript IE 内存释放
    关于ie的内存泄漏与javascript内存释放
    Java遍历HashMap并修改(remove)
    java 中类的加载顺序
    java类的加载以及初始化顺序 .
    JavaScript也谈内存优化
    JavaScript 的垃圾回收与内存泄露
  • 原文地址:https://www.cnblogs.com/pyyyyyy/p/11317625.html
Copyright © 2011-2022 走看看