1. 程式人生 > 其它 >[NOIP2016 提高組] 天天愛跑步

[NOIP2016 提高組] 天天愛跑步

lca+樹上差分

[NOIP2016 提高組] 天天愛跑步

因為本人很菜所以儘量寫得通俗一點, 講錯的地方歡迎指出

參考:https://www.cnblogs.com/dmoransky/p/11406515.html orz墨染空dalao 並自己加上了一點補充

順便彌補了一下luogu題解圖炸了所以看不懂的悲劇。 但是講得超好!!!


題目描述

小c同學認為跑步非常有趣,於是決定製作一款叫做《天天愛跑步》的遊戲。《天天愛跑步》是一個養成類遊戲,需要玩家每天按時上線,完成打卡任務。

這個遊戲的地圖可以看作一一棵包含 \(n\) 個結點和 \(n-1\) 條邊的樹,每條邊連線兩個結點,且任意兩個結點存在一條路徑互相可達。樹上結點編號為從 \(1\)

\(n\) 的連續正整數。

現在有 \(m\) 個玩家,第 \(i\) 個玩家的起點為 \(Si\) ,終點為 \(ti\) 。每天打卡任務開始時,所有玩家在第 \(0\) 秒同時從自己的起點出發,以每秒跑一條邊的速度,不間斷地沿著最短路徑向著自己的終點跑去,跑到終點後該玩家就算完成了打卡任務。 (由於地圖是一棵樹,所以每個人的路徑是唯一的)

小c想知道遊戲的活躍度,所以在每個結點上都放置了一個觀察員。在結點 \(j\) 的觀察員會選擇在第 \(wj\) 秒觀察玩家,一個玩家能被這個觀察員觀察到當且僅當該玩家在第 \(wj\) 秒也正好到達了結點 \(j\) 。小c想知道每個觀察員會觀察到多少人?

注意:我們認為一個玩家到達自己的終點後該玩家就會結束遊戲,他不能等待一段時間後再被觀察員觀察到。 即對於把結點 \(j\) 作為終點的玩家:若他在第 \(wj\) 秒前到達終點,則在結點 \(j\) 的觀察員不能觀察到該玩家;若他正好在第 \(wj\) 秒到達終點,則在結點 \(j\) 的觀察員可以觀察到這個玩家。


離線賽被虐爆了qaq。

初步分析

  • 按著題目要求說, 我們需要看每個觀察員會觀察到幾個人
  • 而每個觀察員可以觀察到幾個人和 \(w[]\) 陣列有關, 也就是找滿足要求的點
  • 我們可以拆分一下。
  • \(s\)\(t\) 的路徑 (經過lca) 分成兩段

這樣算每段的貢獻加起來就是答案了。


算出什麼點能有貢獻

如果是在一條鏈上, 即

明顯可以發現:

對於所有如上圖所示的點t, 若 \(dep[t]== dep[x]+ w[x]\) 那都可以算入貢獻。 這裡 \(dep\) 指深度

\(dep[x]+ w[x]\) 是一個固定的值, 在 \(x\) 的子樹外的點一定不會對 \(x\) 產生貢獻,不在這鏈上的在鏈上但沒有經過的也不會對 \(x\) 產生貢獻。

所以我們要求的就是 \(x\) 的子樹內的, 在這條鏈上的, 且 \(dep[t]== dep[x]+ w[x]\) 的所有點 \(t\) 的個數 這是第一種情況


寫法

先來看一看怎麼寫(寫法很巧妙)。

  • 我們開一個桶, 存 \(dep\)

解決在子樹內的問題

  • 首先要滿足在其子樹內。 不可能每個點開一個桶存其子樹的資訊, 但是我們要求, 怎麼辦。

