小清新数据结构题
XII.小清新数据结构题
太 清 新 了
话说就我一个人看到这道题后兴冲冲的以为暴力LCT就能过然后发现LCT如果维护子树信息的话只有根节点处的信息是正确的吗(没错,就我一个)
闲话少说,正片开始。
法一:推一种式子,然后LCT/树剖维护
我们设为节点的值,然后为根节点为时以为根的子树的子树和。
则根为时的答案即为。设其为。
我们看一下当位置的增大后,会如何变化(其中意为的路径,而为这一路径上的节点数量):
与都可以很方便地使用LCT或树剖维护。这里我选用LCT,毕竟这题的LCT如果用无根LCT(即根固定为的LCT)的话,直接access
一下即可打包出来这条路径,非常方便。
而的变化,实际上仅仅是在增加时,每个全都增加而已。打个tag就解决了。
维护完这些东西后,我们便可以在点权变化的时候同时维护了就可以拿到了。
现在我们考虑根不是了,它变到了。则新的(设为)又会怎么变化呢?
我们有,其中为新的值。
但是,对于大多数情况,都仍有——准确的说,除了到路径上的点,其它的都没有发生变化。我们仍然设这条路径为,这里我们从到将路径上的点依次编号为,并令。
如果我们画出图来观察一下,就会惊讶地发现,必然有
而这上面所有的东西,全都等于整棵树的权值和。
因此我们有
套用我们上面对和的定义(注意到这里则有),我们得到
刚好是我们之前LCT维护的东西,因此直接搬过来用即可。
复杂度。
代码:
#include<cstdio>
#include<vector>
using namespace std;
typedef long long ll;
#define lson t[x].ch[0]
#define rson t[x].ch[1]
int n,m,val[200100];
ll res;
struct LCT{
int fa,ch[2],val,tag,sz;
ll sum;
}t[200100];
inline int identify(int x){
if(x==t[t[x].fa].ch[0])return 0;
if(x==t[t[x].fa].ch[1])return 1;
return -1;
}
inline void ADD(int x,int y){
t[x].val+=y,t[x].tag+=y,t[x].sum+=1ll*t[x].sz*y;
}
inline void pushdown(int x){
if(lson)ADD(lson,t[x].tag);
if(rson)ADD(rson,t[x].tag);
t[x].tag=0;
}
inline void pushup(int x){
t[x].sum=t[x].val,t[x].sz=1;
if(lson)t[x].sum+=t[lson].sum,t[x].sz+=t[lson].sz;
if(rson)t[x].sum+=t[rson].sum,t[x].sz+=t[rson].sz;
}
inline void rotate(int x){
register int y=t[x].fa;
register int z=t[y].fa;
register int dirx=identify(x);
register int diry=identify(y);
register int b=t[x].ch[!dirx];
if(diry!=-1)t[z].ch[diry]=x;t[x].fa=z;
if(b)t[b].fa=y;t[y].ch[dirx]=b;
t[y].fa=x,t[x].ch[!dirx]=y;
pushup(y),pushup(x);
}
inline void pushall(int x){
if(identify(x)!=-1)pushall(t[x].fa);
pushdown(x);
}
inline void splay(int x){
pushall(x);
while(identify(x)!=-1){
register int fa=t[x].fa;
if(identify(fa)==-1)rotate(x);
else if(identify(x)==identify(fa))rotate(fa),rotate(x);
else rotate(x),rotate(x);
}
}
inline void access(int x){
for(register int y=0;x;x=t[y=x].fa)splay(x),rson=y,pushup(x);
}
inline void makeroot(int x){
access(x),splay(x);
}
vector<int>v[200100];
void dfs(int x,int fa){
t[x].val=val[x];
for(auto y:v[x])if(y!=fa)t[y].fa=x,dfs(y,x),t[x].val+=t[y].val;
res+=1ll*t[x].val*t[x].val;
pushup(x);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
for(int i=1;i<=n;i++)scanf("%d",&val[i]);
dfs(1,0);
for(int i=1,x,y,z;i<=m;i++){
scanf("%d%d",&x,&y),makeroot(y);
if(x==1)scanf("%d",&z),z-=val[y],res+=1ll*z*z*t[y].sz+2ll*z*t[y].sum,ADD(y,z),val[y]+=z;
else pushall(1),printf("%lld\n",res+1ll*t[1].val*(1ll*(t[y].sz+1)*t[1].val-2ll*t[y].sum));
}
return 0;
}
法二:推另一种式子,然后点分树维护
我们仍然令为单点权值,但这里的为对于一次询问,以被询问节点为树根时,子树权值和。再令。
则我们要求的是。
看着不太爽吧?毕竟动态点分治更侧重于路径的维护(这一点跟LCT类似,但是动态点分治比起LCT还要更“路径”一点——它几乎维护不了子树信息)。
我们尝试将其变化成
后一半,我们发现乘起来的两部分,可以被抽象为由一条边连接着的两个子树的权值和乘一起——这恰恰证明了后面的东西与根无关,因为这个它枚举了每一条边。
我们进一步可以把它拆成两个集合和表示两半子树,则它实际上等价于
我们发现,对于每一对,它们会在两点间路径上的每一条边处被计算一次。
因此上面实际上也可以被转成。这里就只与路径信息有关了。并且,因为这个值与根无关,我们实际上还可以做的更多。
现在话归前一半。很容易维护,关键是求出。
它可以等价于,因为每个点的点权会贡献给它所有的祖先——这一数量等于。
我们将其拆开,便得到了
我们发现后面这一半东西就可以轻松用点分治维护了。我们设。
则最终该式即被转换成。
我们考虑设后面一大坨为。
考虑当增大后,会发生什么变化:
则只需要在修改时顺手维护掉即可。
则最终答案即为。
至此本题解决。
明显该算法复杂度为——假如你用RMQ求的话。但是其常数远大于LCT——大约是其倍。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,val[200100],fa[200100],dep[200100],in[200100],tot,mn[400100][20],LG[400100];
namespace Tree{
vector<int>v[200100];
int sz[200100],SZ,msz[200100],ROOT;
bool vis[200100];
void getsz(int x,int fa){
sz[x]=1;
for(auto y:v[x])if(!vis[y]&&y!=fa)getsz(y,x),sz[x]+=sz[y];
}
void getroot(int x,int fa){
sz[x]=1,msz[x]=0;
for(auto y:v[x])if(!vis[y]&&y!=fa)getroot(y,x),sz[x]+=sz[y],msz[x]=max(msz[x],sz[y]);
msz[x]=max(msz[x],SZ-sz[x]);
if(msz[x]<msz[ROOT])ROOT=x;
}
void solve(int x){
getsz(x,0);
vis[x]=true;
for(auto y:v[x]){
if(vis[y])continue;
ROOT=0,SZ=sz[y],getroot(y,0),fa[ROOT]=x,solve(ROOT);
}
}
void getural(int x,int fa){
mn[++tot][0]=x,in[x]=tot;
for(auto y:v[x])if(y!=fa)dep[y]=dep[x]+1,getural(y,x),mn[++tot][0]=x;
}
}
int MIN(int i,int j){
return dep[i]<dep[j]?i:j;
}
int LCA(int i,int j){
if(i>j)swap(i,j);
int k=LG[j-i+1];
return MIN(mn[i][k],mn[j-(1<<k)+1][k]);
}
int DIS(int i,int j){
return dep[i]+dep[j]-dep[LCA(in[i],in[j])]*2;
}
namespace cdt{
ll sf[200100],pa[200100],all,sz[200100],ALL;
ll ask(int x){
ll res=0;
for(int u=x;u;u=fa[u]){
res+=sf[u];
res+=1ll*DIS(u,x)*sz[u];
if(fa[u])res-=pa[u],res-=1ll*DIS(fa[u],x)*sz[u];
}
return res;
}
void change(int x,int delta){
ALL+=1ll*ask(x)*delta;
for(int u=x;u;u=fa[u]){
sz[u]+=delta;
sf[u]+=DIS(u,x)*delta;
if(fa[u])pa[u]+=DIS(fa[u],x)*delta;
}
val[x]+=delta,all+=delta;
}
ll solve(int x){
return all*(all+ask(x))-ALL;
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),Tree::v[x].push_back(y),Tree::v[y].push_back(x);
Tree::msz[0]=0x3f3f3f3f,Tree::SZ=n,Tree::getroot(1,0),Tree::solve(Tree::ROOT);
Tree::getural(1,0);
for(int i=2;i<=tot;i++)LG[i]=LG[i>>1]+1;
for(int j=1;j<=LG[tot];j++)for(int i=1;i+(1<<j)-1<=tot;i++)mn[i][j]=MIN(mn[i][j-1],mn[i+(1<<(j-1))][j-1]);
for(int i=1,x;i<=n;i++)scanf("%d",&x),cdt::change(i,x);
for(int i=1,x,y,z;i<=m;i++){
scanf("%d%d",&x,&y);
if(x==1)scanf("%d",&z),cdt::change(y,z-val[y]);
else printf("%lld\n",cdt::solve(y));
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?