1. 程式人生 > >動態點分治入門 ZJOI2007 捉迷藏

動態點分治入門 ZJOI2007 捉迷藏

問題 完全 發現 最長 代碼 push rmq 註意 真的

傳送門

這道題好神奇啊……如果要是不帶修改的話那就是普通的點分治了,每次維護子樹中距離次大值和最大值去更新。

不過這題要修改,而且還改500000次,總不能每改一次都點分治一次吧。

所以我們來認識一個新東西:帶修改的點分治,動態點分治!

它可以強勢解決帶修改點分治問題(但是這玩意真的太難了我這個菜雞只能學到入門)

首先我們要建立一棵樹(點分樹),這棵樹是由點分治每次所分治的所有子樹的重心串起來的。為什麽要這麽做呢?因為對於每次的修改,其實並沒有影響到特別多的結果,它只會影響它自己所在的子樹的重心,以及這個所對應重心在點分樹上的祖先。

為什麽可以這樣做?因為我們考慮到,點分治中,每一個重心其實只會維護自己的子樹中的情況,其他的情況於這個重心是不相幹的。所以其實對於一次修改,它能影響的就是它所在子樹重心的答案(這個是顯然的),然後對於這個重心,它所在的樹必然是它自己在點分樹上的父親(也就是一棵更大的樹的重心,當前樹是其子集)的一棵小子樹,所以也會對之產生影響,然後再往上遞歸也是同理,這樣的話,我們使用點分樹維護,每次就只需要更改log個節點。

建立點分樹怎麽建?聽起來或許很難但實際上特別簡單,因為我們本身就是遞歸訪問的,每次在遞歸求完子樹重心的時候只要記錄一下它當前的父親是誰(就是當前的重心)就可以了。

先看一下代碼。

void solve(int x)
{
    vis[x] = 1;
    for(int i = head[x];i;i = e[i].next)
    {
        int t = e[i].to;
        if(vis[t]) continue;
        sum = sz[t],G = 0;
        getG(t,x),fq[G] = x;solve(G);
    }
}

其中建樹的就是fq[G] = x那一行……是不是非常好做?

然後假設我們現在建完了這棵點分樹(也就是我們遍歷了整棵樹),之後對於無窮無盡的修改操作我們該咋辦呢……

我們還是想剛才那個事,如果要是不修改的話,我們只要求出來當前重心所有子樹中的最長和次長距離來更新答案就可以,雖然現在帶上修改了,但是我們計算答案的方法是並不會變的!

所以我們發現了這幾件事:

1.我們可以對於每一個重心(點分樹上的每一個節點)都開一個堆來維護當前子樹中的最大值(C堆)

2.對於每一個重心,再開一個堆,記錄它所有子樹中的距離最大值和次大值(也就是上面C堆中兩個最大的堆頂元素)(B堆)

3.開一個全局的堆,記錄所有重心的距離最大值和次大值(也就是B堆中兩個最大的堆頂元素)(A堆)

這樣我們就可以進行維護了!每次修改是log的,用堆維護也是log的,總復雜度是log^2的。

說的如此輕描淡寫該咋做呀……(這玩意簡直不是一般難寫,而且還賊難理解)

我們不選擇使用set而是開一個結構體,裏面有兩個堆,其中一個用來存有用的狀態一個存無用狀態。(啥叫有用無用狀態?)我們直接用set查找元素進行刪除其實比較慢,我們可以這樣做,每次把要刪除的狀態存到另一個堆裏面,等真正要進行刪除操作的時候,我們再將其刪除。

是不是感覺沒聽懂?對其實我也不大懂。

大致意思就是,因為一些修改操作使得一些狀態變得不合法,我們不用什麽find函數之類的直接給他刪了,而是加到無用狀態裏,只有在堆頂是無用狀態的時候我們把其刪除即可。

好。之後我們先考慮把白點變黑點的操作(我們姑且稱之為“關燈操作”)

我們每次求的時候,首先更新一下當前節點的B堆,把一個0的情況加進去(因為你相當於沒走嘛),之後如果當前的B的大小是2,也就說明有了最大和次大值,我們就向A堆裏面添加一下這個值。

