树上分治算法 + 路径剖分
感谢qzc大佬的论文
上个学期就听说了点分治,但是一直没学过,看了da lao的论文后,醍醐灌顶。
我们熟悉的分治 是 线性的分治(参考归并排序)。树上分治就是把一个大的关于路径的问题,变成一个个小问题组成,大问题的答案可以由小问题的答案合并而来。
点分治
POJ 1741
点分治模板题,统计树上点对间距离小于等于K的对数。
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
int n,k;
struct node{
int to,w;
};
vector<node>v[10050];
int cnt=0;
int ans=0,root,tot;
int siz[10040],mx[10040],vis[10040];
int st[10040];
void getroot(int x,int pre){
siz[x]=1;mx[x]=0;
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(to==pre||vis[to])continue;
getroot(to,x);
siz[x]+=siz[to];
mx[x]=max(mx[x],siz[to]);
}
mx[x]=max(mx[x],tot-siz[x]);
if(mx[x]<mx[root])root=x;
}
void getdis(int x,int pre,int dis){
st[++cnt]=dis;
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(to==pre||vis[to])continue;
getdis(to,x,dis+v[x][i].w);
}
}
void getans(int x,int dis,int w){
for(int i=1;i<=cnt;i++)st[i]=0;
cnt=0;
getdis(x,0,dis);
sort(st+1,st+1+cnt);
int stt=1,ed=cnt;
while(stt<ed){
if(st[stt]+st[ed]<=k){
ans+=(ed-stt)*w;
stt++;
}else ed--;
}
}
void divide(int x){
vis[x]=1;
getans(x,0,1);
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(vis[to])continue;
getans(to,v[x][i].w,-1);
root=0,tot=siz[to];
getroot(to,0);
divide(root);
}
}
int main(){
while(~scanf("%d%d",&n,&k)){
if(!n&&!k)break;
ans=0,cnt=0;
for(int i=1;i<n;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
v[a].push_back({b,c});
v[b].push_back({a,c});
vis[i]=0;
}
vis[n]=0;
root=0,mx[0]=1e9,tot=n;
getroot(1,0);
divide(root);
printf("%d\n",ans);
for(int i=1;i<=n;i++)v[i].clear();
}
return 0;
}
题意:
给你一颗树,每个节点不是黑点就是白点。求一条经过不超过K个黑点的最长路径的长度。
思路:
考虑点分治,对于每一层,我们只需要计算出经过当前重心的不超过K个黑点的距离。
首先我们考虑像树形DP的写法,我们每次只需要计算出当前子树,经过
i
i
i个黑点的最长长度,再和已经遍历过的子树去结合,得到不超过K个黑点的最长距离,当然,我们需要预处理出 前缀最大值,这样就能
O
(
子
树
链
上
最
大
黑
点
数
)
O(子树链上最大黑点数)
O(子树链上最大黑点数)的求得当前子树的答案。
如果按照随意顺序的话,遍历一颗子树就是
m
a
x
(
当
前
子
树
链
上
最
大
黑
点
数
,
已
经
遍
历
过
的
子
树
链
上
最
大
黑
点
数
)
max(当前子树链上最大黑点数,已经遍历过的子树链上最大黑点数)
max(当前子树链上最大黑点数,已经遍历过的子树链上最大黑点数),这个复杂度是会到
O
(
n
2
)
O(n^2)
O(n2)级别的。
所以我们考虑启发式合并,我们从 子树链上最大黑点数 小的子树开始遍历,遍历完所有的子树,最多是
O
(
n
)
O(n)
O(n)的。
这样复杂度就是 分治一个
l
o
g
N
logN
logN,排序一个
l
o
g
N
logN
logN,计算答案是
O
(
N
)
O(N)
O(N)的,所以总的复杂度就是
O
(
N
l
o
g
2
N
)
O(Nlog^2N)
O(Nlog2N)的。
还有一种用树状数组的,就不用启发式合并了,之后补。
ops:
找遍全网没有找到一个用树形DP的写法的,所以就自己写了,调了很久。不用快读的话,过不过全看评测姬,我有一发1.06s过的,也有一发1.05s TLE 的。上了快读就平稳过了。
updated 2021-10-25:
找到了之前wa的原因,之前在分治的时候把合法儿子存下来,想着节省时间复杂度的,但是因为是递归算法+全局变量,所以在下一层会改变全局变量,然后回到这一层的时候,存的儿子已经是假的了,就wa了,哭哭……
启发式合并(树形DP)纯享版:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline int qread(){
int s=0,w=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')w=-1;
for (;ch>='0'&&ch<='9';ch=getchar())s=(s<<1)+(s<<3)+(ch^48);
return (w==-1?-s:s);}
int col[200050];
int n,k,m;
struct node{
int to,w;
};
vector<node>v[200050];
int dep[200050];
int g[200050],f[200050];
int vis[200050],mx[200050],siz[200050];
int root,tot,ans=0,pre;
void getroot(int x,int pre){
siz[x]=1,mx[x]=0;
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(to==pre||vis[to])continue;
getroot(to,x);
siz[x]+=siz[to];
mx[x]=max(mx[x],siz[to]);
}
mx[x]=max(mx[x],tot-siz[x]);
if(mx[x]<mx[root])root=x;
}
struct nod{
int len,son,cnt;
}z[400050];
int cc=0;
void getcnt(int x,int pre,int pos,int cnt){
cnt+=col[x];
if(cnt>k)return;
if(cnt>z[pos].cnt)z[pos].cnt=cnt;
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(to==pre||vis[to])continue;
getcnt(to,x,pos,cnt);
}
}
bool cmp(nod a,nod b){
return a.cnt<b.cnt;
}
void getdis(int x,int pre,int dis,int cnt){
cnt+=col[x];
if(cnt>k)return;
if(dis>f[cnt])f[cnt]=dis;
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(to==pre||vis[to])continue;
getdis(to,x,dis+v[x][i].w,cnt);
}
}
void divide(int x){
vis[x]=1;
int cc=0;
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(vis[to])continue;
cc++;
z[cc].len=v[x][i].w;
z[cc].son=to;z[cc].cnt=0;
getcnt(to,0,cc,0);
}
sort(z+1,z+1+cc,cmp);
for(int i=0;i<=z[cc].cnt;i++)g[i]=0;
for(int i=1;i<=cc;i++){
int to=z[i].son;
for(int j=0;j<=z[i].cnt;j++)f[j]=-1e9;
getdis(to,0,z[i].len,0);
for(int j=1;j<=z[i].cnt;j++)f[j]=max(f[j-1],f[j]);
for(int j=0;j<=z[i].cnt;j++){
int id=min(z[i-1].cnt,k-j-col[x]);
if(id<0)break;
ans=max(ans,f[j]+g[id]);
}
for(int j=0;j<=z[i].cnt;j++)g[j]=max(g[j],f[j]);
for(int j=1;j<=z[i].cnt;j++)g[j]=max(g[j-1],g[j]);
}
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(vis[to])continue;
root=0,tot=siz[to];
getroot(to,0);
divide(root);
}
}
int main(){
n=qread(),k=qread(),m=qread();
for(int i=1;i<=m;i++){
int a;a=qread();
col[a]=1;
}
for(int i=1;i<n;i++){
int a,b,c;
a=qread(),b=qread(),c=qread();
v[a].push_back({b,c});
v[b].push_back({a,c});
}
tot=n,root=0,mx[0]=1e8;
getroot(1,0);
divide(root);
printf("%d\n",ans);
return 0;
}
树状数组【舒适版】
时间比上面那份要多,常数较大,但是我过了。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline int qread(){
ll s=0,w=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')w=-1;
for (;ch>='0'&&ch<='9';ch=getchar())s=(s<<1)+(s<<3)+(ch^48);
return (w==-1?-s:s);}
int col[200050];
int n,k,m;
struct node{
int to,w;
};
vector<node>v[200050];
int dep[200050];
int vis[200050],mx[200050],siz[200050];
int root,tot,ans=0,pre;
int d[200005];
ll lowbit(ll i){
return i&(-i);
}
void add(int i,int val){
while(i<=n+1){
d[i]=max(d[i],val);
i+=lowbit(i);
}
}
void del(int i){
while(i<=n+1){
d[i]=0;
i+=lowbit(i);
}
}
int query(int i){
int ans=0;
while(i){
ans=max(ans,d[i]);
i-=lowbit(i);
}
return ans;
}
void getroot(int x,int pre){
siz[x]=1,mx[x]=0;
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(to==pre||vis[to])continue;
getroot(to,x);
siz[x]+=siz[to];
mx[x]=max(mx[x],siz[to]);
}
mx[x]=max(mx[x],tot-siz[x]);
if(mx[x]<mx[root])root=x;
}
int f[200050];
void getdis(int x,int pre,int dis,int cnt){
f[cnt]=max(f[cnt],dis);
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(to==pre||vis[to])continue;
if(col[to])getdis(to,x,dis+v[x][i].w,cnt+1);
else getdis(to,x,dis+v[x][i].w,cnt);
}
}
void getdep(int x,int pre){
dep[x]=0;
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(vis[to]||to==pre)continue;
getdep(to,x);
dep[x]=max(dep[to],dep[x]);
}
if(col[x])dep[x]++;
}
void divide(int x){
vis[x]=1;
getdep(x,0);
for(int i=0;i<=dep[x];i++)del(i+1);
int pre=0;
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(vis[to])continue;
for(int j=0;j<=dep[to];j++)f[j]=-1e9;
getdis(to,0,v[x][i].w,col[to]);
for(int j=0;j<=dep[to];j++){
int id=min(pre,k-j-col[x]);
if(id<0)break;
ans=max(ans,f[j]+query(id+1));
}
for(int j=0;j<=dep[to];j++)add(j+1,f[j]);
pre=max(pre,dep[to]);
}
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(vis[to])continue;
root=0;tot=siz[to];
getroot(to,0);
divide(root);
}
}
int main(){
n=qread();k=qread();m=qread();
for(int i=1;i<=m;i++){
int a;a=qread();
col[a]=1;
}
for(int i=1;i<n;i++){
int a,b,c;
a=qread();b=qread();c=qread();
v[a].push_back({b,c});
v[b].push_back({a,c});
}
dep[0]=0;
tot=n,root=0,mx[0]=1e8;
getroot(1,0);
divide(root);
printf("%d\n",ans);
return 0;
}
路径剖分
树链剖分之后就可以把树上一条路径分成
l
o
g
log
log个连续的区间,就可以搭配线段树等数据结构啦。
当然也可以支持一些比较复杂的跳祖先的操作,这样能比较简单地维护一些子树信息。
SPOJ 375
一道模板树链剖分题,试试手吧
题意:
给你一颗有边权的树,你需要支持两种操作:
1、把第
i
i
i条边的权值改成
t
i
ti
ti;
2、询问点
a
a
a到点
b
b
b的简单路径上的最大边权
思路:
首先,肯定是边权转为点权,选择把边权转化成深度较大的点的点权 ,然后询问路径就靠树剖+线段树的
O
(
n
l
o
g
2
n
)
O(nlog^2n)
O(nlog2n)来好啦。
需要注意的点就是,建树以及询问的时候记得分清楚是原点还是 dfs序的点;以及,询问路径的时候,注意lca那个点的点权代表的 不是路径上的点,记得要跳过。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
int t,n;
struct node{
int to,w;
};
struct nod{
int a,b;
}z[10050];
vector<node>v[10050];
int dis[10050];
int cnt=0;
int siz[10050],dep[10050],son[10050],top[10050],id[10050],dfn[10050],fa[10050];
int tr[40060];
void dfs1(int x,int pre){
fa[x]=pre;siz[x]=1;dep[x]=dep[pre]+1;son[x]=0;
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(to==pre)continue;
dis[v[x][i].to]=v[x][i].w;
dfs1(to,x);
siz[x]+=siz[to];
if(siz[to]>siz[son[x]])son[x]=to;
}
}
void dfs2(int x,int pre){
if(son[pre]==x)top[x]=top[pre];
else top[x]=x;
id[x]=++cnt;
dfn[cnt]=x;
if(son[x]!=0)dfs2(son[x],x);
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(to==pre||to==son[x])continue;
dfs2(to,x);
}
}
void build(int p,int l,int r){
if(l==r){
tr[p]=dis[dfn[l]];
return ;
}
int mid=l+r>>1;
build(2*p,l,mid);
build(2*p+1,mid+1,r);
tr[p]=max(tr[2*p],tr[2*p+1]);
}
void update(int p,int l,int r,int x,int w){
if(l==r){
tr[p]=w;
return ;
}
int mid=l+r>>1;
if(x<=mid)update(2*p,l,mid,x,w);
else update(2*p+1,mid+1,r,x,w);
tr[p]=max(tr[2*p],tr[2*p+1]);
}
int query(int p,int l,int r,int x,int y){
if(x<=l&&r<=y){
return tr[p];
}
int mid=l+r>>1;
int ans=0;
if(x<=mid)ans=max(ans,query(2*p,l,mid,x,y));
if(mid<y)ans=max(ans,query(2*p+1,mid+1,r,x,y));
return ans;
}
int sum(int x,int y){
int ans=0;
while(top[x]!=top[y]){
if(dep[top[x]]>dep[top[y]]){
ans=max(ans,query(1,1,n,id[top[x]],id[x]));
x=fa[top[x]];
}else{
ans=max(ans,query(1,1,n,id[top[y]],id[y]));
y=fa[top[y]];
}
}
if(x==y)return ans;
if(dep[x]>dep[y])ans=max(ans,query(1,1,n,id[y]+1,id[x]));
else ans=max(ans,query(1,1,n,id[x]+1,id[y]));
return ans;
}
char s[10];
int main(){
scanf("%d",&t);
while(t--){
scanf("%d",&n);
for(int i=1;i<n;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
v[a].push_back({b,c});
v[b].push_back({a,c});
z[i].a=a;z[i].b=b;
}
cnt=0;
dfs1(1,0);dfs2(1,0);
build(1,1,n);
while(1){
scanf("%s",s);
if(s[0]=='Q'){
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",sum(a,b));
}else if(s[0]=='C'){
int a,b;
scanf("%d%d",&a,&b);
int x;
if(dep[z[a].a]<dep[z[a].b])x=z[a].b;
else x=z[a].a;
update(1,1,n,id[x],b);
}else{
break;
}
}
for(int i=1;i<=n;i++)v[i].clear();
}
return 0;
}
黑暗爆炸 3319
题意:
给你一颗边有黑白两种颜色的树,初始所有边都是白色,要求两种操作:
1、把从点a到点b的路径上的边都变成黑色
2、查询从u到根节点的一条黑边的编号。
思路:
树链剖分+线段树维护区间赋值,查询操作就通过跳链接近树,如果一条链的中有黑边,我们再去二分这条链即可。
代码:
Emmm……
这份代码是过不了黑暗爆炸上的那道题的,但是前七个样例都过了,死在了超时上。
复杂度是
O
(
n
l
o
g
2
n
)
O(nlog^2n)
O(nlog2n)的,这道题就当做是 用树剖进行爬祖先操作的示例吧。(过的人基本都是靠并查集维护的,因为他只有从白变成黑,所有变白之后直接用并查集 合并一下就好了)。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
namespace fastIO
{
static char buf[100000],*h=buf,*d=buf;//缓存开大可减少读入时间,看题目给的空间
#define gc h==d&&(d=(h=buf)+fread(buf,1,100000,stdin),h==d)?EOF:*h++//不能用fread则换成getchar
template<typename T>
inline void read(T&x)
{
int f = 1;x = 0;
char c(gc);
while(c>'9'||c<'0'){
if(c == '-') f = -1;
c=gc;
}
while(c<='9'&&c>='0')x=(x<<1)+(x<<3)+(c^48),c=gc;
x *= f;
}
template<typename T>
void output(T x)
{
if(x<0){putchar('-');x=~(x-1);}
static int s[20],top=0;
while(x){s[++top]=x%10;x/=10;}
if(!top)s[++top]=0;
while(top)putchar(s[top--]+'0');
}
}
using namespace fastIO;
vector<int>v[1000050];
struct node{
int a,b;
}z[1000060];
int n,m,cnt=0;
int dep[1000050],siz[1000050],son[1000050],fa[1000050];
int top[1000050],id[1000050],dfn[1000050];
int t[4000050],laz[4000050];
int ans[1000050];
void dfs1(int x,int pre){
siz[x]=1;son[x]=0;dep[x]=dep[pre]+1;fa[x]=pre;
for(int i=0;i<v[x].size();i++){
int to=v[x][i];
if(to==pre)continue;
dfs1(to,x);
siz[x]+=siz[to];
if(siz[to]>siz[son[x]])son[x]=to;
}
}
void dfs2(int x,int pre){
if(son[pre]==x)top[x]=top[pre];
else top[x]=x;
id[x]=++cnt;
dfn[cnt]=x;
if(son[x])dfs2(son[x],x);
for(int i=0;i<v[x].size();i++){
int to=v[x][i];
if(to==pre||to==son[x])continue;
dfs2(to,x);
}
}
void pushdown(int p,int l,int r){
if(!laz[p])return;
int mid=l+r>>1;
if(t[2*p]!=mid-l+1){
laz[2*p]=laz[p];t[2*p]=mid-l+1;
}
if(t[2*p+1]!=r-mid){
laz[2*p+1]=laz[p];
t[2*p+1]=r-mid;
}
laz[p]=0;
}
void update(int p,int l,int r,int x,int y){
if(x>y)return;
if(t[p]==r-l+1)return;
if(x<=l&&r<=y){
if(t[p]!=r-l+1){
t[p]=r-l+1;
laz[p]=1;
}
return;
}
int mid=l+r>>1;
pushdown(p,l,r);
if(x<=mid)update(2*p,l,mid,x,y);
if(mid<y)update(2*p+1,mid+1,r,x,y);
t[p]=t[2*p+1]+t[2*p];
}
int query(int p,int l,int r,int x,int y){
if(x>y)return 0;
if(x<=l&&r<=y)return t[p];
int mid=l+r>>1;
int ans=0;
pushdown(p,l,r);
if(x<=mid)ans+=query(2*p,l,mid,x,y);
if(mid<y)ans+=query(2*p+1,mid+1,r,x,y);
return ans;
}
int query2(int p,int l,int r,int k){
if(l==r)return l;
int mid=l+r>>1;
pushdown(p,l,r);
int w=t[2*p];
if(w<k)return query2(2*p+1,mid+1,r,k-w);
else return query2(2*p,l,mid,k);
}
void add(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]>dep[top[y]]){
update(1,1,n,id[top[x]],id[x]);
x=fa[top[x]];
}else{
update(1,1,n,id[top[y]],id[y]);
y=fa[top[y]];
}
}
if(x==y)return;
if(dep[x]>dep[y])update(1,1,n,id[y]+1,id[x]);
else update(1,1,n,id[x]+1,id[y]);
}
int sum(int x){
while(x){
int res=query(1,1,n,id[top[x]],id[x]);
if(res!=0){
int su=query(1,1,n,1,id[x]);
int y=query2(1,1,n,su);
return ans[dfn[y]];
}
x=fa[top[x]];
}
return 0;
}
int main(){
read(n);read(m);
for(int i=1;i<n;i++){
read(z[i].a);read(z[i].b);
v[z[i].a].push_back(z[i].b);
v[z[i].b].push_back(z[i].a);
}
dep[0]=0;fa[0]=0;
dfs1(1,0);dfs2(1,0);
for(int i=1;i<n;i++){
int x;
if(dep[z[i].a]>dep[z[i].b])x=z[i].a;
else x=z[i].b;
ans[x]=i;
}
ans[1]=0;
for(int i=1;i<=m;i++){
int op;
read(op);
if(op==1){
int a;read(a);
output(sum(a));
putchar('\n');
}else{
int a,b;
read(a);read(b);
add(a,b);
}
}
return 0;
}
链分治
树链剖分 可以看成是一种树上 基于链的分治。
明白了这一点 就可以解决看上去和路径剖分无关的题目。
如何区分普通树链剖分 和 链分治呢,普通树链剖分是以点到根的路径作为维护对象,能较快地维护一条路径的信息;而链分治 是 通过分治的方法维护一个全局最优路径。
洛谷 P4115 / SPOJ 2666
题意:
给定一颗包含N个结点的树(
N
<
=
100000
N<=100000
N<=100000),边权的绝对值小于等于1000,每个结点要么是黑色,要么是白色,初始都是白色。要求维护两种操作:
1、改变某个结点的颜色(白变黑,黑变白);
2、询问最远的两个白色结点之间的距离。
思路:
考虑如何计算路径的最高点在此条链中的最优值。
对于一条链,记此链的结点个数为 N,
D
(
i
)
D(i)
D(i)、
D
2
(
i
)
D2(i)
D2(i) 为此链的第
i
i
i个结点向下至某个白色结点的路径中长度的最大值和次大值。(两条路径仅在头结点(路径第一个结点)处相交,如果至白色结点的路径不存在,那么长度为
−
I
N
F
-INF
−INF )
我们的目标就是要求出满足与此链的重合部分在
[
1
,
N
]
[1,N]
[1,N] 的路径的最大长度。我们可以利用线段树来维护。
对于一个区间
[
L
,
R
]
[L,R]
[L,R] ,我们记录:
M
a
x
L
MaxL
MaxL =
m
a
x
max
max {
d
i
s
(
L
,
i
)
+
D
(
i
)
dis(L,i)+D(i)
dis(L,i)+D(i) }
M
a
x
R
MaxR
MaxR =
m
a
x
max
max {
D
(
i
)
+
d
i
s
(
i
,
R
)
D(i)+dis(i,R)
D(i)+dis(i,R) }
M
a
x
V
a
l
MaxVal
MaxVal= 与此链的重合部分在
[
L
,
R
]
[L,R]
[L,R] 的路径的最大长度
d
i
s
(
i
,
j
)
dis(i,j)
dis(i,j) 表示链上的第
i
i
i 个结点到第
j
j
j 个结点的距离。
设区间
[
L
,
R
]
[L,R]
[L,R] 的结点编号为
P
P
P,
L
c
Lc
Lc,
R
c
Rc
Rc 分别表示
P
P
P 的左右两个儿子,区间
[
L
,
m
i
d
]
[L,mid]
[L,mid] 和
[
m
i
d
+
1
,
R
]
[mid+1,R]
[mid+1,R] ,我们可以得到如下转移:
M
a
x
L
(
P
)
MaxL(P)
MaxL(P) =
M
a
x
Max
Max {
M
a
x
L
(
L
c
)
,
d
i
s
(
L
,
m
i
d
+
1
)
+
M
a
x
L
(
R
c
)
MaxL(Lc), dis(L,mid+1) +MaxL(Rc)
MaxL(Lc),dis(L,mid+1)+MaxL(Rc) }
M
a
x
R
(
P
)
MaxR(P)
MaxR(P) =
M
a
x
Max
Max {
M
a
x
R
(
R
c
)
,
M
a
x
R
(
L
c
)
+
d
i
s
(
m
i
d
,
R
)
MaxR(Rc), MaxR(Lc)+dis(mid,R)
MaxR(Rc),MaxR(Lc)+dis(mid,R) }
M
a
x
V
a
l
(
P
)
MaxVal(P)
MaxVal(P)=
M
a
x
Max
Max {
M
a
x
V
a
l
(
L
c
)
,
M
a
x
V
a
l
(
R
c
)
,
M
a
x
R
(
L
c
)
+
M
a
x
L
(
R
c
)
+
d
i
s
(
m
i
d
,
m
i
d
+
1
)
MaxVal(Lc),MaxVal(Rc),MaxR(Lc)+MaxL(Rc)+dis(mid,mid+1)
MaxVal(Lc),MaxVal(Rc),MaxR(Lc)+MaxL(Rc)+dis(mid,mid+1) }
这个转移很像最大字段和的转移
对于公式中的
d
i
s
dis
dis 我们可以用前缀和处理快速得到。
对于边界情况
[
L
,
L
]
[L,L]
[L,L] ,设此区间的结点编号为
P
P
P,此链的第
L
L
L 个结点为
x
x
x,那么
M
a
x
L
(
P
)
MaxL(P)
MaxL(P) =
D
(
L
)
D(L)
D(L)
M
a
x
R
(
P
)
MaxR(P)
MaxR(P) =
D
(
L
)
D(L)
D(L)
点
x
x
x 为白点时,
M
a
x
V
a
l
(
P
)
MaxVal(P)
MaxVal(P) =
M
a
x
Max
Max {
D
(
L
)
,
D
(
L
)
+
D
2
(
L
)
D(L),D(L)+D2(L)
D(L),D(L)+D2(L) }
否则,
M
a
x
V
a
l
(
P
)
MaxVal(P)
MaxVal(P) =
D
(
L
)
+
D
2
(
L
)
D(L)+D2(L)
D(L)+D2(L)
问题就只剩下如何维护
D
D
D 和
D
2
D2
D2 的值
我们记
C
1...
C
k
C1...Ck
C1...Ck 表示
x
x
x 的
k
k
k 个儿子(不包括同层结点(同层指的是 是否是一条重链)),
L
i
Li
Li 表示
C
i
Ci
Ci 所在的链的线段树根节点,
C
o
s
t
(
p
)
Cost(p)
Cost(p) 表示
(
x
,
p
)
(x,p)
(x,p) 的边权。那么点
x
x
x 向下至某个白色结点的路径长度集合为:
{
M
a
x
L
(
L
i
)
+
C
o
s
t
(
C
i
)
,
0
MaxL(Li)+Cost(Ci),0
MaxL(Li)+Cost(Ci),0 } ——
x
x
x为白点
{
M
a
x
L
(
L
i
)
+
C
o
s
t
(
C
i
)
MaxL(Li)+Cost(Ci)
MaxL(Li)+Cost(Ci) } ——
x
x
x为黑点
我们可以用堆来维护这个集合,这样
D
(
x
)
D(x)
D(x),
D
2
(
x
)
D2(x)
D2(x) 的获取就是
O
(
1
)
O(1)
O(1) 的了。
对于询问操作,我们可以用一个堆存贮每条链的最优值,这样我们就可以每次询问
O
(
1
)
O(1)
O(1) 处理了。
对于修改操作,由于一个点只会影响
O
(
l
o
g
N
)
O(log N)
O(logN) 条链,每次都需要更改堆中的值和线段树,所以每次修改的时间复杂度为
O
(
l
o
g
2
N
)
O(log^2N)
O(log2N) 。
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline int qread(){
int s=0,w=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')w=-1;
for (;ch>='0'&&ch<='9';ch=getchar())s=(s<<1)+(s<<3)+(ch^48);
return (w==-1?-s:s);}
struct node{
int to,w;
};
vector<node>v[100040];
int n,m,cnt=0,tot=0;
int fa[100050],siz[100050],son[100050],dis[100050];
int top[100050],id[100050],dfn[100050],len[100050],root[100050];
int ls[400050],rs[400050],lv[400050],rv[400050],mv[400050];
int w[100050];
struct nod{
multiset<int,greater<int> >s;
void push(int x){
s.insert(x);
}
void pop(int x){
auto it=s.lower_bound(x);
if(it!=s.end())s.erase(it);
}
int top(){
if(s.empty())return -1e8;
else return *s.begin();
}
}hp[100050],ans;
void dfs1(int x,int pre){
siz[x]=1;fa[x]=pre;
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(to==pre)continue;
dis[to]=dis[x]+v[x][i].w;
dfs1(to,x);
siz[x]+=siz[to];
if(siz[to]>siz[son[x]])son[x]=to;
}
}
void dfs2(int x,int pre){
if(son[pre]==x)top[x]=top[pre];
else top[x]=x;
len[top[x]]++;
id[x]=++cnt;
dfn[cnt]=x;
if(son[x])dfs2(son[x],x);
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(to==pre||to==son[x])continue;
dfs2(to,x);
}
}
void pushup(int p,int l,int r){
int mid=l+r>>1;
lv[p]=max(lv[ls[p]],lv[rs[p]]+dis[dfn[mid+1]]-dis[dfn[l]]);
rv[p]=max(rv[rs[p]],rv[ls[p]]+dis[dfn[r]]-dis[dfn[mid]]);
mv[p]=max(max(mv[ls[p]],mv[rs[p]]),dis[dfn[mid+1]]-dis[dfn[mid]]+rv[ls[p]]+lv[rs[p]]);
}
void build(int p,int l,int r){
if(l==r){
int x=dfn[l];
for(int i=0;i<v[x].size();i++){
int to=v[x][i].to;
if(to==fa[x]||to==son[x])continue;
hp[x].push(lv[root[to]]+v[x][i].w);
}
int d1=hp[x].top();
hp[x].pop(d1);
int d2=hp[x].top();
hp[x].push(d1);
lv[p]=rv[p]=max(0,d1);
mv[p]=max(0,max(d1,d1+d2));
return;
}
int mid=l+r>>1;
if(!ls[p])ls[p]=++tot;
build(ls[p],l,mid);
if(!rs[p])rs[p]=++tot;
build(rs[p],mid+1,r);
pushup(p,l,r);
}
void update(int p,int l,int r,int x,int tp){
if(l==r){
if(x!=tp)hp[x].push(lv[root[tp]]+dis[tp]-dis[x]);
int d1=hp[x].top();
hp[x].pop(d1);
int d2=hp[x].top();
hp[x].push(d1);
if(w[x]){
lv[p]=rv[p]=d1;
mv[p]=d1+d2;
}else{
lv[p]=rv[p]=max(0,d1);
mv[p]=max(0,max(d1,d1+d2));
}
return ;
}
int mid=l+r>>1;
if(id[x]<=mid)update(ls[p],l,mid,x,tp);
else update(rs[p],mid+1,r,x,tp);
pushup(p,l,r);
}
void change(int x){
int pre=x;
while(x){
int tp=top[x];
int p1=mv[root[tp]];
if(fa[tp])hp[fa[tp]].pop(lv[root[tp]]+dis[tp]-dis[fa[tp]]);
update(root[tp],id[tp],id[tp]+len[tp]-1,x,pre);
int p2=mv[root[tp]];
if(p1!=p2){
ans.pop(p1);
ans.push(p2);
}
pre=tp;
x=fa[tp];
}
}
int main(){
n=qread();
for(int i=1;i<n;i++){
int a,b,c;
a=qread();b=qread();c=qread();
v[a].push_back({b,c});
v[b].push_back({a,c});
}
dfs1(1,0);dfs2(1,0);
for(int i=n;i>=1;i--){
int x=dfn[i];
if(x==top[x]){
if(!root[x])root[x]=++tot;
build(root[x],id[x],id[x]+len[x]-1);
ans.push(mv[root[x]]);
}
}
m=qread();
int num=n;
while(m--){
char s[10];
scanf("%s",s);
if(s[0]=='C'){
int a;
a=qread();
w[a]^=1;
if(w[a])num--;
else num++;
change(a);
}else{
if(!num)puts("They have disappeared.");
else printf("%d\n",ans.top());
}
}
return 0;
}
边分治
边分治是挑一条边就把树分成两颗子树,然后子树合并 相较点分治更加简单;
但是考虑菊花图,边分治会T,怎么办呢;
在 lzc 大佬的论文中,边分治的复杂度和 结点的最大度数有关,如果每个结点的度数是2或3,时间复杂度就是
n
l
o
g
n
nlogn
nlogn ,那么我可以把树转成一颗二叉树,这样最大度数就是 3;
但是用边分治,一定要保证改变树的结构不会影响答案。
重建树
void rebuild(int x,int pre){
int tmp=0;
int las=0;
int len=v[x].size();
for(int i=0;i<len;i++){
int to=v[x][i].to;
int val=v[x][i].w;
if(to==pre)continue;
tmp++;
if(tmp==1){
add(x,to,val);
add(to,x,val);
las=x;
}else if(tmp==len-(x!=1)){
add(las,to,val);
add(to,las,val);
}else{
m++;
add(las,m,0);add(m,las,0);
las=m;
add(m,to,val);add(to,m,val);
}
}
for(int i=0;i<len;i++){
int to=v[x][i].to;
if(to==pre)continue;
rebuild(to,x);
}
}
还没敲过边分的题(实际是敲了一发 S P O J 1825 SPOJ 1825 SPOJ1825 wa了) 牢记新点不能对答案构成影响。
下次补……(其实边分能做的题,点分也能做,只不过比较麻烦 555~)
SPOJ 1825 / SPOJ 2666