• NOIP2017 列队


    题目描述:https://www.luogu.org/problemnew/show/P3960

    这题当时我在NOIP的时候只会暴力模拟,然后拿了30pts……

    今天肖大佬给我们讲课的时候讲了这道题……正解有树状数组,有线段树,有splay……大佬给我们讲的是线段树做法。

    首先先说一下,线段树怎么动态开点。这道题因为列队的长和宽都很大,达到了3*10^5,而询问次数也达到了3*10^5,直接把线段树全部建出来显然不现实。那我们就考虑动态开点,你只有在访问到他的时候你才会去使用这块空间。这个用指针比较好写(可是蒟蒻学不会指针)所以就学了数组模拟。对于每一次询问,你在线段树维护的时候只需要log大小的空间去维护,所以总空间并不是非常大。在老版线段树中,我们不记录一个点的左右儿子,而是直接用p<<1 , p<<1|1来做,但是其实大部分的空间是访问不到的,都会被浪费掉,而动态开点就是只使用要用的空间。对于每次传入的节点,如果这个节点目前不存在,就新建一个节点,并为其设置初值。

    这个是怎么实现的呢,我们想,对于一棵线段树,我们在上面进行查找的时候,主要是用给定区间l,r和要找的位置,去进行二分查找,而每次传入的节点只是一个节点编号,他们之间不一定要存在什么关系(比如p<<1之类的),只要每次都记录一下当前节点的左右儿子编号都是谁就可以,这就大大节省了空间。

    可能光说不好理解,上代码看一眼。

    struct node//使用结构体记录元素信息
    {
        ll lch, rch;
        ll cnt, id;
        node(ll _cnt = 0) : lch(0), rch(0), cnt(_cnt), id(0) { }//构造函数,对于每个新创的节点都赋初值
    }pool[10000000];
    
    if(!pool[u].lch)
    {
        pool[u].lch = ++pc,pool[pc] = node(mid - l + 1);
        pool[u].rch = ++pc,pool[pc] = node(r - mid);
    }//这是最主要的操作,其他都没什么。

    感觉好像上代码也没咋理解……?不管啦,一会看完整的。

    现在考虑怎么用线段树维护呢?我们观察后发现,能影响本次操作的,只有当前点,当前点所在行的最后一个点,和最后一列的最后一个点。所以我们直接建立n+1棵线段树,前n棵分别用于维护n行的前m-1个元素,最后一棵用于维护最后一列的n个元素。

    首先考虑n=1的比较简单的情况。对于每一个人,当其在列队中,其位置所对应的值为1,否则位置所对应的值为0.这样的话,我们在询问某一行的第y位是谁的时候,我们只要找在线段树上前缀和为y的那个点所存储的节点编号是多少就可以了。

    之后,当这个人出队的时候,我们将其位置所对应的值设为0,然后再把他重新插入到线段树的末尾。这样以后再次访问的时候,还是找前缀和为y的位置所对应的元素下表就可以了。(虽然会出现一些空间为空的状况,不过比起把他们全部前压可要容易得多)

    这是n=1的时候,之后我们在考虑正常的情况应该怎么做,其实和n=1的地方,线段树上的操作都很相似,只是操作次数多了一些。(如果有大佬有更简洁的操作请告知)

    大致是以下几步:

    (1)找到要出队的孩子的位置和编号,并记录

    (2)将出队的孩子的位置的属性全部清空

    (3)找到最后一棵线段树(维护第m列)中与第x行(当前行)对应的孩子,即列队中第x行末尾的孩子的位置和编号,并记录

    (4)把这个孩子压入这一行所对应的线段树中

    (5)在最后一棵线段树上清空这个孩子的属性信息

    (6)把刚刚出队的孩子压入最后一棵线段树的末尾

    上面是,对于不是最后一列的元素的情况。否则的话更简单,因为只涉及到一棵线段树的操作。直接仿照n=1的情况就可以了。

    注意一个是每次返回的节点编号有一些小小的改动。(代码里有注释)还有就是这题其实有可能节点编号爆int,开一下longlong吧。

    这样不断地模拟,每次时间,空间复杂度都是log的,一波nlogn带走。

    不过此题有很大的bug,就是你在动态开点的时候,到底应该怎么计算这个点他的权值到底是多少。

    这棵线段树一开始应该是像这样的一个模型:111111111111111111111……000000000000000000

    不过我们完全可以把后面的0都变为1,因为你并不会访问到那里。所以直接给新节点的权值赋为当前访问区间长度即可。不过这样的问题就是,你在把一个孩纸压入这一行所对应的线段树中的时候,你不能简单的使用m+i,而是要再开一个 变量记录一下当前加到哪里了。

    这样就可以A了……(感谢肖大佬debug)

    上代码看一下。

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<queue>
    #define rep(i,a,n) for(int i = a;i <= n;i++)
    #define per(i,n,a) for(int i = n;i >= a;i--)
    #define enter putchar('
    ')
    using namespace std;
    typedef long long ll;
    const ll INF = 1e9;
    const int M = 300005;
    ll read()
    {
        ll ans = 0,op = 1;
        char ch = getchar();
        while(ch < '0' || ch > '9')
        {
            if(ch == '-') op = -1;
            ch = getchar();
        }
        while(ch >= '0' && ch <= '9')
        {
            ans *= 10;
            ans += ch - '0';
            ch = getchar();
        }
        return ans * op;
    }
    struct node
    {
        ll lch, rch;
        ll cnt, id;
        node(ll _cnt = 0) : lch(0), rch(0), cnt(_cnt), id(0) { }
    }pool[10000000];
    
    ll pc, n, m, q, x, y,root[M],res,cur,used[M];
    
    ll query(ll u, ll l, ll r, ll k, bool pd)
    {
        if (l == r)
        {
            if(pd) res = pool[u].id == 0 ? (x-1) * m + l : pool[u].id;//res记录出来的元素的编号 
            //如果 当前没有修改的话,直接返回其下标即可 
            else res = pool[u].id == 0 ? l * m : pool[u].id;
            return l;
        }
        ll mid = (l + r) >> 1;
        if(!pool[u].lch)
        {
            pool[u].lch = ++pc;
            pool[pc] = node(mid - l + 1);//注意这个地方!!!
            pool[u].rch = ++pc;
            pool[pc] = node(r - mid);
        }
        if (k <= pool[pool[u].lch].cnt) return query(pool[u].lch, l, mid, k, pd);//如果你要找的值小于等于其右子树的权值和,就在右子树中找
        else return query(pool[u].rch, mid + 1, r, k - pool[pool[u].lch].cnt, pd);//否则在左子树中找,线段树二分的操作
    }
    
    void modify(ll u, ll l, ll r, ll p, ll num, ll id)
    {
        if(l == r)
        {
            pool[u].cnt = num,pool[u].id = id;//把这个节点的权值和所对应的孩子编号修改
            return;
        }
        ll mid = (l + r) >> 1;
        if(!pool[u].lch)
        {
            pool[u].lch = ++pc,pool[pc] = node(mid - l + 1);
            pool[u].rch = ++pc,pool[pc] = node(r - mid);
        }
        if(p <= mid) modify(pool[u].lch, l, mid, p, num, id);
        else modify(pool[u].rch, mid + 1, r, p, num, id);
        pool[u].cnt = pool[pool[u].lch].cnt + pool[pool[u].rch].cnt;
    }//常规的线段树修改(+动态开点)
    
    int main()
    {
        n = read(),m = read(),q = read();
        rep(i,1,n) root[i] = ++pc,pool[root[i]] = node(m-1);
        root[n+1] = ++pc,pool[root[n+1]] = node(n);//先把每棵树的根节点都建出来,并且赋权值。
        rep(i,1,q)
        {
            x = read(),y = read();
            if(y <= m - 1)//如果没访问最后一列的元素
            {
                ll p = query(root[x], 1, m + q, y, 1);//先找到出队的孩子的位置和编号 
                cur = res;//cur用于记录出队的孩子 
                modify(root[x], 1, m + q, p, 0, 0);//将出队的地方都设为0 
                p = query(root[n+1], 1, n + q, x, 0);//找到最后一列中与本行相对的孩子的编号
                modify(root[x], 1, m + q, m + used[x]++, 1, res);//将这个孩子的编号压到本行线段树中
                modify(root[n+1], 1, n + q, p, 0, 0);//把压到本行线段树中的孩子的地方都设为0 
                modify(root[n+1], 1, n + q, n + i, 1, cur);//最后再把出队的孩子压回来 
                printf("%lld
    ", cur);
            }
            else//否则
            {
                ll p = query(root[n+1], 1, n + q, x, 0);//找到出队孩子的编号 
                modify(root[n+1], 1, n + q, p, 0, 0);
                modify(root[n+1], 1, n + q, n + i, 1, res);//之后把这个孩子压回来 
                printf("%lld
    ", res);
            }
        }
        return 0;
    }
  • 相关阅读:
    Python笔记_第二篇_面向过程_第二部分_4.常用模块的简单使用_import语句的解释
    Python笔记_第二篇_面向过程_第二部分_4.常用模块的简单使用_系统模块(sys)
    Python笔记_第二篇_面向过程_第二部分_4.常用模块的简单使用_time模块、datetime模块和calendar模块
    Python笔记_第二篇_面向过程_第二部分_4.常用模块的简单使用_语音合成模块(win32con)和语音控制模块(win32con)
    Python笔记_第二篇_面向过程_第二部分_4.常用模块的简单使用_窗体控制模块(win32con、win32gui)
    Python笔记_第二篇_面向过程_第二部分_4.常用模块的简单使用_操作系统模块(os)和队列模块(collections)
    python抓取百度百科点赞数等动态数据
    python 最长公共子序列
    中文unicode范围及unicode编解码
    python爬取并计算成绩
  • 原文地址:https://www.cnblogs.com/captain1/p/9427997.html
Copyright © 2020-2023  润新知