[NOIP2016 提高組] 天天愛跑步
[NOIP2016 提高組] 天天愛跑步
因為本人很菜所以儘量寫得通俗一點, 講錯的地方歡迎指出
參考:https://www.cnblogs.com/dmoransky/p/11406515.html orz墨染空dalao 並自己加上了一點補充
順便彌補了一下luogu題解圖炸了所以看不懂的悲劇。 但是講得超好!!!
題目描述
小c同學認為跑步非常有趣,於是決定製作一款叫做《天天愛跑步》的遊戲。《天天愛跑步》是一個養成類遊戲,需要玩家每天按時上線,完成打卡任務。
這個遊戲的地圖可以看作一一棵包含 \(n\) 個結點和 \(n-1\) 條邊的樹,每條邊連線兩個結點,且任意兩個結點存在一條路徑互相可達。樹上結點編號為從 \(1\)
現在有 \(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;
}
此題最好的地方就是程式碼不太長 (逃)
未來可期。