樹分治 - 點分治

FAKUMARER發表於2024-05-04

樹分治

點分治

本質上是一種 思想,用於統計 帶權樹多條路徑資訊 的 方法之一

一般來講,點分治的基本思路 如下

  1. 將需要統計的 路徑是否經過當前根節點 分成兩類

  2. 某種方法 統計 經過根節點 的部分(由於已經有 經過根節點 這個限制了,簡單一些)

  3. 不經過根節點 則 整個路徑都在 當前根節點一個兒子的子樹內

    遞迴統計 那個兒子的子樹 即可

這樣我們可以把 對於整棵樹的特殊路徑統計 轉化為 對於經過某個點的特殊路徑統計

有些題(\(P5563 ~ ...\))還需要 對端點為當前根 的路徑 單獨討論,但也是簡單的

看上去就 變簡單了很多

需要注意的是,這種結構的複雜度並不固定,取決於具體實現

遞迴次數 是可以 有保證的,注意到我們 每次的根 不要直接選取 那個兒子

而是 選取那棵子樹的重心,這樣 遞迴一次子樹大小至少減半,至多遞迴 \(O(\log N)\)

點分治結構常數巨大,能 一次做掉所有詢問 就不要 每個詢問做一次(即使 複雜度一樣

Luogu P3806 【模板】點分治 1

\(Double ~ EXP\) CF161D Distance in Tree

給定一棵 帶權樹,詢問 樹上是否存在 距離為 \(K\) 的點對

即統計 長為 \(K\) 的路徑

套路分類,於是我們 只需要思考 經過根的長為 \(K\) 的路徑是否存在

顯然 路徑兩端不能在同一個兒子的子樹內,故我們 遍歷當前根的兒子

計算 當前兒子子樹中 每個點到當前根的距離 \(Dis_i\) 並存下來

我們用 bool[] 記錄下(在這個兒子之前的兒子子樹內)到當前根距離為 \(x\) 的點是否存在

由於 \(Dis_{max} = 10 ^ 8\),這裡 bool[] 可能會 \(MLE\),可以使用 std::bitset

於是現在遍歷 \(Dis_i\),查詢 到根距離為 \(K - Dis_i\) 的點 是否存在 即可

遍歷完之後把 令所有 bool[Dis[i]] = 1 並清空 Dis,繼續計算 下一個子樹

遍歷完 所有子樹 即可得到所有 經過當前根的長為 \(K\) 的路徑

每個兒子的子樹內遞迴 即可,注意選取 當前根當前子樹重心

注意這裡 我們把所有詢問離線下來,每次查詢時 遍歷所有詢問來統計答案

雖然這和 線上詢問,每詢問一次就做一次 點分治 的時間複雜度均為 \(O(NM \log N)\)

但是 由於點分治結構常數巨大,故 線上詢問 會獲得 \(12 \sim 14\)左右的 小 常 數

程式碼還好,三個 \(DFS\),分別計算 重心距離答案

#include <bits/stdc++.h>

const int MAXN = 10005;

using namespace std;

struct Edge {
	int to, nxt, w;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v, const int w) {
	E[++ tot] = {v, H[u], w}, H[u] = tot;
	E[++ tot] = {u, H[v], w}, H[v] = tot;
}

bool Vis[MAXN], Ans[MAXN];
int Siz[MAXN], Max[MAXN];
int N, M, K, Cnt, Now, rt;

inline void DFS1 (const int x, const int f) {
	Siz[x] = 1, Max[x] = 0;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS1 (E[i].to, x), Siz[x] += Siz[E[i].to], Max[x] = max (Max[x], Siz[E[i].to]);
	Max[x] = max (Max[x], Now - Siz[x]);
	if (Max[x] < Max[rt] || !rt) rt = x;
}

int P[MAXN], Q[MAXN];

inline void DFS2 (const int x, const int f, const int dis) {
	P[++ Cnt] = dis;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS2 (E[i].to, x, dis + E[i].w);
}

bitset <1 << 27> Ext;
vector <int> S;

inline void DFS3 (const int x, const int f) {
	Ext[0] = 1, S.push_back (0), Vis[x] = 1;
	for (int i = H[x]; i; i = E[i].nxt) {
		if (Vis[E[i].to] || E[i].to == f) continue ;
		Cnt = 0, DFS2 (E[i].to, x, E[i].w);
		
		for (int k = 1; k <= Cnt; ++ k)
			for (int j = 1; j <= M; ++ j)
				if (Q[j] >= P[k]) Ans[j] |= Ext[Q[j] - P[k]];
		for (int k = 1; k <= Cnt; ++ k) Ext[P[k]] = 1, S.push_back (P[k]);
	}
	while (!S.empty ()) Ext[S.back ()] = 0, S.pop_back ();
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			Now = Siz[E[i].to], rt = 0, DFS1 (E[i].to, x), DFS3 (rt, x);
}

int u, v, w;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> M;
	
	for (int i = 2; i <= N; ++ i)
		cin >> u >> v >> w, Add_Edge (u, v, w);
	
	for (int i = 1; i <= M; ++ i) cin >> Q[i];
	
	Now = N, DFS1 (1, 0), DFS3 (rt, 0);
	
	for (int i = 1; i <= M; ++ i)
		cout << (Ans[i] ? "AYE" : "NAY") << '\n';
	
	return 0;
}

Luogu P2634 [國家集訓隊] 聰聰可可

記錄 距離 \(\bmod 3\) 餘數為 \(0, 1, 2\) 的點的個數

然後和 板子題 一樣做就行

#include <bits/stdc++.h>

const int MAXN = 100005;

using namespace std;

struct Edge {
	int to, nxt, w;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v, const int w) {
	E[++ tot] = {v, H[u], w}, H[u] = tot;
	E[++ tot] = {u, H[v], w}, H[v] = tot;
}

bool Vis[MAXN];
int Siz[MAXN], Max[MAXN], Tmp[5];
int N, Cnt, Now, rt;
long long Ans, Gcd;

inline void DFS1 (const int x, const int f) {
	Siz[x] = 1, Max[x] = 0;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS1 (E[i].to, x), Siz[x] += Siz[E[i].to], Max[x] = max (Max[x], Siz[E[i].to]);
	Max[x] = max (Max[x], Now - Siz[x]);
	if (Max[x] < Max[rt] || !rt) rt = x;
}

int P[MAXN];

inline void DFS2 (const int x, const int f, const int dis) {
	P[++ Cnt] = dis % 3;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS2 (E[i].to, x, dis + E[i].w);
}

inline void DFS3 (const int x, const int f) {
	Vis[x] = 1, Tmp[0] = 1;
	for (int i = H[x]; i; i = E[i].nxt) {
		if (Vis[E[i].to] || E[i].to == f) continue ;
		Cnt = 0, DFS2 (E[i].to, x, E[i].w);
		for (int i = 1; i <= Cnt; ++ i) Ans += Tmp[(3 - P[i]) % 3];
		for (int i = 1; i <= Cnt; ++ i) Tmp[P[i]] ++ ;
	}
	Tmp[0] = Tmp[1] = Tmp[2] = 0;
	for (int i = H[x]; i; i = E[i].nxt) 
		if (!Vis[E[i].to] && E[i].to != f)
			Now = Siz[E[i].to], rt = 0, DFS1 (E[i].to, x), DFS3 (rt, x);
}

int u, v, w;

int main () {
	
	cin >> N;
	
	for (int i = 2; i <= N; ++ i)
		cin >> u >> v >> w, Add_Edge (u, v, w);
	
	Now = N, DFS1 (1, 0), DFS3 (rt, 0);
	
	Ans = Ans * 2 + N, Gcd = __gcd (1ll * N * N, Ans);
	
	cout << Ans / Gcd << '/' << 1ll * N * N / Gcd << '\n';
	
	return 0;
}

Luogu P4149 [IOI2011] Race

這就比較板

給定 帶權樹,求 權值和等於 \(k\)最小邊數路徑

顯然 我們只需要管 路徑權值和小於 \(k\) 的部分,由於 \(k \le 10 ^ 6\)

直接存所有 長度小於 \(k\) 的路徑的 最小深度 即可

遍歷兒子子樹時,對這樣的路徑 更新最小深度

每遍歷完一棵子樹,嘗試與 前面兒子子樹 內的答案 合併

即若這棵子樹中存在路徑 \(Len = x < k\),那麼找到前面記錄下來的長度為 \(k - x\)最小深度

兩個加起來,與 當前最小深度 取 最小值,注意特判 端點在當前根 的情況即可

#include <bits/stdc++.h>

const int MAXN = 200005;
const int MAXV = 1000005;

using namespace std;

struct Edge {
	int to, nxt, w;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v, const int w) {
	E[++ tot] = {v, H[u], w}, H[u] = tot;
	E[++ tot] = {u, H[v], w}, H[v] = tot;
}

bool Vis[MAXN];
int Siz[MAXN], Max[MAXN];
int N, K, Now, Cnt, rt;

inline void DFS1 (const int x, const int f) {
	Siz[x] = 1, Max[x] = 0;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS1 (E[i].to, x), Siz[x] += Siz[E[i].to], Max[x] = max (Max[x], Siz[E[i].to]);
	Max[x] = max (Max[x], Now - Siz[x]);
	if (Max[x] < Max[rt] || !rt) rt = x;
}

int Tmp[MAXV];
int Ans = 1e9;

struct Node {
	int dis, dep;
	
	inline bool operator < (const Node &a) const {
		return dis < a.dis;
	}
} P[MAXN];

inline void DFS2 (const int x, const int f, const int dis, const int dep) {
	P[++ Cnt] = {dis, dep};
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)	
			DFS2 (E[i].to, x, dis + E[i].w, dep + 1);
}

vector <int> S;

inline void DFS3 (const int x, const int f) {
	Vis[x] = 1, Tmp[0] = 0;
	for (int i = H[x]; i; i = E[i].nxt) {
		if (Vis[E[i].to] || E[i].to == f) continue ;
		Cnt = 0, DFS2 (E[i].to, x, E[i].w, 1);
		for (int i = 1; i <= Cnt; ++ i) {
			if (P[i].dis > K) continue ;
			else if (P[i].dis == K) Ans = min (Ans, P[i].dep);
			else if (Tmp[K - P[i].dis]) Ans = min (Ans, P[i].dep + Tmp[K - P[i].dis]);
		}
		for (int i = 1; i <= Cnt; ++ i) 
			if (P[i].dis <= K)
				S.push_back (P[i].dis), Tmp[P[i].dis] = Tmp[P[i].dis] ? min (Tmp[P[i].dis], P[i].dep) : P[i].dep;
	}
	
	while (!S.empty ()) Tmp[S.back ()] = 0, S.pop_back ();
	
	for (int i = H[x]; i; i = E[i].nxt) 
		if (!Vis[E[i].to] && E[i].to != f)
			Now = Siz[E[i].to], rt = 0, DFS1 (E[i].to, x), DFS3 (rt, x);
}

int u, v, w;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> K;
	
	for (int i = 2; i <= N; ++ i)
		cin >> u >> v >> w, ++ u, ++ v, Add_Edge (u, v, w);
	
	Now = N, DFS1 (1, 0), DFS3 (rt, 0);
	
	cout << (Ans == 1e9 ? -1 : Ans)  << '\n';
	
	return 0;
}

Luogu P4178 Tree

和上面的板子題 區別不大,從 統計 \(= K\) 的路徑 變成 統計 \(\le K\) 的路徑

同時 增大 \(N\),但 詢問變成單次

同樣 只需要考慮經過根的 \(\le K\) 的路徑條數

可以使用一個 std::set,每次遍歷一棵子樹,統計 每個點到當前根距離 \(Dis_i\)

遍歷 \(Dis_i\) 並在 std::set 中 查詢 有多少小於等於 \(K - Dis_i\) 的點

遍歷完之後把當前所有 \(Dis\) 插入 std::set 即可

但是這樣常數比較大,考慮一種高明的做法

注意到 std::set 甚至不能 方便的查詢小於等於 \(x\) 的數的個數

沒人想 手寫平衡樹

我們把 當前根子樹內所有點到當前根距離 \(Dis_i\) 全部計算並存下來

同時需要記錄 每個點屬於哪個兒子

對這個東西 \(Dis\) 排序,然後使用 雙指標 \(L, R\) 維護

我們考慮 一條有一端點位於 \(L\) 的路徑,找到 \(R\) 使得 \(Dis_L + Dis_R \le K\)

\((L, R]\)所有點(除了和 \(L\) 在 同一個兒子子樹內 的)滿足條件

顯然,隨著 \(L\) 變大(向右),\(R\) 單調不增,正確性存在

這時候開一個陣列 Tmp 記錄一下當前 \([L, R]\)在某個兒子子樹內 的點有多少 就行

每次答案加上 R - L + 1 - Tmp[Son[L]] 即可

#include <bits/stdc++.h>

const int MAXN = 100005;

using namespace std;

struct Edge {
	int to, nxt, w;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v, const int w) {
	E[++ tot] = {v, H[u], w}, H[u] = tot;
	E[++ tot] = {u, H[v], w}, H[v] = tot;
}

bool Vis[MAXN];
int Siz[MAXN], Max[MAXN], Tmp[MAXN];
int N, K, Now, Cnt, rt;
long long Ans;

struct Node {
	int dis, top;
	
	inline bool operator < (const Node &a) const {
		return dis < a.dis;
	}
} P[MAXN];

inline void DFS1 (const int x, const int f) {
	Siz[x] = 1, Max[x] = 0;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS1 (E[i].to, x), Siz[x] += Siz[E[i].to], Max[x] = max (Max[x], Siz[E[i].to]);
	Max[x] = max (Max[x], Now - Siz[x]);
	if (Max[x] <= Max[rt] || !rt) rt = x;
}

inline void DFS2 (const int x, const int f, const int top, const int dis) {
	P[++ Cnt] = {dis, top};
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS2 (E[i].to, x, top, dis + E[i].w);
}

