[noip2015]运输计划(LCA,二分)
运输计划[做题笔记]
挺难绷的。。。
题意
概括:给定 \(n\) 个节点的树和 \(n-1\) 条边的权值,现在可以将一条边的权值改为 \(0\) 。找出一条边,使得将这条边权值赋为 \(0\) 时,\(T\) 组节点 \(u,v\) 之间的距离最大值最小,输出最小值。
思路分析
一开始想假了,天真的以为被 \(T\) 组节点 \(u,v\) 覆盖的次数最多,且权值较大的边就是要删去的边,\(38\)分,查题解才知道是二分答案。
不过确实,求最大值最小
,的确是要二分的。若 \(t1<t2\) ,且 \(t1\) 合法,那么\(t2\)当然合法,单调性不就出来了?明确要二分后,先预处理出 \(T\) 组节点 \(u,v\) 的 \(LCA\) 和距离 \(len\) ,那么答案二分的区间就是 \([0,MAXLEN]\) 。
二分答案的精髓是什么?是check()函数
————miaomiao
首先为了不产生歧义,我们定义节点\(u,v\)之间的树上路径为链,路径长为链长;下文的边即为树的边。
那么,对于当前 \(check\) 的 \(x\),有:
- \(First\),要使 \(x\) 合法,那么删去这条边后,所有链长均小于等于 \(x\)
- \(Second\),要找出这条边,首先要枚举所有的链长 \(len\) ,记录比\(x\)大的个数 \(sum\) ,并将这些链在树上差分。如果有一条边被上述链同时覆盖,显然这就是我们要赋\(0\)的最优边(当然可能存在多条,显然要取边权最大的 \(max\_ len\))
- \(Third\),用 \(MAXLEN-max\_ len\),这是赋\(0\)后的最长链的长,如果结果\(<=x\),显然\(x\)合法(最长边都满足,底下那些小弟就更不用说了)
嗯,一切都这么美妙,但如果不会树上差分,不就祭了?那么,首先我们要会正常的差分 (不会建议回普及组重修),说白了差分就是一个反映相对大小的东西
如果你觉得一个知识点很简单,下一步,把它挂到树上去。
————波波
定义一个差分数组 \(spx[\ ]\) 。对于这种求边被覆盖的次数,首先边权肯定都是\(0\),边上差分很不方便,那么我们把边权下放到点上,\(spx_i\) 表示 \(i\) 和 \(i\) 的父节点之间的边的差分,当我们要将 \(u_i,v_i\) 这条链放到覆盖到树上,显然我们只需要修改 \(u_i\) 节点, \(v_i\) 节点和 \(LCA(u_i,v_i)\) 的差分,\(spx[u_i]\)++, \(spx[v_i]\)++, \(spx[lca(u_i,v_i)]\)-=\(2\)。为啥是\(2\)?很显然我们将链 \((u_i,v_i)\) 分成了 \((u_i,lca_i)\) 和 \((v_i,lca_i)\) 两条链进行差分,显然要减\(2\)。
差分完之后,实际的边权\(k\)就是它的子树中差分数组的和(类似于正常差分中前缀和),这可以通过一遍\(dfs\)实现。
服了,求\(LCA\)必须用\(tarjan\),倍增过不了,卡\(95\)……挺难崩的,现学\(tarjan\)求\(LCA\)(狂\(D\)不止)不过\(tarjan\)求确实比倍增快,\(O(n+m)\) 比 \(O(n\ log_2n)\) 友好多了
呃,如果有dalao发现我的代码和您差不多,那我可能是贺的您的,别D我(逃,%%%
\(AC \ code\)
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define N 300010
#define int long long
int n,m;int ans;
struct EDGE{
int next,to,t;
}e[N<<1],d[N<<1];//q是存原树,d是存查询的T组u,v构成的树,为了tarjan求LCA而生
int spx[N];//这是差分数组,数组名只是数组名,没啥特殊含义
int he[N],tot;
int hd[N];
int edge[N];//树上差分,将边权下放到点上进行差分,edge[i]表示节点i代表的边
int MAXLEN;//最长路径
struct SOLVE{
int u,v,lca,len;
}q[N];//这个结构体存的是T条链(ui,vi)的信息
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+c-'0',c=getchar();
return f*x;
}
void write(int x)
{
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
return;
}
void add(int u,int v,int t)
{
tot++;
e[tot].to=v;
e[tot].next=he[u];
he[u]=tot;
e[tot].t=t;
return ;
}
void add_d(int u,int v)
{
tot++;
d[tot].to=v;
d[tot].next=hd[u];
hd[u]=tot;
return;
}
int fa[N];//并查集
int dis[N];//前缀和方便计算链长
int find(int x)
{
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
bool f[N];
void tarjan(int x,int pre)//tarjan求LCA
{
int y;
for(int i=he[x];i>0;i=e[i].next)
{
y=e[i].to;
if(y!=pre)
{
edge[y]=i;
dis[y]=dis[x]+e[i].t;
tarjan(y,x);
int fa_x=find(x);
int fa_y=find(y);
if(fa_x!=fa_y)
fa[fa_y]=fa_x;
f[y]=1;
}
}
for(int i=hd[x];i>0;i=d[i].next)
{
y=d[i].to;
if(f[y]){
int now=(i+1)>>1;//因为存的是双向边,所以第i条边对应的编号为(i+1)/2
q[now].lca=find(y);
q[now].len=dis[x]+dis[y]-2*dis[q[now].lca];
MAXLEN=max(MAXLEN,q[now].len);
}
}
}
int sum,max_len;//sum代表比当前check的x大的边的个数,即需要减小的边数
void dfs_spx(int x,int pre)//树上差分
{
int y;
for(int i=he[x];i>0;i=e[i].next)
{
y=e[i].to;
if(y!=pre)
{
dfs_spx(y,x);
spx[x]+=spx[y];
}
}
if(spx[x]==sum)//如果该边能够让所有链都覆盖
max_len=max(max_len,e[edge[x]].t);
}
bool check(int x)
{
for(int i=0;i<=n;i++) spx[i]=0;
sum=0;max_len=0;
for(int i=1;i<=m;i++)
{
if(q[i].len>x)
{
sum++;
spx[q[i].u]++;
spx[q[i].v]++;
spx[q[i].lca]-=2;
}
}
dfs_spx(1,0);
if(MAXLEN-max_len<=x)
return 1;
return 0;
}
signed main()
{
n=read();m=read();
for(int i=1;i<n;++i)
{
int a,b,c;
a=read(),b=read(),c=read();
add(a,b,c);add(b,a,c);
}
tot=0;
for(int i=1;i<=n;++i) fa[i]=i;//并查集初始化
for(int i=1;i<=m;++i)
{
int u,v;
u=read(),v=read();
q[i].u=u;q[i].v=v;
add_d(u,v);add_d(v,u);
}
tarjan(1,0);
int st=0,ed=MAXLEN;
while(st<ed)
{
int mid=(st+ed)>>1;
if(check(mid)) ed=ans=mid;
else st=mid+1;
}
write(ans);
return 0;
}
\(Update 5.15\)
貌似树剖求 \(LCA\) 更快,因为它常数小且严格跑不满,所以 \(O(nlogn)\) 此时比 \(O(n+m)\) 还快。。。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define swap(x,y) (x^=y,y^=x,x^=y)
#define read read()
#define pt puts("")
inline int read{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+c-'0',c=getchar();
return f*x;
}
void write(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
return;
}
#define N 300010
int n,m;
int MAX;
struct EDGE{int next,to,t;}e[N<<1];
int head[N],total;
void add(int u,int v,int t){e[++total]={head[u],v,t};head[u]=total;}
int edge[N];
namespace Tree_Chain_Partition{
int fa[N],siz[N],wson[N],depth[N];
int top[N];
int dis[N];
void dfs1(int x){
depth[x]=depth[fa[x]]+1;
siz[x]=1;
for(int i=head[x];i;i=e[i].next){
int y=e[i].to;
if(y==fa[x]) continue;
edge[y]=i;
dis[y]=dis[x]+e[i].t;
fa[y]=x;
dfs1(y);
if(siz[y]>siz[wson[x]]) wson[x]=y;
siz[x]+=siz[y];
}
}
void dfs2(int x,int tp){
top[x]=tp;
if(wson[x]) dfs2(wson[x],tp);
for(int i=head[x];i;i=e[i].next){
int y=e[i].to;
if(y==wson[x]||y==fa[x]) continue;
dfs2(y,y);
}
}
int LCA(int u,int v){
while(top[u]!=top[v]){
if(depth[top[u]]<depth[top[v]]) swap(u,v);
u=fa[top[u]];
}
return (depth[u]<depth[v]?u:v);
}
} using namespace Tree_Chain_Partition;
int u[N],v[N],lca[N],len[N];
int b[N];
int sum,reduce;
void dfs(int x){
for(int i=head[x];i;i=e[i].next){
int y=e[i].to;
if(y==fa[x]) continue;
dfs(y);
b[x]+=b[y];
}
if(b[x]==sum) reduce=max(reduce,e[edge[x]].t);
}
bool check(int x){
for(int i=1;i<=n;i++) b[i]=0;
sum=reduce=0;
for(int i=1;i<=m;i++){
if(len[i]>x){
sum++;
b[u[i]]++;
b[v[i]]++;
b[lca[i]]-=2;
}
}
dfs(1);
if(MAX-reduce<=x) return 1;
return 0;
}
int ans;
signed main()
{
#ifndef ONLINE_JUDGE
freopen("lty.in","r",stdin);
freopen("lty.out","w",stdout);
#endif
n=read,m=read;
for(int a,b,c,i=1;i<n;i++){
a=read,b=read,c=read;
add(a,b,c),add(b,a,c);
}
dfs1(1),dfs2(1,1);
for(int i=1;i<=m;i++){
u[i]=read,v[i]=read;
lca[i]=LCA(u[i],v[i]);
len[i]=dis[u[i]]+dis[v[i]]-2*dis[lca[i]];
MAX=max(MAX,len[i]);
}
int st=0,ed=MAX;
while(st<ed){
int mid=(st+ed)>>1;
if(check(mid))
ed=ans=mid;
else
st=mid+1;
}
write(ans);
return 0;
}