思路
点分树 + 根号重构 + *高速平衡树。
点分树的两种常见用法无非是 直接做和路径有关的暴力 还有 处理这种有关单点和整树的问题,后者的另一个经典题目是 P3241 [HNOI2015]开店。
回到这个题目,处理路径考虑先上点分治,暂时不考虑强制在线的限制。
因为每次加上一个新点,所以可以考虑在加点的过程中动态维护答案,于是问题变成维护每次加点之后答案的增量。
也就是说对于新加入的点 u,每次询问原树中满足 dist(u,v)≤ru+rv 的点 v 的个数。
等式变形成 dist(u,v)−ru≤rv,似乎不太好在点分的过程中处理 dist(u,v).
一般情况下考虑的是把 u,v 之间的路径拆成 {u,⋯,lca(u,v)} 和 {lca(u,v),⋯,v} 两部分,但是 lca 在点分的过程中难以钦定。
又因为实际上可以从路径上的任意一点断开,所以可以联想到点分树的一个很好的性质:点分树上两点的 lca 在原树中一定在这两点的路径上,并且点分树的树高是 O(log) 的。
这意味着我们可以从点 u 向上枚举 lca 然后处理点 v 的个数。
先考虑静态询问怎么做。假设当前枚举到 u 的祖先 x。原式可以化成 dist(u,x)+dist(x,v)≤ru+rv,调整成关于 v 的限制就是 dist(u,x)−ru≤rv−dist(x,v).
注意到等式的左边在枚举祖先的时候可以视作一个定值,于是我们只需要知道 x 的子树中满足 rv−dist(x,v) 大于等于某一个定值的点的个数就可以了。
常规的树论做法都不太可行,考虑一些和点分树性质有关的暴力。
因为点分树的树高是 O(log) 级别的,所以对于每个结点大力存储下它子树中信息的总复杂度是 O(nlogn).
这意味着我们可以预先将 x 子树中所有的结点的 rv−dist(x,v) 用某种神秘的数据结构维护一下,之后询问就是在上面查询的事情。
因为动态加点没法离散化,所以选择用平衡树实现。
注意到向上枚举的过程中会算重子结点的子树,所以还需要维护 rv−dist(fax,v) 用来容斥。
然后再考虑如何维护动态插入结点。
不考虑点分树的性质,那么直接将新点接在原树父亲下面就行,可以看作是钦定在父亲处点分一次,然后在这个孤点在进行一次点分。现在的问题是这样做不能保证复杂度。
于是考虑类似替罪羊树的思路,当某棵子树失衡的时候就暴力调整。钦定一个比例系数 α,如果插入点 u 后其祖先 x 满足 size(x)≥α⋅size(fax),就直接暴力重建 x 的子树。
具体的复杂度就是调参,α 在 0.8 左右的时候点分树的树高大概还是 O(log)(但我不会证,有懂的老哥麻烦教一下
如果要再偷懒的话可以不写平衡树,用 vector + 根号重构平替。具体考虑维护两个 vector,一个当作缓存池存当前加入的数据,另一个用来存溢出的数据。当缓存池超过根号大小的时候就暴力归并。
查询直接在 vector 里二分。
这个在很多阴间根号题都有应用,O(n√n) 应该比较快。
复杂度懒得算了,反正常数已经挺离谱了。
代码
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
#define il inline
typedef long long ll;
const int maxn = 1e5 + 5;
const int maxe = maxn << 1;
const int lg_sz = 17;
const int lim = 350;
const int inf = 0x3f3f3f3f;
const double alpha = 0.95;
struct node
{
int to, nxt;
} edge[maxe];
int n, ecnt;
int r[maxn], head[maxn], sz[maxn];
bool vis[maxn];
ll last_ans;
namespace IO
{
int len = 0;
char ibuf[(1 << 20) + 1], *iS, *iT, out[(1 << 26) + 1];
#define gh() (iS == iT ? iT = (iS = ibuf) + fread(ibuf, 1, (1 << 20) + 1, stdin), (iS == iT ? EOF : *iS++) : *iS++)
#define reg register
il int read()
{
reg char ch = gh();
reg int x = 0;
reg char t = 0;
while (ch < '0' || ch > '9') t |= ch == '-', ch = gh();
while (ch >= '0' && ch <= '9') x = x * 10 + (ch ^ 48), ch = gh();
return t ? -x : x;
}
il void putc(char ch) { out[len++] = ch; }
template<class T>
il void write(T x)
{
if (x < 0) putc('-'), x = -x;
if (x > 9) write(x / 10);
out[len++] = x % 10 + 48;
}
il void flush()
{
fwrite(out, 1, len, stdout);
len = 0;
}
}
using IO::read;
using IO::write;
using IO::flush;
using IO::putc;
il void add_edge(int u, int v)
{
ecnt++;
edge[ecnt].to = v, edge[ecnt].nxt = head[u];
head[u] = ecnt;
}
il pair<int, int> get_rt(int u, int f, int tot)
{
sz[u] = 1;
int res = 0;
pair<int, int> ans = make_pair(inf, 0);
for (int i = head[u], v; i; i = edge[i].nxt)
{
v = edge[i].to;
if ((!vis[v]) && (v != f))
{
ans = min(ans, get_rt(v, u, tot));
sz[u] += sz[v], res = max(res, sz[v]);
}
}
ans = min(ans, make_pair(max(res, tot - sz[u]), u));
return ans;
}
namespace tree
{
int f[lg_sz][maxn], dep[maxn], dis[maxn];
il void insert(int u, int fa, int w)
{
f[0][u] = fa, dep[u] = dep[fa] + 1, dis[u] = dis[fa] + w;
for (int i = 1; i < lg_sz; i++) f[i][u] = f[i - 1][f[i - 1][u]];
}
il int lca(int u, int v)
{
if (dep[u] < dep[v]) swap(u, v);
for (int i = lg_sz - 1; ~i; i--)
if (dep[f[i][u]] >= dep[v]) u = f[i][u];
if (u == v) return u;
for (int i = lg_sz - 1; ~i; i--)
if (f[i][u] != f[i][v]) u = f[i][u], v = f[i][v];
return f[0][u];
}
il int calc_dis(int u, int v) { return dis[u] + dis[v] - 2 * dis[lca(u, v)]; }
}
struct item
{
vector<int> lrg, sml;
il void insert(int w)
{
sml.insert(lower_bound(sml.begin(), sml.end(), w), w);
if (sml.size() >= lim)
{
vector<int> res, emp;
int i = 0, j = 0;
for ( ; (i < lrg.size()) && (j < sml.size()); )
if (lrg[i] < sml[j]) res.push_back(lrg[i++]);
else res.push_back(sml[j++]);
while (i < lrg.size()) res.push_back(lrg[i++]);
while (j < sml.size()) res.push_back(sml[j++]);
swap(lrg, res), swap(sml, emp);
}
}
il int calc_rk(int w) { return (upper_bound(lrg.begin(), lrg.end(), w) - lrg.begin()) + (upper_bound(sml.begin(), sml.end(), w) - sml.begin()) - 2; }
il void clear() { lrg.clear(), sml.clear(); }
} subt[maxn << 1], tof[maxn << 1];
namespace divide
{
int fa[maxn], dep[maxn], siz[maxn];
il int query(int u, int w)
{
int res = 0;
for (int i = u, j; fa[i]; i = j)
{
j = fa[i];
int x = w - tree::calc_dis(u, j);
res += subt[j].calc_rk(x) - tof[i].calc_rk(x);
}
return res;
}
il void insert(int u, int w, int anc)
{
subt[u].insert(-w);
for (int i = u, j; i != anc; i = j)
{
j = fa[i];
int x = tree::calc_dis(u, j) - w;
if (j != anc) subt[j].insert(x);
tof[i].insert(x);
}
}
il void clear(int u, int fa, int anc_dep)
{
vis[u] = false;
for (int i = head[u], v; i; i = edge[i].nxt)
{
v = edge[i].to;
if ((v != fa) && (dep[v] > anc_dep)) clear(v, u, anc_dep);
}
}
il int build(int u, int tot_sz, int f, int anc)
{
int rt = get_rt(u, 0, tot_sz).second, bas = sz[u];
subt[rt].clear(), tof[rt].clear();
fa[rt] = f, siz[rt] = 1, vis[rt] = true, dep[rt] = dep[f] + 1;
insert(rt, r[rt], anc);
for (int i = head[rt], v; i; i = edge[i].nxt)
{
v = edge[i].to;
if (!vis[v]) siz[rt] += build(v, bas, rt, anc);
}
return siz[rt];
}
il void rebuild(int u) { clear(u, 0, dep[u]), build(u, siz[u], fa[u], fa[u]); }
il void insert(int u, int w)
{
subt[u].insert(-w), siz[u]++;
int nd = 0;
for (int i = u, j; fa[i]; i = j)
{
j = fa[i];
int x = tree::calc_dis(u, j) - w;
siz[j]++, subt[j].insert(x), tof[i].insert(x);
if (siz[i] > siz[j] * alpha) nd = j;
}
if (nd) rebuild(nd);
}
}
int main()
{
read(), n = read();
for (int i = 1; i <= n; i++)
{
vis[i] = true;
int fa = read() ^ last_ans % (int)1e9, c = read();
r[i] = read();
tree::insert(i, fa, c);
if (i > 1) add_edge(fa, i), add_edge(i, fa);
divide::fa[i] = fa, divide::dep[i] = divide::dep[fa] + 1;
write(last_ans += divide::query(i, r[i])), putc('\n');
divide::insert(i, r[i]);
}
flush();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现