幻想乡战略游戏

题解:幻想乡战略游戏

原题:传送门
给定点的权值,求树上所有点到一点的 路径*权值 之和的最小值。要求在线修改点权,在线回答。
如果是单纯的静态查询,我们可以用点分治来做。但是动态呢?总不可能修改一次做一次点分吧,复杂度会达到 \(O(n^2logn)\) 。那我们就不能用点分治来做了吗?不,我们还有动态点分治!

前置:点分树

整棵树的结构是不变的,被修改的只有点权。那么,我们每一次点分选到的重心也是不变的(动态点分治的前提)。如果我们把点分治每一次选到的中心之间连边,就可以构成一棵高度为 logn 的树。
这棵树,就是我们的点分树(动态点分治)
点分树的构建:对于当前的以 u 为根的树,找到它的重心,删去重心,再对每棵子树递归子过程,并让各子树的重心与 u 的重心连边。只需要在原来的分治板子上加一句 fa[rt]=u;
修改嘛,一般直接在点分树上往上跳就行了。
这里直接套用 lsy 大佬的代码:

inline void work(int u,int father){
  vis[u]=1,fa[u]=father;
  for(int i=head[u];i;i=nex[i]){
    int v=var[i];
    Size=siz[v];rt=0;
    getroot(v,u);
    Addedge(u,rt,v);
    work(rt,u);
  }
}

象征性的Addedge,因为我们可能会需要对点分树中某一个点的子树进行区分,我们就可以通过Addedge时记录的 v 来进行区分。
对于点分树上的每个节点,保存它的所有子树中的一些信息,然后,对于每一次修改,我们就在点分树中从当前节点往上跳,时间复杂度为 logn (毕竟点分树的高不会超过log n)。

那么对于这道题呢?
先画个图帮助理解:
无标题.png

Q1 : 统计答案

我们知道,点分治的统计答案通常是在子树中找到重心再进行容斥,那么动态点分治呢?
仍然类似。
我们假设 v 的子树中的所有节点到 v 的答案已经统计好,现在要求 u 的所有子树中 除了 v 和它的子树 以外的其它的节点到 v 的答案。
对着图比划一下?


sum[u]表示以 u 为根的树中所有节点的点权之和。
求出 u 的答案之后,我们得消除 v 及其子树对于答案的影响,再加上 \((sum[u]-sum[v]) * dist(u,v)\)
不妨再定义两个数组 ans,ansf。
ans[u]表示 u 的所有子树中的节点 到 u 点的 路径 * 点权之和。
ansf[v]表示 v 的所有子树中(当然也包括了v)的点 到v的父节点(也就是u)的路径* 点权之和。
ans[u]-ansf[v]即是 u 的所有子树中 除去v和v的子树以外的 节点到u点的答案。
仍然是容斥。

\[ ans[u]-ansf[v]+(sum[u]-sum[v])*dist(u,v) \]

再求 u及其子树节点外 的节点对答案的贡献呢?只需要在点分树上跳到父节点,再重复上述过程即可。

inline ll calc(int x){
  ll ret=ans[x];
  for(int i=x;p[i];i=p[i]){
    ll l=dis(p[i],x);
    ret+=ans[p[i]]-ansf[i];
    ret+=(sum[p[i]]-sum[i])*l;
  }
  return ret;
}

修改点权时,我们也只需要在点分树上跳,维护三个数组即可。

Q2 : 更新最优解

那么,问题解决了吗?没有!经过测试,我们发现,每一次找答案如果从树根开始找的话会 T,会被卡成 \(O(n^2)\) 。怎么办呢?
解决方法是:从上一次的答案点开始找。
还是上面的那个图,我们从 u 转移到 v,那么在 u 的子树内,答案的变化量就为
\[ \Delta ans = (sum[u]-sum[v])*dist(u,v) - sum[v]*dist(u,v) \]
也就是\(\Delta ans = (sum[u]-2*sum[v])*dist(u,v)\)
显然,只有当 \(sum[v]>sum[u]/2\) 时,答案才可能会变得更优。而这样的点,最多只有一个。
因此,我们可以直接利用上一次询问的答案来进行答案更新,具体来说,就是依次扫描与它相邻的每一个点,并往答案更优的那个点更新,再重复,直至找到最优解。

inline ll query(int x){
  ll anss=calc(x);
  for(int i=head[x];i;i=nex[i]){
    int v=var[i];
    ll tmp=calc(v);
    if(tmp<anss) return query(v);//更新最优解 
  }
  last=x;return anss;
}

顺带着学了一下树上 RMQ,然而依旧有点迷 。
代码实现借鉴了这位大佬:https://blog.csdn.net/qq_34564984/article/details/53791482

