点分治相关
点分治
点分治适合处理大规模的树上路径信息问题。
实现
给定一棵有 \(n\) 个点的树,询问树上距离为 \(k\) 的点对是否存在。
\(n \leq 10^4\)
考虑简单深搜,对于任意子树,把路径分为经过根节点的路径和不经过根节点的路径。
对于不经过根节点的路径,我们递归处理即可。
对于经过根节点的路径,将其视作从子树内一个点到根节点再到子树内另一个点,于是先求出所有点到根的路径长度,再使用双指针合并,注意一条路径会被计算两次。
很好的想法,但是会被卡到 \(O(n^2)\) 。
注意到算完一个点后,该点的子树是互不影响的。于是考虑对于每个子树,先找到重心后以重心为根计算。
显然每次统计时最大子树大小不会超过一半,时间复杂度为 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(const int u, const int v, const int w) {
e[u].emplace_back(v, w);
}
} G;
vector<int> vec;
int siz[N], mxsiz[N], dis[N], top[N], qry[N];
bool vis[N], ans[N];
int n, m, root;
void getroot(int u, int f, int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (auto it : G.e[u]) {
int v = it.first;
if (v == f || vis[v])
continue;
getroot(v, u, Siz), siz[u] += siz[v];
mxsiz[u] = max(mxsiz[u], siz[v]);
}
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
int getsiz(int u, int f) {
int siz = 1;
for (auto it : G.e[u]) {
int v = it.first;
if (v != f && !vis[v])
siz += getsiz(v, u);
}
return siz;
}
void getdis(int u, int f) {
vec.emplace_back(u);
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (v != f && !vis[v])
top[v] = top[u], dis[v] = dis[u] + w, getdis(v, u);
}
}
inline void calc(int u) {
vec.clear(), vec.emplace_back(u);
top[u] = u, dis[u] = 0;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (!vis[v])
top[v] = v, dis[v] = w, getdis(v, u);
}
sort(vec.begin(), vec.end(), [](const int &x, const int &y) { return dis[x] < dis[y]; });
for (int i = 1; i <= m; ++i) {
if (ans[i])
continue;
for (auto itl = vec.begin(), itr = prev(vec.end()); itl != itr;) {
if (dis[*itl] + dis[*itr] < qry[i])
++itl;
else if (dis[*itl] + dis[*itr] > qry[i])
--itr;
else if (top[*itl] == top[*itr]) {
if (dis[*itl] == dis[*next(itl)])
++itl;
else
--itr;
} else {
ans[i] = true;
break;
}
}
}
}
void solve(int u) {
vis[u] = true, calc(u);
for (auto it : G.e[u]) {
int v = it.first;
if (!vis[v])
root = 0, getroot(v, 0, getsiz(v, u)), solve(root);
}
}
signed main() {
n = read(), m = read();
for (int i = 1; i < n; ++i) {
int u = read(), v = read(), w = read();
G.insert(u, v, w), G.insert(v, u, w);
}
for (int i = 1; i <= m; ++i)
ans[i] = !(qry[i] = read());
root = 0, getroot(1, 0, n), solve(root);
for (int i = 1; i <= m; ++i)
puts(ans[i] ? "AYE" : "NAY");
return 0;
}
应用
给定一棵 \(n\) 个点带边权的树,求树上两点距离小于等于 \(k\) 的点对数量。
\(1 \leq n \leq 4 \times 10^4, 0 \leq w \leq 10^3, 0 \leq k \leq 2 \times 10^4\)
与模板类似。
给定一棵 \(n\) 个节点的树,\(m\) 次询问和一个点 \(x\) 距离为 \(k\) 的点的数量。
\(1 \leq n, m \leq 10^5\)
指定一个点为根,那么对于任意一个非根的节点 \(x\) ,与它距离为 \(k\) 的点无非会有两种情况:子树内或子树外。
对于在子树外的节点,记 \(t_i\) 表示与根距离为 \(i\) 的点的个数。如果一个子树外的点 \(y\) 与 \(x\) 的距离为 \(k\) ,那么它们一定满足 \(dep_x + dep_y = k\) 。所以可以直接把对应的桶 \(t_{k - dep_x}\)计入答案。
子树内的结点分治处理即可,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<pair<int, int> > qry[N];
vector<pair<int, int> > tmp;
int siz[N], mxsiz[N], dep[N], buc[N], ans[N];
bool vis[N];
int n, m, root, mxdep;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = c == '-';
while (c < '0' || c > '9')
c = getchar(), sign |= c == '-';
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline void clear() {
G.clear(n);
memset(vis + 1, false, sizeof(bool) * n);
memset(ans + 1, 0, sizeof(int) * n);
for (int i = 1; i <= n; ++i)
qry[i].clear();
}
void getroot(int u, int f, int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (int v : G.e[u])
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
mxsiz[u] = max(mxsiz[u], Siz - mxsiz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
int getsiz(int u, int f) {
int siz = 1;
for (int v : G.e[u])
if (!vis[v] && v != f)
siz += getsiz(v, u);
return siz;
}
void dfs(int u, int f) {
++buc[dep[u]], mxdep = max(mxdep, dep[u]);
for (auto it : qry[u])
if (it.first >= dep[u])
tmp.emplace_back(it.first - dep[u], it.second);
for (int v : G.e[u])
if (v != f && !vis[v])
dep[v] = dep[u] + 1, dfs(v, u);
}
inline void calc(int u) {
tmp.clear(), mxdep = dep[u] = 0, dfs(u, 0);
for (auto it : tmp)
ans[it.second] += buc[it.first];
memset(buc, 0, sizeof(int) * (mxdep + 1));
for (int v : G.e[u])
if (!vis[v]) {
tmp.clear(), mxdep = dep[v] = 1, dfs(v, u);
for (auto it : tmp)
ans[it.second] -= buc[it.first];
memset(buc, 0, sizeof(int) * (mxdep + 1));
}
}
void solve(int u) {
vis[u] = true, calc(u);
for (int v : G.e[u])
if (!vis[v])
root = 0, getroot(v, 0, getsiz(v, u)), solve(root);
}
signed main() {
int T = read();
while (T--) {
n = read(), m = read();
clear();
for (int i = 1; i < n; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
for (int i = 1; i <= m; ++i) {
int x = read(), k = read();
qry[x].emplace_back(k, i);
}
root = 0, getroot(1, 0, n), solve(root);
for (int i = 1; i <= m; ++i)
printf("%d\n", ans[i]);
}
return 0;
}
给出一棵树,每个点有颜色,对每个点 \(x\) 求所有点到 \(x\) 路径上颜色数量的和。
\(n \leq 10^5\)
看到树上路径,考虑点分治。设 \(cnt_i\) 表示当前统计 \(u\) 子树内所有到 \(u\) 的路径有多少条路径出现颜色 \(i\) 。这个是好处理的,只要到 \(u\) 的链上第一次出现颜色 \(i\) 时令 \(cnt_i \leftarrow cnt_i + siz_v\) 即可。
然后剩下的就是对于 \(v\) 子树外的贡献减去 \(v\) 子树内的贡献即可,容斥上应该有一些细节需要处理。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
ll ans[N], Sum;
int col[N], siz[N], mxsiz[N], sta[N], chaincol[N], cnt[N];
bool vis[N];
int n, root, outsiz;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
void getroot(int u, int f, const int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (int v : G.e[u])
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
int getsiz(int u, int f) {
siz[u] = 1;
for (int v : G.e[u])
if (!vis[v] && v != f)
siz[u] += getsiz(v, u);
return siz[u];
}
void addcol(int u, int f) {
if (!chaincol[col[u]])
cnt[col[u]] += siz[u], Sum += siz[u];
++chaincol[col[u]];
for (int v : G.e[u])
if (!vis[v] && v != f)
addcol(v, u);
--chaincol[col[u]];
}
void delcol(int u, int f) {
if (!chaincol[col[u]])
cnt[col[u]] -= siz[u], Sum -= siz[u];
++chaincol[col[u]];
for (int v : G.e[u])
if (!vis[v] && v != f)
delcol(v, u);
--chaincol[col[u]];
}
void dfs(int u, int f, int num, ll sum) {
if (!chaincol[col[u]])
++num, sum += cnt[col[u]];
++chaincol[col[u]], ans[u] += Sum - sum + num * outsiz;
for (int v : G.e[u])
if (!vis[v] && v != f)
dfs(v, u, num, sum);
--chaincol[col[u]];
}
void calc(int u) {
Sum = 0, addcol(u, 0), ++chaincol[col[u]];
for (int v : G.e[u])
if (!vis[v]) {
delcol(v, u), cnt[col[u]] -= siz[v], Sum -= siz[v];
outsiz = siz[u] - siz[v], dfs(v, u, 0, 0);
Sum += siz[v], cnt[col[u]] += siz[v], addcol(v, u);
}
ans[u] += Sum - cnt[col[u]] + siz[u];
--chaincol[col[u]], delcol(u, 0);
}
void solve(int u) {
getsiz(u, 0), vis[u] = true, calc(u);
for (int v : G.e[u])
if (!vis[v])
root = 0, getroot(v, 0, siz[v]), solve(root);
}
signed main() {
n = read();
for (int i = 1; i <= n; ++i)
col[i] = read();
for (int i = 1; i < n; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
root = 0, getroot(1, 0, n), solve(root);
for (int i = 1; i <= n; ++i)
printf("%lld\n", ans[i]);
return 0;
}
点分树
点分树,即动态点分治,是通过更改原树形态使树的层数变为稳定 \(\log n\) 的一种重构树,常用于与树的形态无关的带修改问题。
性质:
- 高度只有 \(\log n\) 级别。
- 对于任意两点 \(u, v\) ,原树的 \(LCA(u, v)\) 一定在点分树中 \(u \to v\) 的路径上,即 \(dis(u, v) = dis(u, lca) + dis(lca, v)\) 。
- 点 \(x\) 在虚树上的子树集合就是原树中以 \(x\) 为重心(分治中心)时所囊括到的连通块。
实现
我们通过点分治每次找重心的方式对原树进行重构。
将每次找到的重心与上一层的重心缔结父子关系,这样树高即为 \(\log n\) 。
如此,很多暴力都可以有正确的正确的时间复杂度。
一个比较常见的套路是这样的:
- 进行一次点分治,求出每个点在点分树上的父节点。
- 对于每个点,开一个数据结构 \(T_1\) 存储点分树子树的贡献,再开一个数据结构 \(T_2\) 存储点分树父亲的贡献用来容斥。
- 对 \(x\) 进行修改时,从 \(x\) 开始不断跳点分树的父亲一直到根,每次对经过的节点的 \(T_1, T_2\) 修改它的贡献。
- 对 \(x\) 进行查询时,从 \(x\) 开始不断跳点分树的父亲一直到根,每次把 \(T_1\) 的贡献添加进答案,把 \(T_2\) 的贡献从答案刨去。
应用
给定一棵 \(n\) 个点的树,\(m\) 次操作:
0 x k
:求所有与 \(x\) 距离 \(\leq k\) 的所有点的点权和1 x y
:修改 \(x\) 的点权为 \(y\) 。\(n, m \leq 10^5\)
要求 \(\sum_{dis(x, y) \leq k} a_y\) 考虑枚举 \(x, y\) 在点分树上的 LCA \(z\) ,显然 \(z\) 的数量是 \(\log n\) 级别的,故答案即为:
注意到满足 \(LCA(x, y) = z\) 的点即为 \(z\) 的所有子树去掉 \(x\) 方向子树的所有点,那么显然我们可以用 \(z\) 子树中满足条件的点权和减去 \(x\) 子树中满足条件的点权和。
对于每个 \(x\) 建一棵动态开点线段树,下标为 \(i\) 的位置维护 \(x\) 子树内所有 \(dis(x, y) = i\) 的 \(a_y\) 和,那么统计答案时区间查询即可。
考虑对每个点再建一棵动态开点线段树,线段树上下标为 \(i\) 的位置维护 \(x\) 子树内到 \(fa_x\) 距离为 \(i\) 的点权和,再统计答案即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7, LOGN = 17, S = N << 5;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int fa[N][LOGN];
int a[N], dep[N], siz[N], mxsiz[N], nfa[N];
bool vis[N];
int n, m, root;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
void dfs(int u, int f) {
fa[u][0] = f, dep[u] = dep[f] + 1;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : G.e[u])
if (v != f)
dfs(v, u);
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1)
if (h & 1)
x = fa[x][i];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
inline int dist(int x, int y) {
int lca = LCA(x, y);
return dep[x] + dep[y] - dep[lca] * 2;
}
int getsiz(int u, int f) {
int siz = 1;
for (int v : G.e[u])
if (!vis[v] && v != f)
siz += getsiz(v, u);
return siz;
}
void getroot(int u, int f, const int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (int v : G.e[u])
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
void build(int u) {
vis[u] = true;
for (int v : G.e[u])
if (!vis[v])
root = 0, getroot(v, u, getsiz(v, u)), nfa[root] = u, build(root);
}
struct SMT {
int lc[S], rc[S], s[S];
int rt[N];
int tot;
void update(int &x, int nl, int nr, int pos, int k) {
if (!x)
x = ++tot;
s[x] += k;
if (nl == nr)
return;
int mid = (nl + nr) >> 1;
if (pos <= mid)
update(lc[x], nl, mid, pos, k);
else
update(rc[x], mid + 1, nr, pos, k);
}
int query(int x, int nl, int nr, int pos) {
if (!x)
return 0;
if (nl == nr)
return s[x];
int mid = (nl + nr) >> 1;
return pos <= mid ? query(lc[x], nl, mid, pos) : s[lc[x]] + query(rc[x], mid + 1, nr, pos);
}
} A, B;
inline void update(int x, int k) {
for (int cur = x; cur; cur = nfa[cur]) {
A.update(A.rt[cur], 0, n - 1, dist(cur, x), k);
if (nfa[cur])
B.update(B.rt[cur], 0, n - 1, dist(nfa[cur], x), k);
}
}
inline int query(int x, int k) {
int ans = 0;
for (int cur = x, pre = 0; cur; pre = cur, cur = nfa[cur]) {
if (k - dist(cur, x) < 0)
continue;
ans += A.query(A.rt[cur], 0, n - 1, k - dist(cur, x));
if (pre)
ans -= B.query(B.rt[pre], 0, n - 1, k - dist(cur, x));
}
return ans;
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= n; ++i)
a[i] = read();
for (int i = 1; i < n; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
dfs(1, 0), root = 0, getroot(1, 0, n), build(root);
for (int i = 1; i <= n; ++i)
update(i, a[i]);
int lstans = 0;
while (m--) {
int op = read(), x = read() ^ lstans, k = read() ^ lstans;
if (op)
update(x, k - a[x]), a[x] = k;
else
printf("%d\n", lstans = query(x, k));
}
return 0;
}
给定一棵 \(n\) 个点的树,\(q\) 次询问点权在 \([l, r]\) 内的所有点到某个点 \(u\) 的距离之和,强制在线。
\(n \leq 1.5 \times 10^5, q \leq 2 \times 10^5\)
首先考虑没有 \([l, r]\) 限制时的做法,记:
询问时先令答案为 \(f_1(x)\) ,之后不断在点分树上跳父亲,记当前点为 \(x\) ,则答案要加上:
现在有了 \([l, r]\) 的限制,只要加上一维用 vector
存储,做一个前缀和,每次二分即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 1.5e5 + 7, LOGN = 19;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
vector<pair<int, ll> > vec[2][N];
ll dis[N];
int fa[N][LOGN];
int a[N], dep[N], siz[N], mxsiz[N], nfa[N];
bool vis[N];
int n, q, A, root;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
void dfs(int u, int f) {
fa[u][0] = f, dep[u] = dep[f] + 1;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (v != f)
dis[v] = dis[u] + w, dfs(v, u);
}
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1)
if (h & 1)
x = fa[x][i];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
inline ll dist(int x, int y) {
int lca = LCA(x, y);
return dis[x] + dis[y] - dis[lca] * 2;
}
int getsiz(int u, int f) {
int siz = 1;
for (auto it : G.e[u]) {
int v = it.first;
if (!vis[v] && v != f)
siz += getsiz(v, u);
}
return siz;
}
void getroot(int u, int f, const int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (auto it : G.e[u]) {
int v = it.first;
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
}
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
void build(int u) {
vis[u] = true;
for (auto it : G.e[u]) {
int v = it.first;
if (!vis[v])
root = 0, getroot(v, u, getsiz(v, u)), nfa[root] = u, build(root);
}
}
inline ll query(int op, int x, int l, int r, ll &siz) {
auto pl = lower_bound(vec[op][x].begin(), vec[op][x].end(), make_pair(l, 0ll)),
pr = upper_bound(vec[op][x].begin(), vec[op][x].end(), make_pair(r, inf));
siz = pr - pl;
ll res = 0;
if (pr != vec[op][x].begin())
res += prev(pr)->second;
if (pl != vec[op][x].begin())
res -= prev(pl)->second;
return res;
}
signed main() {
n = read(), q = read(), A = read();
for (int i = 1; i <= n; ++i)
a[i] = read();
for (int i = 1; i < n; ++i) {
int u = read(), v = read(), w = read();
G.insert(u, v, w), G.insert(v, u, w);
}
dfs(1, 0), root = 0, getroot(1, 0, n), build(root);
for (int i = 1; i <= n; ++i)
for (int cur = i; cur; cur = nfa[cur]) {
vec[0][cur].emplace_back(a[i], dist(i, cur));
if (nfa[cur])
vec[1][cur].emplace_back(a[i], dist(i, nfa[cur]));
}
for (int i = 0; i <= 1; ++i)
for (int j = 1; j <= n; ++j) {
sort(vec[i][j].begin(), vec[i][j].end());
for (int k = 1; k < vec[i][j].size(); ++k)
vec[i][j][k].second += vec[i][j][k - 1].second;
}
ll lstans = 0;
while (q--) {
int x = read(), l = (read<ll>() + lstans) % A, r = (read<ll>() + lstans) % A;
if (l > r)
swap(l, r);
ll sizx, sizf;
lstans = query(0, x, l, r, sizx);
for (int cur = x; nfa[cur]; cur = nfa[cur]) {
lstans += query(0, nfa[cur], l, r, sizf) - query(1, cur, l, r, sizx);
lstans += dist(x, nfa[cur]) * (sizf - sizx);
}
printf("%lld\n", lstans);
}
return 0;
}
维护一颗带点权、边权树(树上点的度数不超过 \(20\) )。现有若干次修改点权的操作,每次操作结束后您需要选出一个核心点 \(x\) 使得 \(\sum_{i = 1}^n dist(x, i) \times a_i\) 最小,求其最小值。
\(n, m \leq 10^5\)
不难发现 \(x\) 就是带权重心。因为最优决策儿子如果存在那么只会存在一个,又因为树上点的度数不超过 \(20\) ,于是可以在点分树上暴力跳儿子,时间复杂度 \(O(n \log^2 n \times d)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7, LOGN = 17;
struct Graph1 {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
struct Graph2 {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} nG;
ll dis[N], sum[N], s1[N], s2[N];
int fa[N][LOGN];
int dep[N], siz[N], mxsiz[N], nfa[N], ori[N];
bool vis[N];
int n, m, root;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
void dfs1(int u, int f) {
fa[u][0] = f, dep[u] = dep[f] + 1;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (v != f)
dis[v] = dis[u] + w, dfs1(v, u);
}
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1)
if (h & 1)
x = fa[x][i];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
inline ll dist(int x, int y) {
int lca = LCA(x, y);
return dis[x] + dis[y] - dis[lca] * 2;
}
int getsiz(int u, int f) {
int siz = 1;
for (auto it : G.e[u]) {
int v = it.first;
if (!vis[v] && v != f)
siz += getsiz(v, u);
}
return siz;
}
void getroot(int u, int f, const int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (auto it : G.e[u]) {
int v = it.first;
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
}
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
void build(int u) {
vis[u] = true;
for (auto it : G.e[u]) {
int v = it.first;
if (vis[v])
continue;
root = 0, getroot(v, u, getsiz(v, u)), ori[root] = v;
nG.insert(nfa[root] = u, root), build(root);
}
}
inline void update(int x, int k) {
for (int cur = x; cur; cur = nfa[cur]) {
sum[cur] += k, s1[cur] += dist(cur, x) * k;
if (nfa[cur])
s2[cur] += dist(nfa[cur], x) * k;
}
}
inline ll query(int x) {
ll res = 0;
for (int cur = x, pre = 0; cur; pre = cur, cur = nfa[cur]) {
res += s1[cur];
if (pre)
res += dist(cur, x) * (sum[cur] - sum[pre]) - s2[pre];
}
return res;
}
ll dfs2(int u) {
ll res = query(u);
for (int v : nG.e[u])
if (query(ori[v]) < res)
return dfs2(v);
return res;
}
signed main() {
n = read(), m = read();
for (int i = 1; i < n; ++i) {
int u = read(), v = read(), w = read();
G.insert(u, v, w), G.insert(v, u, w);
}
dfs1(1, 0), root = 0, getroot(1, 0, n);
int rt = root;
build(root);
while (m--) {
int x = read(), k = read();
update(x, k);
printf("%lld\n", dfs2(rt));
}
return 0;
}
由一颗树,树上每个节点有一种颜色,每次查询给出 \(l, r, x\) ,求保留树上编号在 \([l, r]\) 内的点,\(x\) 所在联通块中颜色种类数。
\(n \leq 10^5\)
先建出点分树,对于一次查询,在点分树上 \(x\) 的祖先中找到深度最小的点 \(pa\) 且满足 \(x\) 只经过编号 \([l,r]\) 内的点在原树上能到达 \(pa\) 。
记每个点 \(i\) 到点分树祖先的路径上所经过的节点编号最小/大值为 \(d_{min}(i,j)\) 和 \(d_{max}(i,j)\) ,则求 \(pa\) 直接暴力跳即可。
可以发现 \(x\) 只经过编号 \([l,r]\) 内的点所在的连通块被完全包含在了 \(pa\) 在点分树上的子树中。把本次询问放到 \(pa\) 节点处,最后再统一离线处理。
枚举虚树上的点 \(rt\),处理该节点处的询问时,对于任意一个 \((l,r,x)\),满足 \(l\leqslant d_{min}(i,rt)\) 且 \(d_{max}(i,rt)\leqslant r\) 的 \(i\) 即为与 \(x\) 在同一连通块内的点,现需要统计这些点的颜色种类,这个二维偏序问题可以排序后树状数组解决。
时空复杂度 \(O(n \log^2 n) - O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct Node {
int l, r, id;
inline bool operator < (const Node &rhs) const {
return l == rhs.l ? id > rhs.id : l > rhs.l;
}
};
vector<Node> nfa[N], nd[N];
int col[N], siz[N], mxsiz[N], lst[N], ans[N];
bool vis[N];
int n, m, root;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
int getsiz(int u, int f) {
int siz = 1;
for (int v : G.e[u])
if (!vis[v] && v != f)
siz += getsiz(v, u);
return siz;
}
void getroot(int u, int f, const int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (int v : G.e[u])
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
void dfs(int u, int f, int mx, int mn, const int rt) {
nfa[u].emplace_back((Node) {mn, mx, rt});
nd[rt].emplace_back((Node) {mn, mx, col[u]});
for (int v : G.e[u])
if (!vis[v] && v != f)
dfs(v, u, max(mx, v), min(mn, v), rt);
}
void build(int u) {
vis[u] = true, dfs(u, 0, u, u, u);
for (int v : G.e[u])
if (!vis[v])
root = 0, getroot(v, u, getsiz(v, u)), build(root);
}
namespace BIT {
int c[N];
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
} // namespace BIT
signed main() {
n = read(), m = read();
vector<int> vec;
for (int i = 1; i <= n; ++i)
vec.emplace_back(col[i] = read());
sort(vec.begin(), vec.end());
vec.erase(unique(vec.begin(), vec.end()), vec.end());
for (int i = 1; i <= n; ++i)
col[i] = lower_bound(vec.begin(), vec.end(), col[i]) - vec.begin() + 1;
for (int i = 1; i < n; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
root = 0, getroot(1, 0, n), build(root);
for (int i = 1; i <= m; ++i) {
int l = read(), r = read(), x = read();
for (Node it : nfa[x])
if (l <= it.l && it.r <= r) {
nd[it.id].emplace_back((Node) {l, r, -i});
break;
}
}
for (int i = 1; i <= n; ++i) {
sort(nd[i].begin(), nd[i].end());
for (Node it : nd[i]) {
if (it.id < 0)
ans[-it.id] = BIT::query(it.r);
else if (!lst[it.id] || lst[it.id] > it.r) {
if (lst[it.id])
BIT::update(lst[it.id], -1);
BIT::update(lst[it.id] = it.r, 1);
}
}
for (Node it : nd[i])
if (it.id > 0 && lst[it.id])
BIT::update(lst[it.id], -1), lst[it.id] = 0;
}
for (int i = 1; i <= m; ++i)
printf("%d\n", ans[i]);
return 0;
}
给出一棵树,每个点有一个颜色,对于所有 \(i\) ,求 \(\sum_{j = 1}^n s(i, j)\) ,其中 \(s(i, j)\) 表示 \(i \to j\) 路径上的颜色数量。
\(n \leq 10^5\)
树上路径相关问题考虑点分治。设 \(cnt_i\) 表示当前统计 \(u\) 子树内所有到 \(u\) 的路径有多少条路径出现颜色 \(i\) 。这个是好处理的,只要到 \(u\) 的链上第一次出现颜色 \(i\) 时令 \(cnt_i \leftarrow cnt_i + siz_v\) 即可。
然后剩下的就是对于 \(v\) 子树外的贡献减去 \(v\) 子树内的贡献即可,容斥上有一些细节需要处理。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
ll ans[N], Sum;
int col[N], siz[N], mxsiz[N], sta[N], chaincol[N], cnt[N];
bool vis[N];
int n, root, outsiz;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
void getroot(int u, int f, const int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (int v : G.e[u])
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
int getsiz(int u, int f) {
siz[u] = 1;
for (int v : G.e[u])
if (!vis[v] && v != f)
siz[u] += getsiz(v, u);
return siz[u];
}
void addcol(int u, int f) {
if (!chaincol[col[u]])
cnt[col[u]] += siz[u], Sum += siz[u];
++chaincol[col[u]];
for (int v : G.e[u])
if (!vis[v] && v != f)
addcol(v, u);
--chaincol[col[u]];
}
void delcol(int u, int f) {
if (!chaincol[col[u]])
cnt[col[u]] -= siz[u], Sum -= siz[u];
++chaincol[col[u]];
for (int v : G.e[u])
if (!vis[v] && v != f)
delcol(v, u);
--chaincol[col[u]];
}
void dfs(int u, int f, int num, ll sum) {
if (!chaincol[col[u]])
++num, sum += cnt[col[u]];
++chaincol[col[u]], ans[u] += Sum - sum + num * outsiz;
for (int v : G.e[u])
if (!vis[v] && v != f)
dfs(v, u, num, sum);
--chaincol[col[u]];
}
void calc(int u) {
Sum = 0, addcol(u, 0), ++chaincol[col[u]];
for (int v : G.e[u])
if (!vis[v]) {
delcol(v, u), cnt[col[u]] -= siz[v], Sum -= siz[v];
outsiz = siz[u] - siz[v], dfs(v, u, 0, 0);
Sum += siz[v], cnt[col[u]] += siz[v], addcol(v, u);
}
ans[u] += Sum - cnt[col[u]] + siz[u];
--chaincol[col[u]], delcol(u, 0);
}
void solve(int u) {
getsiz(u, 0), vis[u] = true, calc(u);
for (int v : G.e[u])
if (!vis[v])
root = 0, getroot(v, 0, siz[v]), solve(root);
}
signed main() {
n = read();
for (int i = 1; i <= n; ++i)
col[i] = read();
for (int i = 1; i < n; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
root = 0, getroot(1, 0, n), solve(root);
for (int i = 1; i <= n; ++i)
printf("%lld\n", ans[i]);
return 0;
}
P5912 [POI2004] JAS / [AGC009D] Uninity
求深度最浅的点分树的深度。
\(n \leq 10^5\)
设 \(d_i\) 表示 \(i\) 在点分树上的高度,一组 \(d_i\) 合法当且仅当对于任意 \(d_u = d_v, u \neq v\) 均满足 \(u \to v\) 路径上存在一点 \(w\) 满足 \(d_w > d_u\) 。
考虑从下到上贪心地给每个点定不和下方冲突的尽可能小的标号即可找到最大标号的最小值。记 \(S_u\) 表示 \(u\) 子树内所有目前不满足条件的 \(d\) ,即 \(v \in subtree(u)\) 且不存在 \(w \in path(u \to v)\) 满足 \(d_w > d_v\) 的 \(d_v\) 。
合并两个子树 \(v, w\) 时,限制条件为 \(d_u > \max(S_u \cap S_v)\) 且 \(d_u \not \in S_u \cup S_v\) ,贪心选择满足条件最小的 \(d_u\) 即可,\(S_u\) 即所有儿子的 \(S_v\) 的并去掉所有 \(< d_u\) 的元素。
形式化的有:
由于 \(d \leq \log n\) ,使用一些位运算技巧可以做到线性。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int f[N], g[N], d[N];
int n;
void dfs(int u, int fa) {
bool son = false;
int state = 0, all = 0;
for (int v : G.e[u])
if (v != fa) {
dfs(v, u), son = true;
f[u] = max(f[u], f[v]);
state |= all & g[v], all |= g[v];
}
if (!son) {
f[u] = d[u] = 0, g[u] = 1;
return;
}
state = ~((1 << (__lg(state) + 1)) - 1) & ~all;
f[u] = max(f[u], d[u] = __lg(state & -state));
g[u] = (all & ~((1 << d[u]) - 1)) | (1 << d[u]);
}
signed main() {
scanf("%d", &n);
for (int i = 1, u, v; i < n; ++i) {
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
dfs(1, 0);
printf("%d", f[1]);
return 0;
}