[SDOI2011]消防/[NOIP2007] 树网的核

消防

题目描述

某个国家有n个城市,这n个城市中任意两个都连通且有唯一一条路径,每条连通两个城市的道路的长度为zi(zi<=1000)。

这个国家的人对火焰有超越宇宙的热情,所以这个国家最兴旺的行业是消防业。由于政府对国民的热情忍无可忍(大量的消防经费开销)可是却又无可奈何(总统竞选的国民支持率),所以只能想尽方法提高消防能力。

现在这个国家的经费足以在一条边长度和不超过s的路径(两端都是城市)上建立消防枢纽,为了尽量提高枢纽的利用率,要求其他所有城市到这条路径的距离的最大值最小。

你受命监管这个项目,你当然需要知道应该把枢纽建立在什么位置上。

输入输出格式

输入格式:

输入包含n行:

第1行,两个正整数n和s,中间用一个空格隔开。其中n为城市的个数,s为路径长度的上界。设结点编号以此为1,2,……,n。

从第2行到第n行,每行给出3个用空格隔开的正整数,依次表示每一条边的两个端点编号和长度。例如,“2 4 7”表示连接结点2与4的边的长度为7。

输出格式:

输出包含一个非负整数,即所有城市到选择的路径的最大值,当然这个最大值必须是所有方案中最小的。

输入输出样例

输入样例#1:

5 2
1 2 5
2 3 2
2 4 4
2 5 3

输出样例#1:

5

输入样例#2:

8 6
1 3 2
2 3 2 
3 4 6
4 5 3
4 6 4
4 7 2
7 8 3

输出样例#2:

5

说明

【数据规模和约定】

对于20%的数据,n<=300。

对于50%的数据,n<=3000。

对于100%的数据,n<=300000,边长小等于1000。

题解

这道题可以说是NOIP2007树网的核的数据加强版
那道题由于数据范围极小,所以(O(n^3),O(n^2),O(nlogsum),O(n))都可以过
而这道题就只能后面两种做法了
但是无论如何,首先这道题需要会求树的直径,如果不会的在这里再贴一下代码+一点点注释

  • 两遍dfs/bfs

Code

void dfs(int u,int fa) {
	for(int i=head[u]i;i=e[i].next)
	{
	int to=e[i].to;
	if(to!=fa) dis[to]=dis[u]+e[i],v,dfs(to,u)
	}
}
int main()
{
	//建边
	int s=0,t=0;
	dfs(1,0); 
	for(int i=1;i<=n;i++) if(!s || dis[i]>dis[s]) s=i;
	memset(dis,0,sizeof(dis)); dfs(s,0);
	for(int i=1;i<=n;i++) if(!t || dis[i]>dis[t]) t=i;
	//原理:树的直径是树上最长的一条链,因此任取树上一个点,离它最远的点必然是树的直径上的一个端点,找到这个端点后再以它为根,找到离它最远的点,则必然是直径的另一个端点
}
  • 树形dp
    定义dp[i]为以i为根的子树的直径

Code

void DP(int u,int fa) {
	for(int i=head[u];i;i=e[i].next) {
		int to=e[i].to;
		DP(to,u);
		ans=max(ans,dp[u]+dp[to]+e[i].v);//此时dp[u]并不包括to这个儿子,
		dp[u]=max(dp[u],dp[to]+e[i].v);	
	}//想一下吧,挺简单的
}

(O(n^3))只要会求树的直径就直接枚举即可

(O(n^2))就是枚举+贪心,在s的范围内,显然越远越好,那么我们确定一段,就可以确定另一段,这样就变成了(O(n^2))

那么再这里就讲一下(O(nlogsum))(O(n))的做法

(O(nlogsum))

sum为树的总权值
我们依然定义偏心距为其他点到所选路径的最大值
容易发现,此题答案具有单调性,可以二分答案,那么问题就为
求一条路径,使得它的偏心距不超过二分值mid

怎么check?
设直径的两个端点u,v.在直径上找到与u的距离不超过mid的前提下,距离u最远的节点,作为p.同样找到一个距离v不超过mid的最远的节点q
根据直径的最长性,任何其他在u,p之间分叉的子树上的节点与p的距离都不会比与u的距离更远,因此,p,q就是尽量靠近树的中心的节点
我们需要判断p,q之间的距离是否超过s,除此之外还要判断它们之间的距离以及离这条路径最远的点的距离是否超过mid,如果都满足,这就是一个合法解,继续二分

(O(n))

我们可以用一个单调队列来维护答案
依然先求出直径的两个端点
设直径上的点为(u_1,u_2...u_t),先把这几个节点标记为已访问,然后通过dfs,求出d[(u_i)],表示从(u_i)出发,不经过直径上的节点能够到达的最远距离
那么以(u_i,u_j)为端点的这条路径的偏心距就是:

[max(d[u_k](i<=k<=j),max(dist[u_1,u_i],dist[u_j,u_t])) ]

这个时候用单调队列维护就可以做到(O(n))的了

#include<bits/stdc++.h>
#define Max(a,b) (a)>(b)?(a):(b)
#define Min(a,b) (a)<(b)?(a):(b)
#define For(i,a,b) for(int (i)=(a);(i)<=(b);(i)++)
using namespace std;
inline int read(int &ans) {
    ans=0;int f=1;char i=getchar();
    while(i<'0' || i>'9') {if(i=='-') f=-1;i=getchar();}
    while(i>='0' && i<='9') {ans=(ans<<1)+(ans<<3)+i-'0';i=getchar();}
    return ans*f;
}
const int N=500010;
struct edge {
    int to,v,next;
}e[2*N];
int n,m,L,cnt,ml,mr,s,ans=2147483647,he,ta;
int head[N],vis[N],dis[N],fa[N];
int q[N],pos[N],f[N];
inline void add(int a,int b,int c) {
    e[++cnt].to=b;
    e[cnt].v=c;
    e[cnt].next=head[a];
    head[a]=cnt;
}
void dfs(int u) {
    for(int i=head[u];i;i=e[i].next) {
        int to=e[i].to; 
        if(to!=fa[u]) fa[to]=u,dis[to]=dis[u]+e[i].v,dfs(to);
    }
}
void find(int u) {
    f[u]=dis[u];
    for(int i=head[u];i;i=e[i].next)
        if(!vis[e[i].to] && e[i].to!=fa[u])
            find(e[i].to),f[u]=Max(f[u],f[e[i].to]);
}
int main()
{
    int u,v,z;read(n);read(s);
    For(i,1,n-1) read(u),read(v),read(z),add(u,v,z),add(v,u,z);
    dfs(1);For(i,1,n) if(!mr||dis[i]>dis[mr]) mr=i;
    memset(dis,0,sizeof(dis)); fa[mr]=0,dfs(mr);
    For(i,1,n) if(!ml||dis[i]>dis[ml]) ml=i;
    int l=ml,r=ml; while(l) vis[l]=1,l=fa[l]; l=ml;
    while(l) {
        while(dis[r]-dis[l]>s) r=fa[r];
        find(l); f[l]-=dis[l];
        while(he<=ta && pos[he]-dis[l]>s) he++;
        while(he<=ta && f[l]>q[ta]) ta--;
        q[++ta]=f[l],pos[ta]=dis[l];
        ans=Min(ans,Max(q[he],Max(dis[l]-dis[mr],dis[ml]-dis[r])));
        l=fa[l];
    }
    printf("%d
",ans);
}

博主蒟蒻,随意转载.但必须附上原文链接

http://www.cnblogs.com/real-l/

原文地址:https://www.cnblogs.com/real-l/p/9502734.html