「NOIP2016」天天愛跑步 題解
(宣告:圖片來源於網路)
「NOIP2016」天天愛跑步 題解
題目
題目描述
小c同學認為跑步非常有趣,於是決定製作一款叫做《天天愛跑步》的遊戲。《天天愛跑步》是一個養成類遊戲,需要玩家每天按時上線,完成打卡任務。
這個遊戲的地圖可以看作一一棵包含\(n\)個結點和\(n-1\)條邊的樹,每條邊連線兩個結點,且任意兩個結點存在一條路徑互相可達。樹上結點編號為從\(1\)到\(n\)的連續正整數。
現在有\(m\)個玩家,第\(i\)個玩家的起點為\(t_i\),終點為\(t_i\) 。每天打卡任務開始時,所有玩家在第\(0\)秒同時從自己的起點出發,以每秒跑一條邊的速度,不間斷地沿著最短路徑向著自己的終點跑去,跑到終點後該玩家就算完成了打卡任務。 (由於地圖是一棵樹,所以每個人的路徑是唯一的)
小c想知道遊戲的活躍度,所以在每個結點上都放置了一個觀察員。在結點\(j\)的觀察員會選擇在第\(w_j\)秒觀察玩家,一個玩家能被這個觀察員觀察到當且僅當該玩家在第\(w_j\)秒也正好到達了結點\(j\)。小c想知道每個觀察員會觀察到多少人?
注意:我們認為一個玩家到達自己的終點後該玩家就會結束遊戲,他不能等待一 段時間後再被觀察員觀察到。 即對於把結點\(j\)作為終點的玩家:若他在第\(w_j\)秒前到達終點,則在結點\(j\)的觀察員不能觀察到該玩家;若他正好在第\(w_j\)秒到達終點,則在結點\(j\)的觀察員可以觀察到這個玩家。
輸入格式
第一行有兩個整數\(n\)和\(m\)
接下來\(n−1\)行每行兩個整數\(u\)和\(v\),表示結點\(u\)到結點\(v\)有一條邊。
接下來一行\(n\)個整數,其中第\(j\)個整數為\(w_j\),表示結點\(j\)出現觀察員的時間。
接下來\(m\)行,每行兩個整數\(s_i\),和\(t_i\),表示一個玩家的起點和終點。
對於所有的資料,保證\(1\leq s_i\),\(t_i\leq n\), \(0\leq w_j\leq n\)。
輸出格式
輸出\(1\)行\(n\)個整數,第\(j\)個整數表示結點\(j\)的觀察員可以觀察到多少人。
輸入輸出樣例
輸入 #1
6 3
2 3
1 2
1 4
4 5
4 6
0 2 5 1 2 3
1 5
1 3
2 6
輸出 #1
2 0 0 1 1 1
輸入 #2
5 3
1 2
2 3
2 4
1 5
0 1 0 3 0
3 1
1 4
5 5
輸出 #2
1 2 1 0 1
First
首先看這道題,因為題目說了輸入的會是一棵數,而在樹中,兩點間的最短路徑為:起點到達他們的lca,再由lca到達終點。
所以先求出這兩點之間的lca,這個很明顯。(可以用Tarjan,亦可用倍增,本題解使用倍增求解)
C++程式碼:
#include <cstdio>
#include <vector>
using namespace std;
const int MAXN = 3e5;
vector<int> v[MAXN];
int W[MAXN], de[MAXN], dp[MAXN][32];
bool vis[MAXN];
int n, m;
void Read();
void Write();
void Init();
void dfs(int, int);
int LCA(int, int);
int main() {
Read();
Init();
Write();
return 0;
}
void dfs(int now, int step) {
de[now] = step;
int SIZ = v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i];
if(!vis[next]) {
vis[next] = true;
dp[next][0] = now;
dfs(next, step + 1);
}
}
}
int LCA(int x, int y) {
if(de[x] < de[y])
swap(x, y);
for(int i = 30; i >= 0; i--)
if(de[x] - (1 << i) >= de[y])
x = dp[x][i];
if(x == y)
return x;
for(int i = 30; i >= 0; i--) {
if(dp[x][i] != dp[y][i]) {
x = dp[x][i];
y = dp[y][i];
}
}
return dp[x][0];
}
void Init() {
vis[1] = true;
dfs(1, 0);
for(int j = 1; j < 31; j++)
for(int i = 1; i <= n; i++)
dp[i][j] = dp[dp[i][j - 1]][j - 1];
}
void Read() {
scanf("%d %d", &n, &m);
for(int i = 1; i < n; i++) {
int A, B;
scanf("%d %d", &A, &B);
v[A].push_back(B);
v[B].push_back(A);
}
for(int i = 1; i <= n; i++)
scanf("%d", &W[i]);
}
void Write() {
for(int i = 1; i <= m; i++) {
int A, B;
scanf("%d %d", &A, &B);
int lca = LCA(A, B);
}
}
在這裡不難想到一種暴力跑\(O(nm)\)的演算法:
對於\(m\)個玩家\(i\),可以對於每一個觀察員來判斷在指定時刻到達該點,若到達,則為該觀察員做出貢獻。
還是可以騙到一些分的。
繼續深入的思考一下:有哪些路徑是重合的呢?
對於這種做法,當然沒有,因為對於每一個節點都有不同的路徑,而對於觀察員來說兩兩關聯並不是很大,所以這種對於每個玩家來進行貢獻統計,不能優化什麼。
Second
既然對於每個玩家跑一遍是行不通的,那麼可以先轉換思路,對於每一個觀察員進行統計,看看那些節點對自己做了貢獻。
初步地來想,好像也是\(O(nm)\)的暴力做法,求出每個玩家的起點與終點的lca,看是否與自己的時間要求相匹配。若可以匹配,則玩家為自己做出了貢獻。
對於這棵樹進行dfs,但是如何簡化求出對自己做出貢獻的節點呢?
情況一:
觀察員在起點到lca的路上
如上圖,滿足上述條件,設e為起點,P為終點,若e為P做了貢獻,不難想到需要滿足以下條件:
deep[e]=w[P]+deep[P]
(deep為該節點的深度,可在求lca是進行處理)
由於P為e的祖先,所以e,P之間的距離就為deep[e]-deep[P],等於時間×速度,時間為w[P],速度又為1(題目已經給出),所以路程為w[P]。移項就轉換為上述條件。
情況二:
觀察員在lca到終點的路上
如上圖,同理可以求出需要滿足該條件:
deep[c]+deep[f]-2*deep[lca]−w[P]=deep[f]−deep[P]
(由於該圖是一顆樹,所以deep[c]+deep[f]-2*deep[lca] 為c到f的距離,下文使用dist來表示)
Third
應該如何統計那些節點對自己做出了貢獻呢?
如果使用列舉的方法,那時間複雜度還是不變。
所以使用一個桶來儲存當前訪問的貢獻值,回溯時就直接呼叫即可。
方法:
情況一:
很明顯,c對於b與a都做出了貢獻,滿足上述情況。但是需要注意的地方是:
c點不應該為e點做出貢獻!
怎麼辦呢?如何統計無法生效的多做了的貢獻。
繼續觀察上圖,可以發現只有桶內原來的值與現在桶內的差值才是所處了的真正貢獻。
情況二:
因為對於該訪問節點now,若是以now為根的子樹,卻不經過經過now節點的值,是必不會為該節點做出貢獻的。
所以及時統計該子樹做出的貢獻,再刪除該貢獻的值,就不會被計入不該計入的樹的貢獻之中(離開這顆樹就什麼都不是)。
C++實現:
#include <cstdio>
#include <vector>
using namespace std;
int Quick_Read() {
int res = 0, op = 1;
char x = getchar();
while(!(x >= '0' && x <= '9')) {
if(x == '-')
op = -1;
x = getchar();
}
while(x >= '0' && x <= '9') {
res = (res << 3) + (res << 1) + x - '0';
x = getchar();
}
return res * op;
}
const int MAXN = 3e5;
vector<int> v[MAXN], Vend[MAXN], Vlca[MAXN];
int dist[MAXN], s[MAXN], t[MAXN], From[MAXN];
int ans[MAXN];
int bucket1[MAXN], bucket2[MAXN * 2];
int W[MAXN], deep[MAXN], dp[MAXN][32];
bool vis[MAXN];
int n, m;
void Read();
void Init();
void Player();
void DP(int);
void dfs(int, int);
int LCA(int, int);
signed main() {
Read();
Init();
Player();
return 0;
}
void DP(int now) {
int Num1 = bucket1[W[now] + deep[now]];
int Num2 = bucket2[W[now] - deep[now] + MAXN];
int SIZ = v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i];
if(dp[now][0] != next)
DP(next);
}
bucket1[deep[now]] += From[now];
SIZ = Vend[now].size();
for(int i = 0; i < SIZ; i++) {
int next = Vend[now][i];
bucket2[dist[next] - deep[t[next]] + MAXN]++;
}
ans[now] += bucket1[W[now] + deep[now]] + bucket2[W[now] - deep[now] + MAXN] - Num1 - Num2;
SIZ = Vlca[now].size();
for(int i = 0; i < SIZ; i++) {
int next = Vlca[now][i];
bucket1[deep[s[next]]]--;
bucket2[dist[next] - deep[t[next]] + MAXN]--;
}
}
void dfs(int now, int step) {
deep[now] = step;
int SIZ = v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i];
if(!vis[next]) {
vis[next] = true;
dp[next][0] = now;
dfs(next, step + 1);
}
}
}
int LCA(int x, int y) {
if(deep[x] < deep[y])
swap(x, y);
for(int i = 30; i >= 0; i--)
if(deep[x] - (1 << i) >= deep[y])
x = dp[x][i];
if(x == y)
return x;
for(int i = 30; i >= 0; i--) {
if(dp[x][i] != dp[y][i]) {
x = dp[x][i];
y = dp[y][i];
}
}
return dp[x][0];
}
void Init() {
deep[1] = 1; vis[1] = true; dp[1][0] = 1;
dfs(1, 1);
for(int j = 1; j < 31; j++)
for(int i = 1; i <= n; i++)
dp[i][j] = dp[dp[i][j - 1]][j - 1];
}
void Read() {
n = Quick_Read(); m = Quick_Read();
for(int i = 1; i < n; i++) {
int A, B;
A = Quick_Read(); B = Quick_Read();
v[A].push_back(B);
v[B].push_back(A);
}
for(int i = 1; i <= n; i++)
W[i] = Quick_Read();
}
void Player() {
for(int i = 1; i <= m; i++) {
s[i] = Quick_Read();
t[i] = Quick_Read();
int lca = LCA(s[i], t[i]);
dist[i] = deep[s[i]] + deep[t[i]] - 2 * deep[lca];
From[s[i]]++;
Vend[t[i]].push_back(i);
Vlca[lca].push_back(i);
if(deep[lca] + W[lca] == deep[s[i]])
ans[lca]--;
}
DP(1);
for(int i = 1; i <= n; i++) {
printf("%d", ans[i]);
if(i != n)
printf(" ");
}
}