inline void DFS3 (const int x, const int f) {
	Cnt = 0, P[++ Cnt] = {0, 0}, Vis[x] = 1;
	for (int i = H[x]; i; i = E[i].nxt) 
		if (!Vis[E[i].to] && E[i].to != f)
			DFS2 (E[i].to, x, E[i].to, E[i].w);
	sort (P + 1, P + Cnt + 1);
	for (int i = 1; i <= Cnt; ++ i) Tmp[P[i].top] ++ ;
	int L = 1, R = Cnt;
	while (L < R) {
		while (P[L].dis + P[R].dis > K) Tmp[P[R --].top] -- ;
		if (R < L) break ;
		Ans += R - L - Tmp[P[L].top] + 1, Tmp[P[L ++].top] -- ;
	}
	for (int i = 1; i <= Cnt; ++ i) Tmp[P[i].top] = 0;
	for(int i = H[x]; i; i = E[i].nxt) 
		if (!Vis[E[i].to] && E[i].to != f)
			Now = Siz[E[i].to], rt = 0, DFS1 (E[i].to, x), DFS3 (rt, x);
}

int u, v, w;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N;
	
	for (int i = 2; i <= N; ++ i)
		cin >> u >> v >> w, Add_Edge (u, v, w);
	
	cin >> K;
	
	Now = N, DFS1 (1, 0);
	
	DFS3 (rt, 0);
	
	cout << Ans << '\n';
	
	return 0;
}

CF293E Close Vertices

\(P4178\)加強版,對 邊數距離 兩維做出了限制

二維偏序,考慮 一維排序,一維 樹狀陣列

然後應當是簡單的,就是把 \(P4178\) 統計答案的部分 Ans += R - L + 1

換成 Ans += Sum (K - P[L].dep),同時 樹狀陣列 中動態維護 \([L, R]\) 區間的 \(Dep\) 即可

顯然 \(Dep\) 值域比較小,適合插入 樹狀陣列,用 \(Dis\) 就比較壞

注意 去除同一子樹貢獻 時,由於 每個點有 兩維屬性 需要滿足

故不能用 \(P4178\)記錄 \([L, R]\) 之間在當前子樹的點個數 這種方式

考慮樸素的方法,即 算完整個樹答案 然後減去 每個兒子子樹獨立的答案 即可

這題細節有點煩的

考慮由於 樹狀陣列 不能插 \(0\),為了插入 當前根

故可以把 所有點 \(Dep + 1\),然後查詢時查 \(K - Dep_L + 1\)

(當然也可以 特判加上當前根的貢獻)

這樣的話就要注意 \(K - Dep_L + 1\) 可能有 負數,在 查詢時要判掉

然後一個最佳化,我們可以更改 答案貢獻的順序

先減去兒子子樹的獨立貢獻,再加上 整棵樹的貢獻,這樣不用 \(DFS\) 兩遍

#include <bits/stdc++.h>

const int MAXN = 100055;

using namespace std;

struct Edge {
	int to, nxt, w;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v, const int w) {
	E[++ tot] = {v, H[u], w}, H[u] = tot;
	E[++ tot] = {u, H[v], w}, H[v] = tot;
}

bool Vis[MAXN];
int Siz[MAXN], Max[MAXN];
int N, K, W, Cnt, Now, rt;
long long Ans;

inline void DFS1 (const int x, const int f) {
	Siz[x] = 1, Max[x] = 0;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS1 (E[i].to, x), Siz[x] += Siz[E[i].to], Max[x] = max (Max[x], Siz[E[i].to]);
	Max[x] = max (Max[x], Now - Siz[x]);
	if (Max[x] < Max[rt] || !rt) rt = x;
}

struct Node {
	int dis, dep;
	
	inline bool operator < (const Node &a) const {
		return dis < a.dis;
	}
} P[MAXN];

inline void DFS2 (const int x, const int f, const int dis, const int dep) {
	P[++ Cnt] = {dis, dep};
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS2 (E[i].to, x, dis + E[i].w, dep + 1);
}

namespace BIT {
	int T[MAXN];
	
	#define lowbit(x) (x & -x)
	
	inline void Add (int x) {
		while (x <= N + 2) ++ T[x], x += lowbit (x);
	}
	
	inline void Del (int x) {
		while (x <= N + 2) -- T[x], x += lowbit (x);
	}
	
	inline int Sum (int x) {
		if (x < 0 || x > N + 2)  return 0;
		int Ret = 0;
		while (x) Ret += T[x], x -= lowbit (x);
		return Ret;
	}
}

using namespace BIT;

inline void DFS3 (const int x, const int f) {
	Vis[x] = 1, Cnt = 0;
	
	int L, R, C = 1;
	
	for (int i = H[x]; i; i = E[i].nxt) {
		if (Vis[E[i].to] || E[i].to == f) continue ;
		
		DFS2 (E[i].to, x, E[i].w, 1);
		sort (P + C, P + Cnt + 1);
		
		L = C, R = Cnt;
		
		for (int i = C; i <= Cnt; ++ i) Add (P[i].dep + 1);
		
		while (L <= R) {
			while (R >= L && P[L].dis + P[R].dis > W) Del (P[R --].dep + 1);
			if (R < L) break ;
			Del (P[L].dep + 1), Ans -= Sum (K - P[L ++].dep + 1);
		}
		
		C = Cnt + 1;
	}
	
	P[++ Cnt] = {0, 0}, L = 1, R = Cnt;
	
	sort (P + 1, P + Cnt + 1);
	
	for (int i = 1; i <= Cnt; ++ i) Add (P[i].dep + 1);
	
	while (L <= R) {
		while (R >= L && P[L].dis + P[R].dis > W) Del (P[R --].dep + 1);
		if (R < L) break ;
		Del (P[L].dep + 1), Ans += Sum (K + 1 - P[L ++].dep);
	}
	
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			Now = Siz[E[i].to], rt = 0, DFS1 (E[i].to, x), DFS3 (rt, x);
}

int v, w;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> K >> W;
	
	for (int i = 2; i <= N; ++ i) cin >> v >> w, Add_Edge (v, i, w);
	
	Now = N, DFS1 (1, 0), DFS3 (rt, 0);
	
	cout << Ans << '\n';	
	
	return 0;
}

Luogu P5563 [Celeste-B] No More Running

每次 處理出每個兒子的子樹內每個點到當前根的距離,放到一個 std::multiset

然後遍歷這些點,每次把 當前點所在子樹的所有點std::multiset 裡面 刪掉

之後 multiset.lower_bound 查一下 MOD - Dis[i] - 1

可以得到 經過當前根,一端點為 \(i\) 的 最長路徑,更新答案即可

這裡應當 同時更新這條路徑兩端的答案,然後注意 端點為當前根的情況

最後 遍歷完一個兒子的子樹 時 把這個子樹所有點 重新插回 std::multiset

