直径上的乱搞 bzoj1999求树直径上的结点+单调队列,bzoj1912负权树求直径+求直径边
直径上的乱搞一般要求出这条直径上的点集或者边集
bzoj1999:对直径上的点集进行操作
/* 给出一颗树,在树的直径上截取长度不超过s的路径 定义点u到s的距离为u到s的最短路径长度 定义s的偏心距为所有点到s的最大距离 定义树网的核为偏心距最小的s 给定s,请求出最小偏心距 题目中的结论:树的直径不唯一,但所有直径必定相交于直径的中点 推论:任意直径上求出的最小偏心距都相等 将树转化成另一个模型:即所有直径以外的分支都挂载在直径左右侧, 提取出直径,设直径上的结点u1,u2,u3...ut 那么用i,j两个指针,i从u1开始,j一直在直径上右移直到uj距离ui不超过s, 用单调队列维护在挂载在区间[i,j]的最大分支长度,那么这一段区间对应的偏心距就是 max(max{d(uk)},dist(u1,ui),dist(uj,ut)) ,k在区间[i,j]内 d数组是uk挂载的分支最大深度 由于没有负权,可以用dfs跑两遍求出直径,并用pre数组将直径记录下来 然后从每个直径上的点出发再进行一次dfs求出这个直径上挂载的分支的深度 然后用单调队列维护结果即可 */ #include<bits/stdc++.h> using namespace std; #define maxn 500005 struct Edge{int to,nxt,w;}edge[maxn<<1]; int head[maxn],tot,n,s; void init(){ memset(head,-1,sizeof head); tot=0; } void addedge(int u,int v,int w){ edge[tot].nxt=head[u];edge[tot].to=v;edge[tot].w=w; head[u]=tot++; } int dep[maxn],fa[maxn],is[maxn],dis[maxn]; void dfs(int u,int pre,int deep){//返回深度最大的点 dep[u]=deep;fa[u]=pre; for(int i=head[u];i!=-1;i=edge[i].nxt){ int v=edge[i].to; if(v==pre)continue; dfs(v,u,deep+edge[i].w); } } int dfs2(int u,int pre){//求直径上的点挂载的分支的深度 int res=0; for(int i=head[u];i!=-1;i=edge[i].nxt){ int v=edge[i].to; if(v==pre || is[v])continue; res=max(dfs2(v,u)+edge[i].w,res); } return res; } int main(){ init(); cin>>n>>s; for(int i=1;i<n;i++){ int u,v,w;cin>>u>>v>>w; addedge(u,v,w);addedge(v,u,w); } //两次dfs求出直径 int S=1;dfs(1,0,0); for(int i=1;i<=n;i++) if(dep[S]<dep[i])S=i; memset(dep,0,sizeof dep); int T=1;dfs(S,0,0); for(int i=1;i<=n;i++) if(dep[T]<dep[i])T=i; // cout<<s<<" "<<t<<endl; fa[S]=0; for(int i=T;i;i=fa[i])is[i]=1;//标记直径上的点 for(int i=T;i;i=fa[i])dis[i]=dfs2(i,fa[i]);//求出直径上挂载的分支的最大深度 //单调队列 int q[maxn]={},l=1,r=1,j=T,ans=1<<30; q[1]=T; for(int i=T;i;i=fa[i]){ while(fa[j]!=0 && dep[i]-dep[fa[j]]<=s){//j往上移,把j加入单调队列 j=fa[j]; while(l<=r && dis[q[r]]<=dis[j])r--; q[++r]=j; ans=min(ans,max(dis[q[l]],max(dep[j]-dep[S],dep[T]-dep[i]))); } while(q[l]==i)++l; } cout<<ans<<endl; }
bzoj1912 负权树求直径(只能用树形dp),然后记录直径上的边来回溯到两端点,同时将直径上的边权变负
/* 分情况讨论问题 一条边也不加的情况,显然每条边要扫描两次, 该情况的答案是2(n-1) 只加一条边的情况,找到直径,将其变成一个环,在这个环上的所有边只要扫描一次,剩下的边就要扫描两次 设直径为L,该情况下的答案是 2(n-1-L)+L+1=2n-L-1=2(n-1)-(L-1) 加两条边的情况,在加入第一条边出现环的情况下,再加入一条边形成的环会和原来的环有重合 重合的部分任然要扫描两次,所以新加入的边要使形成的环和原来的环非重合的边和重合的边的差最大 那么如何快速求新加入的边?只要将第一条边加入后形成的环的边权值变成-1,然后再求一次直径即可 设第一次的直径L1,第二次的直径L2,那么该情况下的答案是2(n-1)-(L1-1)-(L2-1) */ #include<bits/stdc++.h> using namespace std; #define maxn 100005 struct Edge{int to,nxt,w;}edge[maxn<<1]; int head[maxn],tot,n,k,ans; void init(){ memset(head,-1,sizeof head); tot=0; } void addedge(int u,int v){ edge[tot].w=1;edge[tot].to=v;edge[tot].nxt=head[u];head[u]=tot++; } int pre1[maxn],pre2[maxn],dp[maxn],mx;//记录x的最长和次长子链的边的下标 int dfs(int x,int fa) { int mx1=0,mx2=0; for(int i=head[x];i!=-1;i=edge[i].nxt) if(edge[i].to!=fa) { int v=edge[i].w+dfs(edge[i].to,x); if(v>mx1)mx2=mx1,mx1=v,pre2[x]=pre1[x],pre1[x]=i; else if(v>mx2)mx2=v,pre2[x]=i; } if(mx1+mx2>ans)ans=mx1+mx2,mx=x; return mx1; } int main(){ init(); memset(pre1,-1,sizeof pre1); memset(pre2,-1,sizeof pre2); cin>>n>>k; int tot=2*n-2; for(int i=1;i<n;i++){ int u,v;cin>>u>>v; addedge(u,v);addedge(v,u); } dfs(1,0); tot=tot-ans+1; if(k==2) { ans=0; for(int i=pre1[mx];i!=-1;i=pre1[edge[i].to])edge[i].w=edge[i^1].w=-1; for(int i=pre2[mx];i!=-1;i=pre1[edge[i].to])edge[i].w=edge[i^1].w=-1; dfs(1,0);tot=tot-ans+1; } cout<<tot<<endl; }