树分治 - 点分治

树分治

\(2024.05.04 ~ UPD:\) 修正了 \(P3292\) 前缀线性基法时间复杂度线性基合并\(\log ^ 2\)

\(O((Q + N)(\log N + \log V)) \to O((Q + N)(\log N + \log ^ 2V))\)

点分治

本质上是一种 思想,用于统计 带权树多条路径信息 的 方法之一

一般来讲,点分治的基本思路 如下

  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 ^ 2 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;
}

posted @ 2024-05-04 07:38  FAKUMARER  阅读(16)  评论(0编辑  收藏  举报