Description
一棵 n 个结点的树,有正边权。
用 y 条链覆盖这棵树,满足:
- 所有链连通(有重点即算作相连)
- 点 x 被覆盖
- 被覆盖的边的权值和尽可能大
q 次给出 x, y,询问最大边权和,强制在线。
n, q ≤ 10510^5105
Solution
- 可以发现一些比较显然的性质:
- 权值是正的,那么链的端点一定可以全部调整成叶子。
- 如果链不连通,可以交换链的端点使它们连通。
- 使用 k 条路径就可以覆盖一棵有 2k 个叶子的树。(先以任意方式匹配叶子。如果有两条路径不相交,可以调整成相交的情况。
不断调整就可以让任意两条路径都相交,于是显然覆盖了整棵树。)
- 现在问题是寻找 2 ∗ y 个叶子,使 x 被包含,使被包含的边权和最大。
- 考虑单次询问怎么办。如果只有一次询问,我们就把询问的点 x 提作根,如果 x 的度数不为 1 ,那么选 y 条路径的最优方案必然可以转化为选 2y 个叶子,然后求这 2y 个叶子到 x 的路径的并的长度。如果 x 的度数为 1 的话,那么就是选 2y-1 个叶子,因为有一条路径要从 x 出发。
- 那么显然就可以贪心的选当前贡献最大的叶子加入答案了。
- 如果有多组询问呢?每次询问都换根显然不现实,但发现每次询问中,一定存在一种方案使得直径的两端中至少有一端被选取。
- 那我们就可以把直径的两个端点分别作为根来考虑,最后询问的时候取最大值即可。
- 考虑对每棵树预处理出 ansians_iansi 表示当前选了 i 个叶子的最大权值和,这个可以用 DFS序的线段树或者是长链剖分来实现。
- 线段树实现:每个叶子节点的贡献是 它的深度-它到根路径上已经被其它路径覆盖的部分的长度,用线段树维护一下叶子的贡献,每次贪心的选取贡献最大的叶子
- 长链剖分实现:一个子树内,最先被选的显然是深度最大的叶子,那么一个叶子的贡献,就是在长链剖分后,所在重链以及上方第一条轻边的距离。
- 然后考虑一次询问 x,y。如果 ans2y−1ans_{2y-1}ans2y−1中包含了 x 点,那么直接输出 ans2y−1ans_{2y-1}ans2y−1即可。否则,加入 x 子树内最深叶子,然后需要删去一个之前选的叶子,使得减少的权值最小:
- 这个叶子可能是最后一个被选的叶子,也就是贡献最小的那个。
- 由于 x 的加入,与 x 的 lca 最深的被选叶子的贡献可能会减小,而被删去。(找离 x 最近的有叶子被选的祖先的一条路径,砍掉一半后跟 x 的子树中深度最大的点接上。)
- 可以发现,第一种情况好办,倍增跳到到第一个被访问时间 ≤2y−2\leq 2y-2≤2y−2 祖先 u ,那么答案就是 ans2y−2−dep[u]+mx[x]ans_{2y-2}-dep[u]+mx[x]ans2y−2−dep[u]+mx[x]。
- 然后考虑第二种情况。第二种情况乍一看需要维护在所有时间所有点的子树内选的叶子节点的权值的最小值,这个很麻烦。但是仔细想想可以发现,只有在这个祖先的子树内只有一个叶子节点被选的时候,这种情况才可能有第一种情况优。因为如果多于两个叶子,那么删掉最小的那个叶子节点所减去的依旧是这个点加进来时候的权值,既然是减去加进来的权值那么第一种情况显然是最小的。如果只有一个叶子,那么这个叶子所代表的链跟 x 到根的路径是有交的,所以才有可能更短。实现方面可以倍增跳到第一个被访问时间 ≤2y−1\leq 2y-1≤2y−1的节点 u,那么答案就会是 ans2y−1−mx[u]+mx[x]ans_{2y-1}-mx[u]+mx[x]ans2y−1−mx[u]+mx[x]。
- 那么最终的答案就是两颗树的两种情况的最大值了。
- 时间复杂度 O((n+q)logn)O((n+q)\log n)O((n+q)logn)。
疑问:就网上的其它博客和我自己的提交来看,直径两端点分别作为根的情况好像不用都考虑,随便找一种就好。这样做为什么是对的?知道的朋友可以麻烦解答一下吗?
Code
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+5;
struct Edge{int v,w,nxt;
}edge[N<<1];
int n,m,head[N],cnt,rt,mxd,lstans,ans[N];
int fa[N][25],dep[N],son[N],dfn[N],ind,ln[N],md[N],low[N];
pair<int,int> mx[N<<2];int tag[N<<2];
int vis[N];
void add(int u,int v,int w){edge[++cnt].v=v;edge[cnt].w=w;edge[cnt].nxt=head[u];head[u]=cnt;
}
void find(int u,int f,int d){if(d>mxd){rt=u;mxd=d;} for(int i=head[u];i;i=edge[i].nxt){int v=edge[i].v;if(v==f) continue;find(v,u,d+edge[i].w);}
}
void dfs(int u,int f){ln[dfn[u]=++ind]=u;md[u]=dep[u];for(int i=1;i<=20;i++){if(!fa[fa[u][i-1]][i-1]) break;fa[u][i]=fa[fa[u][i-1]][i-1];}for(int i=head[u];i;i=edge[i].nxt){int v=edge[i].v;if(v==f) continue;dep[v]=dep[u]+edge[i].w;fa[v][0]=u;dfs(v,u);md[u]=max(md[u],md[v]);}low[u]=ind;
}
void pushup(int u){if(mx[u<<1].first<mx[u<<1|1].first) mx[u]=mx[u<<1|1];else mx[u]=mx[u<<1];
}
void build(int u,int l,int r){if(l==r){mx[u]=make_pair(dep[ln[l]],ln[l]);return;}int mid=(l+r)>>1;build(u<<1,l,mid);build(u<<1|1,mid+1,r);pushup(u);
}
void pushdown(int u,int l,int r){if(tag[u]){mx[u<<1].first+=tag[u];mx[u<<1|1].first+=tag[u];tag[u<<1]+=tag[u];tag[u<<1|1]+=tag[u];tag[u]=0;}
}
void update(int u,int l,int r,int a,int b,int w){if(a<=l&&r<=b){mx[u].first+=w;tag[u]+=w;return;}pushdown(u,l,r);int mid=(l+r)>>1;if(a<=mid) update(u<<1,l,mid,a,b,w);if(b>mid) update(u<<1|1,mid+1,r,a,b,w);pushup(u);
}
void prework(){dfs(rt,0);build(1,1,n);//贪心:每次选取贡献最大的点,选完后修改其它点的贡献 for(int i=2;i<=n;i++){ans[i]=ans[i-1]+mx[1].first;for(int j=mx[1].second;j&&!vis[j];j=fa[j][0]){vis[j]=i;update(1,1,n,dfn[j],low[j],dep[fa[j][0]]-dep[j]);}}
}
int solve(int x,int y){y=min(y,n);if(vis[x]<=y) return ans[y];//x已经被覆盖int u=x;for(int i=20;i>=0;i--)if(vis[fa[x][i]]>y) x=fa[x][i];x=fa[x][0];return ans[y]+md[u]-dep[x]-min(dep[x],min(ans[y]-ans[y-1],md[x]-dep[x]));
}
int main(){scanf("%d%d",&n,&m);for(int i=1;i<n;i++){int u,v,w;scanf("%d%d%d",&u,&v,&w);add(u,v,w);add(v,u,w);}find(1,0,0);//找直径端点 prework();for(int i=1;i<=m;i++){int u,v;scanf("%d%d",&u,&v);u=(u+lstans-1)%n+1;v=(v+lstans-1)%n+1;printf("%d\n",lstans=solve(u,v<<1));}return 0;
}
参考文章:
https://www.luogu.com.cn/blog/46396/solution-cf526g