CF1000G Two-Paths 题解
CF1000G Two-Paths 题解
题意
给定一颗树,询问一条以 u
,v
为起点与重点的路径的 点权和-边权和,
每条边最多经过两次,点权仅能算一次所能得到的最大值。
思路:换根DP
首先对于上面这个图,我们可以发现整个路径的特征,边的编号表示一种可能的遍历顺序:
1.(其实可以先遍历 u
的子树,只不过图中未画出。)
2.在 u
到 LCA
的路径上的一点 x
,可以选择遍历完 x
的子树后再往 x
的父亲走。
3.到达了 LCA
后,可以选择遍历完 LCA
的子树后再遍历 LCA
向上的那一部分。
4.从 LCA
到 v
的路径上一点 x
可以选择遍历完 x
除了向 v
的那一部分在向 v
那边走.
于是设一个dp
方程 dp[x][0]
与 dp[x][1]
dp[x][0]
表示从 x
往下走最终回到 x
的最大价值。
dp[x][1]
表示从 x
随便走(即以 x
为出发点的全局最大值),最终回到 x
的最大价值。
PS:上文的遍历是在最优选择下的遍历,并不是全部遍历完的意思。
DP的转移:
仅仅选择子树的话很简单,设当前点为 u
, 出点为 v
。
如果当前 u
最优可以选择 v
,一定是可以获得的价值 >0 ,即 dp[v][0]-edge[i]-edge[i^1]
,edge[i]
表示这条边,而edge[i^1]
就是这条边的反边。(因为过去还要回来,所以是这个代价)
于是方程也很简单:dp[u][0]
的初始值首先设为 a[x]
然后 dp[x][0]+=max(0,dp[y][0]-edge[i]-edge[i^1])
也就是一个简单的 Dfs
可以解决。
而 dp[u][1]
仅仅需要一个换根就解决了。
但是注意转移时,因为 dp[u][1]
表示的是全局,可能会对 dp[v][1]
产生重复计算,这个时候我们需要剪掉重复计算的部分,即 dp[v][0]
,当然,dp[v][0]
可能对 dp[u][1]
无贡献。计算一下是否 dp[v][0]-edge[i]-edge[i^1]
是否>0即可,转移时同样计算 dp[u][1]
是否可以对它的产生贡献,也就是除去重复的贡献后再剪掉两个边的边权,距离可以看代码。
两个 Dfs
即可搞定,一次预处理,一次换根就可以了。
处理答案
首先发现 u->LCA->v
是必定经过的,所以预先处理出这个边权,
设 h(u,v)
为 v
是否计入 u
的贡献,即 \(\max(0,dp[v][0]-e_{u->v}-e_{v->u})\)
发现整个 u->LCA
的代价可以表示为 \(\sum_{i=1}^n dp[u_i][0]-h(fa_{u_i},u_i)\) (这个式子的意思就是选完路径上每一个点的最优子树然后剪掉重复贡献。
同理的v->LCA
的代价也珂以用一个类似的式子表示,最后加上 \(dp[LCA][1]\) 再减去路径边权即可。
然后上面的那个式子可以用一个前缀和优化掉。这样就可以直接求出答案了。
用了树剖求 LCA
,于是处理路径的边权和会包含在了树剖中的 DFS1
里面。
Code:
#include<bits/stdc++.h>
using namespace std;
template <typename T>
inline void read(T &x){
x=0;char ch=getchar();bool f=false;
while(!isdigit(ch)) f|=ch=='-',ch=getchar();
while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
x=f?-x:x;return;
}
template <typename T>
inline void print(T x){
if(x<0) putchar('-'),x=-x;
if(x>9) print(x/10);
putchar(x%10^48);return;
}
#define int long long
const int N=3e5+3;
int head[N],to[N<<1],Next[N<<1],edge[N<<1],tot=1,n,a[N],q;
inline void Addedge(int u,int v,int w){
to[++tot]=v,edge[tot]=w,Next[tot]=head[u],head[u]=tot;
return;
}
//Tree&DP
int fa[N],siz[N],son[N],dep[N],up[N],down[N];
inline void Dfs1(int x,int f){
dep[x]=dep[f]+1,siz[x]=1,fa[x]=f;
for(register int i=head[x];i;i=Next[i]){
int y=to[i];if(y==f) continue;
down[y]=down[x]+edge[i],up[y]=up[x]+edge[i^1];
Dfs1(y,x);siz[x]+=siz[y];
son[x]=siz[son[x]]<siz[y] ? y :son[x];
}
return;
}
int top[N];
inline void Dfs2(int x){
top[x]=son[fa[x]]==x ? top[fa[x]] : x;
if(son[x]) Dfs2(son[x]);
for(register int i=head[x];i;i=Next[i]){
int y=to[i];
if(y==fa[x]||y==son[x]) continue;
Dfs2(y);
}
return;
}
inline int LCA(int u,int v){
while(top[u]!=top[v]){
if(dep[top[u]]>dep[top[v]]) u=fa[top[u]];
else v=fa[top[v]];
}
return dep[u]>dep[v] ? v: u;
}
//DP
int dp[N][2];
inline int Get(int v,int i){return dp[v][0]-edge[i]-edge[i^1];}
int g[N];
inline void Dfs(int x){
dp[x][0]=a[x];
for(register int i=head[x];i;i=Next[i]){
int y=to[i];
if(y==fa[x]) continue;
Dfs(y);
if(Get(y,i)>0) dp[x][0]+=Get(y,i);
}
return;
}
inline void Croot(int x){
for(register int i=head[x];i;i=Next[i]){
int y=to[i];
if(y==fa[x]) continue;
int tmp=0;if(Get(y,i)>0) tmp=Get(y,i);
if(dp[x][1]-tmp-edge[i]-edge[i^1]>0) dp[y][1]=dp[y][0]+dp[x][1]-tmp-edge[i]-edge[i^1];
else dp[y][1]=dp[y][0];
g[y]=g[x]+dp[y][0]-tmp;
Croot(y);
}
return;
}
signed main(){
read(n),read(q);
for(register int i=1;i<=n;++i) read(a[i]);
for(register int i=1;i<n;++i){
int x,y,z1,z2;read(x),read(y),read(z1),z2=z1;
Addedge(x,y,z1),Addedge(y,x,z2);
}
Dfs1(1,0),Dfs2(1);
Dfs(1);dp[1][1]=dp[1][0];Croot(1);
while(q--){
int u,v;read(u),read(v);
int l=LCA(u,v);
int ans1=up[u]-up[l]+down[v]-down[l],ans2=g[u]+g[v]-2*g[l]+dp[l][1];
print(ans2-ans1),putchar('\n');
}
return 0;
}