# APCS實作題2020年7月第4題:病毒演化
> 日期:2024年8月29日
> 作者:王一哲
> [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=f582)
## 題目
### 問題描述
科學家發現了 $n$ 種病毒,編號分別是 $1$ 到 $n$,已知每一種病毒可以用一個RNA 序列來表達,RNA序列是一個長度為 $m$ 的字串,其中包含 A、U、C、G、@ 等字元,其中 @ 為科學家沒觀察清楚的位置,可能為 A、U、C、G 其中任何一種。
科學家也研究出了這些病毒的演化關係,除了一個最原始的病毒以外,每一種病毒都是從另一個病毒演化而來的,這些病毒會構成一個樹狀結構的病毒族譜(如圖)。
<br />
<img height="80%" width="80%" src="https://zerojudge.tw/ShowImage?id=1671" style="display: block; margin-left: auto; margin-right: auto;"/>
<br /><br />
兩個 RNA 序列的的距離定義為它們的漢明距離,也就是相異的位數個數。更具體的說,對於兩個長度都是 $m$ 的 RNA 序列 $a, b$,它們的漢明距離就是有幾個位置 $i$ 滿足 $a_i \neq b_i$。你想知道目前的病毒族譜的每一個 RNA 序列中的 @ 字元的填入 A、U、C、G 中的其中一個字元後,每一個病毒與它演化來源的病毒的距離總合最小值是多少?
### 輸入說明
第一行包含兩個正整數 $n, m~ (1 \leq n \leq 1000,~ 1 \leq m \leq 80)$。
接下來 $n$ 行,每行表示一種病毒,包含兩個整數 $i, j$ 與一個字串 $s$,表示編號 $i$ 的病毒是從編號 $j$ 的病毒演化而來的,且編號 $i$ 的病毒的 RNA 序列為 $s$。若編號 $i$ 的病毒是最原始的病毒,則 $j = i$,保證這樣的病毒只會有一個。
配分
- 20分:RNA 序列中沒有 @,每個病毒都滿足 $j < i$。
- 40分:每個病毒都滿足 $j < i$。
- 40分:同原題目限制。
<br />
### 輸出說明
輸出每種病毒到它演化來源的病毒的距離總和最小值。
<br />
### 範例輸入1
```
2 3
1 1 AAC
2 1 A@@
```
### 範例輸出1
```
0
```
<br />
### 範例輸入2
```
6 1
1 1 @
2 1 @
3 1 C
4 1 C
5 2 A
6 2 A
```
### 範例輸出2
```
1
```
<br />
## Python 程式碼
費時最久約為 0.3 s,使用記憶體最多約為 4.6 MB,通過測試。
```python=
"""
cost[v][i] 這個節點 v 的父節點是字元 i 時,v 節點以下子樹的總變異量
狀況1,rna[v] 是 AUCG,v 節點以下子樹的總變異量可確定
(1) 先加上 v 之下所有子樹的總變異量,可以直接結算,加到儲存答案的全域變數 total
(2) 再加上 v 與 par[v] 的變異量,如果兩者相等為 0,否則為 1
狀況2,rna[v] 是 @
(1) 先計算 v 之下所有子樹對應到 AUCG 的總變異量,取其中的最小值為 mcost
(2) 如果 par[v] 對應的字元可以使變異量為 mcost,則 cost[v][i] = mcost
(3) 如果 par[v] 對應的字元無法使變異量為 mcost
(a) 如果要達成 mcost @ 對應的字元不等於 par[v],變異量為 mcost+1
(b) 如果 @ 對應的字元等於 par[v],變異量至少為 mcost+1
要達到最小變異量,取 (a)。
"""
n, m = map(int, input().split()) # 節點(病毒)數量 n,RNA 序列長度 m
deg = [0]*(n+1) # 各節點對應的子節點數量,因為編號為 1 ~ n,故長度取 n+1
par = [0]*(n+1) # 各節點對應的父節點編號,因為編號為 1 ~ n,故長度取 n+1
rna = [[0]*m for _ in range(n+1)] # 各節點對應的字串(RNA 序列)
cov = {'A': 0, 'U': 1, 'C': 2, 'G': 3, '@': 4} # 轉換字元、整數的對照表
for i in range(1, n+1): # 讀取 n 行資料
a, b, s = input().split() # 子節點 a,父節點 b,字串 s
a = int(a); b = int(b) # 轉成整數
if a != b: # 不是根節點(最原始的病毒)
par[a] = b # 節點 a 的父節點是 b
deg[b] += 1 # 節點 b 的子節點數量加 1
rna[a] = [cov[c] for c in s] # 節點 a 對應的字串為 s
""" 找出 bottom-up 序列 """
seq = [i for i in range(1, n+1) if deg[i] == 0] # 放入葉節點
head = 0 # 從 seq 讀取資料的索引值
while len(seq) < n: # 結束時 seq 長度為 n
v = seq[head]; head += 1 # 讀取節點 v,head 加 1
p = par[v] # 讀取節點 v 的父節點 p
if p == 0: break # 遇到根節點 break
deg[p] -= 1 # p 的子節點數減 1
if deg[p] == 0: seq.append(p) # 如果 p 已經沒有子節點,將 p 加入 seq
# 結束 while 迴圈
# 判斷是否為不合規則的狀況,但是在 ZeroJudge 上不會遇到
if len(seq) != n or par[seq[-1]] != 0: # seq 數量不等於 n 或最後一項不是根節點
print("wrong case"); print(seq)
""" 找完序列 """
""" 主要的解題過程 """
total = 0 # 答案,總變異量
for i in range(m): # RNA 序列長度為 m,要跑 m 次
cost = [[0]*4 for _ in range(n+1)] # cost[v][i] 這個節點 v 的父節點是字元 i 時,v 節點以下子樹的總變異量
for v in seq: # 依序由 seq 讀取要測試的節點 v
ch = rna[v][i] # 節點 v 的第 i 個字元
if ch < 4: # 如果是 AUCG
total += cost[v][ch] # 結算 cost[v][ch]
for j in range(4): # 處理 v 與 par[v] 的變異量
if j != ch: cost[par[v]][j] += 1 # 不相等,加 1
else: # 如果是 @
mcost = min(cost[v]) # 取出 v 的最小變異量
total += mcost # 結算 mcost
for j in range(4): # 處理 v 與 par[v] 的變異量
if cost[v][j] != mcost: # 如果要達成 mcost @ 對應的字元不等於 par[v],加 1
cost[par[v]][j] += 1
""" 結束主要的解題過程 """
print(total) # 印出答案
```
<br /><br />
## C++ 程式碼
費時最久約為 6 ms,使用記憶體最多約為 688 kB,通過測試。
```cpp=
#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
ios::sync_with_stdio(0); cin.tie(0); // 加速
int n, m; cin >> n >> m; // 節點(病毒)數量 n,RNA 序列長度 m
int a, b; string s; // 子節點 a,父節點 b,字串 s
int par[n+1] = {0}, deg[n+1] = {0}; // par 各節點對應的父節點編號,deg 各節點對應的子節點數量,因為編號為 1 ~ n,故長度取 n+1
int rna[n+1][m] = {{0}}; // 各節點對應的字串(RNA 序列)
map<char, int> cov = {{'A', 0}, {'U', 1}, {'C', 2}, {'G', 3}, {'@', 4}}; // 轉換字元、整數的對照表
for(int i=1; i<=n; i++) { // 讀取 n 行資料
cin >> a >> b >> s; // 子節點 a,父節點 b,字串 s
if (a != b) { // 不是根節點(最原始的病毒)
par[a] = b; // 節點 a 的父節點是 b
deg[b]++; // 節點 b 的子節點數量加 1
}
for(int j=0; j<m; j++) rna[a][j] = cov[s[j]]; // 節點 a 對應的字串為 s
}
/* 找出 bottom-up 序列 */
vector<int> seq; // 序列
for(int i=1; i<=n; i++) if (deg[i] == 0) seq.push_back(i); // 放入葉節點
int head = 0; // 從 seq 讀取資料的索引值
while((int)seq.size() < n) { // 結束時 seq 長度為 n
int v = seq[head]; head++; // 讀取節點 v,head 加 1
int p = par[v]; // 讀取節點 v 的父節點 p
if (p == 0) break; // 遇到根節點 break
deg[p]--; // p 的子節點數減 1
if (deg[p] == 0) seq.push_back(p); // 如果 p 已經沒有子節點,將 p 加入 seq
}
/* 主要的解題過程 */
int total = 0; // 答案,總變異量
for(int i=0; i<m; i++) { // RNA 序列長度為 m,要跑 m 次
int cost[n+1][4] = {{0}}; // cost[v][i] 這個節點 v 的父節點是字元 i 時,v 節點以下子樹的總變異量
for(int v : seq) { // 依序由 seq 讀取要測試的節點 v
int ch = rna[v][i]; // 節點 v 的第 i 個字元
if (ch < 4) { // 如果是 AUCG
total += cost[v][ch]; // 結算 cost[v][ch]
for(int j=0; j<4; j++) // 處理 v 與 par[v] 的變異量
if (j != ch) cost[par[v]][j]++; // 不相等,加 1
} else { // 如果是 @
int mcost = *min_element(cost[v], cost[v]+4); // 取出 v 的最小變異量
total += mcost; // 結算 mcost
for(int j=0; j<4; j++) { // 處理 v 與 par[v] 的變異量
if (cost[v][j] != mcost) // 如果要達成 mcost @ 對應的字元不等於 par[v],加 1
cost[par[v]][j]++;
}
}
}
}
cout << total << "\n"; // 印出答案
return 0;
}
```
<br /><br />
## 參考資料
1. 吳邦一教授的 APCS 解題影片〈[PyAp45 Python APCS 題解:病毒演化(AP202007_4), ZeroJudge f582](https://youtu.be/9d0eD7fODHg?si=vpRa4u8un_G39l_W)〉
2. 吳邦一教授的 APCS 解題講義〈[AP325-Python 第8章 樹上演算法 (III)](https://hackmd.io/@bangyewu/Hy2kbYLI6/%2FE-EfMMi1Tw6DeoIY-WJzeQ#Q-8-16-%E7%97%85%E6%AF%92%E6%BC%94%E5%8C%96-APCS202007)〉
---
###### tags:`APCS`、`Python`、`C++`