NOIP2017 列隊——動態開點線段樹


Description:

Sylvia 是一個熱愛學習的女♂孩子。

前段時間,Sylvia 參加了學校的軍訓。眾所周知,軍訓的時候需要站方陣。

Sylvia 所在的方陣中有n×m名學生,方陣的行數為 n,列數為 m。

為了便於管理,教官在訓練開始時,按照從前到后,從左到右的順序給方陣中 的學生從 1 到 n×m 編上了號碼(參見后面的樣例)。即:初始時,第 i 行第 j 列 的學生的編號是(i1)×m+j。

然而在練習方陣的時候,經常會有學生因為各種各樣的事情需要離隊。在一天 中,一共發生了 q件這樣的離隊事件。每一次離隊事件可以用數對(x,y)(1xn,1ym)描述,表示第 x 行第 y 列的學生離隊。

在有學生離隊后,隊伍中出現了一個空位。為了隊伍的整齊,教官會依次下達 這樣的兩條指令:

  1. 向左看齊。這時第一列保持不動,所有學生向左填補空缺。不難發現在這條 指令之后,空位在第 x 行第 m 列。

  2. 向前看齊。這時第一行保持不動,所有學生向前填補空缺。不難發現在這條 指令之后,空位在第 n 行第 m 列。

教官規定不能有兩個或更多學生同時離隊。即在前一個離隊的學生歸隊之后, 下一個學生才能離隊。因此在每一個離隊的學生要歸隊時,隊伍中有且僅有第 n 行 第 m 列一個空位,這時這個學生會自然地填補到這個位置。

因為站方陣真的很無聊,所以 Sylvia 想要計算每一次離隊事件中,離隊的同學 的編號是多少。

注意:每一個同學的編號不會隨着離隊事件的發生而改變,在發生離隊事件后 方陣中同學的編號可能是亂序的。

Hint

Solution

一年前暴力敲了30pts

一年后暴力敲了60pts

沒什么長進啊

還是不會正解。

 

1.不懂樹狀數組

2.不想寫平衡樹

所以我們寫動態開點線段樹

 

首先發現,對於x=1的點,可以想到對這個鏈開一棵長度為max(n,m)+q的線段樹。每次找第k個有數的地方,然后放到最后的位置。

發現,每次向前對齊只有最后一列要動,

向左看齊,只是當前的行會向左移動。

所以,為了便於操作,我們開n+1棵線段樹,前n棵維護i行,1~m-1的答案

最后一棵n+1,維護最后一列n個答案。

 

然后我們就得到了一個優秀的MLE做法辣!~~

所以就要動態開點線段樹。

 

(因為我比較弱)所以簡單講解一下動態開點線段樹。

發現,有的時候,線段樹需要維護的區間很大很大,但是實際用到的節點很少很少。

那么,我們干脆就不要開這么多的節點,用到的時候再向內存要。

也就是說,我們建立了一棵殘疾的線段樹,缺少很多枝葉,但是絕對夠用了。

畫個圖大概理解一下(雖然也不太對)

實心邊框的點都是我們申請內存給的,虛的點是沒用的。就算申請也不用,實在是浪費資源。

所以,

我們開局只有一個根,裝備葉子全靠給。

例如我們要建立一個權值線段樹,但是在線操作不讓你離散化,值域又是inf級別的,

像這樣,即使這個區間的范圍很大,但是如果詢問q比較少的話,我們只需要qloginf個節點,就可以辦到。

 

(發現和主席樹有點像,但是省空間的思想還是有些不同的。)

 

然后我們用動態開點線段樹來做這個題。

線段樹根節點維護的區間是max(n,m)+q;

開始每個線段樹甚至連根也不用建,需要的時候會建起來。

每個線段樹節點記錄sz,子樹實際的人數大小。(開始的時候,只有1~n(m-1)是sz=r-l+1的)

sz可以用一個函數處理。雖然並沒有這么多的葉子,但是實際上,建出這么多的葉子,也是這個sz(這也是能動態開點的條件)

再記錄一個val(long long型需注意),記錄當前節點所代表的人的編號

這個編號val只有在葉子節點才有用。

 

其實每次詢問引起的變化是:樹x的第y個人走了,進入了樹n+1的末尾,樹n+1的第x走了,進入樹x的末尾。

每次詢問,如果y==m就進入線段樹n+1查詢,否則進入線段樹x查詢,找到答案ans輸出

查詢的時候,順便sz--,刪掉途經點的sz(就不用pushup了)

把ans這個編號放進n+1線段樹的末尾(新開一個位置)

同樣,途經sz++

如果y!=m說明,第x棵線段樹最后進來一個人。就把n+1的第x個人查詢(刪除),放進線段樹x的末尾(新開一個位置)。

這樣子,其實每棵線段樹根節點的sz都保持為m-1(或n)

 

Code

#include<bits/stdc++.h>
#define mid ((l+r)>>1)
using namespace std;
typedef long long ll;
const int N=3e5+5;
const int M=1e7+2;
ll n,m,q;
struct node{
    int ls,rs;
    int sz;
    ll val;
}t[M];
int id,tot;
int rt[N];
ll now;
int cur[N];
int up;
int get(int l,int r){
    if(now==n+1){
        if(r<=n) return r-l+1;
        if(l<=n) return n-l+1;
        return 0;
    }
    if(r<m) return r-l+1;
    if(l<m) return m-l;
    return 0;
}
ll query(int &x,int l,int r,int c){
    if(!x){
        x=++tot;
        t[x].sz=get(l,r);
        if(l==r){
            if(now==n+1) t[x].val=l*m;
            else t[x].val=(now-1)*m+l;
        }
    }
    t[x].sz--;
    if(l==r) return t[x].val;
    if((!t[x].ls&&c<=get(l,mid))||c<=t[t[x].ls].sz) return query(t[x].ls,l,mid,c);
    else{
        if(!t[x].ls) c-=get(l,mid);
        else c-=t[t[x].ls].sz;
        return query(t[x].rs,mid+1,r,c);
    }
}
void upda(int &x,int l,int r,int to,ll d){
    if(!x){
        x=++tot;
        t[x].sz=get(l,r);
        if(l==r){
            t[x].val=d;
        }
    }
    t[x].sz++;
    if(l==r) return;
    if(to<=mid) return upda(t[x].ls,l,mid,to,d);
    else return upda(t[x].rs,mid+1,r,to,d);
}
int main()
{
    scanf("%lld%lld%lld",&n,&m,&q);
    int x,y;
    ll ans;
    up=max(n,m)+q;
    while(q--){
        scanf("%d%d",&x,&y);
        if(y==m) now=n+1,ans=query(rt[now],1,up,x);
        else now=x,ans=query(rt[now],1,up,y);
        printf("%lld\n",ans);
        
        now=n+1;
        upda(rt[now],1,up,n+(++cur[now]),ans);
        if(y!=m){
            now=n+1;
            ans=query(rt[now],1,up,x);
            now=x;
            upda(rt[now],1,up,m-1+(++cur[now]),ans);
        }
    }
    return 0;
}

 

 

upda:2018.11.2

感覺這個動態開點線段樹其實不算是典型的動態開點23333

一般的線段樹都是區間表示連續一些下標之類。動態開點也是如此。

但是這個做法的話,愣是把線段樹寫成了平衡樹的存儲方式。

區間的長度僅僅代表的是預留空間。

就是把許多點壓成了一個點。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM