[题解]P2993 [FJOI2014]最短路径树问题

P2993

很裸的一道点分治,感觉就是这道题的升级版。

显然我们就是要建出最短路径树,然后直接在树上跑点分治就行了。

首先,关于建树

题目要求我们按字典序最小的路径建树,但由于官方数据过水,貌似大部分题解都没管字典序(不过后来添加了hack数据)。我们可以用一个贪心思想来建树:首先通过dfs来建树,将以当前点为起点的在最短路上且儿子不在树上的边加到树上,然后对所有在树上连了边的子节点继续dfs,在这里我们可以贪心地按编号从小到大作为顺序来搜索。因为对于后面的一个子孙节点来说,当前点的子节点在字典序上的位权一定是一样的。就像题干中给出的1,32,11和1,3,2,11。因为无论后面的路径长短,前面部分的路径一定是一样的。比如1的儿子有32和3,此时32和3之间我们就应该选编号较小的3。

建树部分代码:

struct tmp{int v,w;bool operator <(const tmp&x)const{return v<x.v;}};
void build(int x)
{
	vector<tmp>son;
	for(re int i=h[x];i;i=e[i].nxt)
		if(dis[e[i].v]==dis[x]+e[i].w)
			son.push_back((tmp){e[i].v,e[i].w});//将儿子加入vector中方便排序
	sort(son.begin(),son.end());//按编号从小到大排序
	for(re int i=0;i<son.size();++i)
		if(!ontree[son[i].v])//当前儿子不在树上
		{
			ontree[son[i].v]=true;
			divide::add(x,son[i].v,son[i].w);
			divide::add(son[i].v,x,son[i].w);//加边
			build(son[i].v);//继续建树
		}
}

当建好树之后,我们就可以考虑如何统计答案了。

问题:

请问,在这棵最短路径树上,最长的包含K个点的简单路径长度为多长?长度为该最长长度的不同路径有多少条?

对于最长路径的长度,做法可以参考这里。我们可以开两个栈,一个存链长,一个存深度,若当前链经过节点数为\(x\)那么这条链就应该和之前子树中的一条经过\(k-x-1\)个节点的链组成答案(减1是因为当前的重心也是链上的点),如果比答案更长则更新长度和方案,如果一样长就直接累加方案。

点分治部分代码:

namespace divide
{
	int cnt,h[N];
	struct edge{int v,w,nxt;}e[N<<1];
	il void add(int u,int v,int w){e[++cnt]=(edge){v,w,h[u]};h[u]=cnt;}
	int rt,sum;
	int siz[N],son[N],dis[N],dep[N],len[N],tot[N];
	//len[i]表示经过i个点的最长链长度
	//tot[i]表示经过i个点的最长链的条数
	bool vis[N];
	void getrt(int x,int fa)
	{
		siz[x]=1;son[x]=0;
		for(re int i=h[x],v;i;i=e[i].nxt)
			if(!vis[v=e[i].v]&&v^fa)
			{
				getrt(v,x);
				siz[x]+=siz[v];
				son[x]=max(son[x],siz[v]);
			}
		son[x]=max(son[x],sum-siz[x]);
		rt=son[x]<son[rt]?x:rt;
	}//求重心模板
	int st1[N],st2[N],top;
	//st1存距离,st2存深度
	void getdis(int x,int fa)
	{
		if(dep[x]>k) return;
		st1[++top]=dis[x];
		st2[top]=dep[x];
		for(re int i=h[x],v;i;i=e[i].nxt)
			if(!vis[v=e[i].v]&&v^fa)
			{
				dis[v]=dis[x]+e[i].w;
				dep[v]=dep[x]+1;
				getdis(v,x);
			}
	}
	il int solve(int x)
	{
		re int pre=top=0,res=0;
		memset(tot,0,sizeof(tot));
		memset(len,0,sizeof(len));
		tot[0]=1;//当前重心也有可能是一条最长链的起点!!!因为这里wa了好多次QAQ
		for(re int i=h[x],v;i;i=e[i].nxt)
			if(!vis[v=e[i].v])
			{
				dis[v]=dis[x]+e[i].w;
				dep[v]=dep[x]+1;
				pre=top;
				getdis(v,x);
				//处理出当前子树中的信息
				for(re int j=pre+1;j<=top;++j)
				{
					if(len[k-st2[j]-1]+st1[j]>ans[0])
						ans[0]=len[k-st2[j]-1]+st1[j],res=tot[k-st2[j]-1];
						//当前子树上存在可以和之前子树上的链组成更优答案的链
					else if(len[k-st2[j]-1]+st1[j]==ans[0]) res+=tot[k-st2[j]-1];
					//当前子树上存在可以和之前子树上的链组成最优答案的链,那么累加方案数
				}
				for(re int j=pre+1;j<=top;++j)//给下一棵子树用
				{
					if(len[st2[j]]==st1[j]) ++tot[st2[j]];//累加方案
					if(len[st2[j]]<st1[j]) tot[st2[j]]=1,len[st2[j]]=st1[j];//更新方案、长度
				}
			}
		return res;
	}
	void getdiv(int x)
	{
		vis[x]=true;dis[x]=0;dep[x]=0;
		re int pre=ans[0],tmp=solve(x);
		if(ans[0]^pre) ans[1]=tmp;//更新方案
		else ans[1]+=tmp;//累加方案
		for(re int i=h[x],v;i;i=e[i].nxt)
			if(!vis[v=e[i].v])
			{
				son[rt=0]=sum=siz[v];
				getrt(v,0);
				getrt(rt,0);
				//其实这一次搜索可以不用,目的是将子树中的siz更新为以rt作为根的siz
				getdiv(rt);
			}
		//点分治主体
	}
}

由于代码的核心部分上面都已经给出,这里就不给完整代码了。

posted @ 2020-12-26 09:45  watermonster1y1  阅读(62)  评论(0编辑  收藏  举报