Computer
Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 3782 Accepted Submission(s): 1909
![](https://i-blog.csdnimg.cn/blog_migrate/3f5ab5c3d97280e16503ce9ef614b5c5.jpeg)
Hint: the example input is corresponding to this graph. And from the graph, you can see that the computer 4 is farthest one from 1, so S1 = 3. Computer 4 and 5 are the farthest ones from 2, so S2 = 2. Computer 5 is the farthest one from 3, so S3 = 3. we also get S4 = 4, S5 = 4.
5 1 1 2 1 3 1 1 1
3 2 3 4 4
这道题是一道很经典的树形DP题。给人的思考很多啊。。
这道题的题意很简单,求出一棵树上分别到每个节点最远的距离。是求解树的直径题目的翻版。要求出分别到每个结点的最远距离最暴力的方法就是n次dfs,对每个结点用一次。但是这样想一想就不行,数据范围根本不允许,当然求解图中的最短路的做法也是不行的,时间复杂度都太高了。然后发现dfs一次之后只能确定到根结点(这个说法不准确,因为是无根树)的最远距离。而到其他的节点的距离是不确定的。因而我们需要对到其他的结点的最远距离进行更新?这样就想到至少还需要一次dfs。可是如何更新呢?
首先要说的是对于树形dp的题要分清是无根树还是有根树。一般对于无根树需要将其转化为有根树求解。
其次,如何更新结点的最远距离?
对于一个结点v,设它的父节点为u。然后我们分析到结点v的最远距离的来源。到v的最远距离可能来源于v的子树,即dfs完与v相连的结点(不包括u)后得到的最大距离。还可能来自v的父节点u的最远距离再加上u和v之间的距离。当然对于到达u的最远距离还要进行分析。可能到达u的最远距离正好来自v这个子树。那么对于到v的最远距离来说就不能是u的最远距离加上他们之间的距离。因而要求出在u的子树中u可以到达的第二远的距离。当然在转移的时候还有可能最远距离来自u的祖先加上u与v之间的距离。
因而我们可以记录下最远距离和次远距离从而进行结点距离的更新。当然代码中还有很多需要注意的地方。
参考代码1:
#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<string>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<queue>
#include<ctime>
#include<cstdlib>
#include<iomanip>
#include<utility>
#define pb push_back
#define mp make_pair
#define CLR(x) memset(x,0,sizeof(x))
#define _CLR(x) memset(x,-1,sizeof(x))
#define REP(i,n) for(int i=0;i<n;i++)
#define Debug(x) cout<<#x<<"="<<x<<" "<<endl
#define REP(i,l,r) for(int i=l;i<=r;i++)
#define rep(i,l,r) for(int i=l;i<r;i++)
#define RREP(i,l,r) for(int i=l;i>=r;i--)
#define rrep(i,l,r) for(int i=l;i>r;i--)
#define read(x) scanf("%d",&x)
#define put(x) printf("%d\n",x)
#define ll long long
#define lson l,m,rt<<1
#define rson m+1,r,rt<<11
using namespace std;
int n;
int dp[10010];
int n1,head[10010];
struct edge
{
int v,cost,next,sum;
} e[20010]; //使用邻接表存储树的边,防止使用邻接矩阵导致超时。
void addedge(int u,int v,int cost) //加边的操作
{
e[++n1].next=head[u];
e[n1].v=v;
e[n1].sum=0;
e[n1].cost=cost;
head[u]=n1;
}
void dfs(int u,int rt)
{
for(int i=head[u]; i; i=e[i].next)
{
int v=e[i].v;
if(v==rt) //对子节点进行访问时,不能访问父节点。否则会无限递归
continue;
dfs(v,u);
e[i].sum=dp[v]+e[i].cost; //记录从u->v的这条边所获得的最大距离。即u到v的子树的最大距离。dp[v]是v到以v为根节点的子树中结点的最大距离。
dp[u]=max(dp[u],dp[v]+e[i].cost); //dp[u]记录的是u到u的子树中的最大距离。
}
}
void dfs1(int u,int rt) //更新过程
{
int shu=0; //注意这个更新过程是从上到下依次更新,更新完父节点才能更新子节点。而上面那个dfs是在更新完子节点之后再更新父节点,注意更新的次序。
for(int i=head[rt];i;i=e[i].next)
{
int v=e[i].v;
if(v!=u)
shu=max(shu,e[i].sum); //找出根节点rt到根节点rt的其他除过u的子树中的最远距离
}
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==rt)
{
e[i].sum=e[i].cost+shu; //从整棵树的根节点开始对子树进行更新的时候,由于根节点本来的最远距离在第一次dfs就可以求出。所以根节点的子结点的最远距离除了来自自己的子树之外,还有就是来自根节点到另外的子树的最远距离。这是e[i].sum只表示的是从根节点的其他子树获得的最大距离。而继续向下更新时,e[i].sum表示的就不单纯是rt到rt的非u的子树中的最大距离。因为在上面第一个for循环的时候,rt访问的结点包括其他子节点,当然还有rt结点的父节点。即u的祖先结点。这样更新出来的e[i].sum就是来自rt的祖先的最远距离和来自rt的非u的子树的最远距离的最大值加上rt与u之间的距离。这里边还包含这一层意思。实际上是两种截然不同的状态转移。
break;
}
}
for(int i=head[u]; i; i=e[i].next)
{
int v=e[i].v;
dp[u]=max(dp[u],e[i].sum); //这样之前的dp[u]表示u到自己的子树的最大距离,e[i].sum表示rt与u的边到其他结点的最远距离。这样两个取最大值即可。
if(v==rt) //不能访问父节点
continue;
dfs1(v,u);
}
}
int main()
{
while(~read(n))
{
CLR(dp);CLR(head);
n1=0;
REP(i,2,n)
{
int v,cost;
scanf("%d%d",&v,&cost);
addedge(i,v,cost);
addedge(v,i,cost);
}
dfs(1,0);
for(int i=head[1]; i; i=e[i].next)
{
int v=e[i].v;
dfs1(v,1);
}
REP(i,1,n)
printf("%d\n",dp[i]);
}
}
下面的解法就不得不联系树的直径这个经典问题。下面说下树的直径的求解方法。通过学习了解到树的直径求解方法有好几种。一种是求出所有结点到其他结点的最远距离然后取最大值。第二种是利用了树的直径的一个性质:距某个点最远的叶子节点一定是树的某一条直径的端点。然后从任意一点开始dfs一次记录到达的最远的结点的位置,然后再从这个点开始再dfs一次得到的最大距离就是树的直径。第三种方法就是树的直径必然来自某个结点到其他结点的最远距离和次远距离之和。然后用一次dfs就行了。
下面的方法就是利用第三种方法的性质。
参考代码2:
#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<string>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<queue>
#include<ctime>
#include<cstdlib>
#include<iomanip>
#include<utility>
#define pb push_back
#define mp make_pair
#define CLR(x) memset(x,0,sizeof(x))
#define _CLR(x) memset(x,-1,sizeof(x))
#define REP(i,n) for(int i=0;i<n;i++)
#define Debug(x) cout<<#x<<"="<<x<<" "<<endl
#define REP(i,l,r) for(int i=l;i<=r;i++)
#define rep(i,l,r) for(int i=l;i<r;i++)
#define RREP(i,l,r) for(int i=l;i>=r;i--)
#define rrep(i,l,r) for(int i=l;i>r;i--)
#define read(x) scanf("%d",&x)
#define put(x) printf("%d\n",x)
#define ll long long
#define lson l,m,rt<<1
#define rson m+1,r,rt<<11
using namespace std;
int n;
int dp[10010],f[10010],g[10010],res[10010]; //f数组记录的是结点到自己的子树的最大距离,g数组记录的是结点到自己的子树的次大距离。res数组记录的是每个结点到它的子树的最大距离的那个子结点与该节点之间的边的编号,其实这个数组是没必要的。用这个数组的主要目的是判断根节点到子树的最大距离是否在该子节点的子树上。满足f[u]=f[v]+e[i].cost就行。。
int n1,head[10010];
struct edge
{
int v,cost,next;
} e[20010];
void addedge(int u,int v,int cost)
{
e[++n1].next=head[u];
e[n1].v=v;
e[n1].cost=cost;
head[u]=n1;
}
void dfs(int u,int rt)
{
for(int i=head[u]; i; i=e[i].next)
{
int v=e[i].v;
if(v==rt)
continue;
dfs(v,u);
if(f[v]+e[i].cost>f[u])
{
f[u]=f[v]+e[i].cost;
res[u]=i;
}
}
g[u]=0;
for(int i=head[u];i;i=e[i].next) //找出u到子树的次大距离,多出来这个循环会让时间增加,程序效率上会降低一些。。应该向大牛学习,在这些细节上再改进一些。。直接在上面的for循环中一遍求出次大距离
{
int v=e[i].v;
if(v==rt)
continue;
if(i!=res[u])
g[u]=max(g[u],f[v]+e[i].cost);
}
}
void dfs1(int u,int rt)
{
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==rt)
return;
if(i==res[u]) //下面这个地方的dp需要注意,需要好好思考一下才行。这里要对dp[v]进行更新。我们考虑dfs的顺序,首先在访问到v结点的时候,必然访问到u结点,所以dp[u]的含义是u从u和它的祖先的那条边向上的最远距离。因此这个dfs的目的是对一个结点从他的父节点来的两种最远距离的dp过程。而第一种方法没有这么明显的展现出来而已。这一点想了很久。想明白dfs的更新顺序,再理解下面的状态转移方程就好理解多了。。
dp[v]=max(dp[u],g[u])+e[i].cost;
else
dp[v]=max(dp[u],f[u])+e[i].cost;
dfs1(v,u);
}
}
int main()
{
while(~read(n))
{
CLR(dp);CLR(head);CLR(f);
CLR(g);CLR(res);
n1=0;
REP(i,2,n)
{
int v,cost;
scanf("%d%d",&v,&cost);
addedge(i,v,cost);
addedge(v,i,cost);
}
dfs(1,0);
dp[1]=0;
dfs1(1,0);
REP(i,1,n)
printf("%d\n",max(f[i],dp[i])); //这里f[i]是来自子树的最大距离,dp[i]是来自i结点的父节点的最大距离,两者取最大
}
}
后来又看了kuangbin大神的代码,发现代码效率高多了。。代码的主要思想是动态更新一个结点到整个树的最远距离与次远距离。这样减少了一些状态。由于子节点从父节点更新而来,所以该方法相当于动态的每次把访问的点当成根节点,然后动态更新到其他结点的最远距离和次远距离就行了。这个方法里边的最远距离和次远距离是针对整棵树的所有结点而言的,而上面方法二中的最远距离和次远距离是该结点到自己的子树的最远距离和次远距离。
参考代码3(By kuangbin)
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int MAXN=10010;
struct Node
{
int to;
int next;
int len;
}edge[MAXN*2];//因为存无向边,所以需要2倍
int head[MAXN];//头结点
int tol;
int maxn[MAXN];//该节点往下到叶子的最大距离
int smaxn[MAXN];//次大距离
int maxid[MAXN];//最大距离对应的序号
int smaxid[MAXN];//次大的序号
void init()
{
tol=0;
memset(head,-1,sizeof(head));
}
void add(int a,int b,int len)
{
edge[tol].to=b;
edge[tol].len=len;
edge[tol].next=head[a];
head[a]=tol++;
edge[tol].to=a;
edge[tol].len=len;
edge[tol].next=head[b];
head[b]=tol++;
}
//求结点v往下到叶子结点的最大距离
//p是v的父亲结点
void dfs1(int u,int p)
{
maxn[u]=0;
smaxn[u]=0;
for(int i=head[u];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if(v==p)continue;//不能往上找父亲结点
dfs1(v,u);
if(smaxn[u]<maxn[v]+edge[i].len)
{
smaxn[u]=maxn[v]+edge[i].len;
smaxid[u]=v;
if(smaxn[u]>maxn[u])
{
swap(smaxn[u],maxn[u]);
swap(smaxid[u],maxid[u]);
}
}
}
}
//p是u的父亲结点,len是p到u的长度
void dfs2(int u,int p)
{
for(int i=head[u];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if(v==p)continue;
if(v==maxid[u])
{
if(edge[i].len+smaxn[u]>smaxn[v])
{
smaxn[v]=edge[i].len+smaxn[u];
smaxid[v]=u;
if(smaxn[v]>maxn[v])
{
swap(smaxn[v],maxn[v]);
swap(smaxid[v],maxid[v]);
}
}
}
else
{
if(edge[i].len+maxn[u]>smaxn[v])
{
smaxn[v]=edge[i].len+maxn[u];
smaxid[v]=u;
if(smaxn[v]>maxn[v])
{
swap(smaxn[v],maxn[v]);
swap(maxid[v],smaxid[v]);
}
}
}
dfs2(v,u);
}
}
int main()
{
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
int n;
int v,len;
while(scanf("%d",&n)!=EOF)
{
init();
for(int i=2;i<=n;i++)
{
scanf("%d%d",&v,&len);
add(i,v,len);
}
dfs1(1,-1);
dfs2(1,-1);
for(int i=1;i<=n;i++)
printf("%d\n",maxn[i]);
}
return 0;
}