本場比賽的最后一題,不過好像並沒有任何防AK的作用。
至於YQOI,那是沒前綴名看了不順眼。
樹鏈剖分模板題?有點像。
題目大意
給定一棵樹和每個點的初始狀態(標記或不標記),每次修改一個點的狀態(狀態取反)或詢問樹上所有標記點到\(u->v\)的簡單路徑的最短距離之和。
以下是數據范圍:
無腦暴力
我們把矛頭盯准前4個點。
\(n,m\leq 200\),這意味着什么?
直接按照題意模擬,先找出\(u->v\)簡單路徑上的所有點並標記,然后以每個點為根節點,找到最近的被標記的點,這個點顯然最優,記錄累加答案即可。
時間復雜度:\(O(n^2m)\)期望得分:\(20\).
不過由於數據比較水,放過了第\(7,8\)兩個全特殊約束的點,可以拿到\(30pts.\)
高端暴力
解決了\(n,m\leq 200\)的點,我們可以看到接下來的數據為\(n,m\leq 2000\).
通俗易懂的講我們要在\(O(nm)\)時間內完成答案。
在分析一下,不難發現,對於每次詢問我們要做到\(O(n)\)的時間復雜度。
我們枚舉鏈上的每個點,當枚舉到點\(x\)時,考慮有哪一些標記點的最短距離是到\(x\)的。
容易發現去掉和它相鄰的點所囊括的范圍之后包含的點即為\(x\)點的范圍。
可以這樣理解:
圖中紅圈內的點到鏈上的最短距離都是到\(x\)。
設\(f[x]\)表示以\(x\)為根的所有標記點到\(x\)的距離,\(num_x\)表示以\(x\)為根的子樹有多少個有標記,轉移是顯然的:
累加答案即可。
時間復雜度\(O(nm)\),期望得分\(40\).
定鏈求和
這里針對特殊約束2.
不難發現,如果詢問\(u->v\)不改變的話,我們只需要針對每次修改更新答案即可。
修改點\(x\)時,我們需要快速找到\(u->v\)路徑上的最近點。
如果以\(u\)為根的話,那么我們要找的點就是\(LCA(x,v)\).
時間復雜度:\(O(nlogn+mlogn)\),期望得分:\(30\).
樹鏈求和
這里針對特殊約束1.
樹退化成了一條鏈,我們只要在這條鏈上尋找答案即可。
我們不妨把樹鏈抽象成一個數列,
詢問\(l->r\)時,我們只需要求出\(1->l\)中的點到\(l\)的距離和加上\(r->n\)中的點到\(r\)的距離和就行了。
這里僅考慮到\(l\)的部分(到\(r\)的部分同理)。
發現\(dis(i,l)=dis(i,n)-dis(l,n)\),
求和時,\(ans=\sum_{i=1}^ldis(i,n)-num_{1->l}*dis(l,n)\).
顯然\(\sum_{i=1}^ldis(i,n)\)和\(num_{1->l}\)都是可以用某些單點修改區間求和的數據結構來維護的。
那么這幾分就拿到了。
時間復雜度:\(O(nlogn+mlogn)\),期望得分:\(35\).
不帶修改的離線做法
做到這里,應該就會有些眉目了。
發現有些點的1操作數量不會很多,只要每次修改后暴力重構,我們就只要處理詢問部分。
考慮優化"高端暴力",我們發現處理這種暴力的時候每次都會重復累加好多點。
事實上,我們可以直接預處理暴力時所求的范圍。
考慮倍增,設\(g[x][j]\)表示\(x\)的\(2^j\)祖先的\(f\)值去掉\(f[x]\)所剩下的值,\(G[x][j]\)表示x到祖先的同剛才那個區域到這條鏈上的距離。
在不考慮修改的情況下,\(f[x]\)是可以事先求出來的。
在從\(x\)跳上\(x\)的\(2^j\)祖先\(anc\)的過程中,我們只要累加上\(G[x][j]\)即可,對於外面的世界,用\(g[x][j]\)處理就行了。
時間復雜度:\(O(k(nlogn+mlogn))\)其中\(k\)是修改數,
期望得分:\(60\).(有點小卡常)。
Part A
假設所求路徑\(u,v\)的\(LCA(u,v)=lca\).
我們考慮\(lca\)子樹內所有的點對答案的貢獻。
前面我們說到倍增是沒有辦法修改點的,這里我們嘗試樹鏈剖分。
首先要清楚我們所維護的值,不妨設點有沒有被標記為數組\(tag[i]\).顯然
具體可以參考樹鏈部分。
顯然\(\sum_{tag[i],LCA(x,i)==x}dep[i]\)和\(num_i\)是可以用歐拉序加上數據結構維護的。
這意味着我們可以在\(logn\)時間內算出\(f[x]\)。
再考慮上面倍增所說到的\(g\)。
這里我們設\(g[x]\)表示\(f[x]\)減去它的重兒子的\(f\)值。
分析一下每次修改的時候我們需要更新哪些點的\(g\)值。
圖中虛線代表輕鏈,實線代表重鏈。
顯然那些紅色圈的點才需要更新\(g\)值。
這些點都是某個點\(x\)的\(top[x]\)的父親節點,易得這樣的點有\(logn\)個,是可以做到更新的。
更新完成以后,我們嘗試着求答案。
先考慮\(u->lca\)的部分。
如果\(u\)要往上跳到\(fa[top[u]]\)的話,我們的任務就是累加這一部分的和。
容易發現,當我們跳到點\(x\)時,我們若要跳到\(top[x]\),就要累加\(x->top[x]\)的這一條鏈上的\(g\)值。
而要從\(top[x]\)跳到\(fa[top[x]]\)時,
由於\(top[x]\)是輕兒子,我們答案應該要累加\(f[fa[top[x]]-f[top[x]]-num_{top[x]}*val_{top[x],fa[top[x]]}.\)
我們可以驚喜的發現,利用\(f\)和\(g\)可以做到以上所有操作。
這樣的時間復雜度為\(O((n+m)log^2n)\).也可以過特殊約束3.
期望得分:\(65\),加上無腦暴力可以拿到\(80pts\).
Part B
接下來我們要考慮\(lca\)以外的部分,這部分的所有點到這條鏈上的最短距離都是到\(lca\).
不妨設\(lca\)外面的某個點為\(x\)。
顯然\(dis(x,lca)=dep[x]+dep[lca]-2*dep[LCA(x,lca)].\)
那么這部分答案就為
前一部分是很好求的,至於\(\sum dep[LCA(x,lca)]\)這部分我們依舊用類似的辦法。
為了方便描述,設\(LCA(x,lca)=Lca.\)
往上跳的時候,我們把之前\(g[x]\)所維護的值換為\(num_x*dep[x]\).
我們依舊可以用同樣的方法向上跳。
\(g[x]\)換掉了,同樣這里的\(f[x]\)也要換掉。
搬回剛才的圖,我們發現,從\(top[x]->fa[top[x]]\)的過程中,我們少掉的是\(fa[top[x]]\)的子樹去除\(top[x]\)的子樹這部分。
那么我們累加的答案就應是\((num_{fa[top[x]]}-num_{top[x]})*dep[fa[top[x]]\).
於是這一部分就做完了。
時間復雜度:\(O((n+m)log^2n)\),期望得分\(100\).
Summary
-
由於本題需要修改點的標記,我們考慮樹鏈剖分。
-
定義四個樹狀數組(其他數據結構也行)\(A,B,C,D\).
- \(A_x\)表示以\(x\)為根的子樹有多少個標記點。
- \(B_x\)表示以\(x\)為根的子樹所有標記點的深度和。
- \(C_x\)表示以\(x\)為根的子樹去掉其重兒子為根的子樹所形成的范圍中的標記點到\(x\)的距離和。
- \(D_x\)表示以\(x\)為根的子樹去掉其重兒子為根的子樹所形成的范圍中的標記點的數量乘以\(dep[x]\).
- 換成表達式的話:\(A_x=num_x,B_x=A_x*dep[x],D_x=(A_x-A_{son[x]})*dep[x].\)
- 顯然對於一次修改,\(A,B\)只需修改一次,\(C,D\)要修改\(logn\)次。
-
定義四個函數\(f_x,g_x,F_x,G_x\)(不需要維護)
- \(f_x\)表示以\(x\)為根的子樹中所有特殊點到\(x\)的距離和。顯然\(f_x=B_x-A_x*dep_x.\)
- \(g_x\)表示以\(x\)為根的子樹去掉其重兒子為根的子樹所形成的范圍中的標記點到\(x\)的距離和。顯然\(g_x=C_x.\)
- \(F_x\)表示以\(x\)為根的子樹所有標記點的數量乘以\(dep_x\)。顯然\(F_x=A_x*dep_x.\)
- \(G_x\)表示以\(x\)為根的子樹去掉其重兒子為根的子樹所形成的范圍中的標記點的數量乘以\(dep[x]\)。顯然\(G_x=D_x.\)
- 可以發現,以上每一個函數至少可以在\(logn\)內求出。
-
在\(lca\)子樹內的部分
- 如果要從\(x\)跳到\(fa_x\),且\(x\)是\(fa_x\)的重兒子,直接加上\(g_x\)即可。
- 面對一條鏈上的點,所有點的父親的重兒子都是該點,則可以直接加上\(\sum g_x\)。
- 對於一條鏈上的\(top\)點,它是它的父親的輕兒子,所以我們應該加上\(f_{fa_x}-f_x-A_x*val_{x,fa_x}\)。
- 值得注意的是,當你從\(u->lca\)和\(v->lca\)跳完之后,我們會多加上一個\(f_{lca}\),所以答案應該減去\(f_{lca}\),在從\(u,v\)開始往上跳時,我們需要加上\(f_u,f_v\),即\(u,v\)的子樹部分對答案的貢獻。
-
在\(lca\)外的部分
- 對於從\(lca\)以外的部分,每個點到鏈上的最短距離都是到\(lca\)。
- 從前面我們得知,所需要加的答案為\(\sum dep[x]+num*dep[lca]-2*\sum dep[Lca]\)。
- 也就是\((B_{root}-B_{lca})+(A_{root}-A_{lca})*dep[lca]-2*\sum dep[Lca]\)。
- 處理\(\sum dep[Lca]\)時,注意到\(Lca\)都是\(lca\)到\(root\)(樹根)上的點,我們依舊用之前的方法處理從\(lca\)到\(root\)的路徑。
- 如果要從\(x\)跳到\(fa_x\),且\(F\)是\(fa_x\)的重兒子,直接加上\(G_x\)即可。
- 面對一條鏈上的點,所有點的父親的重兒子都是該點,則可以直接加上\(\sum G_x\)。
- 對於一條鏈上的\(top\)點,它是它的父親的輕兒子,所以我們應該加上\(F_{fa_x}-F_x-A_x*val_{x,fa_x}\)。
-
以上為大概思路,具體實現應該還有一些細節。
然而值得注意的是,此題的細節極多,需要一定的代碼難度。
完整代碼:
#include<bits/stdc++.h>
#define LL long long
#define maxn 210000
using namespace std;
int n,m,tag[maxn],last[maxn],p,id[maxn],ID = 0;
int size[maxn],top[maxn],tail[maxn],fa[maxn],son[maxn],type;
LL Dep[maxn],dep[maxn];
LL ans = 0;
struct tree_array
{
LL d[maxn];
LL lowbit(LL x) {return x & (- x);}
LL Insert(int x,LL val) {while(x <= n) {d[x] += val; x += lowbit(x);}}
LL getsum(int x) {return x ? getsum(x - lowbit(x)) + d[x] : 0;}
LL query(int l, int r) {if(l > r) swap(l, r); return getsum(r) - getsum(l - 1);}
}A,B,C,D;
struct edge
{
int x, y, next;
LL val;
void Add_edge(int X, int Y, LL Val)
{
x = X; y = Y; val = Val;
next = last[x]; last[x] = p;
}
}e[maxn * 2];
LL getdis(int u, int v) {return abs(dep[u] - dep[v]);}
LL sumA(int x) {return A.query(id[x], id[x] + size[x] - 1);}
LL sumB(int x) {return B.query(id[x], id[x] + size[x] - 1);}
void init1(int x)
{
size[x] = 1;
for(int k = last[x]; k; k = e[k].next)
if(e[k].y != fa[x])
{
int y = e[k].y; fa[y] = x;
dep[y] = dep[x] + e[k].val;
Dep[y] = Dep[x] + 1;
init1(y);
size[x] += size[y];
if(size[y] > size[son[x]]) son[x] = y;
}
}
void init2(int x)
{
id[x] = ++ ID;
if(son[fa[x]] == x) top[x] = top[fa[x]];
else top[x] = x;
if(son[x]) init2(son[x]);
for(int k = last[x]; k; k = e[k].next)
if(e[k].y != fa[x] && e[k].y != son[x])
init2(e[k].y);
if(!son[x]) tail[x] = x;
else tail[x] = tail[son[x]];
}
int LCA(int u, int v)
{
while(u != v)
{
if(Dep[top[u]] < Dep[top[v]]) swap(u, v);
if(top[u] == top[v]) return Dep[u] < Dep[v] ? u : v;
else u = fa[top[u]];
}
return u;
}
void modify(int x)
{
tag[x] ^= 1; LL flag = tag[x] * 2 - 1;
int pos = x;
A.Insert(id[x], flag);
B.Insert(id[x], flag * dep[x]);
while(x)
{
C.Insert(id[x], flag * getdis(pos, x));
D.Insert(id[x], flag * dep[x]);
x = fa[top[x]];
}
}
LL getF(int x) {return abs(sumB(x) - dep[x] * sumA(x));}
void findsum(int x, int anc)
{
ans += getF(x);
while(x != anc)
{
if(top[x] == top[anc]) {ans += C.query(id[fa[x]], id[anc]); return;}
else
{
if(x == top[x])
ans += getF(fa[x]) - getF(x) - sumA(x) * getdis(x, fa[x]);
else
{
ans += getF(fa[top[x]]) - getF(top[x]) - sumA(top[x]) * getdis(top[x], fa[top[x]]);
ans += C.query(id[fa[x]], id[top[x]]);
}
x = fa[top[x]];
}
}
}
void Sumlink(int x)
{
while(x != 1)
{
if(top[x] == 1) {ans -= 2 * D.query(id[fa[x]], id[1]); break;}
else
{
if(x == top[x]) {ans -= 2 * ((sumA(fa[x]) - sumA(x)) * dep[fa[x]]);}
else {
ans -= 2 * D.query(id[fa[x]], id[top[fa[x]]]);
ans -= 2 * ((sumA(fa[top[x]]) - sumA(top[x])) * dep[fa[top[x]]]);
}
x = fa[top[x]];
}
}
}
LL query(int x,int y)
{
int lca = LCA(x, y); ans = 0;
findsum(x, lca);
findsum(y, lca);
ans -= getF(lca);
if(lca != 1)
{
ans += sumB(1) - sumB(lca) + (sumA(1) - sumA(lca)) * dep[lca];
Sumlink(lca);
}
return ans;
}
int main()
{
scanf("%d%d%d", &n, &m, &type);
for(int i = 1;i <= n - 1;i ++)
{
int u,v;LL w; scanf("%d%d%lld", &u, &v, &w);
e[++ p].Add_edge(u, v, w);
e[++ p].Add_edge(v, u, w);
}
init1(1); init2(1);
for(int i = 1;i <= n;i ++)
{
int opt; scanf("%d", &opt);
if(opt) modify(i);
}
for(int i = 1;i <= m;i ++)
{
int opt, x, y;
scanf("%d%d", &opt, &x);
if(opt == 2) scanf("%d", &y);
if(opt == 1) modify(x);
else printf("%lld\n", query(x, y));
}
}
記得開\(long long\)哦。