之後我們先計算一下當前重心的在點分樹上的父親和這點的距離。因為你是把當前點變成了黑點,所以這個點的答案必然是合法的,我們把其壓入當前的C堆中。之後,如果這個值大於當前C堆中的最大值,那說明我們這次修改對這個範圍的答案是有影響的,肯定是要進行一次修改了。那我們先統計這個重心的父親的B堆中的最大和次大值,之後把堆頂元素彈出(因為當前這個值已經更大了說明它沒用了),把這個更大的答案加進去。之後再次計算當前重心的父親的B堆中的最大和次大值,如果要是比原來大,並且B中有兩個以上的元素(說明有最大值和次大值,只有一個是不能刪除的,因為舊的答案可以成為次大值),那麽我們在A堆中把這個答案刪除,再把新答案添加進去即可。註意添加答案的前提是,B堆中至少也有兩個元素,這樣才保證有了最大值和次大值。才可以更新。

之後我們重復上述操作,向上遞歸更新。這樣關燈操作就完成了。

與之對應的是開燈操作。

開燈操作大部分都是相對應的,因為開燈之後,我們的答案將變得不合法,所以我們需要刪去這些答案。

這裏的操作只有在你當前的答案就是堆頂元素的時候才會去更新。更新的方法和上面都是對應且相反的,直接看代碼即可。

然後最後一個問題是,如何O(1)求出樹上兩點之間的距離。這個可以使用dfs序,st表轉化為RMQ問題解決(這個要好好復習了)

所以我們總結一下做這題的步驟。

1.先手點分治,把點分樹建出來同時遍歷每棵樹,確定初始的最長距離。

2.初始化關於RMQ的一些數組和函數

3.建立三個堆,每個堆裏面再用兩個堆去模擬set進行維護

4.把所有的點全部關一次燈。

5.開始修改,每次修改對應上面的開,關燈操作,每次輸出結果即可。

最後還有啥不懂的看一下代碼,要是還不懂我們慢慢來(其實我也沒完全理解,還是慢慢來)

// luogu-judger-enable-o2//這題不開O2的話會T……
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<iostream>
#include<queue>
#include<set>
#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(‘\n‘)

using namespace std;
typedef long long ll;
const int M = 200005;
const int INF = 1e9+7;

int read()
{
    int 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;
}

int n,m,G,ecnt,sum,fq[M],dep[M],maxs[M],sz[M],head[M],x,y;
int anc[20][M<<1],tot1,bin[20],Log[M],dfn,num[M];
bool col[M],vis[M];

struct edge
{
    int next,to;    
}e[M<<1];

void add(int x,int y)
{
    e[++ecnt].to = y;
    e[ecnt].next = head[x];
    head[x] = ecnt;
}
struct heap
{
    priority_queue<int> A,B;//優先隊列模擬堆
    void push(int x)//添加一個狀態
    {
        A.push(x);
    }
    void erase(int x) //刪除狀態
    {
        B.push(x);
    }
    void pop()//把A堆的堆頂元素彈出
    {
        while(B.size() && (A.top() == B.top())) A.pop(),B.pop();
        A.pop();
    }
    int top()//求A堆堆頂元素
    {
        while(B.size() && (A.top() == B.top())) A.pop(),B.pop();
        if(!A.size()) return 0;
        return A.top();
    }
    int size()//返回當前狀態數 = 總狀態-無用狀態
    {
        return A.size() - B.size();
    }
    int stop()//求A堆次大元素
    {
        if(size() < 2) return 0;
        int x = top();pop();
        int y = top();push(x);
        return y;
    }
}A,B[150000],C[150000];

void init()//這個是處理2的冪和每個數對應的log值,RMQ初始化
{
    bin[0] = 1;rep(i,1,19) bin[i] = bin[i-1] << 1;
    Log[0] = -1;rep(i,1,200000) Log[i] = Log[i>>1] + 1;
}

void dfs(int x,int fa)//同樣是RMQ初始化,記錄dfs序
{
    anc[0][++dfn] = dep[x],num[x] = dfn;    
    for(int i = head[x];i;i = e[i].next)
    {
        int t = e[i].to;
        if(t == fa) continue;
        dep[t] = dep[x] + 1;
        dfs(t,x);
        anc[0][++dfn] = dep[x];
    }
}

void ST()//ST表操作
{
    rep(i,1,Log[dfn])
    rep(j,1,dfn) 
    if(j + bin[i] - 1 <= dfn) 
    anc[i][j] = min(anc[i-1][j],anc[i-1][j + bin[i-1]]);
}

