主要就是整理一下dsu on tree的進階版習題。
\(0x01\) \(\rm Cf375D\) Tree and Queries
給出一棵\(n\)個結點的樹,每個結點有一個顏色\(c_i\) 。 詢問\(q\)次,每次詢問以\(v\)結點為根的子樹中,出現次數 \(≥k\)的顏色有多少種。樹的根節點是\(1\)。
考慮維護子樹里面每種顏色出現的次數,但是顯然詢問的是一個\(buc[c_i]\)的后綴和,於是考慮上線段樹來維護這個東西,calc
到每個點的時候先del
掉原來的再upd
新的信息……然后就做完了233
然而一開始的時候我調了好久,因為我是這么寫的:
void do_do(int u, int fa){
ts[base[u]] ++ ;
for (int k = head[u] ; k ; k = E[k].next)
if (to(k) != fa && !vis[to(k)]) do_do(to(k), u) ;
}
void do_del(int u, int fa){
ts[base[u]] -- ;
for (int k = head[u] ; k ; k = E[k].next)
if (to(k) != fa && !vis[to(k)]) do_del(to(k), u) ;
}
void _count(int u, int fa, int val){
bool fg = 1 ;
if (!chk[base[u]])
chk[base[u]] = 1,
update(1, 1, N, ts[base[u]] + 1, val), fg = 0 ;
for (int k = head[u] ; k ; k = E[k].next)
if (to(k) != fa && !vis[to(k)]) _count(to(k), u, val) ;
if (!fg) chk[base[u]] = 0 ;
}
void calc(int u, int fa){
_count(u, fa, -1) ; do_do(u, fa) ; _count(u, fa, 1) ;
}
void del(int u, int fa){
_count(u, fa, -1) ; do_del(u, fa) ; _count(u, fa, 1) ;
}
看上去很對的亞子,但是錯就錯在必須每個點獨立計算完貢獻才能考慮下一個點,否則下一個點的信息就是錯誤的——也就是說不能整體del
再整體upd
,必須逐個逐個的del
和upd
。。然而事實上關鍵問題還是在\(buc\)的統計上出了問題233
於是最后的代碼:
void calc(int u, int fa){
update(1, 1, N, ts[base[u]] + 1, -1) ;
ts[base[u]] ++ ;
update(1, 1, N, ts[base[u]] + 1, 1) ;
for (int k = head[u] ; k ; k = E[k].next)
if (to(k) != fa && !vis[to(k)]) calc(to(k), u) ;
}
void del(int u, int fa){
update(1, 1, N, ts[base[u]] + 1, -1) ;
ts[base[u]] -- ;
update(1, 1, N, ts[base[u]] + 1, 1) ;
for (int k = head[u] ; k ; k = E[k].next)
if (to(k) != fa && !vis[to(k)]) del(to(k), u) ;
}
void dfs(int u, int fa, int mk){
for (int k = head[u] ; k ; k = E[k].next)
if (to(k) != fa && to(k) != son[u]) dfs(to(k), u, 0) ;
if (son[u])
dfs(son[u], u, 1), vis[son[u]] = 1 ;
calc(u, fa) ;
for (int k = 0 ; k < qs[u].size() ; ++ k)
ans[u].pb(query(1, 1, N, qs[u][k] + 1, N)) ;
vis[son[u]] = 0 ; if (!mk) del(u, fa) ;
}
\(0x02\) \(\rm Cf741D\) Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
一棵根為\(1\)的樹,每條邊上有一個字符(\(a-v\)共\(22\)種)。 一條簡單路徑被稱為\(\rm Dokhtar-kosh\)當且僅當路徑上的字符經過重新排序后可以變成一個回文串。 求每個子樹中最長的\(\rm Dokhtar-kosh\)路徑的長度。
似乎是Cf570D
的升級版,因為路徑可以跨過根所以會顯得比較復雜,不過結論還是可以用的:
我們令一個字符的權值\(val(x)=\text{1<<(x-'a')}\),那么對與一個串\(\rm S\),我們令\(k=\rm{Xor}_{i=1}^n\it val\rm( S[i])\),那么重排之后可以構成回文串\(\Longleftrightarrow\) \(size(k)\leq 1\),其中\(size(\rm S)\)指集合\(\rm S\)內的元素個數,也就是二進制表示中\(1\)的個數
然后就是考慮怎么維護這個東西。
-
不經過根的路徑,分治做下去就好,每一層\(u\)對所有的\(son[u]\)的\(ans\)取\(\max\).
-
經過根的路徑,發現對於一個\(u\),和\(v\)組合后可以產生貢獻,我們只需要關心深度最大的\(v\).所以自然想到用一個桶來維護二進制數值的最大深度。但是這個地方還有個問題,就是統計路徑的話,\(u\)和\(v\)不能在同一棵子樹中,容易發現只要滿足不在同一棵子樹中,那就一定滿足\((u,v)\)這條路徑經過\(root\)。所以這個地方,對於一個點\(u\),考慮一棵子樹一棵子樹地計算答案,深度做差求;而“經過根節點的路徑”包括起點和終點在根節點上的路徑,所以需要對\(root\)單獨計算一次。
看上去應該這么實現:
void _delete(int u, int fa){
f[dis[u]] = 0 ;
for (int k = head[u] ; k ; k = E[k].next)
if (to(k) != fa && !vis[to(k)]) _delete(to(k), u) ;
}
void calc(int u, int fa, int & ans, int d){
if (f[dis[u]])
ans = max(ans, f[dis[u]] + dep[u] - 2 * d) ;
for (int i = 0 ; i <= 21 ; ++ i)
if (f[dis[u] ^ (1 << i)])
ans = max(ans, f[dis[u] ^ (1 << i)] + dep[u] - 2 * d) ;
for (int k = head[u] ; k ; k = E[k].next)
if (to(k) != fa && !vis[to(k)]) calc(to(k), u, ans, d) ;
}
void update(int u, int fa){
f[dis[u]] = max(f[dis[u]], dep[u]) ;
for (int k = head[u] ; k ; k = E[k].next)
if (to(k) != fa && !vis[to(k)]) update(to(k), u) ;
}
void dfs(int u, int fa, int mk){
for (int k = head[u] ; k ; k = E[k].next){
if (to(k) == fa || to(k) == son[u]) continue ;
dfs(to(k), u, 0), ans[u] = max(ans[u], ans[to(k)]) ;
}
if (son[u])
dfs(son[u], u, 1), vis[son[u]] = 1,
ans[u] = max(ans[u], ans[son[u]]) ;
for (int k = head[u] ; k ; k = E[k].next)
if (to(k) != son[u] && to(k) != fa)
calc(to(k), u, ans[u], dep[u]), update(to(k), u) ;
if (f[dis[u]])
ans[u] = max(ans[u], f[dis[u]] - dep[u]) ;
for (int i = 0 ; i <= 21 ; ++ i)
if (f[dis[u] ^ (1 << i)])
ans[u] = max(ans[u], f[dis[u] ^ (1 << i)] - dep[u]) ;
f[dis[u]] = max(f[dis[u]], dep[u]) ; vis[son[u]] = 0 ; if (!mk) _delete(u, fa) ;
}
總感覺……復雜度不是很對?感覺單次運行dfs復雜度很高的亞子……然而還是套用“一個點到根節點最多有$\log n \(個輕祖先”這個理論,每個點被訪問的次數還是不變的——畢竟子樹之間訪問不會重復。於是時間復雜度\)n\log n$。
唔,感覺這個題還是比較有技巧性的233
\(0x03\) \(\rm NOIP2018\)模擬 · 樹
這道題是從一個神仙的blog里嫖來的,提交的話可以到Luogu上提交:戳這里\(\rm Link\)
題面:
給定一棵樹。
令\([L,R]\)描述的是序號在\([L,R]\)內的點的集合。
同時,令函數\(\bold F(\{ \rm S\})\)表示令集合\(\rm S\)內的點聯通的需要的最小邊數。
問題則是求:
\[\sum_{i=1}^{n}\sum_{j=i}^n \bold F([i,j]) \]\(n\leq 100,000\)
一步轉化成求每條邊的貢獻。結合正難則反可知,一條邊的總貢獻至多是\(\binom{n}{2}\),算多了的集合是那些位於這條邊兩側中的其中一側,不經過這條邊的集合。所以考慮分別維護子樹內和子樹外的兩個答案。
子樹內的比較容易維護,考慮假設現在有了\(\{1,2,3\},\{5,6\}\)兩個集合,將其視作兩個連通塊,當加進來\(\{4\}\)時,會和左右都相連接,不妨假設先與\(\{1,2,3\}\)合並,那么最后會產生\((1,4),(2,4),(3,4)\)三個新的連通塊,原來的依舊要加入。所以考慮用並查集+並查集的\(size\)來維護。由於子樹內的點在暴力時只會插入不會刪除,所以並查集是\(\rm van\)全沒問題的。
之后是子樹外的。子樹外的和子樹內的情況差不多,但是由插入變成了刪除。然后就可以考慮用set維護,因為這東西自帶的單調性比較nice,並且支持刪除操作。所以流程大概就是考慮把刪除的點丟到set里面,最初的ans_out
顯然是\(\binom{n}{2}\),每刪除一個新的點,設其編號為\(x\),set
里面第一個比\(x\)小的元素設為\(x_p\)第一個比\(x\)大的元素設為\(x_s\),那么\([x_{p}+1,x-1]\)還是連續的,\([x+1,x_s-1]\)還是連續的,所以新的貢獻變成了
原來的舊貢獻\(calc(x_s-1-(x_p+1)+1)\)理應減去。
所以就做完了,感覺神清氣爽,總體來說算是一道很好的題吧。
set <int> s ;
int vis[MAXN], op[MAXN] ;
LL calc(LL x){ return x * (x - 1) / 2 ; }
void _clear(){
s.clear() ;
ansout = calc(N), ansin = 0,
s.insert(0), s.insert(N + 1) ;
}
int _find(int x){
return x == fr[x] ? x : fr[x] = _find(fr[x]) ;
}
void fuck(int u){
s.insert(u) ; op[u] = 1 ;
set <int> :: iterator l, r, mid ;
l = r = mid = s.find(u), l --, r ++ ;
ansout += calc(*r - *mid - 1) + calc(*mid - *l - 1) - calc(*r - *l - 1) ;
if (op[u - 1]){
int f1 = _find(u - 1), f2 = _find(u) ;
ansin += bg[f1] * bg[f2], fr[f1] = f2, bg[f2] += bg[f1] ;
}
if (op[u + 1]){
int f1 = _find(u + 1), f2 = _find(u) ;
ansin += bg[f1] * bg[f2], fr[f1] = f2, bg[f2] += bg[f1] ;
}
}
void _update(int u, int fa){
fuck(u) ; //cout << u << endl ;
for (int k = head[u] ; k ; k = E[k].next)
if (to(k) == fa || vis[to(k)]) continue ; else _update(to(k), u) ;
}
void _delete(int u, int fa){
op[u] = 0, fr[u] = u, bg[u] = 1 ;
for (int k = head[u] ; k ; k = E[k].next)
if (to(k) == fa) continue ; else _delete(to(k), u) ; // 1
}
void dfs(int u, int fa, int mk){
for (int k = head[u] ; k ; k = E[k].next){
if (to(k) == fa || to(k) == son[u]) continue ;
dfs(to(k), u, 0) ;
}
if (son[u]) dfs(son[u], u, 1), vis[son[u]] = 1 ;
_update(u, fa), ans += calc(N) - ansout - ansin ;
if (!mk) _delete(u, fa), _clear() ; vis[son[u]] = 0 ;
}
\(\rm Warning\)
-
注意一個地方:
vis[son[u]] = 0 ; if (!mk) _delete(u, fa) ;
把這兩句寫反了會調一下午,歡迎嘗試quq