闲话 22.10.12
闲话
好今天又是水闲话的一天呢
今日中午放了《Moon Halo》
好耶 唤起了我玩崩三的记忆
是很怀念的往事(
然后晚上回来后发现黑板上全是同学们推的歌
然后lyin上去 我们开始鼓掌
lyin写了个《turn all the light on》
然后虎哥进来了
打这段的时候在查Moon Halo的百科词条
我听出了三个人 我也听出了她们配的是谁
我还听出了陈阿姨
然后 gtm 想到了 In the hell we live,lament
这确实是首很好的英文歌 而且显然得两个人唱
然后他开始提议对唱 然后我说我不记得词
2022-10-12 20:39
Jijidawangwhere is 今日闲话
Here is 今日闲话
あなたになりたくて
いつからだろう口もきけなくて
さよなら言わないだけで
あなたは金色のシャンデリー
点分树
因为场上遇到了QtreeV而且之前没学过一点点分树于是考场上发明了点分树这件事
而且很久没打求重心了于是又发明了一个求重心
介绍点分树,首先需要说一下点分治。
点分治主要是在解决一类树上路径统计问题。我们想要计数不重不漏,于是可以每次从树上选择一个顶点,统计经过他的所有路径,再把他从树上删除。容易发现这样能做到不重不漏。
但是选择的顶点也是有讲究的。我们发现选择顶点并删除会使得树分为数个联通块,然后递归求解子问题。这样做每一层会遍历一次整棵树,所以单层求解的复杂度为 \(O(n)\)。递归当然要选择将子问题分得越均匀越好,于是可以每次选择重心求解。选择重心保证了最多 \(O(\log n)\) 层,因此这部分的总时间复杂度 \(O(n \log n)\)。求解的复杂度另说。
这就是点分治了。理解不是很难但是想用好不简单。
但是点分树和点分治具体做法没关系。点分树是原树的一棵重构树(?),我们将点分治每层选出的重心和联通块内上一层的重心连边,就得到了点分树。
容易发现点分树的层数是 \(O(\log n)\) 的。然后点分树有个比较好的性质,就是两点点分树上的LCA肯定位于原树上两点的路径上。这点比较显然。
所以对于许多树上带插删点集路径长度的信息都可以这么维护了。举例来说吧。
给一棵n个点的树。每个点可能有两种颜色:黑或白。我们定义 \(\text{dis}(a,b)\) 为点 \(a\) 至点 \(b\) 路径上的边个数。一开始所有的点都是黑色的,要求作以下操作:
0 i
将点i的颜色反转(黑变白,白变黑)
1 v
询问 \(\text{dis}(u,v)\) 的最小值。\(u\) 点必须为白色(\(u\) 与 \(v\) 可以相同),显然如果 \(v\) 是白点,查询得到的值一定是0。特别地,如果1操作时树上没有白点,输出 \(-1\)。
\(n,q\le 10^5\)。
我们先建出点分树。树上每个节点用有序结构维护子树内白点到这个点的距离。插删直接跳父亲到根,路上在结构内维护对应信息。查询直接跳父亲到根,每次查找路径上有序结构内最小值更新答案。
容易发现查询的正确性。如果一条路径是答案则跳的过程中必有一点在路径上,因此答案能够得到。而其余路径长度都大于答案,因此答案不会被更新。
由于点分树的深度为 \(O(\log n)\),选取set作为有序结构的总时间复杂度为 \(O(n\log^2 n)\)。
听说可以用lct维护动态点分,但众所周知lct等动态树是10级考点,只有cts才会考察
code
#include <bits/stdc++.h>
#include <bits/extc++.h>
using namespace std;
#define rep(i,a,b) for (register int i = (a), i##_ = (b) + 1; i < i##_; ++i)
#define pre(i,a,b) for (register int i = (a), i##_ = (b) - 1; i > i##_; --i)
const int N = 1e5 + 10;
int n, q, t1, t2, typ, pos, totrt, col[N];
#define Aster(s) for (register int i = head[s]; i; i = e[i].next)
#define v e[i].to
int head[N], mlc;
struct ep {
int to, next;
} e[N << 1];
void adde(int f, int t) {
e[++mlc] = {t, head[f]};
head[f] = mlc;
}
bool vis[N];
int nowrt, siz[N], f[N], par[N];
void find_rt(int u, int fa) {
siz[u] = 1; f[u] = 0;
Aster(u) {
if (vis[v]) continue;
if (v == fa) continue;
find_rt(v, u);
siz[u] += siz[v];
f[u] = max(f[u], siz[v]);
} f[u] = max(f[u], siz[0] - siz[u]);
if (f[u] < f[nowrt]) nowrt = u;
}
void build(int u, int fa){
par[u] = fa;
vis[u] = 1;
Aster(u) {
if (vis[v]) continue;
nowrt = 0; siz[0] = siz[v]; nowrt = 0;
find_rt(v, u);
build(nowrt, u);
}
}
int dep[N];
int far[N][20], t, o[N];
void dfs(int u, int fa) {
dep[u] = dep[fa] + 1;
far[u][0] = fa;
for (t = 0; far[u][t]; ++ t) far[u][t+1] = far[far[u][t]][t];
o[u] = t;
Aster(u) {
if (v == fa) continue;
dfs(v, u);
}
}
int lca(int x, int y) {
if (dep[x] < dep[y]) swap(x, y);
pre(i,o[x],0) if (dep[far[x][i]] >= dep[y]) x = far[x][i];
if (x == y) return x;
pre(i,o[x],0) if (far[x][i] != far[y][i]) x = far[x][i], y = far[y][i];
return far[x][0];
}
int dis(int x, int y) {
int LCA = lca(x, y);
return dep[x] + dep[y] - (dep[LCA] << 1);
}
set <pair<int,int> > st[N];
void add(int pos) {
int ptr = pos;
while (ptr != 0) {
st[ptr].emplace( dis(pos, ptr) , pos );
ptr = par[ptr];
}
}
void del(int pos) {
int ptr = pos;
while (ptr != 0) {
st[ptr].erase( {dis(pos, ptr) , pos} );
ptr = par[ptr];
}
}
int query(int pos) {
int ptr = pos, ans = 1e9;
while (ptr != 0) {
if (st[ptr].size()) {
ans = min(ans, (*st[ptr].begin()).first + dis(pos, ptr));
} ptr = par[ptr];
} return (ans == 1e9 ? -1 : ans);
}
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n;
rep(i,2,n) cin >> t1 >> t2, adde(t1, t2), adde(t2, t1);
f[0] = n + 1; siz[0] = n;
find_rt(1, 0); totrt = nowrt;
build(nowrt, 0);
dfs(totrt, 0);
cin >> q;
while (q--) {
cin >> typ >> pos;
if (typ == 0) {
col[pos] ^= 1;
if (col[pos] == 1) add(pos);
else del(pos);
} else {
cout << query(pos) << '\n';
}
}
}
线段树分治
?我才发现我没学过可撤销并查集
具体地,假设说我们有一系列操作,形如
- 将一个不存在的元素插入集合
- 在集合中删除一个存在的元素
- 每次插删后输出元素信息
这时就需要线段树分治。
我们直接在时间维开一棵线段树。每一个出现过的元素对应一段存在的时间,我们在线段树上这段时间对应的 \(O(\log n)\) 个节点加入这个元素。容易发现线段树上共 \(O(n \log n)\) 个元素。
然后我们遍历这棵线段树。我们到达一个节点后,首先把这个节点对应的元素插入集合,然后递归向两个子节点。到达叶子节点 \(p:[l,l]\) 后,当前维护的集合就是 \(l\) 次操作后的集合。直接求答案即可。回溯时把对应元素删除即可。
比方说给你一张图。支持加边删边询问两点连通性。
可以直接线段树分治,用可撤销并查集维护连通性。
可撤销并查集就是不加路径压缩只有按秩合并,因此每次操作只会更改最多两个点的信息。记录后可以删除最新一次操作的效果。和线段树分治挺配的。
以下是博客签名,与正文无关。
请按如下方式引用此页:
本文作者 joke3579,原文链接:https://www.cnblogs.com/joke3579/p/chitchat221012.html。
遵循 CC BY-NC-SA 4.0 协议。
请读者尽量不要在评论区发布与博客内文完全无关的评论,视情况可能删除。