1. 程式人生 > >NOIP2016(D1T2)天天愛跑步題解

NOIP2016(D1T2)天天愛跑步題解

swa 統計 滿足 有效 const 獲取 dfs 內部 得出

首先聲明這不是一篇算法獨特的題解,仍然是“LCA+桶+樹上差分”,但這篇題解是為了讓很多很多看了很多題解仍然看不懂的朋友們看懂的,其中就包括我,我也在努力地把解題的“思維過程”呈現出來,希望能幫助到別人。實在是佩服那些考場AC的大牛,再次向你們獻上敬意!

1. 第一步
  • 首先可以初步判斷這個題肯定要計算LCA,方法有倍增/Tarjan-DFS,我們就寫個簡單的倍增吧,使用鏈式前向星存儲邊。
  • 選擇1號結點開始dfs,別的結點也可以
  • dfs過程中計算fa[][]數組(fa[x][i]表示 \(x\) 結點的 \(2^i\) 代祖先是誰)和deep[]數組(deep[x]表示結點 \(x\) 在樹中的深度)
#include<bits/stdc++.h>
using namespace std;
const int SIZE=300000;
int n, m, tot, h[SIZE], deep[SIZE], fa[SIZE][20], w[SIZE];      //w[i]表示i結點出現觀察員的時間
struct edge
{
    int to, next;
}E[SIZE*2], e1[SIZE*2], e2[SIZE*2];                             //邊集數組e1,e2留待備用

void add(int x, int y)                                          //加邊函數
{
    E[++tot].to=y;
    E[tot].next=h[x];
    h[x]=tot;
}

void dfs1(int x)                                                //dfs的過程中完成“建樹”,預處理fa[][]數組, 計算deep[]數組
{
    for(int i=1; (1<<i)<=deep[x]; i++)
        fa[x][i]=fa[fa[x][i-1]][i-1];                           //x的2^i代祖宗就是x的2^{i-1}代祖宗的2^{i-1}代祖宗
    for(int i=h[x]; i; i=E[i].next)
    {
        int y=E[i].to;
        if(y==fa[x][0]) continue;                               //如果y是父結點,跳過
        fa[y][0]=x;
        deep[y]=deep[x]+1;
        dfs1(y);
    }
}

int get_lca(int x, int y)                                      //計算x和y的最近公共祖先
{
    if(x==y) return x;                                         //沒有這一行,遇到 lca(x, x) 這樣的詢問時會掛掉
    if(deep[x]<deep[y]) swap(x, y);                            //保持x的深度大於y的深度
    int t=log(deep[x]-deep[y])/log(2);
    for(int i=t; i>=0; i--)                                    //x向上跳到和y同樣的深度
    {
        if(deep[fa[x][i]]>=deep[y])
            x=fa[x][i];
        if(x==y)
            return x;
    }
    t=log(deep[x])/log(2);
    for(int i=t; i>=0; i--)                                    //x和y一起向上跳
    {
        if(fa[x][i]!=fa[y][i])
            x=fa[x][i], y=fa[y][i];
    }
    return fa[x][0];
}

int main()                                                     //先把主函數寫上一部分
{
    scanf("%d%d", &n, &m);
    for(int i=1; i<n; i++)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        add(u, v);
        add(v, u);
    }
    deep[1]=1;
    fa[1][0]=1;
    dfs1(1);
    for(int i=1; i<=n; i++) scanf("%d", &w[i]);
    
    /////////////////////////////////////////////////////////////
    ////////////////////////未完待續///////////////////////////
    /////////////////////////////////////////////////////////////
    
    return 0;
}
2. 第二步

大概分析一下,m個玩家對應m條路徑,有了起點和終點的 lca 後,如果我們模擬這個過程:

直覺

  • 從起點 \(S_i\) 跑到 \(LCA\) 在樹長得很勻稱的情況下為 \(O(lgn)\)
  • 從起點 \(LCA\) 跑到 \(T_i\) 在樹長得很勻稱的情況下為 \(O(lgn)\)
  • 因此,模擬一個玩家的跑步過程為 \(O(lgn)\),m個玩家為 \(O(mlgn)\)
  • 理想情況下是可行的,但現實就是不理想
  • 題目清楚告訴你,樹會退化成一條鏈,因此模擬一個過程變成 \(O(n)\),總的就是。。。\(O(mn)\),必掛無疑
  • 此法不是正解!