int RMQ(int x,int y)//真實RMQ
{
    x = num[x],y = num[y];
    if(x > y) swap(x,y);
    int t = Log[y-x+1];
    return min(anc[t][x],anc[t][y-bin[t]+1]);
}

int dis(int x,int y)//計算兩點之間距離!
{
    return dep[x] + dep[y] - 2 * RMQ(x,y);
}

void getG(int x,int fa)//找重心
{
    sz[x] = 1,maxs[x] = 0;
    for(int i = head[x];i;i = e[i].next)
    {
        int t = e[i].to;
        if(t == fa || vis[t]) continue;
        getG(t,x);
        sz[x] += sz[t];
        maxs[x] = max(maxs[x],sz[t]);
    }
    maxs[x] = max(maxs[x],sum - sz[x]);
    if(maxs[x] < maxs[G]) G = x;
}

void solve(int x)//點分治+建立點分樹
{
    vis[x] = 1;
    for(int i = head[x];i;i = e[i].next)
    {
        int t = e[i].to;
        if(vis[t]) continue;
        sum = sz[t],G = 0;
        getG(t,x),fq[G] = x;solve(G);//在這裏建立點分樹
    }
}

void turnoff(int x,int v)//關燈
{
    if(x == v)//第一個節點
    {
        B[x].push(0);
        if(B[x].size() == 2) A.push(B[x].top());//有最大和次大即更新
    }
    if(!fq[x]) return;
    int f = fq[x],D = dis(f,v),tmp = C[x].top();//計算當前兩點間距離和這個點C堆的最大值
    C[x].push(D);//把合法答案壓入
    if(D > tmp)//如果這個值更優,說明修改產生了影響,要更新
    {
        int maxn = B[f].top() + B[f].stop(),size = B[f].size();//計算當前最大值(最大和次大更新)和B的大小
        if(tmp) B[f].erase(tmp);//把這個無用的刪了
        B[f].push(D);//把有用的加進來
        int cur = B[f].top() + B[f].stop();//重計算一下答案
        if(cur > maxn)//如果新答案更優
        {
            if(size >= 2) A.erase(maxn);//把這個無用的刪了
            if(B[f].size() >= 2) A.push(cur);//把這個新的加進來
        }
    }
    turnoff(f,v);//繼續向上遞歸關燈
}

void turnon(int x,int v)//開燈
{
    if(x == v)
    {
        if(B[x].size() == 2) A.erase(B[x].top());
        B[x].erase(0);//和上面是正好相反的
    }
    if(!fq[x]) return;
    int f = fq[x],D = dis(f,v),tmp = C[x].top();
    C[x].erase(D);//把這個答案給刪了(不合法)
    if(D == tmp)//如果這個答案=堆頂元素,說明這次修改產生了影響
    {
        int maxn = B[f].top() + B[f].stop(),size = B[f].size();
        B[f].erase(D);
        if(C[x].top()) B[x].push(C[x].top());
        int cur = B[f].top() + B[f].stop();
        if(cur < maxn)//這些和上面都是相同的操作了,註意這次變成了小於
        {
            if(size >= 2) A.erase(maxn);
            if(B[f].size() >= 2) A.push(cur);
        }
    }
    turnon(f,v);//遞歸向上開燈
}

int main()
{
    init();n = read();
    rep(i,1,n-1) x = read(),y = read(),add(x,y),add(y,x);
    dfs(1,0),ST();//前面都預處理出來
    sum = n;maxs[G] = INF;
    getG(1,0);
    fq[G] = 0,solve(G);//找到重心開始建立點分樹
    rep(i,1,n) col[i] = 1,turnoff(i,i),tot1++;//把每個點都關燈
    m = read();
    while(m--)//開始修改
    {
        char c = getchar();
        if(c == G) 
        {
            if(tot1 <= 1) printf("%d\n",tot1-1);//要是只有一個點那就是0,要是沒有直接輸出-1,tot1記錄當前黑點數
            else printf("%d\n",A.top());//否則輸出最大值
        }
        else
        {
            x = read();
            if(col[x]) turnon(x,x),tot1--;//開燈
            else turnoff(x,x),tot1++;//關燈
            col[x] ^= 1;//轉變開關燈情況
        }
    }
    return 0;
}

動態點分治入門 ZJOI2007 捉迷藏