[bzoj]1999: [Noip2007]Core樹網的核

原題鏈接:樹網的核

題目中說了:樹的直徑不唯一,但是中點唯一。

如何證明中點唯一(雖然沒有必要)

假設一條直徑被一個節點分成了左和右兩段,我們假設左邊小於於右邊。

現在要保證直徑等於len[left]+len[right]。

①從節點連出長度等於左段的子樹,明顯,中點還是在右端

②從節點連出長度等於右段的子樹,中點位置改變。但是len[left]+len[right]<2*len[right]。直徑改變。

不嚴謹的證明(逃

引理:任何直徑上求出的最小偏心距都相等

證明:先確定直徑,也是分成左右兩段。

①在分割點上添加子樹:無法超過左段長度,無論怎麼添加,分割點到右段的距離必定小於(子樹&&左段)到分割點的距離

②在左右兩段添加子樹:明顯子樹長度不能大於子  樹的根節點到左/右的長度。所以子樹到核的距離必定小於左右端點到核的距離

由於多條直徑相當於交換子樹和直徑(子樹->直徑,直徑->子樹)所以可以證明引理。

然後題目就成了計算任意一條直徑上的最大偏心距大最小值。

算法一:暴力O(n^3)

計算出一條直徑,在直徑上枚舉p,q,滿足dis[p][q]<=s,然後暴力枚舉核上的點O(n^2)到其他點的最大值O(n)即可。答案爲最大值中的最小值。

算法二:貪心O(n^2)

容易發現,核越長,偏心距的最大值越小,下面給出證明

當前核的長度爲k,點u到核的距離爲min{d(u,i)}1<=i<=k

若核的長度加1,那麼u到核的距離爲min{d(u,i)}1<=i<=k+1,明顯包含長度爲k的情況,而且又可以變得更小。

得證。

那麼我們只需要枚舉p的位置,然後讓q儘可能長(靠近s),然後枚舉到其他店的最大值即可,由於省略了枚舉q的操作,時間複雜度爲O(n^2)。可以通過noip的數據了。

下面是O(n^2)代碼

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=500018;
int n,s,head[N],ver[2*N],edge[2*N],next[2*N],tot=1,p=1,len=0,q=1,fa[N],dis[N];
bool vis[N],f=0;
void add(int x,int y,int val);
void dfs(int x,int k);
int main()
{
	int x,y,z;
	scanf("%d%d",&n,&s);
	for(int i=2;i<=n;i++){
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	//第一次dfs,求出到1最遠的點爲p
	memset(dis,0,sizeof(dis));
	dfs(1,0);
	memset(fa,0,sizeof(fa));
	for(int i=1;i<=n;i++)
		if(dis[i]>dis[p])p=i;
	//第二次dfs,求出到p最遠的點爲q,p->q爲直徑。
	q=p;
	dis[p]=0;
	dfs(p,0);
	for(int i=1;i<=n;i++)
		if(dis[i]>dis[q])q=i;
	int ans=(1<<30)-1,j=q;
	//ans的下界爲直徑上中間點到直徑兩端的距離
	for(int i=q;i;i=fa[i]){
		//標記直徑上的點
		vis[i]=1;
		while(fa[j]&&dis[i]-dis[fa[j]]<=s)j=fa[j];
		ans=min(ans,max(dis[j],dis[q]-dis[i]));
	}
	for(int i=q;i;i=fa[i])dis[i]=0,dfs(i,0);
	for(int i=1;i<=n;i++)ans=max(ans,dis[i]);
	printf("%d",ans);
	return 0;
}
void add(int x,int y,int val){
	ver[++tot]=y;edge[tot]=val;next[tot]=head[x];head[x]=tot;
	ver[++tot]=x;edge[tot]=val;next[tot]=head[y];head[y]=tot;
}
void dfs(int x,int k){
	for(int i=head[x];i;i=next[i]){
		int y=ver[i];
		if(vis[y]||y==k)continue;
		fa[y]=x;
		dis[y]=dis[x]+edge[i];
	//	printf("1");
		dfs(y,x);
	}
}

算法三:二分,O(nlogsum),sum爲所有邊的長度和

枚舉最終答案mid,問題變成了是否存在覈使最大偏心距不超過mid。

可以假設核的端點是p,q,直徑的端點是u,v,考慮兩種偏心距。

①分叉點在u,p或者q,v之間,根據直徑是最長的路,p到u的距離必定大於p+子樹的長度。只要保證u到p,q到v的距離小於mid即可。

②分叉點在p,q之間,dfs判斷小於mid即可。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=500018;
int n,s,head[N],ver[2*N],edge[2*N],next[2*N],tot=1,p=1,len=0,q=1,fa[N],dis[N],son[N],dik[N];
bool vis[N],f=0;
void add(int x,int y,int val);
void dfs(int x,int k);
bool check(int mid);
int main()
{
	int x,y,z,l=0,r=0,mid;
	scanf("%d%d",&n,&s);
	for(int i=2;i<=n;i++){
		scanf("%d%d%d",&x,&y,&z);
		r+=z;
		add(x,y,z);
	}
	//第一次dfs,求出到1最遠的點爲p
	memset(dis,0,sizeof(dis));
	dfs(1,0);
	memset(fa,0,sizeof(fa));
	memset(son,0,sizeof(son));
	for(int i=1;i<=n;i++)
		if(dis[i]>dis[p])p=i;
	//第二次dfs,求出到p最遠的點爲q,p->q爲直徑。
	q=p;
	dis[p]=0;
	dfs(p,0);
	for(int i=1;i<=n;i++)
		if(dis[i]>dis[q])q=i;
	for(int i=q;fa[i];i=fa[i]){
		son[fa[i]]=i;
	}
	//標記直徑上的點
	for(int i=q;i;i=fa[i])vis[i]=1;
	f=1;
	while(l<=r){
		mid=(l+r)/2;
		if(check(mid))r=mid-1;
		else l=mid+1;
	}
	printf("%d\n",l);
	return 0;
}
void add(int x,int y,int val){
	ver[++tot]=y;edge[tot]=val;next[tot]=head[x];head[x]=tot;
	ver[++tot]=x;edge[tot]=val;next[tot]=head[y];head[y]=tot;
}
void dfs(int x,int k){
	for(int i=head[x];i;i=next[i]){
		int y=ver[i];
		if(vis[y]||y==k)continue;
		fa[y]=x;
		if(!f)dis[y]=dis[x]+edge[i];
		else dik[y]=dik[x]+edge[i];
	//	printf("1");
		dfs(y,x);
	}
}
bool check(int mid){
	int u=p,v=q;
	while(fa[v]&&dis[q]-dis[fa[v]]<=mid)v=fa[v];
	while(son[u]&&dis[son[u]]<=mid&&u!=v)u=son[u];
	if(u==v)return true;
	if(dis[v]-dis[u]>s)return false;
    memset(dik,0,sizeof(dik));
	for(int i=v;i!=fa[u];i=fa[i]){
        dfs(i,0);
	}
	for(int i=1;i<=n;i++)
		if(dik[i]>mid)return false;
	return true;

}

雖然不知道爲什麼沒有那個O(n^2)的算法跑得快,可能那個剪枝優化比較厲害。

算法四、O(n)

觀察算法三中答案值的來源可以觀察到

d[i]表示點i到其他非直徑點的最遠距離,dis[x][y]表示x到y的距離。

p,q是核的左右端點,u,v是直徑的左右端點

ans=max{d[k],dis[p][u],dis[q][v]}i<=k<=j

可以想到用單調隊列優化,時間複雜度爲O(n)

但實際上根本不需要單調隊列優化

由於在如果1<=k<i的話,那麼d[k]的值必然小於dis[p][u]同理j<k<=t也一樣(t爲直徑長度)

那麼k的取值範圍可以擴充到1<=k<=t,發現max{d[k]}爲定值。

接下來只需要枚舉dis[p][u]和dis[q][v]即可。

最終答案就是ans中的最小值

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=500018;
int n,s,head[N],ver[2*N],edge[2*N],next[2*N],tot=1,p=1,len=0,q=1,fa[N],dis[N],son[N],dik[N];
bool vis[N],f=0;
void add(int x,int y,int val);
void dfs(int x,int k);
bool check(int mid);
int main()
{
	int x,y,z,ans=0x7FFFFFFF;
	scanf("%d%d",&n,&s);
	for(int i=2;i<=n;i++){
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	//第一次dfs,求出到1最遠的點爲p
	memset(dis,0,sizeof(dis));
	dfs(1,0);
	memset(fa,0,sizeof(fa));
	memset(son,0,sizeof(son));
	for(int i=1;i<=n;i++)
		if(dis[i]>dis[p])p=i;
	//第二次dfs,求出到p最遠的點爲q,p->q爲直徑。
	q=p;
	memset(dis,0,sizeof(dis));
	dfs(p,0);
	for(int i=1;i<=n;i++)
		if(dis[i]>dis[q])q=i;
	for(int i=q;fa[i];i=fa[i]){
		son[fa[i]]=i;
	}
	//標記直徑上的點
	for(int i=q;i;i=fa[i])vis[i]=1;
	//求出min(max(dis[p][u],dis[q][v]))
	int j;
	for(int i=q;i;i=fa[i]){
		j=i;
		while(fa[j]&&dis[i]-dis[fa[j]]<=s)j=fa[j];
		ans=min(ans,max(dis[j],dis[q]-dis[i]));
		if(!fa[j])break;
	}
	memset(dis,0,sizeof(dis));
	for(int i=q;i;i=fa[i])dfs(i,0);
	for(int i=1;i<=n;i++)ans=max(ans,dis[i]);
	printf("%d\n",ans);
	return 0;
}
void add(int x,int y,int val){
	ver[++tot]=y;edge[tot]=val;next[tot]=head[x];head[x]=tot;
	ver[++tot]=x;edge[tot]=val;next[tot]=head[y];head[y]=tot;
}
void dfs(int x,int k){
	for(int i=head[x];i;i=next[i]){
		int y=ver[i];
		if(vis[y]||y==k)continue;
		fa[y]=x;
		dis[y]=dis[x]+edge[i];
		dfs(y,x);
	}
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章