看下面偽虛擬碼(不會虛擬碼qaq

\(s[]\) 表示那個桶


void dfs(int u)
   res= 要求的值   val= s[res]
   for(all vertex that u could reach )     //就是任何u能遍歷到的點, 嘗試高大上一點但是失敗了     這裡沒有考慮樹的情況
       dfs(j)
   s[res]++ 
   ans[u]= s[res]- val

啊不用管一些細節問題, 大概理解思路就好了。

也就是進入子樹了, 記錄一個以前的值, 記錄一個最後的值, 減去就是多出的值了

成功解決。

如何解決不在鏈上的問題

我們可以發現, 每次有一個起點 \(s\) 和終點 \(t\), 只有那一段區間是可以加的。 (這裡可能有點表述不清, 我們在上上圖已經把一條鏈拆成了兩段, 看第一段)

那好了, 我們可以用樹上差分的思想, 把每一次操作放在點上, 在遍歷到起點時,我們加一, 終點的時候, 我們減1

如圖


另一條鏈的寫法

差不多是一樣的。 另一條鏈如果要產生貢獻必須要 \(dep[s]+ dep[x]- dep[lca(s, t)]== w[x]\) 這個可以自己畫圖看一看

移項: \(dep[s]- dep[lca(s, t)]== dep[x]+ w[x]\)

然後就和上面一樣了


程式碼

#include <bits/stdc++.h>
using namespace std;
 
const int N= 300005, M= N* 2;
 
typedef pair<int, int> PII;
int h[N], e[M], ne[M], idx;
int dep[N], fa[N][25], w[N];
int d1[N<< 1], d2[N<< 1];
int ans[N];
int n, m; 
 
void init()              //這裡使用倍增求lca
{
    memset(dep, 0x3f, sizeof dep);
    dep[0]= 0, dep[1]= 1; queue<int> q; q.push(1);
    while(q.size())
    {
        int u= q.front(); q.pop();
        for(int i= h[u]; ~i; i= ne[i])
        {
            int j= e[i];
            if(dep[j]> dep[u]+ 1)
            {
                dep[j]= dep[u]+ 1; fa[j][0]= u;
                for(int k= 1; k<= 24; k++ )
                   fa[j][k]= fa[fa[j][k- 1]][k- 1];
                q.push(j);
            }
        } 
    }
    return ;
}
 
 
void add(int a, int b)
{
    e[idx]= b, ne[idx]= h[a], h[a]= idx++ ;
    return;
}
 
int lca(int a, int b)
{
    if(dep[a]< dep[b]) swap(a, b);
    for(int i= 24; i>= 0; i-- ) 
       if(dep[fa[a][i]]>= dep[b]) a= fa[a][i];
    if(a== b) return a;
    for(int i= 24; i>= 0; i-- )
       if(fa[a][i]!= fa[b][i]) a= fa[a][i], b= fa[b][i];
    return fa[a][0];
}
 
 
vector<PII> query1[N], query2[N];
void update(int s, int t)            //儲存詢問
{
    int p= lca(s, t);          //query1[i].first 表示此點能給出的貢獻, second是1或-1
    query1[s].push_back((PII){dep[s], 1}); query1[fa[p][0]].push_back({dep[s], -1});        //分成兩段分開來存
    query2[t].push_back((PII){dep[s]- 2* dep[p]+ n, 1}); query2[p].push_back({dep[s]- 2* dep[p]+ n, -1});
    return ;
}
 
void dfs(int u, int fa)
{
    int v1= w[u]+ dep[u], v2= w[u]- dep[u]+ n;
    int res1= d1[v1], res2= d2[v2];        //和上述虛擬碼差不多。。?
    for(int i= h[u]; ~i; i= ne[i])
    {
        int j= e[i];
        if(j== fa) continue;         //防止重複到一個點
        dfs(j, u);
    }
    for(int i= 0; i< query1[u].size(); i++ ) d1[query1[u][i].first]+= query1[u][i].second;       //樹上差分的思路
    for(int i= 0; i< query2[u].size(); i++ ) d2[query2[u][i].first]+= query2[u][i].second;
    ans[u]= (d1[v1]- res1)+ (d2[v2]- res2);      //上述講過, 一個常見的套路, 做差
    return ;
}
 
int main()
{
    cin>> n>> m;
    memset(h, -1, sizeof h); 
    for(int i= 1; i<= n- 1; i++ )
    {
        int a, b; scanf("%d%d", &a, &b);
        add(a, b); add(b, a);                  
    }
    for(int i= 1; i<= n; i++ ) scanf("%d", &w[i]);
    init();            //預處理出倍增陣列
    for(int i= 1; i<= m; i++ ) 
    {
        int s, t; scanf("%d%d", &s, &t);
        update(s, t);
    }
    dfs(1, -1);
    for(int i= 1; i<= n; i++ ) printf("%d ", ans[i]);
    puts("");
     
    return 0;
}

此題最好的地方就是程式碼不太長 (逃)

未來可期。