http://blog.sina.com.cn/s/blog_61034ad90100jqqw.html
题目简述:在树中找三个点A,B,C。使得min(dis[A][C],dis[B][C])+dis[A][B]最大。
本题的一种流传较广的解法是基于分叉点的,即在每一个分叉点的三棵子树中找三条最长的链作为一种解,取最优解,这种算法用dfs初始化之后,dp就是O(N)的了,是非常优秀的算法,详情请见国家集训队2007年陈瑜希的论文。经过我的独立思考,我提出了一种O(N)的,且更加容易实现的算法。
题目简析:认真分析不难发现,其实对于A,B而言,dis[A][C],dis[B][C]谁小谁大是不重要的,因为如果我一定要先去A,在dis[A][C]>dis[B][C]的情况下完全可以将AB调换,因此关键的应该是AB的路径选择。
结论:最优方案中,AB必定为树中的最长链。
证明:
引理1:最优方案中AB必为叶子节点(或只有一个儿子的根),即不存在任何条与A或B关联且不与AB相交的链。
证明:
不妨设B不是叶子节点,如图1,必有B’可以与B相连。显然AB’>AB
(由于等于的情况等价于二者皆可,因此证明中略去等于,下同)
(1)如果CA>CB
原路径为CB+AB.
(1.1)如果CA>CB’.
新路径为CB’+B’A.因为CB’>CB,B’A>BA,因此AB’优于AB。
(1.2)如果CA<CB.
新路径为CA+AB’,因为CA>CB,AB’>AB,因此AB’优于AB.
(2)如果CA<CB
原路径为CA+AB
(1.1)因为CB’>CB,因此CB’>CA,新路径为CA+AB’,优于AB.
综上,命题得证。
我们将这种两端都不可拓展的链叫做极长链。
注:对于任意满足CB’>CB>CA的情况,都是AB’优于AB,下不说明。
引理2:设两条极长链AB,AB’,两条链的公共部分为AO,AB’>AB,除OA和OB外的由O引出的链中,B’O是最长的(如图2),仍有AB不优于AB’。
在这种情况下,C的位置又3种,我们依次讨论。
(2.1)当C处于C1时。
因为B’O>BO,因此CB’>CB
(2.1.1)如果CB>CA,因为CB’>CB,成立。
(2.1.2)如果CA>CB.
(2.1.2.1)如果CA>CB’,CB’+B’A>CB+BA. 成立
(2.1.2.2)如果CB’>CA,CA+AB’>CB+AB成立
综上AB’优于AB。
(2.2)当C处于C2时。
因为B’O>BO,因此CB’>CB
(2.1.1)如果CB>CA,因为CB’>CB,成立。
(2.1.2)如果CA>CB。
(2.1.2.1)如果CA>CB’,CB’+B’A>CB+BA. 成立
(2.1.2.2)如果CB’>CA,CA+AB’>CB+AB成立
综上AB’优于AB。
(2.3)当C处于C3时,
此时原路径为CB+BA或CA+BA.这取决于OA和OB的长度,不妨设OA>BO,则原路径为OB+CO+AB.因此路径长度由CO决定,因为CO<B’O,如果我们可以将B’和C交换,那么原长度将更长,而这种情况下,如果以B’为B,原来的B为C,总的路径长度仍不变。
因此AB不优于AB’.
综上,命题得证。
引理3:树中最长链不差于其他任何链。
证明:对于两条不相交的链a,b.必定存在一条链c与a,b相交,且满足len(a)>len(c)>len(b),如图3。
AB>CB>CD。
其中BF>EF+ED,CE+EF<AF.
根据引理2,CD不优于CB,CB不优于AB,因此CD不优于AB。总体而言CD不优于AB。
因此对于任意两条链a,b,如果len(a)>len(b),则b不优于a。
因此,命题得证。
于是,只要先在树中找一条最长链,链两端就是AB,然后枚举C,求最优解即可。
求最长链可以通过两次搜索完成,同时可以完成A到其他的距离的初始化,然后由B出发遍历每一个点,计算出B到其他点的距离,这样计算总距离就O(1)了。
总的时间复杂度是O(N)。
*Author:rj;
*Problem:NOI 2003 逃学的小孩
*Language:C++;
*state:Solved 2010.6.28 19:09
*Memo:树的最长链
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
using namespace std;
const int MaxN=200005;
typedef long long LL;
int h[MaxN]={0},next[MaxN<<1],data[MaxN<<1],value[MaxN<<1],cnt;
int N,M;
int A,B,C;
LL disA[MaxN],disB[MaxN];
bool vis[MaxN];
int q[MaxN];
void AddEdge(int x,int y,int z)
{
next[++cnt]=h[x];h[x]=cnt;data[cnt]=y;value[cnt]=z;
}
void Init()
{
int x,y,z;
scanf("%d%d",&N,&M);
cnt=1;
while (M--)
{
scanf("%d%d%d",&x,&y,&z);
AddEdge(x,y,z);
AddEdge(y,x,z);
}
}
void BFS(int st,LL dis[],int &last)
{
int l,r,p,x,y;
dis[last=st]=0;
memset(vis,0,N+1);
for (vis[q[l=r=0]=st]=1;l<=r;l++)
for (p=h[x=q[l]];p;p=next[p])
if(!vis[y=data[p]])
{
dis[y]=dis[x]+value[p];
if(dis[y]>dis[last])last=y;
vis[q[++r]=y]=1;
}
}
void LongestLink()
{
BFS(1,disA,A);
BFS(A,disA,B);
}
inline LL Min(LL a,LL b){return a>b?b:a;}
inline void GetMax(LL &a,LL b){if(a<b)a=b;}
void Solve()
{
int i,T;
LL ans=0;
LongestLink();
BFS(B,disB,T);
for (i=1;i<=N;i++)GetMax(ans,Min(disA[i],disB[i]));
printf("%I64d",ans+disA[B]);
}
int main()
{
Init();
Solve();
return 0;
}
以下来自陈瑜希论文《多角度思考 创造性思维》
枚举分叉点
将某个点a当作分叉点时,以其为根构造一棵树,对节点Y,就有两种情况:1Y就是节点a;2Y在a的某个孩子节点的子树上。对于情况1,可以把它转化为情况2,只需给a加一个空的孩子节点,认为它和a之间的距离是0即可。既然a是分叉点,那么X和Z就不能在同一棵子树上,X和Y,Y和Z也不能在同一棵子树上。题目中要求的是使|XY|+|YZ|最大,也就是要求2|Ya|+|Za|+|Xa|最大。至此,思路已完全明确,对于以a为分叉点的情形,只需求出到a的最远的3个点,而且这3个点分别处于a的3棵不同的子树之中。如果采用枚举分叉点的方法的话,每次都需要的计算才行,时间复杂度就又成了。
两次遍历
这里,需要改变一下思路。以点1为根,计算出要求的值后,不去枚举其它的节点,而把这棵树再遍历一遍,进行一次BFS,深度小的先访问,深度大的后访问,就保证了访问到某一个节点的时候,其父亲节点已经被访问过了。假设我们现在访问到了点a,我们现在要求的是距点a的3个最远的点,且这3个点到a的路径上不经过除a外的相同节点。显然,这3个点要么位于以a为根的子树中,要么位于以a为根的子树外。如果在以a为根的子树外,那么是一定要通过a的父亲节点与a相连的。至此,思路逐渐清晰起来。此次遍历时,对于点a,检查其父亲节点,只需求出到其父亲节点的最远的,且不在以a为根的子树中的那点即可,再与第一次遍历求得的值进行比较,就可以求出以该点为分叉点时,|XY|+|YZ|的最大值了。具体方法为,每个点记录最大的两个值,并记录这最大的两个值分别是从哪个相邻节点传递来的。当遍历到其某个孩子节点时,只需检查最大值是否是从该孩子节点传递来的,如果是,就取次大值,如果不是,就可以取最大值。这样一来,该算法的时间复杂度和空间复杂度都为,是个非常优秀的算法。
注意
这里提醒大家注意一点,计算过程中的值和最后的结果可能会超过长整型的范围,所以这里需要使用int64或者double类型。
对于树必须进行两次遍历,才能求得最终的最大值。该例题的分析中提出了分叉点的想法,是比较具有创造性的,需要从多个角度思考。因此,在平时的练习中,当对一道题目束手无策时,可从多个角度思考,创造新思维,使题目得到解决。此题遍历两次的方法具有普遍性,在许多题目中都可以得到应用:记录最大的两个值,并记录是从哪个节点传递来的思想方法。这种遍历两次和记录最大的多个值的思想是值得学习的,也是动态规划在树上进行使用的经典方法。
本题的树型动态规划复杂度是线形的,但是也有一部分问题,在线形的时间内无法出解。这类问题的变化更多,从状态的确立到状态的转移,都对思考角度有着更高的要求。这里先举2个例子来说明。
代码来自http://blog.csdn.net/crazy______/article/details/9421359
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn=210000;
struct Node{
int flag;
long long t;
};
long long Max;
Node cnt[maxn][4];
bool vis[maxn];
vector<Node> son[maxn];
bool cmp(Node a,Node b)
{
return a.t>b.t;
}
void DFS1(int x)
{
int i,v;
for(i=0;i<3;i++)
cnt[x][i].t=0;
for(i=0;i<son[x].size();i++)
{
v=son[x][i].flag;
if(!vis[v])
{
vis[v]=1;
DFS1(v);
cnt[x][3].t=cnt[v][0].t+son[x][i].t;
cnt[x][3].flag=v;
sort(cnt[x],cnt[x]+4,cmp);
}
}
if(cnt[x][0].t+2*cnt[x][1].t+cnt[x][2].t>Max)
Max=cnt[x][0].t+2*cnt[x][1].t+cnt[x][2].t;
}
void DFS2(int x,long long ff)
{
int i,v;
for(i=0;i<son[x].size();i++) //保证父节点先处理完。
{
v=son[x][i].flag;
if(vis[v])
{
if(cnt[v][0].flag!=x)
cnt[x][3].t=cnt[v][0].t;
else
cnt[x][3].t=cnt[v][1].t;
cnt[x][3].flag=v;
cnt[x][3].t+=ff;
sort(cnt[x],cnt[x]+4,cmp);
if(cnt[x][0].t+2*cnt[x][1].t+cnt[x][2].t>Max)
Max=cnt[x][0].t+2*cnt[x][1].t+cnt[x][2].t;
}
}
for(i=0;i<son[x].size();i++)
{
v=son[x][i].flag;
if(!vis[v])
{
vis[v]=1;
DFS2(v,son[x][i].t);
}
}
}
int main()
{
int N,M;
int i,u,v;
long long t;
Node tmp;
while(scanf("%d%d",&N,&M)==2)
{
for(i=1;i<=N;i++)
son[i].clear();
for(i=1;i<=M;i++)
{
scanf("%d%d%lld",&u,&v,&t);
tmp.flag=v; tmp.t=t; son[u].push_back(tmp);
tmp.flag=u; son[v].push_back(tmp);
}
Max=0;
memset(vis,0,sizeof(vis));
vis[1]=1;
DFS1(1);
memset(vis,0,sizeof(vis));
vis[1]=1;
DFS2(1,0);
printf("%lld\n",Max);
}
return 0;
}