嘗試

  • 我們能不能改變模擬跑步的過程,從 \(O(n)\)
    優化到 \(O(lgn)\) 呢?思前想後不可能,有 \(n\) 個觀察員矗在那裏,你可以對哪個視而不見?
  • 路已走到盡頭

轉換

  • 這時候需要放大招,轉換思想!或許解決問題的思路壓根就不是一個玩家一個玩家模擬,而是整體處理呢?
  • 也就是說,我們不枚舉每個運動員而是枚舉每個觀察員i,看看哪些結點會為這個觀察員i做貢獻(剛好在\(w_i\)秒跑到他這兒)。
  • 枚舉觀察員的過程就是DFS整顆樹的過程,我們可以在 \(O(n)\) 內搞定!
  • 對於觀察員i,哪些人會為他做貢獻呢?

深入分析

  • 對於結點 \(P\), 如果他位於一條起點、終點分別為 \(s_i\)\(t_i\) 的跑步路徑上,如何判斷這名選手會不會為 \(P\) 作貢獻呢?
  • 分情況考慮
  • 如果 \(P\) 是在從 \(s_i\)\(LCA\) 的路上,如下圖:

技術分享圖片

  • 我們可以得出結論:當起點 $ s_i $ 滿足 $ deep[s_i]=w[P]+deep[P] $時,起點 \(s_i\)會為 \(P\) 觀察員做一個貢獻(運動員從\(s_i\)出發,可以被\(P\)處的觀察員在\(w[P]\)秒看到)

  • 如果 \(P\) 是在從 \(LCA\)\(t_i\) 的路上,如下圖:

技術分享圖片

  • 定義 \(dist[s_i, t_i]\)為從 \(s_i\)出發到\(t_i\)的路徑長度,如果運動員從\(s_i\)出發,可以被\(P\)處的觀察員在\(w[P]\)秒觀察到,可以由上圖得出以下式子:
  • \(dist[s_i, t_i]-w[P]=deep[t_i]-deep[P]\),移項後得到:
  • $ dist[s_i, t_i]-deep[t_i]=w[P]-deep[P] $
  • 我們可以得出結論:當終點 $ t_i $ 滿足 $ dist[s_i, t_i]-deep[t_i]=w[P]-deep[P] $時,終點 \(t_i\)會為 \(P\) 觀察員做一個貢獻
  • 做一個重要的總結:上行過程中,滿足條件的起點可以做貢獻,下行過程中,滿足條件的終點可以做貢獻,但無論是哪一種情形,能對 \(P\) 做貢獻的起點或終點一定都在以\(P\)為根的子樹上,這使得可以在DFS回溯的過程中處理以任意節點為根的子樹。
3. 第三步

如何統計子樹貢獻

  • 遞歸以\(P\)為根的子樹時,可以統計出其子樹中所有的起點和終點對它的貢獻
  • 這裏又需要轉換
  • 子樹中有的起點和終點對\(P\)產生了貢獻,有些不對其產生貢獻但對\(P\)以外的結點產生了貢獻
  • 所以我們不能枚舉每個點(子樹根),找子樹中哪些點對其產生貢獻,這樣復雜度就上去了
  • 而是對於樹上的任何一個起點和終點,把其產生的貢獻放在桶裏面,回溯到子樹根的時候再到桶裏面查詢結果
  • 有人產生疑問了,也是很多人看不懂這裏桶用法的地方,疑問如圖:

技術分享圖片

  • \(c\)點產生貢獻放在桶的\(deep[c]\)位置,計算\(b\)點獲得的貢獻時當然是從\(bucket1[deep[b]+w[b]]\)位置獲取,於是得到1個貢獻,你發現\(a\)結點也是用的同一個桶,這個還好,因為\(c\)確實給他做了貢獻,可是\(e\)點呢?他是不應該獲得貢獻的!既然我會給和我無關的結點做貢獻,那麽其它無關的結點難免也會給我做貢獻!
  • 問題總結一下,對於一個點\(P\)來說,究竟哪些點在桶裏面產生的貢獻才是有效的。
  • 答案是:\(P\)為根遞歸整顆子樹過程中在桶內產生的差值才是有效的