然後就做完了,竟然只要 \(95 ~ Lines\)(無空行 \(68 ~ Lines\)

#include <bits/stdc++.h>

const int MAXN = 100005;
int MOD = 1;

using namespace std;

struct Edge {
	int to, nxt, w;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v, const int w) {
	E[++ tot] = {v, H[u], w}, H[u] = tot;
	E[++ tot] = {u, H[v], w}, H[v] = tot;
}

bool Vis[MAXN];
int Siz[MAXN], Max[MAXN], Ans[MAXN];
int N, K, Cnt, Now, rt;

inline void DFS1 (const int x, const int f) {
	Siz[x] = 1, Max[x] = 0;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS1 (E[i].to, x), Siz[x] += Siz[E[i].to], Max[x] = max (Max[x], Siz[E[i].to]);
	Max[x] = max (Max[x], Now - Siz[x]);
	if (Max[x] < Max[rt] || !rt) rt = x;
}

struct Node {
	int dis, x, nod;
	
	inline bool operator < (const Node &a) const {
		return dis == a.dis ? nod < a.nod : dis > a.dis;
	}
} P[MAXN];

inline void DFS2 (const int x, const int f, const int nod, const int dis) {
	P[++ Cnt] = {dis % MOD, x, nod};
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS2 (E[i].to, x, nod, dis + E[i].w);
}

multiset <Node> S;

inline void DFS3 (const int x, const int f) {
	Vis[x] = 1, Cnt = 0, P[++ Cnt] = {0, x, 0};
	for (int i = H[x]; i; i = E[i].nxt) 
		if (!Vis[E[i].to] && E[i].to != f) 
			DFS2 (E[i].to, x, E[i].to, E[i].w);
	
	for (int i = 1; i <= Cnt; ++ i) S.insert (P[i]);
	
	int RPos = 2, LPos = 2;
	for (int i = H[x]; i; i = E[i].nxt) {
		if (Vis[E[i].to] || E[i].to == f) continue ;
		while (P[RPos].nod == E[i].to && RPos <= Cnt) S.erase  (P[RPos]), ++ RPos;
		
		for (int k = LPos; k < RPos; ++ k) {
			auto it = S.lower_bound (Node {MOD - P[k].dis - 1, 0, 0});
			Ans[P[k].x] = max (Ans[P[k].x], P[k].dis + (*it).dis);
			Ans[(*it).x] = max (Ans[(*it).x], P[k].dis + (*it).dis);
			Ans[x] = max (Ans[x], P[k].dis % MOD);
		}
		
		while (P[LPos].nod == E[i].to && LPos <= Cnt) S.insert (P[LPos]), ++ LPos;
	}
	
	S.clear ();
	
	for (int i = H[x]; i; i = E[i].nxt) 
		if (!Vis[E[i].to] && E[i].to != f)
			Now = Siz[E[i].to], rt = 0, DFS1 (E[i].to, x), DFS3 (rt, x);
}

int u, v, w;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> MOD;
	
	for (int i = 2; i <= N; ++ i)
		cin >> u >> v >> w, Add_Edge (u, v, w);
	
	Now = N, DFS1 (1, 0), DFS3 (rt, 0);
	
	for (int i = 1; i <= N; ++ i) cout << Ans[i] << '\n';
	
	return 0;
}

Luogu P2664 樹上游戲

有點神秘的這個題

相當於說 任選兩點,路徑上 顏色數

顯然任選的話一共 \(N ^ 2\) 種 情況,不需要再什麼 \(\div 2\) 之類的

我們把 貢獻分成兩部分來算,設當前分治中心(當前根)為 \(x\)

\(Part~1.\) 端點為 \(x\)路徑統計,即 計算 \(x\) 的答案

我們以 \(Val_x\) 代表 \(x\)顏色\(Siz_x\) 代表 \(x\)子樹大小\(Col_x\) 代表 這個顏色是否出現

顯然,\(Val_x\) 會給 \(x\) 帶來 \(Siz_x\)貢獻,並標記 \(Col_{val_x}\),表示 \(Val_x\) 這個顏色出現過

那麼 \(x\) 的 子樹內 有和 \(x\) 顏色相同 的怎麼辦?

現在 不考慮,到時候算貢獻的時候 會把它的貢獻去除

然後我們 遍歷 \(x\) 每個兒子的子樹

對於每個顏色(除了 \(Val_x\)),當它在 某個兒子子樹第一次出現 時(設出現在 \(u\)

顯然它會給 \(x\) 帶來 \(Siz_u\) 的貢獻

一端為 \(x\),另一端只要在 \(u\) 子樹內,那麼 \(Val_u\) 就會 給這條路徑貢獻 \(1\) 的顏色數

於是我們對 每個兒子的子樹 \(DFS\),當 第一次出現 \(Val_u\) 時,標記 \(Col_{Val_u}\)

同時 \(x\) 的答案 加上 \(Siz_u\),當 退出這個點 時,把標記刪除

顯然 不同兒子子樹 不相互影響,也就是 每個子樹標記獨立

為方便後續,我們給 每個兒子子樹內某顏色第一次出現的位置 \(u\),都打上一個標記 \(Tmp_u\)

也就是 所有當時標記了 \(Col_{Val_u}\) 的點都打上 \(Tmp_u\)不刪除

這時候 \(x\) 的答案就已經 統計完了


\(Part ~ 2.\) 經過 \(x\)路徑統計

我們 同樣列舉兒子子樹,顯然路徑兩端 不能在同一個兒子子樹

記錄 當前貢獻 \(sum\),就是 \(Part ~ 1.\)\(x\) 的答案

還需要記錄 每種顏色的當前貢獻,記作 \(Cac_x\),存在 \(\sum Cac_x = sum\)

於是 進入子樹時,先把 \(sum\) 減去一個 \(Siz_{son}\),退出時 加回來

同時 對於子樹內所有有 \(Tmp_u\) 標記的 \(u\),減去 \(Siz_u\),顯然,子樹內不能互相貢獻

由於每個 \(Tmp_u\) 代表 特定顏色 貢獻,這裡減的時候 對應 \(Cac_{Val_u}\) 也要減

接著 \(DFS\) 這個子樹,對於一般的點,給它的答案加上 \(sum\) 即可

對於有 \(Tmp_u\) 標記的 \(u\),注意到,當 一個端點在 \(u\) 子樹內

另一個端點在 \(x\) 子樹外且不在任意與 \(u\) 同色點的子樹內 時,\(u\) 這個顏色會 有 \(1\) 的貢獻

而這樣的 另一個端點 個數即為 \(Siz_{rt} - Siz_x - Cac_{Val_u}\),於是 \(sum\) 需要加上 這個貢獻

由於 這個貢獻 顯然只對 \(u\) 所在子樹生效,故 進入 \(u\)加上貢獻,退出時 注意撤銷


於是 遍歷完每個 兒子子樹,最後 正常點分治遞迴處理 即可

時間複雜度 \(O(N \log N)\),有 \(O(N)\)神秘做法,但是 太高明瞭,加上不是 點分治,就沒寫

程式碼細節還是有點的,理清楚了再寫

#include <bits/stdc++.h>

const int MAXN = 100005;

using namespace std;

struct Edge {
	int to, nxt;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v) {
	E[++ tot] = {v, H[u]}, H[u] = tot;
	E[++ tot] = {u, H[v]}, H[v] = tot;
}

bool Vis[MAXN];
int Siz[MAXN], Max[MAXN], Val[MAXN];
int Cac[MAXN];
int N, K, Cnt, Now, sum, rt;
long long Sum[MAXN];

inline void DFS1 (const int x, const int f) {
	Siz[x] = 1, Max[x] = 0;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS1 (E[i].to, x), Siz[x] += Siz[E[i].to], Max[x] = max (Max[x], Siz[E[i].to]);
	Max[x] = max (Max[x], Now - Siz[x]);
	if (Max[x] < Max[rt] || !rt) rt = x;
}

int Tmp[MAXN], Col[MAXN];

vector <int> S, G;

inline void add (const int x, const int top) {
	S.push_back (x), Col[Val[x]] = Tmp[x] = top, sum += Siz[x], Sum[top] += Siz[x], Cac[Val[x]] += Siz[x];
}

inline void Del (const int x, const int f, const int top) {
	if (Tmp[x] == top) G.push_back (x), Cac[Val[x]] -= Siz[x], sum -= Siz[x];
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			Del (E[i].to, x, top);
}

inline void Add (const int x, const int f, const int val, const int top) {
	int rval = val - Cac[Val[x]];
	if (Tmp[x] == top) Cac[Val[x]] += rval, sum += rval;
	
	Sum[x] += sum;
	
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			Add (E[i].to, x, val, top);
	if (Tmp[x] == top) Cac[Val[x]] -= rval, sum -= rval;
}

inline void DFS2 (const int x, const int f, const int top) {
	bool tmp = 0;
	if (Col[Val[x]] != top) tmp = 1, add (x, top);
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS2 (E[i].to, x, top);
	if (tmp) Col[Val[x]] = 0;
}

inline void DFS3 (const int x, const int f) {
	Vis[x] = 1, add (x, x);
	for (int i = H[x]; i; i = E[i].nxt) 
		if (!Vis[E[i].to] && E[i].to != f)
			DFS2 (E[i].to, x, x);
			
	for (int i = H[x]; i; i = E[i].nxt) {
		if (Vis[E[i].to] || E[i].to == f) continue ;
		Cac[Val[x]] -= Siz[E[i].to], sum -= Siz[E[i].to];
		Del (E[i].to, x, x), Add (E[i].to, x, Siz[x] - Siz[E[i].to], x);
		Cac[Val[x]] += Siz[E[i].to], sum += Siz[E[i].to];
		while (!G.empty ()) Cac[Val[G.back ()]] += Siz[G.back ()], sum += Siz[G.back ()], G.pop_back ();
	}
	
	while (!S.empty ()) Cac[Val[S.back ()]] = 0, S.pop_back ();
	
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			Now = Siz[E[i].to], sum = rt = 0, DFS1 (E[i].to, x), DFS1 (rt, x), DFS3 (rt, x);
}

int u, v;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N;
	
	for (int i = 1; i <= N; ++ i) cin >> Val[i];
	
	for (int i = 2; i <= N; ++ i) cin >> u >> v, Add_Edge (u, v);	
	
	Now = N, DFS1 (1, 0), DFS1 (rt, 0), DFS3 (rt, 0);
	
	for (int i = 1; i <= N; ++ i) cout << Sum[i] << '\n';
	
	return 0;
}

