[2024-05-30]P3313 [SDOI2014] 旅行
题目描述
S 国有 \(N\) 个城市,编号从 \(1\) 到 \(N\)。城市间用 \(N-1\) 条双向道路连接,满足从一个城市出发可以到达其它所有城市。每个城市信仰不同的宗教,如飞天面条神教、隐形独角兽教、绝地教都是常见的信仰。
为了方便,我们用不同的正整数代表各种宗教,S 国的居民常常旅行。旅行时他们总会走最短路,并且为了避免麻烦,只在信仰和他们相同的城市留宿。当然旅程的终点也是信仰与他相同的城市。S 国为每个城市标定了不同的旅行评级,旅行者们常会记下途中(包括起点和终点)留宿过的城市的评级总和或最大值。
在 S 国的历史上常会发生以下几种事件:
CC x c:城市 \(x\) 的居民全体改信了 \(c\) 教;CW x w:城市 \(x\) 的评级调整为 \(w\);QS x y:一位旅行者从城市 \(x\) 出发,到城市 \(y\),并记下了途中留宿过的城市的评级总和;QM x y:一位旅行者从城市 \(x\) 出发,到城市 \(y\),并记下了途中留宿过的城市的评级最大值。
由于年代久远,旅行者记下的数字已经遗失了,但记录开始之前每座城市的信仰与评级,还有事件记录本身是完好的。请根据这些信息,还原旅行者记下的数字。 为了方便,我们认为事件之间的间隔足够长,以致在任意一次旅行中,所有城市的评级和信仰保持不变。
分析
-
这道题很明显是一道树剖题,可是我们发现不同城市有不同宗教,这就意味着我们要建立多颗线段树,空间需要 \(10^5*10^5\) ,会超空间。
-
引入一种优化线段树空间复杂度的方法: 动态开点线段树。
-
这是普通线段树:

-
当我们只需要存储一个节点(比如节点1)时,就会有很大的空间会被浪费。
-
使用动态开点线段树,只存储需要的节点(如图 红色的节点)。

以下代码实现动态开点线段树,核心思想是 用一个开一个,使用 \(ntot\) 数组存储已开节点数量。
struct node1
{
int l,r,val,maxval;
}t[N*4];
void Update(int &o,int l,int r,int x,int dd)
{
if(x<l||x>r)return;
if(!o)o=++ntot;
if(l==r)
{
t[o].val=dd;
t[o].maxval=dd;
return;
}
int mid=(l+r)>>1;
Update(t[o].l,l,mid,x,dd);
Update(t[o].r,mid+1,r,x,dd);
PushUp(o);
}
void Remove(int o,int l,int r,int x)
{
if(x<l||x>r)return;
if(l==r)
{
t[o].val=0;
t[o].maxval=0;
return;
}
int mid=(l+r)>>1;
Remove(t[o].l,l,mid,x);
Remove(t[o].r,mid+1,r,x);
PushUp(o);
}
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N=1000005;
int n,q,tot=0,ntot=0,tt=0;
int w[N],c[N],head[N],root[N];
int d[N],fa[N],top[N],son[N],siz[N],dfn[N],dfn2[N];
struct node
{
int to,next;
}e[N*4];
struct node1
{
int l,r,val,maxval;
}t[N*4];
void AddEdge(int x,int y)
{
e[++tot].to=y;
e[tot].next=head[x];
head[x]=tot;
}
void PushUp(int o)
{
t[o].val=t[t[o].l].val+t[t[o].r].val;
t[o].maxval=max(t[t[o].l].maxval,t[t[o].r].maxval);
}
void Update(int &o,int l,int r,int x,int dd)
{
if(x<l||x>r)return;
if(!o)o=++ntot;
if(l==r)
{
t[o].val=dd;
t[o].maxval=dd;
return;
}
int mid=(l+r)>>1;
Update(t[o].l,l,mid,x,dd);
Update(t[o].r,mid+1,r,x,dd);
PushUp(o);
}
void Remove(int o,int l,int r,int x)
{
if(x<l||x>r)return;
if(l==r)
{
t[o].val=0;
t[o].maxval=0;
return;
}
int mid=(l+r)>>1;
Remove(t[o].l,l,mid,x);
Remove(t[o].r,mid+1,r,x);
PushUp(o);
}
int SectionSumQuery(int o,int l,int r,int L,int R)
{
if(l>R||r<L)return 0;
if(L<=l&&r<=R)return t[o].val;
int mid=(l+r)>>1,sum=0;
sum+=SectionSumQuery(t[o].l,l,mid,L,R);
sum+=SectionSumQuery(t[o].r,mid+1,r,L,R);
return sum;
}
int SectionMaxQuery(int o,int l,int r,int L,int R)
{
if(l>R||r<L)return 0;
if(L<=l&&r<=R) return t[o].maxval;
int mid=(l+r)>>1,sum=0;
sum=max(sum,SectionMaxQuery(t[o].l,l,mid,L,R));
sum=max(sum,SectionMaxQuery(t[o].r,mid+1,r,L,R));
return sum;
}
void dfs1(int node,int f)
{
fa[node]=f;
d[node]=d[f]+1;
siz[node]=1;
for(int i=head[node];i;i=e[i].next)
{
int v=e[i].to;
if(v!=f)
{
dfs1(v,node);
siz[node]+=siz[v];
if(!son[node]||siz[son[node]]<siz[v])
son[node]=v;
}
}
}
void dfs2(int node,int topx)
{
top[node]=topx,dfn[node]=++tt,dfn2[tt]=node;
if(!son[node])return;
dfs2(son[node],topx);
for(int i=head[node];i;i=e[i].next)
{
int v=e[i].to;
if(v!=fa[node]&&v!=son[node])
dfs2(v,v);
}
}
int TreeSumQuery(int x,int y,int c)
{
int sum=0;
while(top[x]!=top[y])
{
if(d[top[x]]<d[top[y]])swap(x,y);
sum+=SectionSumQuery(root[c],1,n,dfn[top[x]],dfn[x]);
x=fa[top[x]];
}
if(d[x]>d[y])swap(x,y);
sum+=SectionSumQuery(root[c],1,n,dfn[x],dfn[y]);
return sum;
}
int TreeMaxQuery(int x,int y,int c)
{
int sum=0;
while(top[x]!=top[y])
{
if(d[top[x]]<d[top[y]])swap(x,y);
sum=max(sum,SectionMaxQuery(root[c],1,n,dfn[top[x]],dfn[x]));
x=fa[top[x]];
}
if(d[x]>d[y])swap(x,y);
sum=max(sum,SectionMaxQuery(root[c],1,n,dfn[x],dfn[y]));
return sum;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>q;
for(int i=1;i<=n;i++)cin>>w[i]>>c[i];
for(int i=1;i<=n-1;i++)
{
int x,y;
cin>>x>>y;
AddEdge(x,y);
AddEdge(y,x);
}
dfs1(1,1),dfs2(1,1);
for(int i=1;i<=n;i++)
{
Update(root[c[i]],1,n,dfn[i],w[i]);
}
while(q--)
{
string s;
int x,y;
cin>>s>>x>>y;
if(s=="CC")
{
Remove(root[c[x]],1,n,dfn[x]);
Update(root[y],1,n,dfn[x],w[x]);
c[x]=y;
}
if(s=="CW")
{
Remove(root[c[x]],1,n,dfn[x]);
Update(root[c[x]],1,n,dfn[x],y);
w[x]=y;
}
if(s=="QS")
{
cout<<TreeSumQuery(x,y,c[x])<<endl;
}
if(s=="QM")
{
cout<<TreeMaxQuery(x,y,c[x])<<endl;
}
}
return 0;
}
注意
- 空间要开 \(10^6\) 大小,存储边、线段树节点数组要开 \(max*4\) 大小。
- 在对树进行相关操作时,不要忘记使用dfn树剖编号。
其他
- 原题链接 https://www.luogu.com.cn/problem/P3313 。
- 线段树演示图二次创作,原图来自 https://baike.baidu.com/pic 。
- 参考资料 https://blog.csdn.net/CaspianSea/article/details/136145949 。

浙公网安备 33010602011771号