還要考慮一種情況

  • 先看圖:

技術分享圖片

  • 看懂了嗎?對於以\(P\)為根的內部路徑(不經過\(P\)),這條路徑的起點和終點產生的貢獻是不應該屬於\(P\)
  • 所以dfs過程中,在統計當前結點作為起點和終點所產生的貢獻後,繼而計算出當前結點作為“根”上的差值後,在回溯過程中,一定要減去以當前結點為\(LCA\)的起點、終點在桶裏產生的貢獻,這部分貢獻在離開這個子樹後就沒有意義了。

代碼說明

  • e1,tot1,h1,add1是使用鏈式前向星的方法存儲每個結點作為終點對應的路徑集合
  • e2,tot2,h2,add2是使用鏈式前向星的方法存儲每個結點作為LCA對應的路徑集合
  • b1,b2是兩組桶,分別用於上行階段下行階段的貢獻統計
  • js[SIZE]用於統計以每個結點作為起點的路徑條數
  • dist[SIZE], s[SIZE], t[SIZE]用於統計m條路徑對應的長度,起點和終點信息
  • ans[SIZE]存儲最後輸出的答案,是每個結點觀察員看到的人數
int tot1, tot2, h1[SIZE], h2[SIZE];
void add1(int x, int y)
{
    e1[++tot1].to=y;
    e1[tot1].next=h1[x];
    h1[x]=tot1;
}

void add2(int x, int y)
{
    e2[++tot2].to=y;
    e2[tot2].next=h2[x];
    h2[x]=tot2;
}

int b1[SIZE*2], b2[SIZE*2], js[SIZE], dist[SIZE], s[SIZE], t[SIZE], ans[SIZE];

void dfs2(int x)
{
    int t1=b1[w[x]+deep[x]], t2=b2[w[x]-deep[x]+SIZE];      //遞歸前先讀桶裏的數值,t1是上行桶裏的值,t2是下行桶的值
    for(int i=h[x]; i; i=E[i].next)                         //遞歸子樹
    {
        int y=E[i].to;
        if(y==fa[x][0]) continue;
        dfs2(y);
    }
    b1[deep[x]]+=js[x];                                     //上行過程中,當前點作為路徑起點產生貢獻,入桶
    for(int i=h1[x]; i; i=e1[i].next)                       //下行過程中,當前點作為路徑終點產生貢獻,入桶
    {
        int y=e1[i].to;
        b2[dist[y]-deep[t[y]]+SIZE]++;
    }
    ans[x]+=b1[w[x]+deep[x]]-t1+b2[w[x]-deep[x]+SIZE]-t2;   //計算上、下行桶內差值,累加到ans[x]裏面
    for(int i=h2[x]; i; i=e2[i].next)                       //回溯前清除以此結點為LCA的起點和終點在桶內產生的貢獻,它們已經無效了
    {
        int y=e2[i].to;
        b1[deep[s[y]]]--;                                   //清除起點產生的貢獻
        b2[dist[y]-deep[t[y]]+SIZE]--;                      //清除終點產生的貢獻
    }
}

int main()
{
////////////////重復部分跳過////////////
////////////////文末提供完整代碼////////
    for(int i=1; i<=m; i++)                                 //讀入m條詢問
    {
        scanf("%d%d", &s[i], &t[i]);
        int lca=get_lca(s[i], t[i]);                        //求LCA
        dist[i]=deep[s[i]]+deep[t[i]]-2*deep[lca]];         //計算路徑長度
        js[s[i]]++;                                         //統計以s[i]為起點路徑的條數,便於統計上行過程中該結點產生的貢獻
        add1(t[i], i);                                      //第i條路徑加入到以t[i]為終點的路徑集合中
        add2(lca, i);                                       //把每條路徑歸到對應的LCA集合中
        if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;        //見下面的解釋
    }
    dfs2(1);                                                //dfs吧!
    for(int i=1; i<=n; i++) printf("%d ", ans[i]); 
    return 0;
}

一些重要補充

  • 上述代碼中有一行未加解釋if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;
  • 考慮路徑是這樣的,如圖:

技術分享圖片

  • 這個圖可能不太好懂,意思是:
  • 如果路徑起點或終點剛好為LCA且LCA處是可觀察到運動員的,那麽我們在上行統計過程中和下行統計過程中都會對該LCA產生貢獻,這樣就重復計數一次!
  • 好在這種情況很容易發現,我們提前預測到,對相應的結點進行ans[x]--即可。

  • 此外,在使用第二個桶時,下標是w[x]-deep[x]會成為負數,所以使用第二個桶時,下標統一+SIZE,向右平移一段區間,防止下溢。

4. 結束

我不知道自己說清楚沒有,但願大家不要拍磚頭!下面是完整代碼

#include<bits/stdc++.h>
using namespace std;
const int SIZE=300000;
int n, m, tot, h[SIZE], deep[SIZE], fa[SIZE][20], w[SIZE];
struct edge
{
    int to, next;
}E[SIZE*2], e1[SIZE*2], e2[SIZE*2];

void add(int x, int y)
{
    E[++tot].to=y;
    E[tot].next=h[x];
    h[x]=tot;
}

int tot1, tot2, h1[SIZE], h2[SIZE];
void add1(int x, int y)
{
    e1[++tot1].to=y;
    e1[tot1].next=h1[x];
    h1[x]=tot1;
}
void add2(int x, int y)
{
    e2[++tot2].to=y;
    e2[tot2].next=h2[x];
    h2[x]=tot2;
}

void dfs1(int x)
{
    for(int i=1; (1<<i)<=deep[x]; i++)
        fa[x][i]=fa[fa[x][i-1]][i-1];
    for(int i=h[x]; i; i=E[i].next)
    {
        int y=E[i].to;
        if(y==fa[x][0]) continue;
        fa[y][0]=x;
        deep[y]=deep[x]+1;
        dfs1(y);
    }
}

int get_lca(int x, int y)
{
    if(x==y) return x;
    if(deep[x]<deep[y]) swap(x, y);
    int t=log(deep[x]-deep[y])/log(2);
    for(int i=t; i>=0; i--)
    {
        if(deep[fa[x][i]]>=deep[y])
            x=fa[x][i];
        if(x==y)
            return x;
    }
    t=log(deep[x])/log(2);
    for(int i=t; i>=0; i--)
    {
        if(fa[x][i]!=fa[y][i])
            x=fa[x][i], y=fa[y][i];
    }
    return fa[x][0];
}

int b1[SIZE*2], b2[SIZE*2], js[SIZE], dist[SIZE], s[SIZE], t[SIZE], l[SIZE], ans[SIZE];
void dfs2(int x)
{
    int t1=b1[w[x]+deep[x]], t2=b2[w[x]-deep[x]+SIZE];
    for(int i=h[x]; i; i=E[i].next)
    {
        int y=E[i].to;
        if(y==fa[x][0]) continue;
        dfs2(y);
    }
    b1[deep[x]]+=js[x];
    for(int i=h1[x]; i; i=e1[i].next)
    {
        int y=e1[i].to;
        b2[dist[y]-deep[t[y]]+SIZE]++;
    }
    ans[x]+=b1[w[x]+deep[x]]-t1+b2[w[x]-deep[x]+SIZE]-t2;
    for(int i=h2[x]; i; i=e2[i].next)
    {
        int y=e2[i].to;
        b1[deep[s[y]]]--;
        b2[dist[y]-deep[t[y]]+SIZE]--;
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i=1; i<n; i++)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        add(u, v);
        add(v, u);
    }
    deep[1]=1;
    fa[1][0]=1;
    dfs1(1);
    for(int i=1; i<=n; i++) scanf("%d", &w[i]);
    for(int i=1; i<=m; i++)
    {
        scanf("%d%d", &s[i], &t[i]);
        int lca=get_lca(s[i], t[i]);
        dist[i]=deep[s[i]]+deep[t[i]]-2*deep[lca];
        js[s[i]]++;
        add1(t[i], i);
        add2(lca, i);
        if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;
    }
    dfs2(1);
    for(int i=1; i<=n; i++) printf("%d ", ans[i]);
    return 0;
}

NOIP2016(D1T2)天天愛跑步題解