1. 程式人生 > >洛谷 P3835 【模板】可持久化平衡樹

洛谷 P3835 【模板】可持久化平衡樹

正常 發生 using 我沒 持久化 pro 現在 class 小數

這個題也是可以用可持久化線段樹來解決的。

值域線段樹(也有的叫權值線段樹)可以用來維護一個可重集,並實現一些一般情況下平衡樹才能實現的事情。

如果用值來當做區間左右端點,每個葉子節點上存某個值出現的次數,非葉子節點上存一定範圍內的值出現的總次數,就可以建成值域線段樹。可以在上面直接查詢第k大值、小於某值的數的個數等等,具體請百度或參見代碼。

如何將線段樹可持久化呢?線段樹在單點更新的時候會經過log n個節點,每一次更新時顯然也只有這麽多節點會發生變化。

記錄每一個版本的線段樹的根節點,每一次操作前將根節點賦為與這次操作基於的版本的根節點相同。在更新操作時,備份每一個經過的節點(包括各個屬性:左、右子樹以及區間和),然後再進行修改。具體也可以參考可持久化線段樹的題解。

如果直接用可持久化的值域線段樹,顯然空間是不夠的(4*2e9個節點啊...)。現在有兩種選擇:

1.發現這道題沒有加、減操作,所有操作涉及的值都是確定的。因此可以進行離散化,然後再做,想必可以A掉吧(我沒試過)

2.可以寫動態開點線段樹。題目要求的集合一開始是空的,因此如果一開始建一棵完整的線段樹的話,每一個節點記錄的區間和都是0。而總共只有5e5次操作,每一次操作涉及更改節點最多有log2(2e9)=31個,兩者乘起來遠遠小於4*2e9。

可以考慮一開始不真正建樹。規定:如果某節點的某個子節點是一個特殊的標記的話,表明以這個子節點為根的子樹還沒有實際建出來。顯然,一個子樹沒有實際建出來的時候,其表示的區間的和為0。(以下代碼中我用的標記是0)

在進行修改操作的時候,可能需要建出來要走入的那個子節點。在進行查詢操作的時候,可以把未建出的子節點的區間和當做0。

附:寫完後我發現前驅和後繼竟然是最難寫的...

附:註意各種對不存在的節點的查詢/要忽略的操作

附:註意代碼中有一些操作用到的變量被設置成了全局變量,還define了一個mid,表示區間中點,可能比較奇怪...

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<bits/stdc++.h>
#define mid ((l+r)>>1)
#define inf 2147483647
using namespace std;
int lc[20000000],rc[20000000],root[20000000],dat[20000000],ll=-1e9,rr=1e9;
//lc[i]、rc[i]分別表示節點i的左子節點、右子節點編號,如果lc[x]=0則表示x的左子樹尚未建出來,rc[x]同理
//dat[i]表示節點i表示的值域區間中各數的出現次數之和
int n,L,x,mem=1;//因為0號被"未實際建出的節點"的特殊標記占用了,1號被版本0的根節點占用了

void addx(int l,int r,int& num)//更新操作,將線段樹維護的集合中數L的出現次數加上x(x為1或-1)
{
    int t=num;num=++mem;lc[num]=lc[t];rc[num]=rc[t];dat[num]=dat[t];//備份當前節點,如果當前節點原來為空則也可以完成
    if(l==r)
    {
        if(!(dat[num]==0&&x<0)) dat[num]+=x;//如果L出現次數為0且操作為刪除操作,則忽略操作
        return;
    }
    if(L<=mid)  addx(l,mid,lc[num]);
    else    addx(mid+1,r,rc[num]);
    dat[num]=0;
    if(lc[num]) dat[num]+=dat[lc[num]];
    if(rc[num]) dat[num]+=dat[rc[num]];//維護當前節點信息
}
int query(int l,int r,int num)//查詢集合中小於x的數的個數
{
    if(l==r)    return 0;//如果已經到葉子節點了,那麽當前節點等於x,顯然不小於x
    if(!num)    return 0;//如果當前節點為空,那麽該節點表示的子樹中數都沒有,自然返回0
    if(x<=mid)  return query(l,mid,lc[num]);//根據x決定向左/右子樹走
    else    return (lc[num]?dat[lc[num]]:0)+query(mid+1,r,rc[num]);
}
int query_kth(int l,int r,int k,int num)//查詢第k小數
{
    assert(num!=0);//assert(x)表示如果x為false則停止程序,是用來調試的。如果查詢操作是正常進行的,那麽不可能走到未建出的點中
    if(l==r)    {return l;}
    //if(!num)  return 0;//沒有用
    int ls=lc[num]?dat[lc[num]]:0;
    if(ls>=k)   return query_kth(l,mid,k,lc[num]);//根據左子樹中數出現總次數決定向左/右子樹走
    else        return query_kth(mid+1,r,k-ls,rc[num]);
}
int query_time(int l,int r,int num)//查詢數x出現的次數
{
    while(l!=r)
    {
        if(!num)    return 0;//當前節點未建出,表明其子節點均未出現
        if(L<=mid)  r=mid,num=lc[num];
        else    l=mid+1,num=rc[num];
    }
    return dat[num];
}
int query_pre(int l,int r,int num)//查詢數x的前驅
{
    int t=query(l,r,num);//t是集合中比x小的數的個數
    if(t==0)    return -inf;//如果集合中比x小的數有0個,則x是集合中最小的數,不存在前驅
    return query_kth(l,r,t,num);//否則查詢集合中第t小即可
}
int query_nxt(int l,int r,int num)
{
    int t1=query(l,r,num),t2=query_time(l,r,num);//t1是集合中比x小的數的個數,t2是集合中x出現的次數,加起來是集合中小於等於x的數的個數
    x=inf;int t3=query(l,r,num);
    if(t1+t2==t3)   return inf;//如果集合中小於等於x的數與集合中小於等於inf的數相等,則x是集合中最大的數,不存在後繼
    return query_kth(l,r,t1+t2+1,num);//否則查詢集合中第t1+t2+1小即可
}
int main()
{
    int i,v,idx;
    scanf("%d",&n);
    root[0]=1;
    for(i=1;i<=n;i++)
    {
        scanf("%d%d%d",&v,&idx,&x);root[i]=root[v];
        if(idx==1)
        {
            L=x;x=1;
            addx(ll,rr,root[i]);
        }
        else if(idx==2)
        {
            L=x;x=-1;
            addx(ll,rr,root[i]);
        }
        else if(idx==3)
        {
            printf("%d\n",query(ll,rr,root[i])+1);
        }
        else if(idx==4)
        {
            printf("%d\n",query_kth(ll,rr,x,root[i]));
        }
        else if(idx==5)
        {
            L=x;
            printf("%d\n",query_pre(ll,rr,root[i]));
        }
        else if(idx==6)
        {
            L=x;
            printf("%d\n",query_nxt(ll,rr,root[i]));
        }
    }
    return 0;
}

洛谷 P3835 【模板】可持久化平衡樹