CF150E Freezing with Style

給定 帶權樹,求邊數在 \([L, R]\) 間的路徑,使得 路徑邊權中位數最大,輸出 任一可行路徑端點

看到中位數,不好搞,考慮 二分答案 \(Ans\)

把所有 原始邊權 大於等於當前 \(Ans\) 的邊 邊權設為 \(1\),其餘 設為 \(-1\)

於是 判定 就是 樹上是否存在一條 合法路徑 邊權和大於 \(0\),這個 \(trick\) 好像很

於是我們其實就是求 樹上 邊數在 \([L, R]\) 間的路徑 邊權和最大值,和上一道題很像

但實際上還是比 上一個題 要強一些,主要在於 邊數限制是一個區間

上一道題限制 邊權等於 \(k\),相當於 邊數限制是一個常數

我們先跑出 每個點到當前根的邊數 同時 跑出邊權和

同樣的,對於 同一個邊數,我們只記錄 最大邊權和

顯然,對於一條 邊數 \(i\) 的路徑,我們需要 邊數在 \([L - i, R - i]\) 的路徑 與之匹配

容易發現,我們只需要這些路徑裡的 最大值,這就是一個 滑動視窗,於是 單調佇列維護 即可

這裡有一個細節,如果 直接順序遍歷兒子,那麼 單調佇列值域可能上到 \(Size_{max}\)

也就是 單次滑動視窗時間為 \(O(Size_{max})\),即 最大兒子子樹大小複雜度比較假

考慮把兒子 按 子樹最大深度 從小到大 排序,先處理 深度較小的

這樣可以保證處理到 深度為 \(Dep\) 的子樹時,單調佇列只維護 深度小於等於 \(Dep\) 的部分

顯然 前面的子樹深度均不大於 \(Dep\),那麼維護大於 \(dep\) 的部分是 無意義的

這樣一次 滑動視窗的時間就是 \(O(Dep_{max})\),不大於 \(O(Size_{now})\)複雜度正確

顯然,一棵樹上一共 \(N\) 個點,每個點只會被 當成兒子一次

兒子按最大深度 排序這點 不是瓶頸

最後注意 維護邊權和同時維護端點,輸出的可 不是最大中位數

細節還挺多的,比如 不要在 \(DFS\) 裡面 建立 std::deque直接手寫會好一點

可以看程式碼,這個題是 有高階拍子的

#include <bits/stdc++.h>

const int MAXN = 100005;
const int INF  = 1e9;

using namespace std;

struct Edge {
	int to, nxt, w;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v, const int w) {
	E[++ tot] = {v, H[u], w}, H[u] = tot;
	E[++ tot] = {u, H[v], w}, H[v] = tot; 
}

bool Vis[MAXN];
int Siz[MAXN], Max[MAXN], Dep[MAXN], Mxd[MAXN], Val[MAXN];
int N, K, Ans, Now, rt, L, R;
int Pos1, Pos2;

inline bool Cmp (const int a, const int b) {
	return Mxd[a] < Mxd[b];
}

inline void DFS1 (const int x, const int f, const int d = 0) {
	Siz[x] = 1, Max[x] = 0, Dep[x] = d, Mxd[x] = d;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS1 (E[i].to, x, d + 1), Siz[x] += Siz[E[i].to], Max[x] = max (Max[x], Siz[E[i].to]), Mxd[x] = max (Mxd[x], Mxd[E[i].to]);
	Max[x] = max (Max[x], Now - Siz[x]);
	if (Max[x] < Max[rt] || !rt) rt = x;
}

struct DisPos {
	int dis, pos;
	
	inline bool operator < (const DisPos &a) const {
		return dis < a.dis;
	}
} Tmp[MAXN], Dis[MAXN];

inline void DFS2 (const int x, const int f, const int dep, const int dis) {
	if (dis > Tmp[dep].dis) Tmp[dep] = {dis, x};
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS2 (E[i].to, x, dep + 1, dis + E[i].w);
}

int Q[MAXN];

