「CF1000G Two-Paths」
题目大意
给出一棵树,多次查询两个点之间的 \(\mathcal{Two-Path}\) 的权值的最大值,以下为 \(\mathcal{Two-Path}\) 路径权值的定义:
(其中 \(a_i\) 表示 \(i\) 的点权,\(w_e\) 表示边 \(e\) 的边权,\(k_e\) 表示边 \(e\) 被经过的次数,且要求 \(0\leq k_e\leq 2\),即每条边最多被走过两次).
分析
假如给出的树长这个样子:
考虑有一次 \(3\to 6\) 的查询.
显然对于 \(3\to 6\) 这条简单路径上的边是只能走一次的(如果走了至少 \(2\) 次,那么肯定要走至少 \(3\) 次,这样是不合法的),所以考虑贡献的时候只需要去考虑不在这条简单路径上的部分即可,也就是如下部分(蓝色和绿色部分):
其中绿色部分是路径上的点的部分子节点的贡献,蓝色部分仅在 \(\operatorname{LCA}(3,6)\)(每次查询的两个节点的最近公共祖先)处存在.先考虑绿色部分的贡献.
显然可以先处理出 \(f_i\) 表示 \(i\) 节点的所有子节点对 \(i\) 的贡献的最大值,显然有如下转移方程:
(\(VAL_u\) 表示 \(u\) 到 \(u\) 的父亲这条边的边权,\(val_u\) 表示 \(u\) 节点的权值)
但是对于两个点查询的时候并不能把路径上所以的 \(f\) 加起来,如下图:
我们需要的是蓝色部分以及绿色部分的贡献,但是对于 \(4\) 号节点存的贡献是红色部分给它的,但实际应该是只能有绿色部分,也就是说要减去蓝色部分的贡献,再理解理解后就可以得到以下式子:
(\(p\) 表示 \(i,fa\) 的部分对当前这次查询的贡献,\(fa\) 是 \(i\) 的父节点,\(fa,i\) 都是查询两个点再树上的简单路径上的点(\(i\) 为 \(u\) 或 \(v\),如果 \(i\not= u\) 且 \(i\not= v\),那么对于这条路径上的 \(i\) 的子节点也需要减去自身对 \(i\) 的贡献))
其中红色部分其实就是 \(i\) 对 \(fa\) 的贡献,将这部分拎出来就变成了所以非 \(\operatorname{LCA}\) 的节点都需要减去自己为根节点的子树对父节点的贡献.因为这个东西是固定的,所以可以直接树上差分快速计算.这样就可以通过计算一次 \(\operatorname{LCA}\) 的复杂度计算出所有路径节点的子树对路径的贡献.
下面只需要计算出 \(\operatorname{LCA}\) 的父节点对它的贡献即可,可以发现这个东西可以换根 \(\operatorname{dp}\) 求出:
(\(g_i\) 表示 \(i\) 的父节点对 \(i\) 的贡献,具体推导比较显然,不展开)
对于查询两点的简单路径的贡献也可以直接树上差分,所以单次查询的复杂度 \(=\) 计算两点 \(\operatorname{LCA}\) 的复杂度.
因为不会 \(\mathcal{Tarjan\ LCA}\) 所以用了树剖,但还是比倍增做法快了不少.
代码
#include<bits/stdc++.h>
#define REP(i,first,last) for(int i=first;i<=last;++i)
#define DOW(i,first,last) for(int i=first;last<=i;--i)
namespace IO
//快读模板
using namespace IO;
using namespace std;
const int MAXN=3e5+5;
const int MAXM=MAXN<<1;
int n,m;
long long val[MAXN];
struct Edge
{
int to,next;
long long val;
}edge[MAXM];
int edge_head[MAXN];
int edge_cnt=0;
#define FOR(now) for(int edge_i=edge_head[now];edge_i;edge_i=edge[edge_i].next)
#define TO edge[edge_i].to
#define VAL edge[edge_i].val
inline void AddEdge(int from,int to,long long val)
{
edge[++edge_cnt].to=to;
edge[edge_cnt].val=val;
edge[edge_cnt].next=edge_head[from];
edge_head[from]=edge_cnt;
}
long long f[MAXN];//f 同分析中的 f
long long sumf[MAXN];//f 函数的树上前缀和
long long d[MAXN];//每个点对父节点的贡献
long long sumdec[MAXN];//需要减去部分的树上前缀和
long long tof[MAXN];//每个点到父节点的边的边权
long long sump[MAXN],sume[MAXN];//点权与边权的树上前缀和
long long g[MAXN];//g 同分析中的 g
//以下变量为树剖所需变量
int point_deep[MAXN];
int point_father[MAXN];
int point_son[MAXN];
int point_size[MAXN];
int chain_cnt=0;
int point_id[MAXN];
int point_val[MAXN];
int chain_top[MAXN];
//树剖部分不会做说明,如有不理解建议先做模板题
void DFS_1(int now=1)//第一次 DFS
{
sump[now]=sump[point_father[now]]+val[now];//树上前缀点权
int max_size=-1;
point_size[now]=1;
FOR(now)
{
if(point_father[now]!=TO)
{
point_father[TO]=now;
point_deep[TO]=point_deep[now]+1;
tof[TO]=VAL;
sume[TO]=sume[now]+VAL;//树上前缀边权
DFS_1(TO);
f[now]+=d[TO];//计算 f
point_size[now]+=point_size[TO];
if(point_size[TO]>max_size)
{
max_size=point_size[TO];
point_son[now]=TO;
}
}
}
d[now]=max(f[now]-2*tof[now]+val[now],0ll);//计算这个点对父节点的贡献
}
void DFS_2(int now=1,int top=1,long long on=0/*从父节点转移来的 gi*/)
{
g[now]=on;
sumf[now]=sumf[point_father[now]]+f[now];
sumdec[now]=sumdec[point_father[now]]+d[now];
point_id[now]=++chain_cnt;
point_val[chain_cnt]=val[now];
chain_top[now]=top;
if(!point_son[now])
{
return;
}
DFS_2(point_son[now],top,max(on+f[now]+val[now]-d[point_son[now]]-2*tof[point_son[now]],0ll));
FOR(now)
{
if(TO!=point_father[now]&&TO!=point_son[now])
{
DFS_2(TO,TO,max(on+f[now]+val[now]-d[TO]-2*VAL,0ll));
}
}
}
int LCA(int u,int v)
{
while(chain_top[u]!=chain_top[v])
{
if(point_deep[chain_top[u]]<point_deep[chain_top[v]])
{
swap(u,v);
}
u=point_father[chain_top[u]];
}
if(point_deep[v]<point_deep[u])
{
swap(u,v);
}
return u;
}
long long Query(int u,int v)
{
int lca=LCA(u,v);
return sumf[u]+sumf[v]-2*sumf[lca]+f[lca]-sumdec[u]-sumdec[v]+2*sumdec[lca]+g[lca]-sume[u]-sume[v]+2*sume[lca]+sump[u]+sump[v]-sump[lca]-sump[point_father[lca]];//又臭又长的树上差分
}
int main()
{
Read(n,m);
REP(i,1,n)
{
Read(val[i]);
}
int u,v,w;
REP(i,1,n-1)
{
Read(u,v,w);
AddEdge(u,v,w);
AddEdge(v,u,w);
}
DFS_1();
DFS_2();
REP(i,1,m)
{
Read(u,v);
Writeln(Query(u,v));
}
return 0;
}