线段树入门

本文深入解析线段树数据结构,阐述其优势与应用场景,包括区间查询、区间修改等核心操作。通过多个实例,如LostCows、ASimpleProblemwithIntegers、敌兵布阵等,展示线段树在解决复杂问题中的应用。文章还介绍了离散化技巧,以应对大规模数据处理挑战。
摘要由CSDN通过智能技术生成

线段树入门

  • 了解线段树

    线段树是一种二叉搜索树,它将一个区间划分成一些较小的区间,最终划分成单元区间,每个单元区间对应线段树中的一个叶结点,表示线段上一个点。如下图。
    线段树

  • 线段树的优势
    在线段树上进行的操作复杂度是 O(log2n)

    现在,用最朴朴素的方式,在一个一维数组 a[n] 中,我们要修改某一个点的值时,比如将原数组中 a[3]=3 改成 a[3]=6 ,只需要 a[3]=6; 即可,复杂度为O(1);若需要求 a[k] (0≤i≤k<j<n) 的和时,需就需要遍历数组,复杂度为O(n)。

    虽然从单点修改操作看来线段树没有明显的优势,但是,当我们需要进行多次区间修改和区间查询操作时,再使用朴素方法时就是 O(n²) 的复杂度。 而使用线段树时,就是 O(nlog2n) 的复杂度。所以,再多次查询和多次修改的操作中,线段树是一个很有优势的数据结构。

    通过线段树,就可以在 O(log2n) 的时间复杂度下完成区间查询、区间修改、单点查询、单点修改、区间最值等操作。

    接下来以区间求和介绍线段树的思路和写法。

  • 建树

    #include<bits/stdc++.h>
    #define lson l, mid, rt<<1  //左子树
    #define rson mid+1, r, rt<<1|1  //右子树
    using namespace std;
    const int MAX = 100;  //数据量
    int sum[MAX<<2];  //采用顺序存储结构建树,注意建树所用空间是4*MAX
    
    void push_up(int rt){  //向上更新
        sum[rt] = sum[rt<<1] + sum[rt<<1|1];  //根结点的值等于左右子树区间的值的和
    }
    
    void build(int l, int r, int rt){  //l,r是区间范围,rt是根结点下标
        if(l==r){  //当l==r时,就是叶节点了,相当于线段上的某一点
            sum[rt] = 1;  //初始化,根据需要决定
            return ;
        }
        int mid = (l+r)>>1;
        build(lson);  //递归创建左子树
        build(rson);  //递归创建右子树
        push_up(rt);
    }
    
  • 单点查询

    //单点查询
    int query(int x, int l, int r, int rt){  //x是要查询的点,l,r是线段区间,rt是树的根节点
        if(l==r && l==x){  //达到叶结点,并且与要查询的点吻合
            return sum[rt];
        }
        int mid = (l+r)>>1;
        if(x<=mid) query(x, lson);  //查询的点在左区间
        else query(x, rson);  //查询的点在右区间
    }
    
  • 区间查询

    //区间查询
    int query(int b, int e, int l, int r, int rt){  //b,e是求和区间,l,r是线段区间,rt是树的根节点
        if(b<=l && e>=r){  //当前区间完全包含在所求区间内
            return sum[rt]; //返回该结点的值,就是该结点覆盖线段上区间[l,r]的点的值的和
        }
        int mid = (l+r)>>1, ans = 0;
        /*
        这里理解一下,求区间和的时候分成三种情况,所求区间:
        1.完全包含在左子树包含的区间[l, mid]内;
        2.完全包含在右子树包含的区间[mid+1, r]内;
        3.在左子树区间有一部分,右子树的区间有一部分,就要将所求区间分成两部分求和
        */
        if(l<=mid) ans += query(b, e, lson);
        if(e>=mid+1) ans += query(b, e, rson);
        return ans;
    }
    
    //区间查询第二种容易理解的方式
    int query(int b, int e, int l, int r, int rt){  //b,e是求和区间,l,r是线段区间,rt是树的根节点
        if(b==l && e==r){  //当前区间与所求区间吻合
            return sum[rt]; //返回该结点的值,就是该结点覆盖线段上区间[l,r]的点的值的和
        }
        int mid = (l+r)>>1, ans = 0;
        //这里依旧分成三种情况
        if(e<=mid) ans += query(b, e, lson);  //完全包含在左子树包含的区间[l, mid]内
        else if(l>=mid+1) ans += query(b, e, rson);  //完全包含在右子树包含的区间[mid+1, r]内
        else ans = query(b, mid, lson) + query(mid+1, e, rson);  //在左子树区间有一部分,右子树的区间有一部分
        return ans;
    }
    
  • 单点更新

    //单点更新
    void update(int x, int l, int r, int rt){  //x需要更新的点的位置
        if(l==r && l==x){
            sum[rt] = newdata;  //更新值
            return ;
        }
        int mid = (l+r)>>1;
        if(x<=mid) update(x, lson);  //需要更新的点在左子树
        else update(x, rson);  //需要更新的点在右子树
    }
    
  • 区间更新
    对于更新区间[l, r]内的值(区间内每一个点同时加上某一个数),如果使用普通的数组,只需要遍历一边即可,时间复杂度是 O(n) ,同样,如果使用线段树的结构,对区间内每个点的值进行更新时,需要对每个数(O(n))先进行查找(O(log2n))再更改,时间复杂度为O(nlog2n),反而高于普通方法,所以在线段树上进行区间更新时,并不是采用直接更新某些点的方式,而是采用更新区间并标记,在需要时进行标记下放的方式完成的。称为lazy-tag方法。

    lazy-tag方法当修改的是一整个区间时,只对这个线段区间进行整体上的修改,其内部每个元素的内容先不做修改,只有当这部分线段的一致性被破坏时,才把变化的值传递给子区间。那么每次区间修改的时间复杂度就是O(log2n),N次区间修改操作的时间复杂度就是O(nlog2n)。

    下面用图解的形式来说明。与前面保持一致,使用 sum[MAX] 存放树,新增一个 add[MAX] 用来保存相应节点的 lazy 值。
    初始状态:
    初始状态
    首先将区间 [1, 3] 加上 1
    a.[1,3] 分成 [1, 2] 和 [3,3]两个区间
    b.更新两个区间的和以及对应的 lazy 值 在这里插入图片描述再将区间 [2,4] 加 1
    a.区间 [2,4] 分为 [2,2] 和 [3,4] 两个区间。
    b.更新 [2, 2] 时,经过 [1,2] ,[1,2] 区间的 lazy 值不为零,说明该区间的点更新过,需要先将 lazy 标记下放到两个子区间,然后标记清零。再继续深入。
    c.[3,4] 恰好为需要修改的区间,直接更新区间和,并将 lazy 标记。
    在这里插入图片描述

    //标记下放
    void push_down(int rt, int n){  //n是区间长度
        add[rt<<1] += add[rt];  //更新子区间lazy
        add[rt<<1|1] += add[rt];
        sum[rt<<1] += (n-(n>>1))*add[rt];  //更新子区间
        sum[rt<<1|1] += (n>>1)*add[rt];
        add[rt] = 0;  //取消本层lazy
    }
    //区间更新
    void update(int b, int e, int x, int l, int r, int rt){
        if(b<=l && e>=r){
            add[rt] += x;  //lazy增加
            sum[rt] += (r-l+1)*x;  //区间和增加
            return ;
        }
        push_down(rt, (r-l+1));  //lazy下放
        int mid = (l+r)>>1;
        if(b<=mid) update(b, e, x, lson);  //更新左区间
        if(e>=mid+1) update(b, e, x, rson);  //更新右区间
        push_up(rt);
    }
    
  • 离散化
    当节点规模很大时,由于空间限制,很难在程序中建立二叉树时,就需要用到离散化,离散化实际上就是将大的二叉树压缩成较小的二叉树,但是压缩前后子区间的关系不变。

    例如一块宣传栏,横向长度刻度为1→10,贴四张不同颜色的海报,他们和宣传栏等宽,长度分别为[1,3] 、[2,5]、[3,8]、[3,10] ,并且用后者覆盖前者,问最后能见几种颜色的海报。

    离散化步骤如下:
    (1)提取四张海报的八个顶点 1, 3, 2, 5, 3, 8, 3, 10
    (2)排序并删除相同的端点 1, 2, 3, 5, 8, 10
    (3)把原线段上的端点映射到新线段上:
    在这里插入图片描述
    新的 4 张海报为[1,3]、 [2,4]、 [3,5]、 [3,6] ,覆盖关系不变,新的宣传栏长度压缩到 6 。