#include<bits/stdc++.h>
#define ll long long
const int N=1e5+1;
using namespace std;
bool vis[N];
int n,Q,rt,cnt,last=1,Size,f[N],p[N],s[N<<1],id[N],ip[N],dep[N<<1],siz[N],st[N<<1][20];
ll ans[N],ansf[N],dist[N<<1],sum[N];
int head[N],nex[N<<1],var[N<<1],edge[N<<1];
template<typename S>inline void in(S &r){//快读 
    r=0;int f=1;char c='a';
    do{c=getchar();if(c=='-') f=-1;}while(!isdigit(c));
    do{r=(r<<3)+(r<<1)+c-'0';c=getchar();}while(isdigit(c));
    r*=f;
}
inline void add(int x,int v,int w){//加边 
    var[++cnt]=v,edge[cnt]=w,nex[cnt]=head[x],head[x]=cnt;
}
inline void gr(int x,int fa){//找重心 
    siz[x]=1,f[x]=0;
    for(int i=head[x];i;i=nex[i]){
        int v=var[i];
        if(vis[v]||v==fa) continue;
        gr(v,x);
        siz[x]+=siz[v],f[x]=max(f[x],siz[v]);
    }
    f[x]=max(f[x],Size-siz[x]);
    if(f[x]<f[rt]) rt=x;
}
inline void build(int x){//建点分树 
    vis[x]=1;
    for(int i=head[x];i;i=nex[i]){
        int v=var[i];
        if(vis[v]) continue;
        Size=siz[v],rt=0;
        gr(v,0);p[rt]=x;
        build(rt);
    }
}
inline void dfs(int x,int fa){
    s[++cnt]=x;
    if(!id[x]) id[x]=cnt;
    dep[cnt]=dep[ip[fa]]+1;ip[x]=cnt;
    for(int i=head[x];i;i=nex[i]){
        int v=var[i];
        if(v==fa) continue;
        dist[v]=dist[x]+edge[i];
        dfs(v,x);s[++cnt]=x,dep[cnt]=dep[ip[fa]+1];
    }
}
inline void make(){
    for(int i=1;i<=cnt;++i) st[i][0]=i;
    for(int j=1;j<=18;++j)
        for(int i=1;i<=cnt;++i) if(i+(1<<j)-1<=cnt){
            int x=st[i][j-1],y=st[i+(1<<j-1)][j-1];
            if(dep[x]<dep[y]) st[i][j]=x;
            else st[i][j]=y;
        }
}
inline int query2(int l,int r){
    int len=r-l+1,i=0;
    while((1<<i+1)<=len) ++i;
    if(dep[st[l][i]]<dep[st[r-(1<<i)+1][i]]) return st[l][i];
    else return st[r-(1<<i)+1][i];
}//O(1)LCA 
inline int lca(int x,int y){
    if(id[x]>id[y]) swap(x,y);
    return s[query2(id[x],id[y])];
}
inline ll dis(int x,int y){
    int c=lca(x,y);
    return dist[x]+dist[y]-2*dist[c];
}//LCA求路径长 
inline void update(int x,int val){
    sum[x]+=val;
    for(int i=x;p[i];i=p[i]){
        ll l=dis(p[i],x);
        sum[p[i]]+=val;//p[i]子树中总的点权 
        ansf[i]+=val*l;//i子树中所有节点到他爹的距离 
        ans[p[i]]+=val*l;//p[i]子树中所有节点到他的距离 
    }
}
inline ll calc(int x){
    ll ret=ans[x];
    for(int i=x;p[i];i=p[i]){
        ll l=dis(p[i],x);
        ret+=ans[p[i]]-ansf[i];//核心 
        ret+=(sum[p[i]]-sum[i])*l;
    }
    return ret;
}
inline ll query(int x){
    ll anss=calc(x);
    for(int i=head[x];i;i=nex[i]){
        int v=var[i];
        ll tmp=calc(v);
        if(tmp<anss) return query(v);//更新最优解 
    }
    last=x;return anss;
}
int main(){
    in(n),in(Q);
    for(int i=1,a,b,c;i<n;++i){
        in(a),in(b),in(c);
        add(a,b,c),add(b,a,c);
    }
    cnt=0;dfs(1,0);make();//LCA套装 
    Size=f[0]=n;
    gr(1,0);build(rt);//建点分树 
    int a,b;
    while(Q--){
        in(a),in(b);
        update(a,b);//更新当前节点值 
        printf("%lld\n",query(last));//从上一次的最优解开始寻找 
    }
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/zmjqwq/p/10963977.html