<style>
.markdown-body table{
display: unset;
}
</style>
# APCS實作題2017年10月第3題:樹狀圖分析 (Tree Analyses)
> 第1版:2023年6月15日
> 第2版:2024年11月23日
> 作者:王一哲
> 題目來源:[106年10月28日實作題第3題](https://apcs.csie.ntnu.edu.tw/wp-content/uploads/2018/12/1061028APCSImplementation.pdf)
> [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=c463)
<br />
## 題目
### 問題描述
本題是關於有根樹(rooted tree)。在一棵 n 個節點的有根樹中,每個節點都是以 1 ~ n 的不同數字來編號,描述一棵有根樹必須定義節點與節點之間的親子關係。一棵有根樹恰有一個節點沒有父節點(parent),此節點被稱為根節點(root),除了根節點以外的每一個節點都恰有一個父節點,而每個節點被稱為是它父節點的子節點(child),有些節點沒有子節點,這些節點稱為葉節點(leaf)。在當有根樹只有一個節點時,這個節點既是根節點同時也是葉節點。
在圖形表示上,我們將父節點畫在子節點之上,中間畫一條邊(edge)連結。例如,圖一中表示的是一棵 9 個節點的有根樹,其中,節點 1 為節點 6 的父節點,而節點 6 為節點 1 的子節點;又 5、3 與 8 都是 2 的子節點。節點 4 沒有父節點,所以節點 4 是根節點;而 6、9、3 與 8 都是葉節點。
<img height="50%" width="50%" src="https://i.imgur.com/rwJg8H7.png" style="display: block; margin-left: auto; margin-right: auto;"/>
<br />
樹狀圖中的兩個節點 $u$ 和 $v$ 之間的距離 $d(u,v)$ 定義為兩節點之間邊的數量。如圖一中,$d(7, 5) = 2$,而 $d(1, 2) = 3$。對於樹狀圖中的節點 $v$,我們以 $h(v)$ 代表節點 $v$ 的高度,其定義是節點 $v$ 和節點 $v$ 下面最遠的葉節點之間的距離,而葉節點的高度定義為 0。如圖一中,節點 6 的高度為 0,節點 2 的高度為 2,而節點 4 的高度為 4。此外,我們定義 $H(T)$ 為 $T$ 中所有節點的高度總和,也就是說 $H(T) = \sum_{v \in T} h(v)$。給定一個樹狀圖 $T$,請找
出 $T$ 的根節點以及高度總和 $H(T)$。
<br />
### 輸入格式
第一行有一個正整數 n 代表樹狀圖的節點個數,節點的編號為 1 到 n。接下來有 n 行,第 i 行的第一個數字 k 代表節點 i 有 k 個子節點,第 i 行接下來的 k 個數字就是這些子節點的編號。每一行的相鄰數字間以空白隔開。
<br />
### 輸出格式
輸出兩行各含一個整數,第一行是根節點的編號,第二行是 $H(T)$。
<br />
### 範例一:輸入
```
7
0
2 6 7
2 1 4
0
2 3 2
0
0
```
### 範例一:正確輸出
```
5
4
```
<img height="50%" width="50%" src="https://imgur.com/ulVao06.png" style="display: block; margin-left: auto; margin-right: auto;"/>
<div style="text-align:center">範例一</div>
<br />
### 範例二:輸入
```
9
1 6
3 5 3 8
0
2 1 7
1 9
0
1 2
0
0
```
### 範例二:正確輸出
```
4
11
```
<img height="40%" width="40%" src="https://imgur.com/FtdKTfk.png" style="display: block; margin-left: auto; margin-right: auto;"/>
<div style="text-align:center">範例二</div>
<br />
### 評分說明
輸入包含若干筆測試資料,每一筆測試資料的執行時間限制(time limit)均為 1 秒,依正確通過測資筆數給分。測資範圍如下,其中 k 是每個節點的子節點數量上限:
- 第 1 子題組 10 分,$1 \leq n \leq 4$, $k \leq 3$, 除了根節點之外都是葉節點。
- 第 2 子題組 30 分,$1 \leq n \leq 1,000$, $k \leq 3$。
- 第 3 子題組 30 分,$1 \leq n \leq 100,000$, $k \leq 3$。
- 第 4 子題組 30 分,$1 \leq n \leq 100,000$, $k$ 無限制。
<br />
### 提示
輸入的資料是給每個節點的子節點有哪些或沒有子節點,因此,可以根據定義找出根節點。關於節點高度的計算,我們根據定義可以找出以下遞迴關係式:
1. 葉節點的高度為 0
2. 如果 $v$ 不是葉節點,則 $v$ 的高度是它所有子節點的最大高度加一。也就是說,假設 $v$ 的子節點有 a, b 與 c,則 $h(v) = max\{ h(a), h(b), h(c) \}+1$。以遞迴方式可以計算出所有節點的高度。
<br />
## Python 程式碼
```python=
n = int(input()) # 由標準輸入讀取節點數量 n
m = n+1 # 節點編號從1開始,但串列索引值從0開始,以下3個串列長度加1
parents = [0]*m # 每個節點的父節點資料,一維串列,如果為0就是根節點
depth = [-1]*m # 每個節點的深度,一維串列
num = [0]*m # 每個節點的子節點數量,一維串列
leafs = [] # 找深度用的葉節點,一維串列
for i in range(1, n+1):
data = list(map(int, input().split())) # 由標準輸入讀取資料並暫時存成串列
k = data[0] # 索引值為0的資料存入子節點數量 k
children = data[1:] # 之後的資料存入一維串列 children
if k == 0: # 如果子節點數量為 0
leafs.append(i) # 此節點為葉節點,存入一維串列 leafs
depth[i] = 0 # 此節點深度為 0
else: # 如果子節點數量不為 0
num[i] = k # 存入一維串列 num
for j in children:
parents[j] = i # 節點 j 的父節點為 i
while leafs: # 若串列中還有元素繼續執行,最後會多跑一次根節點的深度,存到 deptho[0]
#print(leafs) # 除錯用,印出葉節點
leaf = leafs.pop(0) # 取出 leafs 中最前面的元素
# 比較 leaf 的父節點深度與 leaf 深度加1,取較大者更新 leaf 的父節點深度
depth[parents[leaf]] = max(depth[parents[leaf]], depth[leaf]+1)
#print(depth) # 除錯用,印出節點深度
num[parents[leaf]] -= 1 # leaf 的父節點對應的子節點數量減1
if num[parents[leaf]] == 0: # 若已無子節點,將自己加入 leafs,作為之後要檢測深度的節點
leafs.append(parents[leaf])
print(leaf) # 印出最後一個由 leafs 取出的元素,根節點
print(sum(depth[1:])) # 印出所有節點深度,記得要排除 depth[0]
```
<br />
1. 這個寫法主要是由參考資料1改寫成 Python 版本,每行程式碼的用途已寫在註解之中。
2. 第20 ~ 28行:整個程式最主要的部分,如果輸入資料為範例2,以下是各次迴圈運作時的狀況
<div style="text-align:center">
| 迴圈運作次數 | 原來的 leafs 內容 | 效果 |
| ---------- | -------- | ---- |
| 1 | 3, 6, 8, 9 | 更新節點2的深度為1 |
| 2 | 6, 8, 9 | 更新節點1的深度為1,將節點1加入 leafs |
| 3 | 8, 9, 1 | 節點2的深仍然為1 |
| 4 | 9, 1 | 更新節點5的深度為1,將節點5加入 leafs |
| 5 | 1, 5 | 更新節點4的深度為2 |
| 6 | 5 | 更新節點2的深度為2,將節點2加入 leafs |
| 7 | 2 | 更新節點7的深度為3,將節點7加入 leafs |
| 8 | 7 | 更新節點4的深度為4,將節點4加入 leafs |
| 9 | 4 | 更新節點0的深度為5,**這是多跑的** |
</div>
<br />
3. ZeroJudge 測試結果,花費時間最久為 2.5 s,使用記憶體最多為 27.2 MB,通過測試。
<br /><br />
## C++ 程式碼
```cpp=
#include <iostream>
#include <deque>
#include <cstring>
using namespace std;
int main(int argc, char* argv[]) {
int n; cin >> n; // 由標準輸入讀取節點數量 n
int m = n+1; // 節點編號從1開始,但陣列索引值從0開始,以下3個陣列長度加1
int parents[m], depth[m], num[m], leaf; // leaf 為取出的節點
memset(parents, 0, sizeof(parents)); // 每個節點的父節點資料,一維陣列,如果為0就是根節點
memset(depth, -1, sizeof(depth)); // 每個節點的深度,一維陣列
memset(num, 0, sizeof(num)); // 每個節點的子節點數量,一維陣列
deque<int> leafs; // 找深度用的葉節點,雙端佇列
for(int i=1; i<m; i++) { // 由標準輸入讀取資料,共執行 n 次
int k; cin >> k; // 每行資料開頭存入子節點數量 k
if (k == 0) { // 如果子節點數量為 0
leafs.push_back(i); // 此節點為葉節點,存入 leafs
depth[i] = 0; // 此節點深度為 0
} else { // 如果子節點數量不為 0
for(int j=0; j<k; j++) { // 讀取該行接下來的資料共 k 次
int c; cin >> c; // 讀取節點 i 的子節點資料,存入 c
parents[c] = i; // 節點 c 的父節點為節點 i
}
num[i] = k; // 節點 i 的子節點數量存入 num
}
}
while(!leafs.empty()) { // 若雙端佇列中還有元素繼續執行,最後會多跑一次根節點的深度,存到 deptho[0]
// 除錯用,印出葉節點
//for(deque<int>::iterator it=leafs.begin(); it != leafs.end(); it++) {
// cout << *it << " ";
//}
//cout << endl;
leaf = leafs.front(); // 取出 leafs 中最前面的元素
leafs.pop_front(); // 移除 leafs 中最前面的元素
// 比較 leaf 的父節點深度與 leaf 深度加1,取較大者更新 leaf 的父節點深度
depth[parents[leaf]] = max(depth[parents[leaf]], depth[leaf]+1);
// 除錯用,印出節點深度
//for(int i=0; i<m; i++) {
// cout << depth[i] << " ";
//}
//cout << endl;
num[parents[leaf]]--; // leaf 的父節點對應的子節點數量減1
if (num[parents[leaf]] == 0) { // 若已無子節點,將自己加入 leafs,作為之後要檢測深度的節點
leafs.push_back(parents[leaf]);
}
}
// 計算所有節點深度總合,記得要排除 depth[0]
long long sum = 0;
for(int i=1; i<m; i++) {
sum += depth[i];
}
cout << leaf << endl; // 印出最後一個由 leafs 取出的元素,根節點
cout << sum << endl; // 印出所有節點深度總合
return 0;
}
```
<br />
程式的運作邏輯與前面的 Python 程式碼相同,ZeroJudge 測試結果,花費時間最久為 0.1 s,使用記憶體最多為 1.9 MB,通過測試。
<br /><br />
## 2024年11月23日更新,吳邦一教授的寫法
### Python 程式碼
花費時間最久為 0.6 s,使用記憶體最多為 18 MB,通過測試。
```python=
n = int(input()) # 節點數量
parent = [-1] + [0]*n # 1 ~ n 號節點的父節點
h = [0]*(n+1) # 1 ~ n 號節點的高度
deg = [0]*(n+1) # 1 ~ n 號節點的子節點數量
for i in range(1, n+1): # 讀取 n 行資料
child = list(map(int, input().split())) # 子節點數量及編號
deg[i] = child[0] # 子節點數量
for ch in child[1:]: # 更新對應的父節點
parent[ch] = i
que = [i for i in range(1, n+1) if deg[i] == 0] # 葉節點
head = 0 # 從 que 讀取資料的索引值
ans = 0 # 答案
while head < len(que):
v = que[head]; head += 1
ans += h[v] # 更新 ans
p = parent[v] # v 對應的父節點 p
if p == 0: break # 根節點,中止迴圈
h[p] = max(h[p], h[v]+1) # 更新 h[p],取 h[p] 及 h[v]+1 較大者
deg[p] -= 1 # p 的子節點數量減 1
if deg[p] == 0: que.append(p) # 如果 p 沒有子節點了,加入 que
print(que[-1])
print(ans)
```
### C++ 程式碼
花費時間最久為 39 ms,使用記憶體最多為 3.4 MB,通過測試。
```cpp=
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
typedef long long LL;
using namespace std;
int main() {
ios::sync_with_stdio(0); cin.tie(0);
LL n; cin >> n;
vector<LL> parent (n+1, 0), h (n+1, 0), deg (n+1, 0);
parent[0] = -1;
for(LL i=1; i<=n; i++) {
LL k; cin >> k;
deg[i] = k;
for(LL j=0; j<k; j++) {
LL ch; cin >> ch;
parent[ch] = i;
}
}
queue<LL> que;
for(LL i=1; i<=n; i++) {
if (deg[i] == 0) que.push(i);
}
LL ans = 0, v = 0;
while(!que.empty()) {
v = que.front();
que.pop();
ans += h[v];
LL p = parent[v];
if (p == 0) break;
h[p] = max(h[p], h[v]+1);
deg[p]--;
if (deg[p] == 0) que.push(p);
}
cout << v << "\n";
cout << ans << "\n";
return 0;
}
```
<br /><br />
## 參考資料
1. 黃建庭(2019)。**C++程式設計解題入門 融入程式設計競賽與APCS實作題(第二版)**。臺北市:碁峰資訊。
2. 吳燦銘(2020)。**APCS大學程式設計先修檢測:C++超效解題致勝祕笈**。新北市:博碩文化。
3. 吳燦銘(2019)。**APCS大學程式設計先修檢測:Python超效解題致勝祕笈**。新北市:博碩文化。
4. 吳邦一教授的 **[AP325-Python 第3章 佇列與堆疊](https://hackmd.io/L9sVPW65R8yu0voahymg7g)**
---
###### tags:`APCS`、`C++`、`Python`