练习

Lost Cows
A Simple Problem with Integers
敌兵布阵
Minimum Inversion Number
Just a Hook
I Hate It
Billboard
Ultra-QuickSort
Buy Tickets
Stars
Who Gets the Most Candies?

  • 题意:给出2->n头牛前面有多少头比他编号少的数目,求出每头牛原来的编号,

  • 思路:从后面向前遍历,对第m头牛前面有a头牛,表示此牛在剩余的牛中排名a+1.用线段树维护区间里面未知编号的牛的数量,找到满足条件的区间,区间右端点即为此牛的编号。找到后将此牛标记一下已知编号。

  • 代码

    #include<iostream>
    #include<stdio.h>
    using namespace std;
    int sum[17000];
    int ans[8005], que[8005];
    int q, ran;
    
    void build(int n, int l, int r){
        if(l==r){
            sum[n] = 1;
            return ;
        }
        int m = (l+r)>>1;
        build(2*n, l, m);
        build(2*n+1, m+1, r);
        sum[n] = sum[2*n] + sum[2*n+1];
    }
    
    int query(int n, int l, int r){
        if(l==r) return r;
        int m = (l+r)>>1;
        if(ran<=sum[2*n]){
            return query(2*n, l, m);
        }
        else{
            ran -= sum[2*n];
            return query(2*n+1, m+1, r);
        }
    }
    
    void update(int n, int l, int r){
        if(l==r){
            sum[n]--;
            return ;
        }
        int m = (l+r)>>1;
        if(ran>m) update(2*n+1, m+1, r);
        else update(2*n, l, m);
        sum[n] = sum[2*n] + sum[2*n+1];
    }
    
    int main(){
        scanf("%d", &q);
        for(int i=1; i<q; i++) scanf("%d", &que[i]);
        que[0] = 0;
        build(1, 1, q);
        for(int i=q-1; i>=0; i--){
            ran = que[i]+1;
            ans[i] = query(1, 1, q);
            ran = ans[i];
            update(1, 1, q);
        }
        for(int i=0; i<q; i++) printf("%d\n", ans[i]);
        return 0;
    }
    
  • 模板题,就是区间查询和区间修改。区间修改时,直接修改点必然会超时,这就用到了区间修改的 lazy 标记。还有就是注意一下数据范围,使用 long long 。

  • 代码

    #include<iostream>
    #include<stdio.h>
    #define lson l, mid, rt<<1
    #define rson mid+1, r, rt<<1|1
    #define ll long long
    using namespace std;
    
    const int MAXN = 1e5+10;
    ll sum[MAXN<<2]={0}, add[MAXN<<2]={0};
    
    void push_up(int rt){
        sum[rt] = sum[rt<<1] + sum[rt<<1|1];
    }
    
    void push_down(int rt, int m){
        if(add[rt]){
            add[rt<<1] += add[rt];
            add[rt<<1|1] += add[rt];
            sum[rt<<1] += (m-(m>>1))*add[rt];
            sum[rt<<1|1] += (m>>1)*add[rt];
            add[rt] = 0;
        }
    }
    
    void build(int l, int r, int rt){
        add[rt] = 0;
        if(l == r){
            scanf("%lld", &sum[rt]);
            return ;
        }
        int mid = (l+r)>>1;
        build(lson);
        build(rson);
        push_up(rt);
    }
    
    void update(int a, int b, ll c, int l, int r, int rt){
        if(a<=l && b>=r){
            sum[rt] += (r-l+1)*c;
            add[rt] += c;
            return ;
        }
        push_down(rt, r-l+1);
        int mid = (l+r)>>1;
        if(a<=mid) update(a, b, c, lson);
        if(b>mid) update(a, b, c, rson);
        push_up(rt);
    }
    
    ll query(int a, int b, int l, int r, int rt){
        if(a<=l && b>=r) return sum[rt];
        push_down(rt, r-l+1);
        int mid = (l+r)>>1;
        ll ans = 0;
        if(a<=mid) ans += query(a, b, lson);
        if(b>mid) ans += query(a, b, rson);
        return ans;
    }
    
    int main(){
        int n, m;
        scanf("%d%d", &n, &m);
        build(1, n, 1);
        while(m--){
            char str[2];
            int a, b;
            ll c;
            scanf("%s", str);
            if(str[0] == 'C'){
                scanf("%d%d%lld", &a, &b, &c);
                update(a, b, c, 1, n, 1);
            }
            else{
                scanf("%d%d", &a, &b);
                printf("%lld\n", query(a, b, 1, n, 1));
            }
        }
        return 0;
    }
    
  • 思路:单点更新和区间求和。模板题

  • 代码

    #include<iostream>
    #include<stdio.h>
    using namespace std;
    #define lson l, mid, rt<<1
    #define rson mid+1, r, rt<<1|1
    
    const int MAXN = 50005;
    int sum[MAXN<<2]={0};
    //建树
    void build(int l, int r, int rt){
        if(l == r){
            scanf("%d", &sum[rt]);
            return ;
        }
        int mid = (l+r)>>1;
        build(lson);
        build(rson);
        sum[rt] = sum[rt<<1] + sum[rt<<1|1];
    }
    //单点修改
    void update(int i, int j, int l, int r, int rt){
        if(l==r){
            sum[rt] += j;
            return ;
        }
        int mid = (l+r)>>1;
        if(i<=mid) update(i, j, lson);
        else update(i, j, rson);
        sum[rt] = sum[rt<<1] + sum[rt<<1|1];
    }
    //区间查询
    int query(int i, int j, int l, int r, int rt){
        if(i==l && j==r) return sum[rt];
        int mid = (l+r)>>1;
        if(j>=mid+1 && i<=mid) return query(i, mid, lson)+query(mid+1, j, rson);
        else if(j<=mid) return query(i, j, lson);
        else /*(i>=mid+1)*/ return query(i, j, rson);
    }
    
    int main(){
        int m, n, i, j;
        string str;
        scanf("%d", &m);
        for(int k=1; k<=m; k++){
            printf("Case %d:\n", k);
            scanf("%d", &n);
            build(1, n, 1);
            while(1){
                cin>> str;
                if(str=="End") break;
                scanf("%d%d", &i, &j);
                if(str == "Query"){
                    printf("%d\n", query(i, j, 1, n, 1));
                }
                else{
                    if(str=="Add") update(i, j, 1, n, 1);
                    else update(i, 0-j, 1, n, 1);
                }
            }
        }
        return 0;
    }
    
  • 给你一个数列,求这个数列的逆序数。这是一道求逆序数的模板题。
    那么如何通过线段树求逆序数呢。首先我们知道一个数的逆序数的时候,就看在它的前面出现了几个比他大的数,那么这个数就是它的逆序数。所以,依照输入顺序,输入一个数后a后,就查找区间 [a+1, n] 中有几个数已经存在了,就是 a 的逆序数,然后标记 a 。找完所有数的逆序数再相加就可以了。

  • 代码

    #include<iostream>
    #include<stdio.h>
    #include<string.h>
    using namespace std;
    #define lson l, mid, rt<<1
    #define rson mid+1, r, rt<<1|1
    
    const int MAXN = 5005;
    int sum[MAXN<<2]={0}, pre[MAXN], ma, ans;
    //建树
    void build(int l, int r, int rt){
        if(l == r) return ;
        int mid = (l+r)>>1;
        build(lson);
        build(rson);
        sum[rt] = sum[rt<<1] + sum[rt<<1|1];
    }
    //单点修改
    void update(int x, int l, int r, int rt){
    //    cout<< x<< "--"<< l<< "--"<< r<< endl;
        if(l==r && l==x){
            sum[rt] = 1;
            return ;
        }
        int mid = (l+r)>>1;
        if(x<=mid) update(x, lson);
        else update(x, rson);
        sum[rt] = sum[rt<<1] + sum[rt<<1|1];
    }
    //区间查询
    int query(int i, int j, int l, int r, int rt){
        if(i>j) return 0;
        if(i==l && j==r) return sum[rt];
        int mid = (l+r)>>1;
        if(j>=mid+1 && i<=mid) return query(i, mid, lson)+query(mid+1, j, rson);
        else if(j<=mid) return query(i, j, lson);
        else return query(i, j, rson);
    }
    
    int main(){
        int n;
        while(~scanf("%d", &n)){
            memset(sum, 0, sizeof(sum));
            ma = 0;
            build(1, n, 1);
            for(int i=0; i<n; i++){
                scanf("%d", &pre[i]);
                update(pre[i]+1, 1, n, 1);
                ma += query(pre[i]+2, n, 1, n, 1);
            }
            ans = ma;
            for(int i=0; i<n; i++){
                ma += (n-pre[i]-1)-pre[i];
                if(ma<ans) ans = ma;
            }
            printf("%d\n", ans);
        }
        return 0;
    }
    
  • 模板题区间修改。但这道题中的区间修改与上面讲到的区间修改不同,此题中的区间修改式改变区间中的值为给定值,不是累加关系。

  • 代码

    #include<iostream>
    #include<stdio.h>
    #include<string.h>
    #define lson l, mid, rt<<1
    #define rson mid+1, r, rt<<1|1
    #define ll int
    using namespace std;
    
    const int MAXN = 1e5+10;
    ll sum[MAXN<<2]={0}, add[MAXN<<2]={0};
    
    void push_up(int rt){
        sum[rt] = sum[rt<<1] + sum[rt<<1|1];
    }
    
    void push_down(int rt, int m){
        if(add[rt]){
            add[rt<<1] = add[rt];
            add[rt<<1|1] = add[rt];
            sum[rt<<1] = (m-(m>>1))*add[rt];
            sum[rt<<1|1] = (m>>1)*add[rt];
            add[rt] = 0;
        }
    }
    
    void build(int l, int r, int rt){
        add[rt] = 0;
        if(l == r){
            sum[rt] = 1;
            return ;
        }
        int mid = (l+r)>>1;
        build(lson);
        build(rson);
        push_up(rt);
    }
    
    void update(int a, int b, ll c, int l, int r, int rt){
        if(a<=l && b>=r){
            sum[rt] = (r-l+1)*c;
            add[rt] = c;
            return ;
        }
        push_down(rt, r-l+1);
        int mid = (l+r)>>1;
        if(a<=mid) update(a, b, c, lson);
        if(b>mid) update(a, b, c, rson);
        push_up(rt);
    }
    
    int main(){
        int k;
        scanf("%d", &k);
        for(int i=1; i<=k; i++){
            int N, Q, x, y, z;
            scanf("%d%d", &N, &Q);
            build(1, N, 1);
            while(Q--){
                scanf("%d%d%d", &x, &y, &z);
                update(x, y, z, 1, N, 1);
            }
            printf("Case %d: The total value of the hook is %d.\n", i, sum[1]);
            memset(sum, 0, sizeof(sum));
            memset(add, 0, sizeof(add));
        }
        return 0;
    }
    
  • 模板题,单点修改+区间最值。

  • 代码

    #include<iostream>
    #include<stdio.h>
    #include<string.h>
    #define lson l, mid, rt<<1
    #define rson mid+1, r, rt<<1|1
    #define ll int
    using namespace std;
    
    const int MAXN = 2e5+10;
    ll sum[MAXN<<2]={0};
    
    void push_up(int rt){
        sum[rt] = sum[rt<<1]>sum[rt<<1|1]? sum[rt<<1]:sum[rt<<1|1];
    }
    
    void build(int l, int r, int rt){
        if(l == r){
            scanf("%d", &sum[rt]);
            return ;
        }
        int mid = (l+r)>>1;
        build(lson);
        build(rson);
        push_up(rt);
    }
    
    void update(int a, int b, int l, int r, int rt){
        if(l==r){
            sum[rt] = b;
            return ;
        }
        int mid = (l+r)>>1;
        if(a<=mid) update(a, b, lson);
        else update(a, b, rson);
        push_up(rt);
    }
    
    int query(int a, int b, int l, int r, int rt){
        if(a<=l && b>=r) return sum[rt];
        int mid = (l+r)>>1;
        int x=0, y=0;
        if(a<=mid) x = query(a, b, lson);
        if(b>=mid+1) y = query(a, b, rson);
        return x>y? x:y;
    }
    
    int main(){
        int n, m, a, b;
        char c;
        while(~scanf("%d%d", &n, &m)){
            build(1, n, 1);
            while(m--){
                scanf(" %c%d%d", &c, &a, &b);
            if(c=='Q') printf("%d\n", query(a, b, 1, n, 1));
            else update(a, b, 1, n, 1);
            }
        }
        return 0;
    }
    
  • 单点修改+区间查询(查找到某一点)的模板题。这道题难点在如何使用线段树,方法就是:由于海报的高都是一个单位,所以可以将海报板按单位长度分成一行一行,线段树的每个单元区间(就是线段树的叶节点,也是线段上的一个点)代表一行,单元区间的值表示这个行剩下的长度。因为贴海报的时候是依据上和左优先,其次是下和右的原则,所以只需要从线段上从走向有查找,就相当于从海报板上从上向下查找,对于一行,看是否可以贴(剩余长度是否够)就可以,实际上是不用考虑从左向右的。所以,想明白之后,就是单点修改+区间查询的模板题。

  • 代码

    #include<iostream>
    #include<stdio.h>
    #include<string.h>
    #define lson l, mid, rt<<1
    #define rson mid+1, r, rt<<1|1
    #define ll int
    using namespace std;
    
    const int MAXN = 2e5+5;
    ll sum[MAXN<<2];
    int h, w, n, pos, x;
    
    void push_up(int rt){
        sum[rt] = sum[rt<<1]>sum[rt<<1|1]? sum[rt<<1]:sum[rt<<1|1];  //保存区间最大值,当该区间最大值比海报长度小时,就肯定不贴在该区间,也就不用搜索了。
    }
    
    void build(int l, int r, int rt){
        if(l == r){
            sum[rt] = w;
            return ;
        }
        int mid = (l+r)>>1;
        build(lson);
        build(rson);
        push_up(rt);
    }
    
    void update(int l, int r, int rt){
        if(l==r){
            sum[rt] -= x;
            return ;
        }
        int mid = (l+r)>>1;
        if(pos<=mid) update(lson);
        else update(rson);
        push_up(rt);
    }
    
    bool query(int l, int r, int rt){
        if(l==r){
            if(x<=sum[rt]){
                pos = l;
                return true;
            }
            else return false;
        }
        int mid = (l+r)>>1;
        if(x<=sum[rt<<1]) return query(lson);
        else return query(rson);
    }
    
    int main(){
        while(~scanf("%d%d%d", &h, &w, &n)){
            h = min(h, n);
            build(1, h, 1);
            while(n--){
                scanf("%d", &x);
                if(query(1, h, 1)){
                    printf("%d\n", pos);
                    update(1, h, 1);
                }
                else printf("-1\n");
            }
        }
        return 0;
    }
    
  • 逆序数+离散化。这道题的突破点在于求逆序数。因为排序时是通过相邻元素两两交换完成的,所以实际上排序完成后的交换次数就是给定初始序列的逆序数。实际上就是通过线段树求逆序数的问题了。另外就是离散化的问题。由于要排序的数据可能很大,如果只根据数据建立线段树是行不通的,可以尝试一下,这就还需要先进行离散化,再求逆序数。

  • 代码

    #include<iostream>
    #include<stdio.h>
    #include<algorithm>
    #include<string.h>
    #define lson l, mid, rt<<1
    #define rson mid+1, r, rt<<1|1
    using namespace std;
    const int MAX = 5e5+5;
    int sum[MAX<<2];
    int n;
    __int64 num;
    typedef struct{
        int key1, key2;
        int value;
    }node;
    node in[MAX];
    
    bool cmp1(node n1, node n2){
        return n1.value<n2.value;
    }
    
    bool cmp2(node n1, node n2){
        return n1.key1<n2.key1;
    }
    
    
    void push_up(int rt){
        sum[rt] = sum[rt<<1] + sum[rt<<1|1];
    }
    
    void update(int x, int l, int r, int rt){
        if(l==r){
            sum[rt]=1;
            return ;
        }
        int mid = (l+r)>>1;
        if(x<=mid) update(x, lson);
        else update(x, rson);
        push_up(rt);
    }
    
    int query(int b, int e, int l, int r, int rt){
        if(b>e) return 0;
        if(b<=l && e>=r) return sum[rt];
        int mid = (l+r)>>1;
        int ans = 0;
        if(b<=mid) ans += query(b, e, lson);
        if(e>=mid+1) ans += query(b, e, rson);
        push_up(rt);
        return ans;
    }
    
    int main(){
        while(1){
            scanf("%d", &n);
            if(n==0) break;
            memset(sum, 0, sizeof(sum));
            num = 0;
            for(int i=1; i<=n; i++){
                in[i].key1 = i;
                scanf("%d", &in[i].value);
            }
            stable_sort(in+1, in+n+1, cmp1);
            for(int i=1; i<=n; i++) in[i].key2 = i;
            stable_sort(in+1, in+n+1, cmp2);
    
            for(int i=1; i<=n; i++){
                num += query(in[i].key2+1, n, 1, n, 1);
                update(in[i].key2, 1, n, 1);
            }
            printf("%I64d\n", num);
        }
        return 0;
    }
    
  • 买票插队,每行两个数 a 和 b ,表示编号为 b 的人插在了队伍里第 a 个人后面,问最后的队列顺序(按顺序输出每个人的编号)。这道题和奶牛排序的问题类似,由于最后一个人插进队伍之后,这个人在队伍里的位置就确定了,就可以不考虑这个人对其他人的位置的影响了,所以,采用从后向前遍历的方法来确定某一个人的位置,使用线段树维护区间和表示该区间有多少个人没确定位置就可以了。

  • 代码

    #include<iostream>
    #include<stdio.h>
    #include<algorithm>
    #include<string.h>
    #define lson l, mid, rt<<1
    #define rson mid+1, r, rt<<1|1
    using namespace std;
    const int MAX = 2e5+5;
    int sum[MAX<<2];
    int n, p[MAX], v[MAX], ans[MAX], i;
    
    void push_up(int rt){
        sum[rt] = sum[rt<<1] + sum[rt<<1|1];
    }
    
    void build(int l, int r, int rt){
        if(l==r){
            sum[rt] = 1;
            return ;
        }
        int mid = (l+r)>>1;
        build(lson);
        build(rson);
        push_up(rt);
    }
    
    void query(int x, int l, int r, int rt){
        if(l==r){
            sum[rt] = 0;
            ans[l] = v[i];
            return ;
        }
        int mid = (l+r)>>1;
        if(x<=sum[rt<<1]) query(x, lson);
        else query(x-sum[rt<<1], rson);
        push_up(rt);
    }
    
    int main(){
        while(~scanf("%d", &n)){
            build(1, n, 1);
            for(int i=0; i<n; i++) scanf("%d%d", &p[i], &v[i]);
    
            for(i=n-1; i>=0; i--) query(p[i]+1, 1, n, 1);
    
            for(int i=1; i<=n; i++) printf("%d ", ans[i]);
            cout<< endl;
        }
        return 0;
    }
    
  • 在直角坐标系中,每一个星星占据坐标系上一个点,对于一个星星,它的 level 定义为标系上在它正下方、正左方以及左下方范围内的星星的个数。由于输入采取的原则是按照坐标从左向右、从上到下的原则输入的,忽略纵坐标,只考虑横坐标,相当于在线段上的点上重复放置星星,对于星星 (x, y) 只需要查询区间 [1, x] 内在它之前一共放置了多少个星星就可以了。

  • 代码

    #include<iostream>
    #include<stdio.h>
    #include<algorithm>
    #include<string.h>
    #define lson l, mid, rt<<1
    #define rson mid+1, r, rt<<1|1
    using namespace std;
    const int MAX = 32e3+5;
    int sum[MAX<<2], ans[MAX];
    int n, x, y;
    
    void push_up(int rt){
        sum[rt] = sum[rt<<1] + sum[rt<<1|1];
    }
    
    //void build(int l, int r, int rt){
    //    if(l==r){
    //        sum[rt] = 0;
    //        return ;
    //    }
    //    int mid = (l+r)>>1;
    //    build(lson);
    //    build(rson);
    //    push_up(rt);
    //}
    
    int query(int b, int e, int l, int r, int rt){
        if(b<=l && e>=r){
            return sum[rt];
        }
        int mid = (l+r)>>1;
        int ans = 0;
        if(b<=mid) ans += query(b, e, lson);
        if(e>=mid+1) ans += query(b, e, rson);
        return ans;
    }
    
    void update(int x, int l, int r, int rt){
        if(l==r){
            sum[rt]++;
            return ;
        }
        int mid = (l+r)>>1;
        if(x<=mid) update(x, lson);
        else update(x, rson);
        push_up(rt);
    }
    
    int main(){
        scanf("%d", &n);
        for(int i=0; i<n; i++){
            scanf("%d%d", &x, &y);
            ans[query(1, x+1, 1, 32e3+1, 1)]++;
            update(x+1, 1, 32e3+1, 1);
        }
        for(int i=0; i<n; i++) printf("%d\n", ans[i]);
        return 0;
    }
    
  • 约瑟夫环 + 打表 + 线段树。
    如果一个小孩儿第 n 个出队,那么他得到的糖果就是 n 的因子的个数。然后出队的这个小孩儿手中有一张卡片,写了下一个倒霉蛋的位置 a ,规则是:a > 0,从他开始顺时针第 a 个人出队; a < 0,从他开始逆时针第 a 个人出队。
    要找到糖果数最多的小孩儿,就要找n个小孩中第几个出队的因子数最多,也就是 1 → n 中哪个数的因子最多。
    为了避免超时,就要提前打表,找出每个数的因子个数。打表的时候也要考虑打表的方法,我采用的方法是,对于一个数,它的所有倍数因子数加一。
    然后就是用线段树模拟约瑟夫环。我采用的方法是看要出队的小孩儿前面(包含这个小孩儿)有几个小孩儿,从而确定要出队的小孩儿的位置。至于有几个小孩儿,拿出笔画画很容易发现规律,这里我就直接写出来了:
    卡片上的数是正数:num = ((num-1+children[id].k-1)%sum[1] + sum[1])%sum[1]+1;
    负数:num = ((num-1+children[id].k)%sum[1]+sum[1])%sum[1]+1;
    这里注意取模 +1 就好,取模是为了模拟循环,+1 是线段树的点是从 1 开始的。

  • 代码

    #include<iostream>
    #include<stdio.h>
    #include<algorithm>
    #include<string.h>
    #define lson l, mid, rt<<1
    #define rson mid+1, r, rt<<1|1
    using namespace std;
    const int MAX = 5e5+5;
    int sum[MAX<<2], ans[MAX];
    int n, p, mod;
    struct{
        int k;
        char name[11];
    }children[MAX];
    
    void init(){
        for(int i=1; i<=MAX; i++){
            ans[i]++;
            for(int j=i+i; j<=MAX; j+=i){
                ans[j]++;
            }
        }
    }
    
    int the_max(int n){
        int m = ans[1], j = 1;
        for(int i=2; i<=n; i++){
            if(m<ans[i]){
                m = ans[i];
                j = i;
            }
        }
        return j;
    }
    
    void push_up(int rt){
        sum[rt] = sum[rt<<1] + sum[rt<<1|1];
    }
    
    void build(int l, int r, int rt){
        if(l==r){
            sum[rt] = 1;
            return ;
        }
        int mid = (l+r)>>1;
        build(lson);
        build(rson);
        push_up(rt);
    }
    
    int query(int x, int l, int r, int rt){
        if(l==r) return r;
        int mid = (l+r)>>1;
        if(x<=sum[rt<<1]) query(x, lson);
        else query(x-sum[rt<<1], rson);
    }
    
    void update(int x, int l, int r, int rt){
        if(l==r && l==x){
            sum[rt] = 0;
            return ;
        }
        int mid = (l+r)>>1;
        if(x<=mid) update(x, lson);
        else update(x, rson);
        push_up(rt);
    }
    
    int main(){
        init();
        while(~scanf("%d%d", &n, &p)){
            for(int i=1; i<=n; i++) scanf("%s%d", children[i].name, &children[i].k);
            build(1, n, 1);
            int k = the_max(n), id = p, num = p;
            for(int i=1; i<k; i++){
                update(id, 1, n, 1);
                if(sum[1]==0) break;
                if(children[id].k<0) num = ((num-1+children[id].k)%sum[1]+sum[1])%sum[1]+1;
                else num = ((num-1+children[id].k-1)%sum[1] + sum[1])%sum[1]+1;
                id = query(num, 1, n, 1);
            }
            printf("%s %d\n", children[id].name, ans[k]);
        }
        return 0;
    }
    
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>