• 【五一qbxt】day2 数据结构


    简单数据结构

    入门题:

    在初学OI的时候,总会遇到这么一道题。

    给出N次操作,每次加入一个数,或者询问当前所有数的最大值。

    维护一个最大值Max,每次加入和最大值进行比较。

    时间复杂度O(N).

    给出N次操作,每次加入一个数,删除一个之前加入过的

    数,或者询问当前所有数的最大值。

    N ≤ 100000

    二叉搜索树

    二叉搜索树(BST)是用有根二叉树来存储的一种数据结构,在二叉树中每个节点代表一个数据。

    每个节点包含一个指向父亲的指针,和两个指向儿子的指针。如果没有则为空。
    每个节点还包含一个key值,代表他本身这个点的权值。

     

    二叉搜索树的key值是决定树形态的标准。

    每个点的左子树中,节点的key值都小于这个点。

    每个点的右子树中,节点的key值都大于这个点

    在接下来的介绍中,我们将以ls[x]表示x的左儿子,rs[x]表示x的右儿子,fa[x]表示x的父亲,key[x]表示x这个点的权值。

    基本操作:

    插入一个数,删除一个数,询问最大/最小值,询问第k大值。
    当然,在所有操作结束后,它还能把剩下的数从小到大输出来。

    查询最大/最小值:

    注意到BST左边的值都比右边小,所以如果一个点有左儿子,就往左儿子走,否则这个点就是最小值啦。

     

    插入一个值:(左侧的数要比它的父亲小,右边的要比它的父亲大)

    现在我们要插入一个权值为x的节点。

    为了方便,我们插入的方式要能不改变之前整棵树的形态。

    首先找到根,比较一下key[root]和x,如果key[root] < x,节点应该插在root右侧,否则在左侧。

    看看root有没有右儿子,如果没有,那么直接把root的右儿子赋成x就完事了。

    否则,为了不改变树的形态,我们要去右儿子所在的子树里继续这一操作,直到可以插入为止。

    3 1 5 4 2

    删除一个值:

    删除一个权值为x的点:

    1. 定位一个节点。要删除首先要知道这个点在哪里

    从root开始,想插入一样(判断向左走还是向右走)找某个树

     

    删除:

    方案1:

    直接把结点赋成空的状态(不易查询)

    方案2:

    对这个结点x的儿子进行考虑,若x没有结点,直接删掉。

    如果x有1个儿子,直接把x的儿子接到x的父亲下面就行

    如果x有两个儿子:

    先在右侧找权值最小的点y

    把y的子孙连接到y的父亲上

    用y替换x

    定义x的后继(没有左儿子)y,是x右子树中所有点里,权值最小的点。

    找这个点可以x先走一次右儿子,再不停走左儿子。

    如果y是x的右儿子,那么直接把y的左儿子赋成原来x的左儿子,然后用y代替x的位置。

     

    一个绝妙的实现方法:

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<iostream>
    #include<cstring>
    #include<string>
    #include<cmath>
    #include<ctime>
    #include<set>
    #include<vector>
    #include<map>
    #include<queue>
    
    #define N 300005
    #define M 8000005
    
    #define mid ((l+r)>>1)
    
    #define mk make_pair
    #define pb push_back
    #define fi first
    #define se second
    
    using namespace std;
    
    int i,j,m,n,p,k,ls[N],rs[N],sum[N],size[N],a[N],root,tot,fa[N]; 
    
    void ins(int x)//插入一个权值为x的数字
    {
            sum[++tot]=x;//用tot来表示二叉树里的节点个数  
            size[tot]=1;
            if (!root) root=tot;//没有节点
            else{
                    int now=root; //从根开始
                    for (;;){
                            ++size[now]; 
                            if (sum[now]>sum[tot]){ //判断和当前节点的大小 
                                    if (!ls[now]){ //ls[i]存的是i的左儿子 
                                    //如果没有左儿子,就把新加入的点放在左儿子的位置 
                                        ls[now]=tot; fa[tot]=now;
                                        break;
                                    }
                                    else now=ls[now];//否则的话把当前节点置为它的左儿子,再继续比较 
                            } 
                            else{
                                    if (!rs[now]){
                                        rs[now]=tot; fa[tot]=now;
                                        break;
                                    }
                                    else now=rs[now];
                            }
                    }
            } 
    }
    
    int FindMin()//最小值显然在左侧 
    {
            int now=root;//从树根开始比较 
            while (ls[now]) now=ls[now];//如果有左儿子,把当前节点置为左儿子的位置,继续寻找 
            return sum[now];//sum好像是存的权值 
    }
    
    void build1()//暴力build的方法,每次插入一个值 
    {
        for (i=1;i<=n;++i) ins(a[i]); 
    }
    
    int Divide(int l,int r)
    {
             if (l>r) return 0;
            ls[mid]=Divide(l,mid-1);
            rs[mid]=Divide(mid+1,r);
            fa[ls[mid]]=fa[rs[mid]]=mid; fa[0]=0;
            sum[mid]=a[mid];
            size[mid]=size[ls[mid]]+size[rs[mid]]+1;
            return mid;
    }
    
    void build2()//精巧的构造,使得树高是log N的
    {
            sort(a+1,a+n+1);
            root=Divide(1,n);
            tot=n;
    }
    
    int Find(int x)//查询值为x的数的节点编号 
    {
        int now=root;
        while (sum[now]!=x&&now)
            if (sum[now]<x) now=rs[now]; else now=ls[now];
        return now; 
    }
    
    int Findkth(int now,int k)//递归似的寻找 
    {
            if (size[rs[now]]>=k) return Findkth(rs[now],k);//第k大的值在右边 
            else if (size[rs[now]]+1==k) return sum[now];//当前点是第k大的值 
            else Findkth(ls[now],k-size[rs[now]]-1);//第k大的值在左边 
            //注意到递归下去之后右侧的部分都比它要大 
    }
    
    void del(int x)//删除一个值为x的点
    {
            int id=Find(x),t=fa[id];//找到这个点的编号 
            if (!ls[id]&&!rs[id]) //如果这个点没有左儿子也没有右儿子 
            {
                    if (ls[t]==id) ls[t]=0; else rs[t]=0; //去掉儿子边
                    for (i=id;i;i=fa[i]) size[i]--; //长度-1; 
            }
            else
            if (!ls[id]||!rs[id])//如果这个点没有左儿子或没有右儿子 
            {
                    int child=ls[id]+rs[id];//找存在的儿子的编号 (显然没有的那个儿子编号为0) 
                    if (ls[t]==id) ls[t]=child; else rs[t]=child;//如果这个点是他父亲的左儿子,把它的儿子放到它的位置,否则放右儿子 
                    fa[child]=t;//这个点的儿子的父亲变成了这个点原来的父亲 
                    for (i=id;i;i=fa[i]) size[i]--;//id以上的点的长度-- 
            }
            else//如果既有左儿子又有右儿子 
            {
                    int y=rs[id];//先找到id的右儿子 
                     while (ls[y]) y=ls[y]; //如果id的右儿子有左儿子,那么就更新y为左儿子
                     //目的是找到最小的右儿子 
                    if (rs[id]==y) //如果id右儿子就是右侧最小的点 (这是y是id的右儿子啊) 
                    {
                            if (ls[t]==id) ls[t]=y; //如果id是父亲的左儿子,把左儿子变成y 
                            else rs[t]=y;//否则,右儿子变为y 
                            fa[y]=t;
                            ls[y]=ls[id];//y的左儿子赋成原来x的左儿子
                            fa[ls[id]]=y;
                            for (i=id;i;i=fa[i]) size[i]--;
                            size[y]=size[ls[y]]+size[rs[y]];//y的子树大小需要更新 
                    }
                    else //最复杂的情况    //如果id右儿子不是右侧最小的点     
                    {
                            for (i=fa[y];i;i=fa[i]) size[i]--;//注意到变换完之后y到root路径上每个点的size都减少了1
                            int tt=fa[y]; //先把y提出来(tt即为右侧最小点的父亲) 
                            if (ls[tt]==y)//如果tt的左儿子是y 
                            {
                                    ls[tt]=rs[y];//现在左儿子变为y的右儿子 
                                    fa[rs[y]]=tt;
                            }                    
                            else
                            {
                                    rs[tt]=rs[y];
                                    fa[rs[y]]=tt;
                            }    
                            //再来提出x          
                            if (ls[t]==x)
                            {
                                ls[t]=y;
                                fa[y]=t;
                                ls[y]=ls[id];
                                rs[y]=rs[id];
                            }
                            else
                            {
                                rs[t]=y;
                                fa[y]=t;
                                ls[y]=ls[id];
                                rs[y]=rs[id];
                            }
                            size[y]=size[ls[y]]+size[rs[y]]+1;//更新一下size 
                    }
            }
    }
    
    int main()
    {
            scanf("%d",&n);
            for (i=1;i<=n;++i) scanf("%d",&a[i]);
            build1();
            printf("%d
    ",Findkth(root,2));//查询第k大的权值是什么 
            del(4);
            printf("%d
    ",Findkth(root,2));    
    }

    求解第k大的值

    对每个节点在多记一个size[x]表示x这个节点子树里节点的个数。

    举个例子:

                   *size6

                  / 

                 *size1*size4

                          /|

                         * * *

    从根开始,如果右子树的size ≥ k,就说明第k大值在右侧,

    往右边走,如果右子树size + 1 = k,那么说明当前这个点就是第k大值。

    否则,把k减去右子树size + 1,然后递归到左子树继续操作。

    size维护:

    插入一个结点时,遍历到的点都+1

    删除时:1.直接删除,其上所有父亲size--;

    1. 只有一个儿子,沿id的值,它的所有父亲-1
    2. 两个儿子:从y的父亲开始,向上删除每个父亲的size直到根;

    遍历:

    注意到权值在根的左右有明显区分:

    做一次中序遍历就可以从小到大把所有树排好了。

     

    先访问到最左,直到下方无儿子,输出,然后再访问右儿子

     

    1345679

    回到最初的题:

    一个良好的例子:31245

    一个糟糕的例子 :12345

    二叉搜索树每次操作访问O(h)个节点。

    总结:

    既然他的复杂度与直接暴力删除类似,那我们为什么要学他呢?

    1.因为教学安排里有(大误).

    2.这是第一个能够利用树的中序遍历的性质的数据结构。

    3.扩展性强。更复杂的splay,treap,SGT等都基于二叉搜索树,只是通过一些对树的形态的改变来保证操作的复杂度,且保持树中序遍历的形态。

    4.因为数据很水。随机数据还是很强势的。

    二叉堆:

    满二叉树:除最后一层都是满的;

    用二叉搜索树还是没法解决我们之前的问题。

    堆是一种特殊的二叉树,并且是一棵满二叉树。(这和我学的不太一样qwq)

    第i个节点的父亲是i/2,这样我们就不用存每个点的父亲和儿子了。

    二叉搜索树需要保持树的中序遍历不变,而堆则要保证每个点比两个儿子的权值都小。

    如何建堆:

    最快捷的方法:直接O(nlogn)排序qwq;

    据说堆得所有操作几乎都是O(logn)的

    求最小值

    可以发现每个点都比两个儿子小,那么最小值显然就是a[1]辣,是不是很simple啊。

    simple是simple,但是要写代码啊qwq

    注意到二叉搜索树中的复杂度都是O(h)

    在堆中我们也想让复杂度是O(h) = O(log n).
    这样一来我们就要让树的形态不变,所以我们每次改变的都是权值的位置。

    插入一个值:小根堆

    首先我们先把新加入的权值放入到n + 1的位置。
    然后把这个权值一路往上比较,如果比父亲小就和父亲交换.
    注意到堆的性质在任何一次交换中都满足。

    实现:

    如果某个点x比父亲(position[x]/2)小,就和父亲交换位置

    修改一个点的权值:

    咦,为什么没有删除最小值?
    删除最小值只要把一个权值改到无穷大就能解决辣
    比较简单的是把一个权值变小。
    那只要把这个点像插入一样向上动就行了。

    变大权值:

    那么这个点应该往子树方向走。
    看看这个点的两个儿子哪个比较小。
    如果小的那个儿子的权值比他小,就交换。
    直到无法操作

    定位问题:

    一般来说,堆的写法不同,操作之后堆的形态不同.

    所以一般给的都是改变一个权值为多少的点

    假设权值两两不同,再记录一下某个权值现在哪个位置。

    在交换权值的时候顺便交换位置信息

    删除权值:
    理论上:把需要被删除的点赋成inf,然后下沉一次;
    但是,这样的话会有很多的inf在最下层,所以我们可以把队尾元素移到最上方,再下沉一下即可。一般删除最小的值。

    一种新的建堆方法:

    倒序的把每个结点都下沉。

    显然它是对的qwq;

    复杂度n/2(倒数第二层,显然只需下沉一次) + n/4 * 2 (倒数第二层,显然需下沉两次)+ n/8 * 3 + .... = O(n)

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<iostream>
    #include<cstring>
    #include<string>
    #include<cmath>
    #include<ctime>
    #include<set>
    #include<vector>
    #include<map>
    #include<queue>
    
    #define N 300005
    #define M 8000005
    
    #define ls (t<<1)
    #define rs ((t<<1)|1)
    #define mid ((l+r)>>1)
    
    #define mk make_pair
    #define pb push_back
    #define fi first
    #define se second
    
    using namespace std;
    
    int i,j,m,n,p,k,a[N];
    
    int FindMin()
    {
            return a[1];
    }
    
    void build1()//暴力建堆 
    {
            sort(a+1,a+n+1);
    }
    
    void up(int now)//上浮 
    {
            while (now&&a[now]<a[now/2]) swap(a[now],a[now/2]),now/=2;
    }
    
    void ins(int x)
    {
            a[++n]=x; up(n);
    }
    
    void down(int now)//下沉 
    {
            while (now*2<=n)
            {
                    if (now*2==n)
                    {
                            if (a[now]>a[now*2]) swap(a[now],a[now*2]),now*=2; 
                    }
                    else
                    {
                            if (a[now]<=a[now*2]&&a[now]<=a[now*2+1]) break;
                            if (a[now*2]<a[now*2+1]) swap(a[now],a[now*2]),now*=2;
                            else swap(a[now],a[now*2+1]),now=now*2+1; 
                    }
            }
    }
    
    void del(int x)
    {
            swap(a[x],a[n]); --n;
            up(x);
            down(x);
    }
    
    void change(int x,int val)
    {
            if (a[x]>val)
            {
                a[x]=val;
                up(x);
            }
            else
            {
                a[x]=val;
                down(x);
            }
    }
    
    void build2()//下沉建堆 
    {
            for (i=n/2;i>=1;--i) down(i);
    }
    
    int main()
    { 
         scanf("%d",&n);
         for (i=1;i<=n;++i) scanf("%d",&a[i]);
         build2();
    }
        

    eg2(堆排):洛谷快速排序可以了解一下

     

    #include<iostream>
    #include<algorithm>
    #include<cstdio>
    #include<cstring>
    using namespace std;
    int n,a,b;
    int heap[100010],heap_size;
    int get(){
        int now,next,res;
        res=heap[1];
        heap[1]=heap[heap_size--];
        now=1;
        while(now*2<=heap_size){
            next=now*2;
            if(next<heap_size&&heap[next+1]<heap[next])next++;
            if(heap[now]<=heap[next])break;
            swap(heap[now],heap[next]);
            now=next;
        }
        return res;
    }
    void put(int d){
        int now,next;
        heap[++heap_size]=d;
        now=heap_size;
        while(now>1){
            next=now/2;
            if(heap[now]>=heap[next])break;
            swap(heap[now],heap[next]);
            now=next;
        }
    }
    int main(){
        scanf("%d",&n);
        for(int i=1;i<=n;i++){
            scanf("%d",&a);
            put(a);
        }
        for(int i=1;i<n;i++)
            printf("%d ",get());
        printf("%d",get());
        return 0;
    }

    丑数:

    丑数指的是质因子中仅包含2, 3, 5, 7的数,最小的丑数是1,求前k个丑数。

    K ≤ 6000.

    part1:暴力出奇迹,打表水万物。

    part2:正经的:

     

    考虑递增来构造序列:

    举个eg,已经选中了x,那么接下来可以塞进去x*2,x*3,x*5,x*7。

    然后考虑重复的情况,如果已经在堆里,就不在插入惹。

    Queue

    每次都要写堆太麻烦了有没有什么方便的。

    在C + +的include < queue >里有一个叫priority queue的东西。(优先队列)(疑似大根堆)

    #include<queue>

    using namespace std;

    priority_queue<类型> 队列名

    Q.push()

    Q.top()

    Q.pop()

    Q.clear()清空

    set

    #include<set>

    using namespace std;

    //元素不可重,相同的元素为一个

    set<int>st;

    st.insert(k)//插入

    st.erase(x)//删除

    st.fnd(r)//看某个值是否存在

    st.lower/upper bound()//咱用的那个

    st.begin()/st.end()//返回指向第一个元素的迭代器/返回指向最后一个元素的迭代器

    set<int>::iterator it=st.lower_bound(x);//表示一个下标,代替了下标的功能

    ++it;- -it;

    int x=*it;

    实施维护一个有序的数组。

    区间RMQ问题:

    区间RMQ问题是指这样一类问题。

    给出一个长度为N的序列,我们会在区间上干的什么(比如单点加,区间加,区间覆盖),并且询问一些区间有关的信息(区间的和,区间的最大值)等。

    最简单的问题:

    给出一个序列,每次询问区间最大值.

    N ≤ 100000, Q ≤ 1000000

    ST表:一种处理静态区间可重复计算(一般只最大值和最小值)的数据结构;

    求3~5的最大值

    可以求3~4的最大值,再求出4~5最大值,然后再求

    ST表的思想是先求出每个[i, i + 2k)的最值。

    注意到这样区间的总数是O(N log N)的

    log N这一复杂度是OI最常用复杂度。

    而sqrt(N)是OI最玄学的复杂度。

    预处理:不妨令f[i][j]为区间[i,i+2j)的某个最值(楼下是最小值)

    那么首先fi,0的值都是它本身。

    而fi,j = min(fi,j−1, fi+2j−1,j−1)

    即:

    一段i~i+2j的区间             i— — — — — — — — — — — — — —i+2j

    那么它的最小值就为:    i— — — — — — —i+2j-1— — — — — — —i+2j

    这两段区间最小值中更小的即fi,j = min(fi,j−1, fi+2j−1,j−1)

    O(N log N)

    k=log2(r-l+1)

    区间长度r-l+1;

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<iostream>
    #include<cstring>
    #include<string>
    #include<cmath>
    #include<ctime>
    #include<set>
    #include<vector>
    #include<map>
    #include<queue>
    
    #define N 300005
    #define M 8000005
    #define K 18 
    
    #define ls (t<<1)
    #define rs ((t<<1)|1)
    #define mid ((l+r)>>1)
    
    #define mk make_pair
    #define pb push_back
    #define fi first
    #define se second
    
    using namespace std;
    
    int i,j,m,n,p,k,ST[K+1][N],a[N],Log[N];
    
    int Find(int l,int r)
    {
            int x=Log[r-l+1];
            return max(ST[x][l],ST[x][r-(1<<x)+1]); //注意到对于[l,r],[l,l+2^x-1],[r-2^x+1,r]并起来是[l,r] 
    }
    
    int main()
    {
            scanf("%d",&n);
            for (i=1;i<=n;++i) scanf("%d",&a[i]);
            for (i=1;i<=n;++i) ST[0][i]=a[i];//预 处 理。提前处理好一部分ST 
            for (i=1;i<=K;++i)
                for (j=1;j+(1<<i)-1<=n;++j)
                        ST[i][j]=max(ST[i-1][j],ST[i-1][j+(1<<(i-1))]); //ST[i][j]为从j开始的长度为2^i的区间的最大值 
                                                            //显然[j,j+2^i)=[j,j+2^(i-1))+[j+2^(i-1),j+2^i)=max(ST[i-1][j],ST[i-1][j+2^(i-1)])
            for (i=1;(1<<i)<N;++i) Log[1<<i]=i; //令Log[x]为比x小的最大的2^y 
            for (i=1;i<N;++i) if (!Log[i]) Log[i]=Log[i-1];
            printf("%d
    ",Find(1,3));
    }
    我们将两个区间合并
    因为取最大值
    所以合并起来好操作
    那个
    STlist[][]的定义不是st[i][j]=max{a[i]->a[i+(2^j)-1]}嘛
    那么st[i][j]=max(st[i][j-1],st[i+(2^(j-1))][j-1])就可以推导出来
    意思是st[i][j]可以分成左右两个区间处理
    然后如果j=0,那么st[i][j]=max(a[i],a[i])
    这是预处理
    向下合并的时候,我们可以写一下
    //Log2[]数组也要预处理,这个待会再说
    for (register int i=1;i<=Log2[n];i++)//这个控制的是第二维
    {
        for (register int l=1;l+(1<<i)-1<=n;l++)//这个控制的是左端点
        {
            //整个的区间
            st[l][i]=max(st[l][i-1],st[l+(1<<(i-1))][i-1]);
            //这个重点理解一下
        }
    }
    最后讲一下Log2的推法(直接背过算了)
    int Log2[1000001]={0,0,1};//log2(0)不存在,log2(1)=0,log2(2)=1
    
    inline void Init_log2(int r)
    {
        for (register int i=3;i<=r;i++)
        {
            Log2[i]=Log[i>>1]+1;
        }
    }
    于是我们怎么询问呢
    一个区间[l->r],我们可以拆分成两个重叠的区间(反正取最大最小值不管)
    inline long long query(int l,int r)
    {
        int k=Log2[r-l+1];//区间长度向下去一个log
        return max(st[l][k],st[r-(1<<k)][k]);//即可
        //l->2^k,(r-2^k)->r
    }

    eg

     

    线段树:

    其实线段树被称为区间树比较合适.

    本质是一棵不会改变形态的二叉树.

    树上的每个节点对应于一个区间[a, b](也称线段),a,b通常为整数

     同一层的节点所代表的区间,相互不会重叠

    同一层节点所代表的区间,加起来是个连续的区间
    对于每一个非叶结点所表示的结点[a,b],其左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b](除法去尾取整)
    叶子节点表示的区间长度为1.

    同一层的结点区间长度相差不大于1

    注意到线段树的结构与分治结构差不多深度也是O(log N)的

    区间拆分:

    区间拆分是线段树的核心操作。我们可以将一个区间[a, b]拆分成若干个节点,使得这些节点代表的区间加起来是[a, b],并且相互之间不重叠.
    所有我们找到的这些节点就是”终止节点

    从根节点[1, n]开始,考虑当前节点是[L, R].
    如果[L, R]在[a, b]之内,那么它就是一个终止节点.
    否则,分别考虑[L, Mid],[Mid + 1, R]与[a, b]是否有交,递归两边继续找终止节点.

    举个例子:在1~9中找2~8

    判断2~8和左右是否有交(显然是有的)

    所以左右两边都要递归

    4,5完全被2~8包含,故停下对4,5的访问,再递归1~3

     

    解题方法:

     

    例1:

     

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<iostream>
    #include<cstring>
    #include<string>
    #include<cmath>
    #include<ctime>
    #include<set>
    #include<vector>
    #include<map>
    #include<queue>
    
    #define N 300005
    #define M 8000005
    
    #define ls (t*2)
    #define rs (t*2+1)
    #define mid ((l+r)/2)
    
    #define mk make_pair
    #define pb push_back
    #define fi first
    #define se second
    
    using namespace std;
    
    int i,j,m,n,p,k,add[N*4],sum[N*4],a[N],ans,x,c,l,r;
    
    void build(int l,int r,int t)//建树 
    {
            if (l==r) sum[t]=a[l];
            else
            {
                 build(l,mid,ls);
                 build(mid+1,r,rs);
                 sum[t]=max(sum[ls],sum[rs]); //预先处理区间[l,r]的最大值 
            }
    }
    
    void modify(int x,int c,int l,int r,int t) //将a[x]修改为c,然后需要对所有包含x的区间进行更新 
    {
            if (l==r) sum[t]=c; //只有一个点的时候可以直接计算 
            else 
            {
                    if (l<=x&&x<=mid) modify(x,c,l,mid,ls);
                    else modify(x,c,mid+1,r,rs);
                    sum[t]=max(sum[ls],sum[rs]);//回溯的时候[l,mid],[mid+1,r]的答案已经算出,可以利用两个儿子进行更新 
            }
    }
    
    void ask(int ll,int rr,int l,int r,int t) //询问[ll,rr]这个区间的最大值,l,r,t表示的是当前线段树上位置代表的区间[l,r]和编号t 
    {
            if (ll<=l&&r<=rr) ans=max(ans,sum[t]); //找到了一个完整被[ll,rr]区间包含的区间,直接把答案记进去 
            else
            {
                    if (ll<=mid) ask(ll,rr,l,mid,ls); //如果和左儿子有交就往左儿子走 
                    if (rr>mid)  ask(ll,rr,mid+1,r,rs);  //如果和右儿子有交就往右儿子走 
            }
    }
    
    int main()
    {
            scanf("%d",&n); 
            for (i=1;i<=n;++i) scanf("%d",&a[i]); 
            build(1,n,1); 
            modify(1,5,1,n,1);
            ask(1,5,1,n,1); 
    }

    poj 3264

     

     

    poj 3468

    给定Q个数A1, ..., AQ,多次进行以下操作:
    1.对区间[L, R]中的每个数都加n.
    2.求某个区间[L, R]中的和.
    Q 100000

    如果只记录区间的和?
    进行操作1的时候需要O(N)的时间去访问所有的节点.
    考虑多记录一个值inc,表示这个区间被整体的加了多少.

    延迟更新:

    信息更新时,未必要真的做彻底的更新,可以只是将应该如何更新记录下来,等到真正需要查询准确信息时,才去更新足以应付查询的部分

    在区间增加时,如果要加的区间正好覆盖一个节点,则增加其节 点的inc值和sum值,不再往下走.

    在区间询问时,还是采取正常的区间分解.
    在上述两种操作中,如果我们到了区间[L, R]还要接着往下走,并且inc非0,说明子区间的信息是不对的,我们将inc传送到左儿子和右儿子上,并将inc赋成0,即完成了一次更新

    eg:

     

    右儿子区间长度len/2,左儿子区间:(len+1)/2;

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<iostream>
    #include<cstring>
    #include<string>
    #include<cmath>
    #include<ctime>
    #include<set>
    #include<vector>
    #include<map>
    #include<queue>
    
    #define N 300005
    #define M 8000005
    
    #define ls (t*2)
    #define rs (t*2+1)
    #define mid ((l+r)/2)
    
    #define mk make_pair
    #define pb push_back
    #define fi first
    #define se second
    
    using namespace std;
    
    int i,j,m,n,p,k,lazy[N*4],sum[N*4],a[N],ans,x,c,l,r;
    
    void build(int l,int r,int t)
    {
            if (l==r) sum[t]=a[l];
            else
            {
                 build(l,mid,ls);
                 build(mid+1,r,rs);
                 sum[t]=sum[ls]+sum[rs];
            }
    }
    
    void down(int t,int len) //对lazy标记进行下传 
    {
            if (!lazy[t]) return;
            sum[ls]+=lazy[t]*(len-len/2);
            sum[rs]+=lazy[t]*(len/2);
            lazy[ls]+=lazy[t];
            lazy[rs]+=lazy[t];
            lazy[t]=0;
    }
    
    void modify(int ll,int rr,int c,int l,int r,int t) //[ll,rr]整体加上c
    {
             if (ll<=l&&r<=rr)
             {
                     sum[t]+=(r-l+1)*c; //对[l,r]区间的影响就是加上了(r-l+1)*c 
                    lazy[t]+=c;
             }
             else
             {
                     down(t,r-l+1); 
                     if (ll<=mid) modify(ll,rr,c,l,mid,ls);
                     if (rr>mid)  modify(ll,rr,c,mid+1,r,rs);
                     sum[t]=sum[ls]+sum[rs];
             }
    } 
    
    void ask(int ll,int rr,int l,int r,int t) //对于区间[l,r]进行询问 
    {
            if (ll<=l&&r<=rr) ans+=sum[t]; //代表着找到了完全被包含在内的一个区间 
            else
            {
                    down(t,r-l+1);
                    if (ll<=mid) ask(ll,rr,l,mid,ls);
                    if (rr>mid)  ask(ll,rr,mid+1,r,rs); 
            }
    }
    
    
    int main()
    {
            scanf("%d%d",&n,&m); 
            for (i=1;i<=n;++i) scanf("%d",&a[i]); 
            build(1,n,1); 
    }

    下传到儿子

    poj2528

     

    首先我们对数据进行一些处理,使得1kw的砖块数量减少。

    我们将海报的所有的端点都拿出来,排序去重。

    对于两个端点之间的部分,每块砖要么完全经过他们,要么完全不经过它们,

     

    将两端点之间的部分当成一块砖,然后就可以把砖块数量减到4w块

    从最底层的海报开始,一张一张往上贴

    对于一个区间[L, R],我们记录的信息是这个区间整体被第几张海报覆盖了,初始值设为−1.

    对于一张包含[L, R]的海报i,我们就只需要把[L, R]里面所有的位置都赋成i就可以了.

    注意利用区间分解和延迟更新的方法.

    本题中是否会有标记时间冲突的问题?不会

    标记下传,只可能是后来的覆盖先来了

    zyb画画:

    给出长度为N的序列A,

    Q次操作,两种类型:

    (1 x v),将Ax改成v.

    (2 l r) 询问区间[l, r]中有多少段不同数。例

    如2 2 2 3 1 1 4,就是4段。

    N, Q ≤ 100000.

    线段树上的每个节点都维护三个信息:
    这段区间有多少段不同的数,最右边的数,最左边的数.

    合并的时候,如果中间接上的地方相同,则段数−1.

    非常简单的线段树合并操作.时间复杂度O((N + Q) log N).

    树状数组:

    是一种用来求前缀和的数据结构.

    记lowbit(x)为x的二进制最低位.

    例子:lowbit(8) = 8, lowbit(6) = 2

    fii的最低位.
    i是奇数,fi = 1,否则fi = fi/2 * 2.
    麻烦?lowbit(i) = i& - i.

    对于原始数组A,我们设一个数组C.

    C[i]=a[i-lowbit(i)+1]+...+a[i]

    i > 0的时候C[i]才有用.C就是树状数组

     

    树状数组用于解决单个元素经常修改,而且还反复求不同的区间和的情况

    求和

    树状数组只能够支持询问前缀和.
    我们先找到C[n],然后我们发现现在,下一个要找的点是n - lowbit(n),然后我们不断的减去lowbit(n)并累加C数组.
    我们可以用前缀和相减的方式来求区间和.
    询问的次数和n的二进制里1的个数相同.则是O(log N).

    更新:

    现在我们要修改Ax的权值,考虑所有包含x这个位置的区间个数.

    从C[x]开始,下一个应该是C[y = x + lowbit(x)],再下一个是C[z = y + lowbit(y)]...

    注意到每一次更新之后,位置的最低位1都会往前1.总复杂度也为O(log N).

    eg2:

    求一个数组A1, A2, ..., An的逆序对数.

    n ≤ 100000, |Ai| ≤ 109.

    solution:

    我们将A1, ..., An按照大小关系变成1...n.这样数字的大小范围在[1, n]中.(离散化)
    维护一个数组Bi,表示现在有多少个数的大小正好是i.
    从左往右扫描每个数,对于Ai,累加BAi+1...Bn的和,同时将BAi加1.
    时间复杂度为O(N log N)

    unique(a,a+n+1)-(a+1); //1~n中所有数,去重后的元素个数;去重后指针的位置

    lower_bound找第几个位置

    返回在整个数组里是第几个数

    树及LCA问题:

    LCA:

    在一棵有根树中,树上两点x, yLCA指的是x, y向根方向遇到到第一个相同的点
    我们记每一个点到根的距离为deepx.
    注意到x, y之间的路径长度就是deepx + deepy - 2 * deepLCA.

    LCA原始求法:

    两个点到根路径一定是前面一段不一样,后面都一样.
    注意到LCA的深度一定比x, y都要小.
    利用deep,把比较深的点往父亲跳一格,直到x, y跳到同一个点上.
    这样做复杂度是O(len)

     

    倍增大法:

    考虑一些静态的预处理操作.
    ST表一样,设fai,ji号点的第2j个父亲。
    自根向下处理,容易发现fai,j = fafai,j-1,j-1.

     

    求第k个祖先

    首先,倍增可以求每个点向上跳k步的点.
    利用类似快速幂的想法.
    每次跳2的整次幂,一共跳log次.

     

    LCA
    首先不放假设deepx> deepy.

    为了后续处理起来方便,我们先把x跳到和y一样深度的地方.

    如果xy已经相同了,就直接退出

    否则,由于xyLCA的距离相同,倒着枚举步长,如果x, y的第2j个父亲不同,就跳上去.这样,最后两个点都会跳到离LCA距离为1的地方,在跳一步就行了.

    时间复杂度O(N log N).

     

    如果fa相同,return,否则就向上跳一步。

    考虑二分也可以(从根节点到deepy二分)

    总结

    LCA能发挥很大的用处,具体可以去咨询后天教你们图论的学长.
    倍增这一算法的时空复杂度分别
    O(N log N) - O(log N) O(N log N)

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<iostream>
    #include<cstring>
    #include<string>
    #include<cmath>
    #include<ctime>
    #include<set>
    #include<vector>
    #include<map>
    #include<queue>
    
    #define N 300005
    #define M 8000005
    #define K 18
    
    #define ls (t<<1)
    #define rs ((t<<1)|1)
    #define mid ((l+r)>>1)
    
    #define mk make_pair
    #define pb push_back
    #define fi first
    #define se second
    
    using namespace std;
    
    int i,j,m,n,p,k,fa[N][K+1],deep[N];
    
    vector<int>v[N];
    
    void dfs(int x) //dfs求出树的形态,然后对fa数组进行处理 
    {
            int i;
            for (i=1;i<=K;++i) //fa[x][i]表示的是x向父亲走2^i步走到哪一个节点 
                fa[x][i]=fa[fa[x][i-1]][i-1]; //x走2^i步相当于走2^(i-1)步到一个节点fa[x][i-1],再从fa[x][i-1]走2^(i-1)步 
            for (i=0;i<(int)v[x].size();++i)
            {
                    int p=v[x][i];
                    if (fa[x][0]==p) continue;
                    fa[p][0]=x;
                    deep[p]=deep[x]+1; //再记录一下一个点到根的深度deep_x 
                    dfs(p);
            }
    }
    
    int Kth(int x,int k) //求第k个父亲,利用二进制位来处理 
    {
            for (i=K;i>=0;--i) //k可以被拆分成logN个2的整次幂 
                if (k&(1<<i)) x=fa[x][i];
            return x;
    }
    
    int Find_LCA(int x,int y) //求x,y的LCA 
    {
            int i,k;
            if (deep[x]<deep[y]) swap(x,y);
            x=Kth(x,deep[x]-deep[y]); //把x和y先走到同一深度 
            if (x==y) return x;
            for (i=K;i>=0;--i) //注意到x到根的路径是xa1a2...aic1c2...ck
                               //y到根的路径是        yb1b2...bic1c2...ck 我们要做的就是把x和y分别跳到a_i,b_i的位置,可以发现这段距离是相同的. 
                if (fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i];
            return fa[x][0]; 
    }
    
    int main()
    {
        scanf("%d",&n);
        for (i=1;i<n;++i)
        {
                int x,y;
                scanf("%d%d",&x,&y);
                v[x].pb(y); v[y].pb(x);
        }
        dfs(1);
        printf("%d
    ",Find_LCA(3,5)); 
    }

    并查集:

    简单例题:

     

    solution:

     

    操作示例:

     

    考虑用有根树来维护集合:

     

     

    利用树形结构,记录fai=j表示i的父亲为j

    fai = i,则说明i是根节点,一开始fai = i.

    利用树形结构解决询问:
    询问Query(a, b):调用两次Getroot函数,判断两个根是否相同.

    修改Merge(a, b) :,同样调用Getroot找到a, b的根,如果相同就不管,否则将a的根的父亲设为b.

    注意到Getroot的单次复杂度可能达到O(N).

    尝试优化:

    1. 路径压缩:

    第一种优化看起来很玄学,我们在寻找一个点的顶点的时候,显然可以把这个点的父亲赋成他的顶点,也不会有什么影响.

    看起来玄学,但是他的复杂度是O(N log N)的。

    证明很复杂,有兴趣的同学可以自行翻阅论文。

     

    1. 按秩合并:

    对每个顶点,再多记录一个当前整个结构中最深的点到根的深度deepx.
    注意到两个顶点合并时,如果把比较浅的点接到比较深的节点上.

    如果两个点深度不同,那么新的深度是原来较深的一个.
    只有当两个点深度相同时,新的深度是原来的深度+1.
    注意到一个深度为x的顶点下面至少有2x个点,所以x至多为log N

     

    合并=>法1:显然不够优秀                                  

    =>法2:比较优秀

    比较:

    无论是时间,空间,还是代码复杂度,路径压缩都比按秩合并优秀.
    值得注意的是,路径压缩中,复杂度只是N次操作的总复杂度为O(N log N)。
    按秩合并每一次的复杂度都是严格O(log N)的.

    noi2015传送

    N个变量,M条语句,每条语句为xi = xj,或者xi <> xj
    询问这M条语句是否都有可能成立.
    N 109, M 100000.

    先用离散化处理出所有可能出现的变量.

    可以把相同变量用并查集合并(merge)起来.

    对于一条不同的语句,判断它的两个变量是否在同一个块里

    from洛谷题解:

    先排序,把所有e==1的操作放在前面,然后再进行e==0的操作,在进行e==1的操作的时候,我们只要把它约束的两个变量放在同一个集合里面即可。e==0即存在一条不相等的约束条件,对于它约束的两个变量,如果在一个集合里面,那就不可能满足!如不相等的约束条件都满足,那就YES。

    poj1611

    n个学生,编号0到n - 1, 以及m个团体,0 < n 30000, 0 m 500).一个学生可以属于多个团体,也可以不属于任何团体.一个学生疑似疑似患病,则它所属的整个团体都疑似患病。
    已知0号学生疑似患病,以及每个团体都由哪些学生构成,
    求一共多少个学生疑似患病.

    solution1:互相感染的人,应该属于同一个集合。最终问0所在的集合有几个元素

    solution2:考虑把每个人变成一个点.然后同一个组里的人相互之间有连边,问和0号点连通的有多少点.边数有m * n2条,无法接受,我们给每个团体建一个点,然后所有组里的人向它连边,就把边数减到了n * m条.

    用BFS找出连通块

    poj.1998

    N(N 30, 000)堆方块,开始每堆都是一个方块.方块编号1N. 有两种操作:
    M x y : 表示把方块x所在的堆,拿起来叠放到y所在的堆上。
    C x : 问方块x下面有多少个方块。
    操作最多有P(P 100, 000)次。对每次C操作,输出结果。

    solution:

    首先由于我们要合并的是两个堆,那么至少要维护一个fai,即代表每个方块所在的堆是哪一个.
    那么,我们还需要维护一个underx,表示x这个方块下面有多少方块,初始的时候underx = 0.
    那么要怎么维护underx呢?
    当每次合并x, y时,我们强制将x的父亲连为y,并将underx加上y里面数的个数.

    再维护一个sizex表示x这个并查集的大小,只需要在Merge时维护,underxMergeGetroot时都要更新.

     

    程序自动分析改:

    N个变量,每个变量只有0, 1两种取值.有M条语句,每条
    语句为xi = xj,或者xi <> xj,询问这M 条语句是否都有可能成立.
    N 109, M 100000

    solution:

    要注意一下只有0, 1时的区别.
    把一个点拆成x, x两个点。
    如果yx同一组,说明yx相同.
    如果yx一组,说明yx不同.
    那么只需要xi <> xjij连边,最后查询xx是否在同一组中即可.

  • 相关阅读:
    【foj 1746 A Water Problem】
    【hdu 1049 Climbing Worm】
    【hdu 1070 Milks】
    【B ECJTU_ACM 11级队员2012年暑假训练赛(2)】
    『C Free 5.0 注册码共享』
    【E ECJTU_ACM 11级队员2012年暑假训练赛(2)】
    【J ECJTU_ACM 11级队员2012年暑假训练赛(2)】
    【H ECJTU_ACM 11级队员2012年暑假训练赛(2)】
    【C ECJTU_ACM 11级队员2012年暑假训练赛(2)】
    『为黑客正名』
  • 原文地址:https://www.cnblogs.com/zhuier-xquan/p/10793947.html
Copyright © 2020-2023  润新知