inline bool DFS3 (const int x, const int f) {
	vector <int> P;
	Vis[x] = 1;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			P.push_back (E[i].to), Val[E[i].to] = E[i].w;
	
	sort (P.begin (), P.end (), Cmp);
	
	int now, mxd = 0, l, r;
	
	for (int i = 1; i <= Mxd[x]; ++ i) Dis[i].dis = - INF;
	Dis[0] = {0, x};
	
	for (auto i : P) {
		DFS2 (i, x, Dep[i], Val[i]);
		
		now = max (0, L - min (R, Mxd[i])), l = 1, r = 0;
		
		for (int k = min (R, Mxd[i]); k >= 1; -- k) {
			while (now <= mxd && now <= R - k) {
				while (r >= l && Dis[now].dis > Dis[Q[r]].dis) -- r;
				Q[++ r] = now, ++ now;
			}
			
			while (r >= l && Q[l] < L - k) ++ l;
			if (r >= l && Q[l] <= R - k && Dis[Q[l]].dis + Tmp[k].dis >= 0)	
				return Pos1 = Dis[Q[l]].pos, Pos2 = Tmp[k].pos, 1;
		}
		
		for (int k = Dep[x]; k <= Mxd[i]; ++ k) Dis[k] = max (Dis[k], Tmp[k]);
		for (int k = Dep[x]; k <= Mxd[i]; ++ k) Tmp[k] = {- INF, 0};
		
		mxd = max (mxd, Mxd[i]);
	}
	
	for (auto i : P) {
		Now = Siz[i], rt = 0, DFS1 (i, x), DFS1 (rt, x);
		if (DFS3 (rt, x)) return 1;
	}
	
	return 0;
}

struct edge {
	int u, v, w;
} G[MAXN];

inline bool Check (const int x) {
	memset (H, 0, sizeof H), tot = 0;
	memset (Vis, 0, sizeof Vis);
	for (int i = 0; i <= N; ++ i) Tmp[i].dis = - INF;
	
	for (int i = 2; i <= N; ++ i) Add_Edge (G[i].u, G[i].v, G[i].w >= x ? +1 : -1);
	
	Now = N, rt = 0, DFS1 (1, 0), DFS1 (rt, 0);
	
	return DFS3 (rt, 0);
}


inline void Solve (int l, int r) {
	int m = (l + r) >> 1;
	
	while (l <= r) {
		m = (l + r) >> 1;
		if (Check (m)) Ans = m, l = m + 1;
		else r = m - 1;
	}
	Check (Ans);
}

int MinW, MaxW;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> L >> R;
	
	for (int i = 2; i <= N; ++ i) {
		cin >> G[i].u >> G[i].v >> G[i].w;
		MinW = min (MinW, G[i].w), MaxW = max (MaxW, G[i].w);
	}
	
	Solve (MinW, MaxW);
	
	cout << Pos1 << ' ' << Pos2 << '\n';
		
	return 0;
}

Luogu P4292 [WC2010] 重建計劃

上個題的 雙倍經驗,就是把 中位數 改成 平均值,但是 實數二分

直接改程式碼,把權值從 \(\pm 1\) 改成 \(Ans - w_i\),正確性就有保證,精度沒有被卡

難受的是 被卡常了記憶重心 才能過

注意到 記錄重心 這個技巧,還 不能直接對每個點記錄

因為點分治時並 不是從當前根到當前根兒子 而是到當前根兒子子樹重心

也就是 當前根兒子 可能會被 多次訪問其代表的子樹是不一樣的

將重心記錄到 當前根意義下每個兒子會寄掉

應當把 當前根兒子子樹的重心集 記錄到 當前根 上,這樣就正確了

#include <bits/stdc++.h>

const int MAXN = 100005;
const double INF  = 1e+8;
const double EPS  = 1e-4;

using namespace std;

struct Edge {
	int to, nxt;
	double w;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v, const double w) {
	E[++ tot] = {v, H[u], w}, H[u] = tot;
	E[++ tot] = {u, H[v], w}, H[v] = tot; 
}

bool Vis[MAXN];
int Siz[MAXN], Max[MAXN], Dep[MAXN], Mxd[MAXN];
double Val[MAXN], Ans;
int N, K, Now, rt, L, R;
int Pos1, Pos2;

inline bool Cmp (const int a, const int b) {
	return Mxd[a] < Mxd[b];
}

inline void DFS1 (const int x, const int f, const int d = 0) {
	Siz[x] = 1, Max[x] = 0, Dep[x] = d, Mxd[x] = d;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS1 (E[i].to, x, d + 1), Siz[x] += Siz[E[i].to], Max[x] = max (Max[x], Siz[E[i].to]), Mxd[x] = max (Mxd[x], Mxd[E[i].to]);
	Max[x] = max (Max[x], Now - Siz[x]);
	if (Max[x] < Max[rt] || !rt) rt = x;
}

struct DisPos {
	double dis;
	int pos;
	
	inline bool operator < (const DisPos &a) const {
		return dis < a.dis;
	}
} Tmp[MAXN], Dis[MAXN];

inline void DFS2 (const int x, const int f, const int dep, const double dis) {
	if (dis > Tmp[dep].dis) Tmp[dep] = {dis, x};
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS2 (E[i].to, x, dep + 1, dis + E[i].w);
}

int Q[MAXN];
vector <int> S[MAXN];

inline bool DFS3 (const int x, const int f) {
	vector <int> P;
	
	Vis[x] = 1;
	
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			P.push_back (E[i].to), Val[E[i].to] = E[i].w;
	
	sort (P.begin (), P.end (), Cmp);
	
	int now, mxd = 0, l, r, pos = 0;
	
	for (int i = 1; i <= Mxd[x]; ++ i) Dis[i].dis = - INF;
	Dis[0] = {0, x};
	
	for (auto i : P) {
		DFS2 (i, x, Dep[i], Val[i]);
		
		now = max (0, L - min (R, Mxd[i])), l = 1, r = 0;
		
		for (int k = min (R, Mxd[i]); k >= 1; -- k) {
			while (now <= mxd && now <= R - k) {
				while (r >= l && Dis[now].dis > Dis[Q[r]].dis) -- r;
				Q[++ r] = now, ++ now;
			}
			
			while (r >= l && Q[l] < L - k) ++ l;
			if (r >= l && Q[l] <= R - k && Dis[Q[l]].dis + Tmp[k].dis >= 0)	
				return Pos1 = Dis[Q[l]].pos, Pos2 = Tmp[k].pos, 1;
		}
		
		for (int k = Dep[x]; k <= Mxd[i]; ++ k) Dis[k] = max (Dis[k], Tmp[k]);
		for (int k = Dep[x]; k <= Mxd[i]; ++ k) Tmp[k] = {- INF, 0};
		
		mxd = max (mxd, Mxd[i]);
	}
	
	bool k = S[x].empty ();
	
	for (auto i : P) {
		Now = Siz[i], rt = 0;
		if (k) DFS1 (i, x), S[x].push_back (rt);
		else rt = S[x][pos ++];
		
		DFS1 (rt, x);
		if (DFS3 (rt, x)) return 1;
	}
	
	return 0;
}

struct edge {
	int u, v;
	double w;
} G[MAXN];

inline bool Check (const double x) {
	memset (H, 0, sizeof H), tot = 0;
	memset (Vis, 0, sizeof Vis);
	
	for (int i = 0; i <= N; ++ i) Tmp[i].dis = - INF;
	
	for (int i = 2; i <= N; ++ i) Add_Edge (G[i].u, G[i].v, G[i].w - x);
	
	Now = N, rt = 0, DFS1 (1, 0), DFS1 (rt, 0);
	
	return DFS3 (rt, 0);
}


