# APCS實作題2023年6月第4題:開啟寶盒 > 日期:2024年8月24日 > 作者:王一哲 > [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=k734) ## 題目 ### 問題描述 已知有 $n$ 個寶盒編號 $0$ 到 $n-1$ 以及 $m$ 種鑰匙編號 $0$ 到 $m-1$。一開始你有 $t$ 種鑰匙分別為 $x_1, x_2, \dots, x_t$。 每一個寶盒要打開都需要同時擁有 $k$ 種鑰匙,第 $i$ 個寶盒分別需要 $r_{i1}, r_{i2}, \dots, r_{ik}$ 種類的鑰匙。每個寶盒打開後都會得到 $k$ 種鑰匙,第 $i$ 個寶盒打開後分別會得到 $s_{1i}, s_{2i}, \dots, s_{ki}$ 種類的鑰匙,當拿到新的鑰匙之後可以繼續開啟新的寶盒。保證寶盒內的鑰匙不會重複,並且每種鑰匙可以開啟的寶盒數量不超過 $60$。 請輸出最多可以開啟多少個寶盒。 <br /> ### 輸入說明 第一行輸入三個正整數 $n, m, k ~(1 \leq n, m \leq 10^5,~ 1 \leq k \leq 5)$ 代表有 $n$ 個寶盒,$m$ 種鑰匙以及每個寶盒需要的鑰匙數量 $k$。 接下來輸入一行,第一個數字 $t$ 代表一開始有的鑰匙種類數量,後面有 $t$ 個數字代表這些鑰匙分別的種類。 接下來有 $n$ 行,每一行有 $k$ 個數字,代表要開啟第 $i$ 個寶盒的第 $j$ 種鑰匙為 $r_{ij}$。 最後有 $n$ 行,每一行有 $k$ 個數字,代表開啟第 $i$ 個寶盒後得到的第 $j$ 種鑰匙為 $s_{ij}$。 配分 - 子問題一 20%:$k = 1,~ 1 \leq n, m \leq 100$ - 子問題二 20%:$k = 1,~ 1 \leq n, m \leq 10^5$ - 子問題三 60%:$1 \leq k \leq 5,~ 1 \leq n, m \leq 10^5$ <br /> ### 輸出說明 輸出一個正整數,代表可以開啟的寶盒數量。 <br /> ### 範例輸入1 ``` 5 5 1 2 0 1 0 2 4 3 1 1 2 4 3 3 ``` ### 範例輸出1 ``` 3 ``` <br /> ### 範例輸入2 ``` 10 8 2 3 6 5 2 5 6 2 7 2 0 4 5 5 1 3 0 4 2 2 4 5 3 5 6 5 0 0 6 1 7 3 2 2 1 7 3 4 7 4 5 4 1 7 5 ``` ### 範例輸出2 ``` 5 ``` <br /> ## Python 程式碼 ### 寫法1,用自訂函式及遞迴找出所有可以開啟的寶盒及拿到的鑰匙 以範例輸入1為例,流程為 0. 原有 0、1 號鑰匙 1. 用 0 號鑰匙開啟 0 號寶盒,拿到 1 號鑰匙,目前有 0、1 號鑰匙 2. 用 1 號鑰匙開啟 4 號寶盒,拿到 3 號鑰匙,目前有 0、1、3 號鑰匙 3. 用 3 號鑰匙開啟 3 號寶盒,拿到 3 號鑰匙,目前有 0、1、3 號鑰匙 4. 沒有更多的鑰匙了,總共開啟 3 個寶盒 在第6筆測資開始,會因為遞迴深度過深而被中止;第7筆測資超時。需要再修改成不使用遞迴的版本才行。 ```python= n, m, k = map(int, input().split()) # 讀取寶盒數量 n、鑰匙種類 m、需要的鑰匙數量 k tmp = list(map(int, input().split())) # 讀取起始狀態 t = tmp[0]; now = tmp[1:] # 一開始有 t 把鑰匙,鑰匙編號存入 now needs = [k]*n # 每個寶盒需要的鑰匙數量,預設為 k keys = [[] for _ in range(m)] # 每種鑰匙可以開啟的寶盒編號 for i in range(n): # 讀取 n 行 data = list(map(int, input().split())) # 編號 i 的寶盒需要的鑰匙編號 for d in data: # 依序讀取鑰匙編號 d keys[d].append(i) # 於 keys[d] 新增對的寶盒編號 avails = [[] for _ in range(n)] # 開啟寶盒可以拿到的鑰匙編號 for i in range(n): avails[i] = list(map(int, input().split())) """ 自訂函式,輸入鑰匙編號,找出可以開啟的寶盒,更新已有的鑰匙編號 """ def getkeys(idx): cur = keys[idx] # 編號 idx 鑰匙可以開啟的寶盒編號 for c in cur: # 依序讀取可以開啟的寶盒編號 c needs[c] -= 1 # 將 c 需要的鑰匙數量減 1 if needs[c] == 0: # 如果已歸零,開啟寶盒,得到新的鑰匙 for avail in avails[c]: # 依取讀取新的鑰匙編號 if avail not in now: # 如果 avail 還不在 now 之中 now.append(avail) # 更新已有的鑰匙編號 getkeys(avail) # 遞迴,檢查是否能開啟新的寶盒 """ 結束自訂函式 """ for i in range(t): # 執行 t 次,更新開啟每個寶盒還需要的鑰匙數量 getkeys(now[i]) ans = 0 # 答案預設為 0 for i in range(n): # 依序檢查每個寶盒需要的鑰匙數量,如果已經歸零則 ans 加 1 ans += 1 if needs[i] <= 0 else 0 print(ans) # 印出答案 ``` <br /><br /> ### 寫法2,不使用遞迴 第6筆測資超時。 ```python= n, m, k = map(int, input().split()) # 讀取寶盒數量 n、鑰匙種類 m、需要的鑰匙數量 k st = list(map(int, input().split()))[1:] # 待走訪的鑰匙編號 needs = [k]*n # 每個寶盒需要的鑰匙編號,預設為 k keys = [[] for _ in range(m)] # 每種鑰匙可以開啟的寶盒編號 for i in range(n): # 讀取 n 行 data = list(map(int, input().split())) # 編號 i 的寶盒需要的鑰匙編號 for d in data: # 依序讀取鑰匙編號 d keys[d].append(i) # 於 keys[d] 新增對的寶盒編號 avails = [[] for _ in range(n)] # 開啟寶盒可以拿到的鑰匙編號 for i in range(n): avails[i] = list(map(int, input().split())) """ 主要的解題過程 """ head = 0 # 從 st 讀取資料的索引值 while head < len(st): # 當 head 還沒有出界時繼續執行 s = st[head]; head += 1 # 取出 st[head],head 加 1 cur = keys[s] # 找到編號 s 的鑰匙可以開啟的寶盒編號 for c in cur: # 依序讀取可以開啟的寶盒編號 c needs[c] -= 1 # 將 c 需要的鑰匙數量減 1 if needs[c] == 0: # 如果已歸零,開啟寶盒,得到新的鑰匙 for avail in avails[c]: # 依取讀取新的鑰匙編號 if avail not in st: # 如果 avail 還沒有走訪而且不在 st 之中 st.append(avail) # 將 avail 加入 st """ 結束 while 迴圈 """ ans = 0 # 答案預設為 0 for i in range(n): # 依序檢查每個寶盒需要的鑰匙數量,如果已經歸零則 ans 加 1 ans += 1 if needs[i] <= 0 else 0 print(ans) # 印出答案 ``` <br /><br /> ## C++ 程式碼 ### 寫法1,用自訂函式及遞迴找出所有可以開啟的寶盒及拿到的鑰匙 程式碼運作邏輯與 Python 版本寫法1相同,費時最久約為 0.2 s,使用記憶體最多約為 22.1 MB,通過測試。為了使自訂函式不需要引入較多的參數,在程式碼第5 ~ 9行定義了一些全域變數,但是在這裡只能先定義變數名稱及種類,在 main 之中才能設定變數值。 ```cpp= #include <iostream> #include <vector> using namespace std; int n, m, k, t; vector<bool> now (100001); // 目前有的鑰匙編號,要在 main 裡面設定值 vector<int> needs (100001); // 每個寶盒需要的鑰匙數量,要在 main 裡面設定值 vector<vector<int>> keys (100001, vector<int> ()); // 每種鑰匙可以開啟的寶盒編號 vector<vector<int>> avails (100001, vector<int> ()); // 開啟寶盒可以拿到的鑰匙編號 /* 自訂函式,輸入鑰匙編號,找出可以開啟的寶盒,更新已有的鑰匙編號 */ void getkeys(int idx) { vector<int> cur = keys[idx]; // 編號 idx 鑰匙可以開啟的寶盒編號 for(auto c : cur) { // 依序讀取可以開啟的寶盒編號 c needs[c]--; // 將 c 需要的鑰匙數量減 1 if (needs[c] == 0) { // 如果已歸零,開啟寶盒,得到新的鑰匙 for(auto avail : avails[c]) { // 依取讀取新的鑰匙編號 if (!now[avail]) { // 如果 avail 還不在 now 之中 now[avail] = true; // 更新已有的鑰匙編號 getkeys(avail); // 遞迴,檢查是否能開啟新的寶盒 } } } } } int main() { ios::sync_with_stdio(0); cin.tie(0); // 加速 cin >> n >> m >> k; // 讀取寶盒數量 n、鑰匙種類 m、需要的鑰匙數量 k for(int i=0; i<n; i++) { // 設定 now 及 needs 的初始值 needs[i] = k; now[i] = false; } cin >> t; // 讀取起始狀態 vector<int> have (t, 0); // 一開始有的鑰匙 for(int i=0; i<t; i++) { // 一開始有 t 把鑰匙,鑰匙編號存入 have int tmp; cin >> tmp; now[tmp] = true; have[i] = tmp; } for(int i=0; i<n; i++) { // 讀取 n 行 for(int j=0; j<k; j++) { // 編號 i 的寶盒需要的鑰匙編號 int d; cin >> d; // 依序讀取鑰匙編號 d keys[d].push_back(i); // 於 keys[d] 新增對的寶盒編號 } } for(int i=0; i<n; i++) { // 讀取 n 行 for(int j=0; j<k; j++) { // 編號 i 的寶盒獲得的鑰匙編號 int a; cin >> a; // 依序讀取鑰匙編號 a avails[i].push_back(a); // 於 avails[i] 新增鑰匙編號 } } for(int i=0; i<t; i++) getkeys(have[i]); // 執行 t 次,更新開啟每個寶盒還需要的鑰匙數量 int ans = 0; // 答案預設為 0 for(int i=0; i<n; i++) ans += needs[i]<=0 ? 1 : 0; // 依序檢查每個寶盒需要的鑰匙數量,如果已經歸零則 ans 加 1 cout << ans << "\n"; // 印出答案 return 0; } ``` <br /><br /> ### 寫法2,改用 stack 及 set,不使用遞迴 費時最久約為 0.5 s,使用記憶體最多約為 19.1 MB,通過測試。應該是因為使用了 set 儲存已有的鑰匙,執行速度較慢。 ```cpp= #include <iostream> #include <vector> #include <set> #include <stack> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); // 加速 int n, m, k; cin >> n >> m >> k; // 讀取寶盒數量 n、鑰匙種類 m、需要的鑰匙數量 k int t; cin >> t; // 一開始有的鑰匙數量 set<int> now; // 目前已有的鑰匙編號 stack<int> st; // 待走訪的鑰匙編號 vector<bool> visited (m, false); // 走訪狀態 for(int i=0; i<t; i++) { // 執行 t 次 int tmp; cin >> tmp; // 讀取一開始有的鑰匙編號 now.insert(tmp); st.push(tmp); // 將編號加入 now 及 st } vector<int> needs (n, k); // 每個寶盒需要的鑰匙數量 vector<vector<int>> keys (m, vector<int> ()); // 每種鑰匙可以開啟的寶盒編號 for(int i=0; i<n; i++) { // 讀取 n 行 for(int j=0; j<k; j++) { // 編號 i 的寶盒需要的鑰匙編號 int d; cin >> d; // 依序讀取鑰匙編號 d keys[d].push_back(i); // 於 keys[d] 新增對的寶盒編號 } } vector<vector<int>> avails (n, vector<int> ()); // 開啟寶盒可以拿到的鑰匙編號 for(int i=0; i<n; i++) { // 讀取 n 行 for(int j=0; j<k; j++) { // 編號 i 的寶盒獲得的鑰匙編號 int a; cin >> a; // 依序讀取鑰匙編號 a avails[i].push_back(a); // 於 avails[i] 新增鑰匙編號 } } /* 主要的解題過程 */ while(!st.empty()) { // st 還有資料時繼續執行 int s = st.top(); st.pop(); // 取出並移除 st 最上面的資料 visited[s] = true; // 已走訪 vector<int> cur = keys[s]; // 編號 s 鑰匙可以開啟的寶盒編號 for(auto c : cur) { // 依序讀取可以開啟的寶盒編號 c needs[c]--; // 將 c 需要的鑰匙數量減 1 if (needs[c] == 0) { // 如果已歸零,開啟寶盒,得到新的鑰匙 for(auto avail : avails[c]) { // 依取讀取新的鑰匙編號 if (now.count(avail) == 0) { // 如果 avail 還不在 now 之中 now.insert(avail); // 更新已有的鑰匙編號 st.push(avail); // 將 avail 加入 st } } } } } int ans = 0; // 答案預設為 0 for(int i=0; i<n; i++) ans += needs[i]<=0 ? 1 : 0; // 依序檢查每個寶盒需要的鑰匙數量,如果已經歸零則 ans 加 1 cout << ans << "\n"; // 印出答案 return 0; } ``` <br /><br /> ### 寫法3,由寫法2修改而成,不使用 set,用另一個陣列記錄已有的鑰匙編號 費時最久約為 0.2 s,使用記憶體最多約為 14.9 MB,通過測試。 ```cpp= #include <iostream> #include <vector> #include <stack> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); // 加速 int n, m, k; cin >> n >> m >> k; // 讀取寶盒數量 n、鑰匙種類 m、需要的鑰匙數量 k int t; cin >> t; // 一開始有的鑰匙數量 stack<int> st; // 待走訪的鑰匙編號 vector<bool> visited (m, false), have (m, false); // 走訪狀態,已有的鑰匙編號 for(int i=0; i<t; i++) { // 執行 t 次 int tmp; cin >> tmp; // 讀取一開始有的鑰匙編號 have[tmp] = true; st.push(tmp); // 已有 tmp 號鑰匙,將 tmp 加入 st } vector<int> needs (n, k); // 每個寶盒需要的鑰匙數量 vector<vector<int>> keys (m, vector<int> ()); // 每種鑰匙可以開啟的寶盒編號 for(int i=0; i<n; i++) { // 讀取 n 行 for(int j=0; j<k; j++) { // 編號 i 的寶盒需要的鑰匙編號 int d; cin >> d; // 依序讀取鑰匙編號 d keys[d].push_back(i); // 於 keys[d] 新增對的寶盒編號 } } vector<vector<int>> avails (n, vector<int> ()); // 開啟寶盒可以拿到的鑰匙編號 for(int i=0; i<n; i++) { // 讀取 n 行 for(int j=0; j<k; j++) { // 編號 i 的寶盒獲得的鑰匙編號 int a; cin >> a; // 依序讀取鑰匙編號 a avails[i].push_back(a); // 於 avails[i] 新增鑰匙編號 } } /* 主要的解題過程 */ while(!st.empty()) { // st 還有資料時繼續執行 int s = st.top(); st.pop(); // 取出並移除 st 最上面的資料 visited[s] = true; // 已走訪 vector<int> cur = keys[s]; // 編號 s 鑰匙可以開啟的寶盒編號 for(auto c : cur) { // 依序讀取可以開啟的寶盒編號 c needs[c]--; // 將 c 需要的鑰匙數量減 1 if (needs[c] == 0) { // 如果已歸零,開啟寶盒,得到新的鑰匙 for(auto avail : avails[c]) { // 依取讀取新的鑰匙編號 if (!have[avail]) { // 如果還沒有 avail 號鑰匙 have[avail] = true; // 更新已有的鑰匙編號 st.push(avail); // 將 avail 加入 st } } } } } int ans = 0; // 答案預設為 0 for(int i=0; i<n; i++) ans += needs[i]<=0 ? 1 : 0; // 依序檢查每個寶盒需要的鑰匙數量,如果已經歸零則 ans 加 1 cout << ans << "\n"; // 印出答案 return 0; } ``` <br /><br /> --- ###### tags:`APCS`、`Python`、`C++`