یکی دیگر از مسائلی که در حالت کلی گراف np است اما در درخت ها به سادگی حل می شود مسئله پیدا کردن بلندترین مسیر است! از آنجایی که در درخت ها مسیر بین هر دو راس یکتا است می توان نتیجه گرفت قطر درخت همان بلندترین مسیر درخت می باشد. در این بخش روش های پیدا کردن قطر درخت را با پیچیدگی زمانی \(O(n)\) بررسی می کنیم.
درخت را از راس 1 ریشه دار کنید. با استفاده از برنامه نویسی پویا دو متغیر زیر را به ازای هر راس \(u\) به دست می آوریم.
واضح است که جواب مسئله برابر است با \(ans_1\). حالا تنها مسئله باقی مانده این است که چگونه این دو متغیر را به دست آوریم.
برای به دست آوردن \(dp_u\) کافی است توجه کنید که در اولین حرکت از \(u\) به یکی از بچه هایش می رویم. پس باید به بچه ای برویم که مقدار \(dp\) آن بیشینه است.
برای به دست آوردن \(ans_u\) حالت بندی کنید که راس \(u\) درون قطر باشد یا نباشد.
در کد زیر در \(mx1, mx2\) به ترتیب راس های با بیشترین \(dp\) را نگه داری می کنیم.
const int maxn = 1e5 + 10;
vector<int> g[maxn];
int dp[maxn], ans[maxn];
void dfs(int u, int par = 0){
int mx1 = -1, mx2 = -1;
for(int y : g[u]){
if(y != par){
dfs(y, u);
dp[u] = max(dp[u], 1 + dp[y]);
ans[u] = max(ans[u], ans[y]);
if(mx1 == -1 || dp[mx1] < dp[y]){
mx2 = mx1;
mx1 = y;
}
else if(mx2 == -1 || dp[mx2] < dp[y])
mx2 = y;
}
}
ans[u] = max(ans[u], dp[u]);
if(mx1 != -1 && mx2 != -1){
ans[u] = max(ans[u], 2 + dp[mx1] + dp[mx2]);
}
پس توانستیم الگوریتمی ارائه دهیم که با پیچیدگی زمانی \(O(n)\) قطر درخت را پیدا می کند.
گاهی هدف ما به دست آوردن یک متغیر مثل \(dp\) به ازای هر راس درخت است اما حساب کردن مقدار \(dp_u\) نیاز به داشتن مقدار \(dp\) تمام مجاور های راس \(u\) (و نه فقط بچه های \(u\)) دارد.
ساده ترین مثال برای معرفی این تکنیک مسئله پیدا کردن بیشترین فاصله از هر راس است. فرض کنید می خواهیم به ازای هر راس \(u\) خروج از مرکز این راس را داشته باشیم. جواب راس \(u\) را \(ans_u\) بگیرید. برای به دست آوردن جواب یک راس می توان به راحتی درخت را از این ارتفاع آویزان کرد و در \(O(n)\) ارتفاع درخت را حساب کرد. اما آیا می توان مسئله را به ازای تمام راس با هم در \(O(n)\) حل کرد؟
اولین مشکل ما این است که چون حساب کردن جواب یک راس به داشتن جواب مجاور هایش نیاز دارد نمی دانیم که محاسبه را از کجا شروع کنیم!
درخت را از راس \(u\) ریشه دار کنید. شکاندن مسئله به دو بخش می تواند مفید باشد. فرض کنید \(dpDown_u\) برابر است با بیشترین فاصله از راس \(u\) به راسی درون زیردرخت راس \(u\). همچنین \(dpUp_u\) برابر است با بیشترین فاصله از راس \(u\) به راسی خارج از زیردرخت راس \(u\) (یعنی در اولین گام باید به پدر \(u\) برویم). واضح است که جواب راس \(u\) برابر با بیشینه دو عدد \(dpDown_u\) و \(dpUp_u\) است.
همانطور که در قسمت بالا بررسی کردیم \(dpDown_u\) را می توان از روی \(dpDown\) بچه های راس \(u\) حساب کرد.
برای حساب کردن \(dpUp_u\) توجه کنید که بعد از اینکه از \(u\) به پدر \(u\) رفتیم می توانیم دو راه در پیش بگیریم.
نکته کلیدی این است که نیاز نیست هر بار تمام برادر های \(u\) را بررسی کنیم که راس با \(dpDown\) بیشینه (همان \(w\) را پیدا کنیم). کافی است به ازای \(par\) تنها یک بار دو بچه ای که \(dpDown\) آن ها بیشینه است را به دست بیاوریم. همواره راس \(w\) یکی از دو بچه \(par\) است که \(dpDown\) آنها بیشینه است. (چرا؟)
پس فهمیدیم که به چه صورت باید متغیر ها را به دست بیاوریم. اما همچنان یک مشکل حل نشده باقی مانده است. به چه ترتیبی باید به دست آوردن مقادیر را انجام دهیم. برای به دست آوردن \(dpDown\) نیاز داریم که مقادیر بچه ها و برای به دست آوردن \(dpUp\) نیاز داریم که مقادیر پدر را داشته باشیم. پس از کدام یک باید شروع کنیم؟
جواب ساده و هوشمندانه است. می توانیم طی دومرحله مقادیر را به دست بیاوریم. یک بار \(dpDown\) ها را با استفاده از dfsDown و سپس \(dpUp\) ها را با استفاده از dfsUp به دست بیاوریم! نکته اینجاست که در dfsDown ابتدا مقدار بچه ها به دست می آیند سپس مقدار راس فعلی. اما در dfsUp ابتدا مقدار پدر به دست می آید سپس مقدار بچه ها از روی پدر به دست می آیند!
توجه کنید که در تابع dfsUp وقتی روی یک راس هستیم فرض کرده ایم که \(dpUp\) آن راس به دست آمده است و سپس \(dpUp\) بچه های آن را به دست می آوریم.
const int maxn = 1e5 + 10;
vector<int> g[maxn];
int dpUp[maxn], dpDown[maxn];
void dfsDown(int u, int par = 0){ // aval bayad in taabe ra ejra konim
for(int y : g[u]){
if(y != par){
dfsDown(y, u);
dpDown[u] = max(dpDown[u], dpDown[y] + 1);
}
}
}
void dfsUp(int u, int par = 0){
int mx1 = -1, mx2 = -1;
for(int y : g[u]){
if(y != par){
if(mx1 == -1 || dpDown[mx1] < dpDown[y]){
mx2 = mx1;
mx1 = y;
}
else if(mx2 == -1 || dpDown[mx2] < dpDown[y]){
mx2 = y;
}
}
}
for(int y : g[u]){
if(y != par){
if(y == mx1){
dpUp[y] = dpUp[u] + 1;
if(mx2 != -1)
dpUp[u] = max(dpUp[u], doDown[mx2] + 2);
}
else{
dpUp[y] = max(dpUp[u]+1, doDown[mx1] + 2);
}
dfsUp(y, u);
}
}
}
قطر درخت یک خاصیتی دارد که به ما کمک می کند تا آن را ساده تر پیدا کنیم. آن خاصیت این است: دور ترین راس نسبت به هر راس، سر یکی از خطر های درخت است.
برای اثبات، درخت را از این راس ریشه دار کنید. یکی از قطر های درخت را در نظر بگیرید. این قطر که یک مسیر هم هست، دقیقا یک راس دارد که نزدیک ترین راس به ریشه است. (پایین ترین جد مشترک سر و ته مسیر) اگر دور ترین راس مورد نظر ما درون ریزدرخت این راس باشد، یکی از شاخه ها را حذف می کنیم و شاخه مربوط به این راس را اضافه می کنیم و طول مسیر کوچک تر نمی شود پس هم چنان قطر می ماند. حالت دیگر ممکن نیست چون در این صورت از راس دورتر نسبت به جد مشترک شروع کرده و به این راس می رویم و قطر بزرگ تری پیدا می شود که تناقض است. پس این راس سر یکی از قطر های درخت است.
از این خاصیت می توان استفاده کرد و قطر درخت را پیدا کرد. تابعی می نویسیم که یک راس ورودی بگیرد و به کمک الگوریتم دی اف اس یکی از دور ترین رئوس نسبت به این راس را برگرداند. این تابع را از یک راس دلخواه اجرا می کنیم و نتیجه را \(u\) می نامیم. یکبار هم این تابع را از \(u\) اجرا می کنیم و نتیجه را \(v\) می نامیم. چون از قضیه بالا می دانستیم که راس \(u\) سر یکی از قطر های درخت است، پس مسیر \(uv\) یکی از قطر های درخت است.