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;
}
原文地址:https://www.cnblogs.com/captain1/p/9427997.html