边分树学习笔记
暴力写挂了呜呜呜
边分治
类比点分治,这里枚举中心边,把树拆成两个大小相接近的部分。每次递归下去分治做就好了。容易发现这个东西吊打点分治,因为每次只会分成两个大小相近的部分,所以会多出很多优美的性质来。
但是非常不幸的,在菊花图上这个的复杂度是错的。
但是我们有两倍常数的非常简单的解决办法:把这棵树三度化!根据某个神秘定理,在二叉树上做上面的算法复杂度正确。
但他可不是乱打的啊!要在多叉树转二叉树的过程中保证任意两点距离相同!所以我们需要增加虚点和0权边。
大概就是这样吧,意会一下(((
如果放上建树的代码的话,就是这样:
code
void DFS1(int u, int f) {
int lst = u, flag = 0;
for(int i = g1.h[u]; ~i; i = g1.ne[i]) {
int j = g1.e[i];
if(j == f) continue;
if(flag) {
++now;
g2.add(now, lst, 0);
g2.add(lst, now, 0);
lst = now;
}
flag = 1;
g2.add(lst, j, g1.w[i]);
g2.add(j, lst, g1.w[i]);
dep1[j] = dep1[u] + g1.w[i];
DFS1(j, u);
}
}
除了第一个节点以外,每一个子节点都要新建一个虚点。感觉不难理解。
记录边分治的过程
e,就是对每个点,记录一下每次在边分治的时候它进入了哪个子树,知道是左子树还是右子树就行了。这样的序列长度是
但是只有这个显然用处不大对吧,所以我们还需要顺便在子树的方向存下点到分治中心的距离。为了方便,分治中心边的长度随便分给一边就行了。
这里要额外注意一个细节,这里新建每个点的时候要在上次记录距离的方向新建。(也就是说,上一次确定方向并更新信息,这一次在上次确定的方向上开新点。)
可以不这样写,但是会使得每个点都要建一个虚空根节点,很没必要。
code
void DFS3(int u, int f, ll dis, ll dep, int type) {
//更新每个点的边分树信息
if(u <= n) {
if(!lst[u]) {
tr[u] = ++tot;
}
else {
ch[lst[u]][lstp[u]] = ++tot;
}
sz[tot][type]++;
sum[tot][type] = dep + dis;
lst[u] = tot;
lstp[u] = type;
}
for(int i = t2.h[u]; ~i; i = t2.ne[i]) {
int j = t2.e[i];
if(j == f || vis[i]) continue;
DFS3(j, u, dis, dep + t2.w[i], type);
}
}
(这个东西有什么用?)
hmmmm,好问题。这个东西本身没有什么用,但是注意到这个东西和线段树的一条单链结构很像,所以这个东西和线段树一样是可以合并的。把所有这些链合并起来就构成原树的边分树。
但是比这更重要的是合并的过程,线段树合并具有优美的性质:两个线段树的每一个“分叉”都会被枚举到。这里的“分叉”指的是对应节点往不同方向延申的两个子树。而这恰恰是边分治需要的——合并左右两个分治子树的信息。
[CTSC2018]暴力写挂解题报告
建议更改题目名字为【模板】边分树合并
把题目要求的式子变成
正解直接利用边分树合并的性质,对第一棵树边分树后在第二棵树上合并,每个节点
CF757G-Can Bash Save the Day? 解题报告
第一眼:这不是我们HNOI的开店吗?下次记得标记出处!
笑容逐渐消失。。。
我们需要更好的做法。
换个方向,对于区间的限制,我们除了在点分树上做区间查询以外,还可以把问题离线下来,对于每个右端点维护所有前缀的答案。然后差分一下。
但是它在线啊!所以我们需要可持久化。
但是它带修啊!这个做法凉了吗?
注意到其实修改只有交换相邻项,所以我们可以只用修改一个版本的数据结构,看起来这个对了。
我们仿照上例建立每个点的边分树单链。然后把它们可持久化起来。如果写成可持久化合并的形式就可以减少很多麻烦。
每个点要记录特定方向上的路径条数和路径长度总和。这样计算答案是容易的。
修改其实也没有想象中的麻烦,直接在第
代码出人意料的好写。
code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int P = 998244353;
const int N = 4e5+5;
template<typename T>inline void read(T &a)
{
a = 0;
int f = 1;
char c = getchar();
while(!isdigit(c))
{
if(c == '-') f = -1;
c = getchar();
}
while(isdigit(c))
a = a * 10 + c - 48,c = getchar();
a *= f;
}
template<typename T,typename ...L> inline void read(T &a,L &...l)
{
read(a),read(l...);
}
//我们的边分树实在是太厉害了!
int tot, ch[N * 30][2], tr[N], root[N];//每个点的边分树,根的边分树
ll sum[N * 30][2], sz[N * 30][2], lstans;
int n, q, aa[N], u, v, w, t, a, b, c;
struct Tree {
int h[N], e[N << 1], ne[N << 1], w[N << 1], idx;
Tree() {
memset(h, -1, sizeof h);
}
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
}t1, t2;
namespace conquer {
int S, lst[N], lstp[N];//每个点的上一次位置和拐弯的地方
int nn, vis[N << 1], sze[N], root, res;
void DFS1(int u, int f) {
int lst = u, flag = 0;
for(int i = t1.h[u]; ~i; i = t1.ne[i]) {
int j = t1.e[i];
if(j == f) continue;
if(flag) {
S++;
t2.add(lst, S, 0), t2.add(S, lst, 0), lst = S;
}
flag = 1;
t2.add(lst, j, t1.w[i]), t2.add(j, lst, t1.w[i]);
DFS1(j, u);
}
}
void DFS2(int u, int f) {//求分治中心
sze[u] = 1;
for(int i = t2.h[u]; ~i; i = t2.ne[i]) {
int j = t2.e[i];
if(vis[i] || j == f) continue;
DFS2(j, u);
sze[u] += sze[j];
if(max(sze[j], nn - sze[j]) < res) {
res = max(sze[j], nn - sze[j]), root = i;
}
}
}
void DFS3(int u, int f, ll dis, ll dep, int type) {
//更新每个点的边分树信息
if(u <= n) {
if(!lst[u]) {
tr[u] = ++tot;
}
else {
ch[lst[u]][lstp[u]] = ++tot;
}
sz[tot][type]++;
sum[tot][type] = dep + dis;
lst[u] = tot;
lstp[u] = type;
}
for(int i = t2.h[u]; ~i; i = t2.ne[i]) {
int j = t2.e[i];
if(j == f || vis[i]) continue;
DFS3(j, u, dis, dep + t2.w[i], type);
}
}
void DFS4(int id) {//边分治
vis[id] = vis[id ^ 1] = 1;
int L = t2.e[id], R = t2.e[id ^ 1];
DFS2(L, R), nn = sze[L], res = 0x3f3f3f3f, root = -1, DFS2(L, R);
int rootl = root;
DFS2(R, L), nn = sze[R], res = 0x3f3f3f3f, root = -1, DFS2(R, L);
int rootr = root;
DFS3(L, R, 0, 0, 0), DFS3(R, L, t2.w[id], 0, 1);
if(~rootl) DFS4(rootl);
if(~rootr) DFS4(rootr);
}
}
using namespace conquer;
int merge(int x, int y) {
if(!x || !y) return x + y;
int cur = ++tot;
sum[cur][0] = sum[x][0] + sum[y][0];
sum[cur][1] = sum[x][1] + sum[y][1];
sz[cur][0] = sz[x][0] + sz[y][0];
sz[cur][1] = sz[x][1] + sz[y][1];
ch[cur][0] = merge(ch[x][0], ch[y][0]);
ch[cur][1] = merge(ch[x][1], ch[y][1]);
return cur;
}
ll query(int x, int y) {
if(!x || !y) return 0;
if(sz[y][0]) {
return sum[x][1] + sz[x][1] * sum[y][0] + query(ch[x][0], ch[y][0]);
}
if(sz[y][1]) {
return sum[x][0] + sz[x][0] * sum[y][1] + query(ch[x][1], ch[y][1]);
}
return 0;
}
int main() {
read(n, q);
S = n;
for(int i = 1; i <= n; i++) {
read(aa[i]);
}
for(int i = 1; i < n; i++) {
read(u, v, w), t1.add(u, v, w), t1.add(v, u, w);
}
DFS1(1, 0), res = 0x3f3f3f3f, conquer::root = -1, DFS2(1, 0);
if(~conquer::root) {
DFS4(conquer::root);
}
for(int i = 1; i <= n; i++) {
::root[i] = merge(::root[i - 1], tr[aa[i]]);
}
while(q--) {
read(t);
if(t == 1) {
read(a, b, c);
a ^= (lstans & ((1 << 30) - 1));
b ^= (lstans & ((1 << 30) - 1));
c ^= (lstans & ((1 << 30) - 1));//l, r, v
printf("%lld\n", lstans = (query(::root[b], tr[c]) - query(::root[a - 1], tr[c])));
}
else {
read(a);
a ^= (lstans & ((1 << 30) - 1));
::root[a] = merge(::root[a - 1], tr[aa[a + 1]]);
swap(aa[a], aa[a + 1]);
}
}
}
/*
start coding at:2023/12/8 10:40
finish debugging at:2023/12/8 11:47
stubid mistakes:query没有乘以对应系数,S = n写在最后面,没有更新lstans,root[a]应该由root[a - 1]合并而来
*/
/*
吾日三省吾身:
你排序了吗?
你取模了吗?
你用%lld输出long long 了吗?
1LL<<x写对了吗?
判断=0用了abs()吗?
算组合数判了a<b吗?
线段树build()了吗?
.size()加了(signed)吗?
树链剖分的DFS序是在第二次更新的吗?
修改在询问前面吗?
线段树合并到叶子结点return了吗?
__builtin_ctz后面需要加ll吗?
*/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具