# APCS實作題2016年3月第4題:血緣關係
> 日期:2024年8月26日
> 作者:王一哲
> 題目來源:[105年3月5日實作題第4題](https://apcs.csie.ntnu.edu.tw/wp-content/uploads/2020/10/APCS-%E5%AF%A6%E4%BD%9C%E9%A1%8C-2016.03.05.pdf)
> [ZeroJudge 單筆測資版本題目連結](https://zerojudge.tw/ShowProblem?problemid=h032)
> [ZeroJudge 多筆測資版本題目連結](https://zerojudge.tw/ShowProblem?problemid=b967)
<br />
## 題目
### 問題描述
小宇有一個大家族。有一天,他發現記錄整個家族成員和成員間血緣關係的家族族譜。小宇對於最遠的血緣關係 (我們稱之為"血緣距離") 有多遠感到很好奇。
下圖為家族的關係圖。0 是 7 的孩子,1、2 和 3 是 0 的孩子,4 和 5 是 1 的孩子,6 是 3 的孩子。我們可以輕易的發現最遠的親戚關係為 4(或 5)和 6,他們的"血緣距離"是 4 (4 ~ 1,1 ~ 0,0 ~ 3,3 ~ 6)。
給予任一家族的關係圖,請找出最遠的"血緣距離"。你可以假設只有一個人是整個家族成員的祖先,而且沒有兩個成員有同樣的小孩。
<img height="40%" width="40%" src="https://i.imgur.com/ALDTyWr.png" style="display: block; margin-left: auto; margin-right: auto;"/>
<br />
### 輸入格式
第一行為一個正整數 $n$ 代表成員的個數,每人以 $0$ ~ $n-1$ 之間惟一的編號代表。接著的
$n-1$ 行,每行有兩個以一個空白隔開的整數 $a$ 與 $b$ ($0 \leq a, b \leq n-1$),代表 $b$ 是 $a$ 的孩子。
<br />
### 輸出格式
每筆測資輸出一行最遠"血緣距離"的答案。
<br />
### 範例一:輸入
```
8
0 1
0 2
0 3
7 0
1 4
1 5
3 6
```
### 範例一:正確輸出
```
4
```
(說明)如題目所附之圖,最遠路徑為 4 → 1 → 0 → 3 → 6 或 5 → 1 → 0 → 3 → 6,距離為 4。
<img height="40%" width="40%" src="https://imgur.com/x07ATaE.png" style="display: block; margin-left: auto; margin-right: auto;"/>
<div style="text-align:center">範例一</div>
<br />
### 範例二:輸入
```
4
0 1
0 2
2 3
```
### 範例二:正確輸出
```
3
```
(說明)最遠路徑為 1 → 0 → 2 → 3,距離為 3。
<img height="23%" width="23%" src="https://imgur.com/45gYhbQ.png" style="display: block; margin-left: auto; margin-right: auto;"/>
<div style="text-align:center">範例二</div>
<br />
### 評分說明
輸入包含若干筆測試資料,每一筆測試資料的執行時間限制(time limit)均為 2 秒,依正確通過測資筆數給分。其中,
- 第 1 子題組 10 分,整個家族的祖先最多 2 個小孩,其他成員最多一個小孩,$2 \leq n \leq 100$。
- 第 2 子題組 30 分,$2 \leq n \leq 100$。
- 第 3 子題組 30 分,$101 \leq n \leq 2,000$ 。
- 第 4 子題組 30 分,$1,001 \leq n \leq 100,000$。
<br />
## Python 程式碼
### 寫法1,先由下到上找根節點,再找樹的直徑
單筆測資版,費時最久約 0.9 s,使用記憶體最多 13.3 MB,通過測試。
```python=
n = int(input()) # 成員個數
par = [-1]*n # 編號 0 ~ n-1 對應的父節點,-1 為根節點
deg = [0]*n # 子節點數量,如果是0則是葉節點
for _ in range(n-1): # 讀取 n-1 行資料
a, b = map(int, input().split()) # 讀取 a, b
par[b] = a # a 是 b 的父節點
deg[a] += 1 # a 的子節點數量加 1
""" bottom-up 由下到上找根節點 """
que = [int(x) for x in range(n) if deg[x] == 0] # 找出葉節點
head = 0 # 從 que 讀取資料用的索引值
while head < len(que): # 如果 head 還沒有出界繼續執行
u = que[head]; head += 1 # 由 que 讀取目前走訪的節點 u,head 加 1
v = par[u] # 讀取 u 的父節點 v
if v == -1: break # 找到根節點了
deg[v] -= 1 # v 節子節點數量減 1
if deg[v] == 0: que.append(v) # 將 v 加入要走訪的佇列
""" 由根節點往下找樹的直徑 """
dia = 0 # 樹的直徑,預設為 0
hei = [0]*n # 每個節點的高度
for u in que[:-1]: # 由 que 依序讀取最後一個以外的節點,不要讀到最後的根節點
v = par[u] # 讀取 u 的父節點 v
dia = max(dia, hei[u]+hei[v]+1)
# 如果 u 的高度加 1 較大,更新 v 的高度
hei[v] = max(hei[v], hei[u]+1)
print(dia) # 印出答案
```
<br /><br />
多筆測資版,費時最久約 0.8 s,使用記憶體最多 13.3 MB,通過測試。
```python=
while True:
try: n = int(input()) # 試著讀取成員個數
except EOFError: break # 如果遇到 EOFError 中止 while 迴圈
par = [-1]*n # 編號 0 ~ n-1 對應的父節點,-1 為根節點
deg = [0]*n # 子節點數量,如果是0則是葉節點
for _ in range(n-1): # 讀取 n-1 行資料
a, b = map(int, input().split()) # 讀取 a, b
par[b] = a # a 是 b 的父節點
deg[a] += 1 # a 的子節點數量加 1
""" bottom-up 由下到上找根節點 """
que = [int(x) for x in range(n) if deg[x] == 0] # 找出葉節點
head = 0 # 從 que 讀取資料用的索引值
while head < len(que): # 如果 head 還沒有出界繼續執行
u = que[head]; head += 1 # 由 que 讀取目前走訪的節點 u,head 加 1
v = par[u] # 讀取 u 的父節點 v
if v == -1: break # 找到根節點了
deg[v] -= 1 # v 節子節點數量減 1
if deg[v] == 0: que.append(v) # 將 v 加入要走訪的佇列
""" 由根節點往下找樹的直徑 """
dia = 0 # 樹的直徑,預設為 0
hei = [0]*n # 每個節點的高度
for u in que[:-1]: # 由 que 依序讀取最後一個以外的節點,不要讀到最後的根節點
v = par[u] # 讀取 u 的父節點 v
dia = max(dia, hei[u]+hei[v]+1) # 更新 dia
hei[v] = max(hei[v], hei[u]+1) # 如果 u 的高度加 1 較大,更新 v 的高度
print(dia) # 印出答案
```
<br /><br />
### 寫法2,用 BFS 找最遠的節點,再找樹的直徑
單筆測資版,費時最久約 0.7 s,使用記憶體最多 28.3 MB,通過測試。
```python=
n = int(input()) # 成員個數
adj = [[] for _ in range(n)] # 無向圖,編號 0 ~ n-1 節點各自相連的節點
for _ in range(n-1): # 讀取 n-1 行資料
a, b = map(int, input().split()) # 讀取 a, b
adj[a].append(b) # a、b 相連
adj[b].append(a) # b、a 相連
""" 自訂函式 BFS """
def BFS(s): # 輸入起點 s
dis = [-1]* n # 距離,預設為 -1 代表還沒有走訪
dis[s] = 0 # 起點的距離為 0
que = [s] # 要走訪的節點
head = 0 # 從 que 讀取資料用的索引值
while head < len(que): # 如果 head 還沒有出界繼續執行
u = que[head]; head += 1 # 由 que 讀取目前走訪的節點 u,head 加 1
for v in adj[u]: # 依序讀取與 u 相連的節點 v
if dis[v] < 0: # v 還沒有走訪
dis[v] = dis[u] + 1 # 更新 v 的距離為 u 的距離加 1
que.append(v) # 將 v 加入 que
return que[-1], dis[que[-1]] # 回傳最後一個節點及距離
""" 呼叫 BFS """
u, _ = BFS(0) # 代入節點 0,回傳最遠的節點 u,距離不重要
_, d = BFS(u) # 代入節點 u,回傳最遠的距離 d,節點不重要
print(d) # 印出答案
```
<br /><br />
多筆測資版,費時最久約 0.7 s,使用記憶體最多 28.3 MB,通過測試。
```python=
while True:
try: n = int(input()) # 試著讀取成員個數
except EOFError: break # 如果遇到 EOFError 中止 while 迴圈
adj = [[] for _ in range(n)] # 無向圖,編號 0 ~ n-1 節點各自相連的節點
for _ in range(n-1): # 讀取 n-1 行資料
a, b = map(int, input().split()) # 讀取 a, b
adj[a].append(b) # a、b 相連
adj[b].append(a) # b、a 相連
""" 自訂函式 BFS """
def BFS(s): # 輸入起點 s
dis = [-1]* n # 距離,預設為 -1 代表還沒有走訪
dis[s] = 0 # 起點的距離為 0
que = [s] # 要走訪的節點
head = 0 # 從 que 讀取資料用的索引值
while head < len(que): # 如果 head 還沒有出界繼續執行
u = que[head]; head += 1 # 由 que 讀取目前走訪的節點 u,head 加 1
for v in adj[u]: # 依序讀取與 u 相連的節點 v
if dis[v] < 0: # v 還沒有走訪
dis[v] = dis[u] + 1 # 更新 v 的距離為 u 的距離加 1
que.append(v) # 將 v 加入 que
return que[-1], dis[que[-1]] # 回傳最後一個節點及距離
""" 呼叫 BFS """
u, _ = BFS(0) # 代入節點 0,回傳最遠的節點 u,距離不重要
_, d = BFS(u) # 代入節點 u,回傳最遠的距離 d,節點不重要
print(d) # 印出答案
```
<br /><br />
## C++ 程式碼
### 寫法1,先由下到上找根節點,再找樹的直徑
單筆測資版,費時最久約 32 ms,使用記憶體最多 2.4 MB,通過測試。
```cpp=
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
ios::sync_with_stdio(0); cin.tie(0); // 加速
int n; cin >> n; // 成員數量
vector<int> par (n, -1); // 每個子節點對應的父節點,-1 為根節點
vector<int> deg (n, 0); // 每個節點的子節點數量
for(int i=0; i<n-1; i++) { // 讀取 n-1 行資料
int a, b; cin >> a >> b; // 讀取節點 a、b
par[b] = a; // b 節父節點為 a
deg[a]++; // a 的子節點數量加 1
}
vector<int> que; // 要走訪的葉節點
for(int i=0; i<n; i++) { // 依序讀取 deg 的資料
if (deg[i] == 0) que.push_back(i); // 如果 deg[i] 為 0,將 i 加入 que
}
int head = 0; // 由 que 讀取資料用的索引值
while(head < (int)que.size()) { // 當 head 還沒有出界時繼續執行
int u = que[head]; head++; // 讀取 que[head],head 加 1
int v = par[u]; // 讀取 u 的父節點 v
if (v == -1) break; // 找到根節點了
deg[v]--; // v 的子節點數量減 -1
if (deg[v] == 0) que.push_back(v); // 如果 v 的子節點數量歸零,將 v 加入 que
}
int dia = 0; // 樹的直徑
vector<int> hei (n, 0); // 各節點的高度
for(int i=0; i<(int)que.size()-1; i++) { // 不要讀到 que 最後一項
int u = que[i]; // 讀取節點 u
int v = par[u]; // 讀取 u 的父節點 v
dia = max(dia, hei[v]+hei[u]+1); // 更新 dia
hei[v] = max(hei[v], hei[u]+1); // 如果 u 的高度加 1 較大,更新 v 的高度
}
cout << dia << "\n"; // 印出答案
return 0;
}
```
<br /><br />
多筆測資版,費時最久約 32 ms,使用記憶體最多 2.3 MB,通過測試。
```cpp=
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
ios::sync_with_stdio(0); cin.tie(0); // 加速
int n;
while(cin >> n) { // 如果有讀到成員數量繼續執行
vector<int> par (n, -1); // 每個子節點對應的父節點,-1 為根節點
vector<int> deg (n, 0); // 每個節點的子節點數量
for(int i=0; i<n-1; i++) { // 讀取 n-1 行資料
int a, b; cin >> a >> b; // 讀取節點 a、b
par[b] = a; // b 節父節點為 a
deg[a]++; // a 的子節點數量加 1
}
vector<int> que; // 要走訪的葉節點
for(int i=0; i<n; i++) { // 依序讀取 deg 的資料
if (deg[i] == 0) que.push_back(i); // 如果 deg[i] 為 0,將 i 加入 que
}
int head = 0; // 由 que 讀取資料用的索引值
while(head < (int)que.size()) { // 當 head 還沒有出界時繼續執行
int u = que[head]; head++; // 讀取 que[head],head 加 1
int v = par[u]; // 讀取 u 的父節點 v
if (v == -1) break; // 找到根節點了
deg[v]--; // v 的子節點數量減 -1
if (deg[v] == 0) que.push_back(v); // 如果 v 的子節點數量歸零,將 v 加入 que
}
int dia = 0; // 樹的直徑
vector<int> hei (n, 0); // 各節點的高度
for(int i=0; i<(int)que.size()-1; i++) { // 不要讀到 que 最後一項
int u = que[i]; // 讀取節點 u
int v = par[u]; // 讀取 u 的父節點 v
dia = max(dia, hei[v]+hei[u]+1); // 更新 dia
hei[v] = max(hei[v], hei[u]+1); // 如果 u 的高度加 1 較大,更新 v 的高度
}
cout << dia << "\n"; // 印出答案
}
return 0;
}
```
<br /><br />
### 寫法2,用 BFS 找最遠的節點,再找樹的直徑
單筆測資版,費時最久約 54 ms,使用記憶體最多 8 MB,通過測試。
```cpp=
#include <iostream>
#include <vector>
#include <utility>
#include <algorithm>
using namespace std;
pair<int, int> BFS(int s, vector<vector<int>>& adj) { // 輸入起點 s,節點數量 n,相連節點 adj,輸出最後一個節點及距離
vector<int> dis (adj.size(), -1); // 節點距離,-1 代表未走訪
vector<int> que = {s}; // 要走訪的節點,先放入起點 s
int head = 0; // 由 que 讀取資料的索引值
dis[s] = 0; // 起點距離為 0
while(head < (int)que.size()) { // 如果 head 還沒有出界繼續執行
int u = que[head]; head++; // 讀取 que[head],head 加 1
for(int v : adj[u]) { // 依序取出 u 相連的節點 v
if (dis[v] < 0) { // 如果 v 還沒有走訪
dis[v] = dis[u] + 1; // 更新 v 的距離
que.push_back(v); // 將 v 加入 que
}
}
}
return make_pair(que.back(), dis[que.back()]); // 回傳最後一個節點及距離
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); // 加速
int n; cin >> n; // 成員數量
vector<vector<int>> adj (n, vector<int> ()); // 相連的節點
for(int i=0; i<n-1; i++) { // 讀取 n-1 行資料
int a, b; cin >> a >> b; // 讀取節點 a、b
adj[a].push_back(b); // a、b 相連
adj[b].push_back(a); // b、a 相連
}
pair<int, int> t, d; // 接收回傳結果用的 pair
t = BFS(0, adj); // 代入節點 0,回傳最遠的節點,距離不重要
d = BFS(t.first, adj); // 代入節點 t.first,回傳最遠的距離,節點不重要
cout << d.second << "\n"; // 印出答案
return 0;
}
```
<br /><br />
多筆測資版,費時最久約 63 ms,使用記憶體最多 8 MB,通過測試。
```cpp=
#include <iostream>
#include <vector>
#include <utility>
#include <algorithm>
using namespace std;
pair<int, int> BFS(int s, vector<vector<int>>& adj) { // 輸入起點 s,節點數量 n,相連節點 adj,輸出最後一個節點及距離
vector<int> dis (adj.size(), -1); // 節點距離,-1 代表未走訪
vector<int> que = {s}; // 要走訪的節點,先放入起點 s
int head = 0; // 由 que 讀取資料的索引值
dis[s] = 0; // 起點距離為 0
while(head < (int)que.size()) { // 如果 head 還沒有出界繼續執行
int u = que[head]; head++; // 讀取 que[head],head 加 1
for(int v : adj[u]) { // 依序取出 u 相連的節點 v
if (dis[v] < 0) { // 如果 v 還沒有走訪
dis[v] = dis[u] + 1; // 更新 v 的距離
que.push_back(v); // 將 v 加入 que
}
}
}
return make_pair(que.back(), dis[que.back()]); // 回傳最後一個節點及距離
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); // 加速
int n;
while(cin >> n) { // 如果有讀到成員數量繼續執行
vector<vector<int>> adj (n, vector<int> ()); // 相連的節點
for(int i=0; i<n-1; i++) { // 讀取 n-1 行資料
int a, b; cin >> a >> b; // 讀取節點 a、b
adj[a].push_back(b); // a、b 相連
adj[b].push_back(a); // b、a 相連
}
pair<int, int> t, d; // 接收回傳結果用的 pair
t = BFS(0, adj); // 代入節點 0,回傳最遠的節點,距離不重要
d = BFS(t.first, adj); // 代入節點 t.first,回傳最遠的距離,節點不重要
cout << d.second << "\n"; // 印出答案
}
return 0;
}
```
<br /><br />
## 參考資料
1. 吳邦一教授的 APCS 解題影片〈[PyAp45 ZJ_b967. Python APCS 題解:血緣關係(AP201603_4)](https://youtu.be/WVLEUoR-uT8?si=9YTeNOBXVCCwj0y1)〉
2. 吳邦一教授的 APCS 講義〈[AP325-Python 第8章 樹上演算法 (III)](https://hackmd.io/@bangyewu/rymQ7w7_a#P-8-14-%E8%A1%80%E7%B7%A3%E9%97%9C%E4%BF%82-APCS201603)〉
---
###### tags:`APCS`、`Python`、`C++`