[SDOI2011]消防https://www.luogu.org/problemnew/show/P2491
题目描述
某个国家有n个城市,这n个城市中任意两个都连通且有唯一一条路径,每条连通两个城市的道路的长度为zi(zi<=1000)。
这个国家的人对火焰有超越宇宙的热情,所以这个国家最兴旺的行业是消防业。由于政府对国民的热情忍无可忍(大量的消防经费开销)可是却又无可奈何(总统竞选的国民支持率),所以只能想尽方法提高消防能力。
现在这个国家的经费足以在一条边长度和不超过s的路径(两端都是城市)上建立消防枢纽,为了尽量提高枢纽的利用率,要求其他所有城市到这条路径的距离的最大值最小。
你受命监管这个项目,你当然需要知道应该把枢纽建立在什么位置上。
输入格式:
输入包含n行:
第1行,两个正整数n和s,中间用一个空格隔开。其中n为城市的个数,s为路径长度的上界。设结点编号以此为1,2,……,n。
从第2行到第n行,每行给出3个用空格隔开的正整数,依次表示每一条边的两个端点编号和长度。例如,“2 4 7”表示连接结点2与4的边的长度为7。
输出格式:
输出包含一个非负整数,即所有城市到选择的路径的最大值,当然这个最大值必须是所有方案中最小的。
【数据规模和约定】
对于20%的数据,n<=300。
对于50%的数据,n<=3000。
对于100%的数据,n<=300000,边长小等于1000。
首先,暴力肯定炸得不要不要的.
所以科普一下怎么求树的直径(洛谷标签):
先任选一点u作为起点,一遍bfs或dfs求出最长路,找到终点v,再以v为起点求一遍最长路,终点为v',此时的v~v'即是树的直径.
证明(反证法):
设树的直径为s~t.
1.若所选的u是st路径上一点,那么找到的v点一定是s或t中一点,否则有dis(uv)>dis(ut),此时树的直径应为sv,与假设矛盾.
2.若u不在s~t路径上:
假设找最长路时搜到了s~t路径上一点X,那么找到的v点一定是s或t中一点(1中已证)
若最长路与直径完全不相交,由于树是连通的,此时找到的最长路uv上必有一点a与直径st连通于点b,则dis(av)>dis(ab)+dis(bt),此时树的直径应为sv,矛盾.
dfs
//记得把main函数里面dfs的点vis赋为真
//len和end开全局
void dfs(int now,int Dist)
{
for(int i=last[now];i;i=edge[i].next)
{
int to=edge[i].to;
if(!vis[to])
{
vis[to]=1;
dist[to]=Dist+edge[i].w;
if(dist[to]>len)
{
len=d[to];
end=to;
}
dfs(to,dist[to]);
}
}
}
bfs
int bfs(int s)
{
int end,len;
queue<int> que;
vis[s]=1;
q.push(s);
while(!q.empty())
{
int now=q.front();
q.pop();
for(int i=last[now];i;i=edge[i].next)
{
int to=edge[i].to;
if(!vis[to])
{
dist[to]=dist[now]+edge[i].w;
vis[to]=1;
q.push(to);
if(dist[to]>len)
{
len=dist[to];
end=to;
}
}
}
}
return end;
}
下附正解代码
#include<cstdio>
#include<iostream>
#include<cstring>
#define RG register
using namespace std;
inline int read()
{
RG int x=0;RG char ch=getchar();
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();
return x;
}
const int N=300002,INF=1e9;
int n,s,u,v,w,cnt,S,E,L,R,ans=INF,ECC;
int last[N],que[N],dist[N],path[N],d[N],h[N],f[N],g[N];
bool vis[N];
struct edg{
int to,next,w;
}edge[2*N];
void insert(int u,int v,int w)
{
edge[++cnt]=(edg){v,last[u],w};last[u]=cnt;
edge[++cnt]=(edg){u,last[v],w};last[v]=cnt;
}
int bfs(int start,bool op)//start起点,op表示是第几次bfs
{
for(int i=1;i<=n;i++)que[i]=0,vis[i]=0,dist[i]=0;
int head=0,tail=1,len=0,end;
que[1]=start;
vis[start]=1;
while(head<tail)
{
head++;
for(int i=last[que[head]];i;i=edge[i].next)
{
int v=edge[i].to;
if(!vis[v])
{
vis[v]=1;
tail++;
que[tail]=v;
dist[v]=dist[que[head]]+edge[i].w;
if(op)path[v]=que[head];//第二遍记录前驱
if(dist[v]>len)//最长路 即 直径
{
len=dist[v];
end=v;
}
}
}
}
return end;
}
void Path(int Last)//抠直径
{
if(Last==S)return;
d[++cnt]=path[Last];
Path(d[cnt]);
}
void dfs(int now,int Dist,int x,int left,int right)//计算h不考虑直径上的点,即left,right
{
for(int i=last[now];i;i=edge[i].next)
{
int to=edge[i].to;
if(!vis[to]&&to!=left&&to!=right)
{
vis[to]=1;
dist[to]=Dist+edge[i].w;
h[x]=max(h[x],dist[to]);//h[i]维护非直径上的点到d[i]的最大距离
dfs(to,dist[to],x,left,right);
}
}
}
int main()
{
n=read();s=read();
for(int i=1;i<n;i++)
{
u=read();v=read();w=read();
insert(u,v,w);
}
S=bfs(1,0);
E=bfs(S,1);
cnt=1;
d[cnt]=E;
Path(E);
memset(vis,0,sizeof(vis));
for(int i=1;i<=cnt;i++)vis[d[i]]=1,dfs(d[i],0,i,d[i-1],d[i+1]);
for(int i=1;i<=cnt;i++)f[i]=max(h[i],f[i-1]+dist[d[i-1]]-dist[d[i]]);//f[i]维护树上d[i]左边的点到d[i]的最大距离(左右根据在直径上的位置区分)
for(int i=cnt;i>=1;i--)g[i]=max(h[i],g[i+1]+dist[d[i]]-dist[d[i+1]]);//g[i]维护树上d[i]右边的点到d[i]的最大距离(左右根据在直径上的位置区分)
L=1;R=1;
while(dist[d[L]]-dist[d[R+1]]<=s&&R<=cnt)R++;//尺取求最小值
ECC=max(f[L],g[R]);//每次取f[L]与g[R]以及h[L+1~R-1]的最大值
for(int i=L+1;i<R;i++)ECC=max(ECC,h[i]);
ans=min(ans,ECC);ECC=0;
while(R<=cnt&&L<=cnt)
{
while(dist[d[L]]-dist[d[R+1]]>s)L++;
R++;
ECC=max(f[L],g[R]);
for(int i=L+1;i<R;i++)ECC=max(ECC,h[i]);
ans=min(ans,ECC);ECC=0;
}
printf("%d
",ans);
return 0;
}