inline void Solve (double l, double r) {
	double m = (l + r) / 2.0;
	
	while (l + EPS < r) {
		m = (l + r) / 2.0;
		if (Check (m)) Ans = m, l = m;
		else r = m;
	}
	Check (Ans);
}

double MinW, MaxW;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> L >> R;
	
	for (int i = 2; i <= N; ++ i) {
		cin >> G[i].u >> G[i].v >> G[i].w;
		MinW = min (MinW, G[i].w), MaxW = max (MaxW, G[i].w);
	}
	
	Solve (MinW, MaxW);
	
	cout << fixed << setprecision (3) << Ans << '\n';
	
//	cout << Pos1 << ' ' << Pos2 << '\n';
		
	return 0;
}

UVA12161 鐵人比賽 Ironman Race in Treeland

給定一棵 帶權樹,每一條邊包含 長度 \(Dep\)費用 \(Dis\) 兩個引數

選擇一條 總費用不超過給定值 的路徑,使 路徑總長度最大

感覺沒法簡單維護,直接上 動態開點線段樹

\(DFS\) 兒子子樹得到 每個點關於當前根\(Dep, Dis\)

在詢問之後 每次往對應 \(Dis\) 位置插入 \(Dep\)

詢問即是查詢 \([0, K - Dis]\) 區間內 \(Dep\)最大值,每個點更新就行

注意 多組資料

線段樹的清空 是一個需要注意的細節,可以參考以下實現

#include <bits/stdc++.h>

const int MAXN = 100005;
const int LOGN = 40;
const int INF  = 1e9;

using namespace std;

namespace SegTree {
	struct TNode {
		int L, R;
		int lc, rc;
		int Max;
	} T[MAXN * LOGN];
	
	#define LC T[x].lc
	#define RC T[x].rc
	#define M ((T[x].L + T[x].R) >> 1)
	
	int Tot = 0, Rt = 0;
	
	inline void Maintain (const int x) {
		T[x].Max = max (T[LC].Max, T[RC].Max);
	}
	
	inline void Ins (const int p, const int v, int &x, const int L = 0, const int R = INF) {
		if (!x) x = ++ Tot, T[x].Max = v, LC = RC = 0;
		T[x].L = L, T[x].R = R;
		if (L == R) return T[x].Max = max(T[x].Max, v), void ();
		if (p <= M) Ins (p, v, LC, L, M);
		else Ins (p, v, RC, M + 1, R);
		Maintain (x);
	}
	
	inline void Del (const int p, const int x = 1) {
		if (!x || T[x].L == T[x].R) return ;
		int lc = LC, rc = RC, m = M;
		LC = RC = T[x].L = T[x].R = T[x].Max = 0;
		if (p <= m) Del (p, lc);
		else Del (p, rc);
	}
	
	inline int Query (const int L, const int R, const int x = 1) {
		if (!x || L > T[x].R || T[x].L > R) return 0;
		if (L <= T[x].L && T[x].R <= R) return T[x].Max;
		return max (Query (L, R, LC), Query (L, R, RC));
	}
	
	#undef M
}

struct Edge {
	int to, nxt, w, d;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v, const int w, const int d) {
	E[++ tot] = {v, H[u], w, d}, H[u] = tot;
	E[++ tot] = {u, H[v], w, d}, H[v] = tot;
}

bool Vis[MAXN];
int Siz[MAXN], Max[MAXN];
int N, K, W, Cnt, Now, rt;

inline void DFS1 (const int x, const int f) {
	Siz[x] = 1, Max[x] = 0;
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS1 (E[i].to, x), Siz[x] += Siz[E[i].to], Max[x] = max (Max[x], Siz[E[i].to]);
	Max[x] = max (Max[x], Now - Siz[x]);
	if (Max[x] < Max[rt] || !rt) rt = x;
}

struct Node {
	int dep, dis;
} P[MAXN];

int MaxDep;

inline void DFS2 (const int x, const int f, const int dep, const int dis) {
	P[++ Cnt] = {dep, dis};
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			DFS2 (E[i].to, x, dep + E[i].d, dis + E[i].w);
}

inline void DFS3 (const int x, const int f) {
	Vis[x] = 1, SegTree::Ins (0, 0, SegTree::Rt);
	
	for (int i = H[x]; i; i = E[i].nxt) 
		if (!Vis[E[i].to] && E[i].to != f) {
			Cnt = 0, DFS2 (E[i].to, x, E[i].d, E[i].w);
			for (int k = 1; k <= Cnt; ++ k) 
				if (K >= P[k].dis) MaxDep = max (MaxDep, SegTree::Query (0, K - P[k].dis) + P[k].dep);
			for (int k = 1; k <= Cnt; ++ k) SegTree::Ins (P[k].dis, P[k].dep, SegTree::Rt);
		}
		
	SegTree::Tot = SegTree::Rt = 0;
	
	for (int i = H[x]; i; i = E[i].nxt)
		if (!Vis[E[i].to] && E[i].to != f)
			Now = Siz[E[i].to], rt = 0, DFS1 (E[i].to, x), DFS3 (rt, x);
}

int u, v, w, d;

inline void Solve (const int Sol) {
	cin >> N >> K;
	
	for (int i = 2; i <= N; ++ i)
		cin >> u >> v >> w >> d, Add_Edge (u, v, w, d);
	
	Now = N, rt = 0, DFS1 (1, 0), DFS3 (rt, 0);
	
	cout << "Case " << Sol << ": " << MaxDep << '\n';
	
	memset (H, 0, sizeof H);
	tot = 0, SegTree::Tot = SegTree::Rt = 0, MaxDep = 0;
	memset (Vis, 0, sizeof Vis);
}

int Ti;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> Ti;
	
	for (int i = 1; i <= Ti; ++ i) Solve (i);
	
	return 0;
}

Luogu P3292 [SCOI2016] 幸運數字

點分治 沒啥關係啊,雖然好像有 點分治做法

看到 子集異或最大值,可以想到 異或線性基

注意到 線性基是可以合併的,複雜度 \(O (\log ^ 2 V)\),於是很快想到一個 暴力做法

即 預處理 倍增 \(LCA\)\(2 ^ k\) 級祖先的同時 處理到 \(2 ^ k\) 級祖先這段路異或線性基

最後和 \(LCA\) 一起 倍增合併 即可,時間複雜度 \(O((N + Q) \log N \log ^ 2 V)\)

\(1.4e10\) 左右,即使有 \(6s\) 也只能過 \(90 ~ pts\)考慮最佳化

如果 不想換掉倍增 的話怎麼辦呢?

注意到 \(Q\)\(N\) 整整大了 \(10\) 倍,如果能最佳化 詢問複雜度,這個問題可能就解決了

容易發現,線性基是一種可以重複貢獻的資訊,也就是可以 支援 \(RMQ\) 來做

於是 查詢複雜度就變成了 \(O(Q \log ^ 2 V)\),總複雜度 \(O(N \log N \log ^ 2 V + Q \log ^ 2 V)\)

\(2e9\),但是 常數較小,可以透過

#include <bits/stdc++.h>

