Computer
Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 3782 Accepted Submission(s): 1909
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.
這道題是一道很經典的樹形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;
}