const int MAXN = 20005;
const int LOGN = 60;
const int logn = 15;

using namespace std;

struct Edge {
	int to, nxt;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v) {
	E[++ tot] = {v, H[u]}, H[u] = tot;
	E[++ tot] = {u, H[v]}, H[v] = tot;
}

struct Basis {
	unsigned long long A[LOGN + 5] = {0};
	
	inline void Clr () {
		for (int i = 0; i <= LOGN; ++ i) A[i] = 0;
	}
	
	inline void Ins (unsigned long long x) {
		for (int i = LOGN; ~ i; -- i)
			if ((x >> i) & 1) A[i] ? x ^= A[i] : (A[i] = x, i = 0);
	}
	
	inline unsigned long long Max () {
		unsigned long long Ret = 0;
		for (int i = LOGN; ~ i; -- i) Ret = max (Ret, Ret ^ A[i]);
		return Ret;
	}

} G[MAXN][logn + 1];

inline void Merge (Basis &a, const Basis &b) {
	for (int i = LOGN; ~ i; -- i) a.Ins (b.A[i]);
}

int N, K;
int u, v;
long long V[MAXN];
int D[MAXN], LOG[MAXN];
int F[MAXN][logn + 1];

inline void DFS (const int x, const int f) {
	
	F[x][0] = f, G[x][0].Ins (V[x]), D[x] = D[f] + 1;
	
	for (int i = 1; i <= logn; ++ i) {
		F[x][i] = F[F[x][i - 1]][i - 1];
		G[x][i] = G[x][i - 1];
		Merge (G[x][i], G[F[x][i - 1]][i - 1]);
	}
	
	for (int i = H[x]; i; i = E[i].nxt)
		if (E[i].to != f) DFS (E[i].to, x);
}

inline int LCA (int u, int v) {
	if (D[u] < D[v]) swap (u, v);
	while (D[u] > D[v]) u = F[u][LOG[D[u] - D[v]]];
	if (u == v) return u;
	for (int i = logn; ~ i; -- i)
		if (F[u][i] != F[v][i])
			u = F[u][i], v = F[v][i];
	return F[u][0];
}

inline int Kth (int u, int k) {
	if (k < 0) return u;
	while (k) u = F[u][LOG[k]], k -= (1 << LOG[k]);
	return u;
}

inline unsigned long long Que (int u, int v) {
	if (D[u] < D[v]) swap (u, v);
	int lca = LCA (u, v), lu = LOG[D[u] - D[lca]], lv = LOG[D[v] - D[lca]];
	int su = Kth (u, D[u] - D[lca] - (1 << lu) + 1), sv = Kth (v, D[v] - D[lca] - (1 << lv) + 1);
	Basis x;
	
	Merge (x, G[u][lu]);
	Merge (x, G[su][lu]);
	Merge (x, G[v][lv]);
	Merge (x, G[sv][lv]);
	
	return x.Max ();
}

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> K;
	
	for (int i = 1; i <= N; ++ i) cin >> V[i];
	
	for (int i = 2; i <= N; ++ i) cin >> u >> v, Add_Edge (u, v);
	
	DFS (1, 0);
	
	for (int i = 2; i <= N; ++ i) LOG[i] = LOG[i >> 1] + 1;
	
	for (int i = 1; i <= K; ++ i) {
		cin >> u >> v;
		cout << Que (u, v) << '\n';
	}
	return 0;
}

然後這個東西 跑得比 \(\textsf H \color {red} \textsf {anghang}\) 還慢,火大!

注意到我們的這個路徑 實質上就是一種區間資訊區間子集異或最大值

可以使用 字首線性基 進行最佳化,字首線性基 相關這裡略過

實現上,我們的線性基 從父親處繼承每次只插入當前節點的權值

節點深度 表示 位置

於是我們 只需要處理 \(LCA\),查詢時 \(u \to LCA\)\(LCA \to v\) 兩段 分別區間查詢 即可

時間複雜度 \(O((Q + N) (\log N + \log V))\),實際上只需要 \(\frac {1} {10}\) 的時間 就能跑完

#include <bits/stdc++.h>

const int MAXN = 20005;
const int LOGN = 60;
const int logn = 15;

using namespace std;

struct Edge {
	int to, nxt;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v) {
	E[++ tot] = {v, H[u]}, H[u] = tot;
	E[++ tot] = {u, H[v]}, H[v] = tot;
}

struct Basis {
	unsigned long long A[LOGN + 5] = {0};
	unsigned int P[LOGN + 5] = {0};
	
	inline void Clr () {
		for (int i = 0; i <= LOGN; ++ i) A[i] = 0;
		for (int i = 0; i <= LOGN; ++ i) P[i] = 0;
	}
	
	inline void Ins (unsigned long long x, unsigned int p) {
		for (int i = LOGN; ~ i; -- i) {
			if ((x >> i) & 1) {
				if (A[i]) {
					if (P[i] < p) swap (P[i], p), swap (A[i], x);
					x ^= A[i];
				} else A[i] = x, P[i] = p, i = 0;
			} 
		}
	}
	
	inline unsigned long long Max (const unsigned int L) {
		unsigned long long Ret = 0;
		for (int i = LOGN; ~ i; -- i) { 
			if (P[i] < L) continue ;
			Ret = max (Ret, Ret ^ A[i]);
		}
		return Ret;
	}

} G[MAXN];

inline void Merge (Basis &a, const Basis &b, const unsigned int d) {
	for (int i = LOGN; ~ i; -- i) if (b.A[i] && b.P[i] >= d) a.Ins (b.A[i], b.P[i]);
}

int N, K;
int u, v;
long long V[MAXN];
int D[MAXN], LOG[MAXN];
int F[MAXN][logn + 1];

inline void DFS (const int x, const int f) {
	F[x][0] = f, D[x] = D[f] + 1, G[x] = G[f], G[x].Ins (V[x], D[x]);
	
	for (int i = 1; i <= logn; ++ i) F[x][i] = F[F[x][i - 1]][i - 1];
	for (int i = H[x]; i; i = E[i].nxt)
		if (E[i].to != f) DFS (E[i].to, x);
}

inline int LCA (int u, int v) {
	if (D[u] < D[v]) swap (u, v);
	while (D[u] > D[v]) u = F[u][LOG[D[u] - D[v]]];
	if (u == v) return u;
	for (int i = logn; ~ i; -- i)
		if (F[u][i] != F[v][i])
			u = F[u][i], v = F[v][i];
	return F[u][0];
}

inline unsigned long long Que (int u, int v) {
	int lca = LCA (u, v);
	Basis x; x.Clr ();
	
	Merge (x, G[u], D[lca]);
	Merge (x, G[v], D[lca]);
	
	return x.Max (D[lca]);
}

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> K;
	
	for (int i = 1; i <= N; ++ i) cin >> V[i];
	
	for (int i = 2; i <= N; ++ i) cin >> u >> v, Add_Edge (u, v);
	
	DFS (1, 0);
	
	for (int i = 2; i <= N; ++ i) LOG[i] = LOG[i >> 1] + 1;
	
	for (int i = 1; i <= K; ++ i) {
		cin >> u >> v;
		cout << Que (u, v) << '\n';
	}
	
	return 0;
}

相關文章