# APCS 202306 第4題 開啟寶盒 (ZeroJudge k734) [ZJ題目連結](https://zerojudge.tw/ShowProblem?problemid=k734) (此題解先說明方法再揭示範例程式,包含python與C++與C) ###### tags: `apcs` ## 方法說明 有$n$個寶盒編號與$m$種鑰匙。一開始你有某些鑰匙。每個寶盒需要$k$支鑰匙才能開啟,每開啟一個寶盒可以獲得$k$支鑰匙。這題要計算一共能開啟多少個寶盒。 基本上,這題執行以下的步驟: ``` 紀錄擁有的鑰匙 重複以下步驟,直到沒有新的鑰匙: 檢查擁有的鑰匙可以開啟那些寶盒,若有,獲得該寶盒內的鑰匙 ``` 但是如果每次都重複檢查所有的寶盒,那麼時間會花費太多。如果我們把已經開啟的寶盒丟棄,下次不再檢查,那會稍微改善,但還是不夠好。最壞的狀況下,每次開啟一個新的寶盒,那麼要檢查的寶盒次數為$n+(n-1)+(n-2)+\ldots+1 = O(n^2)$。 **快速檢查寶盒** * 給予每一個寶盒一個倒數計數器,紀錄該寶盒還需要多少支鑰匙才能開啟。 * 對每一支鑰匙,記錄它可以用在哪些寶盒的開啟上。 * 每當獲得一支新鑰匙時,將它可以開啟的寶盒計數錄器減一,若此時計數器歸0,則得到一個新開啟的寶盒。 如此一來,對於每支新獲得的鑰匙,我們只要花該鑰匙所開啟寶盒數的時間就可以完成檢查,因為每個鑰匙可開啟的寶盒總數等於所有寶盒需求鑰匙的總數,也是就$kn$,所以檢查盒子開啟的總時間就是$O(kn)$。因為鑰匙會有重複的,這裡我們還需要避免重複檢查相同的鑰匙,我們可以用一個表格紀錄那些鑰匙已經處理過了。 最後,每次開啟一個寶盒時,我們獲得$k$把鑰匙,對於那些新獲得的鑰匙,將其納入擁有的鑰匙,所以總共的時間複雜度是$O(kn+m)$。 這個計算過程非常類似於在一個Dag中找topological sort的流程,若不熟悉的可以參考該演算法。 ## 完全解 ### Python code 我們用一個串列mykey保存已經擁有但是尚未處理的鑰匙。keybox是list of list,keybox[key]是key所能開啟的寶盒編號的串列,第4\~6行讀入該資料並儲存。boxkey則是開啟寶盒後所能獲得的鑰匙,第8\~9行讀入該資料。visit是每一把鑰匙是否已經獲得的資料,deg則是每一個寶盒還缺幾把鑰匙可以開啟的計數器。 第14行開始的while迴圈會執行到mykey為空為止,每一次進入迴圈會取出一把鑰匙,因為鑰匙的先後順序無關,所以這裡我們以stack的方式來處理,每次從後方pop出來較為方便。 ```python= n,m,k = [int(x) for x in input().split()] mykey = [int(x) for x in input().split()][1:] keybox = [[] for i in range(m)] # list of boxes requiring key for box in range(n): #box i for key in [int(x) for x in input().split()]: keybox[key].append(box) boxkey = [] # keys in box i for box in range(n): boxkey.append([int(x) for x in input().split()]) visit = [False]*m # if already have key deg = [k]*n # num of locked keys to open box for i in mykey: visit[i]=True mybox = 0 #opened box while mykey: # until no more key ikey = mykey.pop() # first or last does not matter for b in keybox[ikey]: #unlock deg[b] -= 1 if deg[b]!=0: continue #not opened yet mybox += 1 # box opened for newkey in boxkey[b]: #obtain new keys if not visit[newkey]: visit[newkey]=True mykey.append(newkey) #endwhile print(mybox) ``` ### C++ code 接下來是C++的範例程式,主要使用到vector以及vector of vector來儲存資料。我們用一個vector<int> mykey保存已經擁有但是尚未處理的鑰匙。keybox是vector of vector,keybox[key]是key所能開啟的寶盒編號,seen則是記錄每一把鑰匙是否已經獲得,用來避免重複處理相同的鑰匙。boxkey則是開啟寶盒後所能獲得的鑰匙。deg則是每一個寶盒還缺幾把鑰匙可以開啟的計數器,一開始全部都設為k。 第28行開始的while迴圈會執行到mykey為空為止,每一次進入迴圈會取出一把鑰匙,因為鑰匙的先後順序無關,所以這裡我們以stack的方式來處理,每次從後方pop出來較為方便。 ```cpp= #include <bits/stdc++.h> using namespace std; int main() { int n, i,j,m,t,k; scanf("%d%d%d",&n,&m,&k); scanf("%d",&t); vector<int> mykey(t),deg(n,k); vector<bool> seen(m,false); for (i=0;i<t;i++) { scanf("%d",&mykey[i]); seen[mykey[i]] = true; } vector<vector<int>> keybox(m,vector<int>()); // key used in box for (i=0; i<n; i++) { for (j=0;j<k;j++) { scanf("%d",&t); keybox[t].push_back(i); } } vector<vector<int>> boxkey(n,vector<int>()); // key in box for (i=0; i<n; i++) { for (j=0;j<k;j++) { scanf("%d",&t); boxkey[i].push_back(t); } } int mybox=0; // number of open box while (!mykey.empty()) { int key=mykey.back(); mykey.pop_back(); for (int box: keybox[key]) { if (--deg[box] == 0) { // opened mybox++; for (int p:boxkey[box]) { if (!seen[p]) { mykey.push_back(p); seen[p] = true; } } } } } printf("%d\n",mybox); return 0; } ``` ### C code 因為也許有些讀者對C++的vector比較不熟悉,以下則是C的版本,這裡只會使用一維與二維陣列。為了偷懶以及避免記憶體不夠,我們把主要變數都放在全域,第4~10行是我們需要的資料結構。 C使用陣列存放不定個數資料時,我們可以用一個變數表示目前陣列內的資料數。例如第7行的mykey[],它將用來存放已經擁有但是尚未處理的鑰匙,而n_key則是目前陣列內資料的個數,將來存放時做法類似於stack。這支程式的註解已經相當清楚,搭配前面說明的方法,應該是容易了解的。 ```clike= //C version #include <stdio.h> #define N 100001 int seen[N]={0}; // if key is already found int keybox[N][60]; // key i is used to open which boxes, [0] is number of box int nkbox[N]={0}; // length of keybox[i] int mykey[N],n_key=0; // int deg[N]; // num of needed key for box i int boxkey[N][5]; // the keys in box i int mybox=0; // number of opened box int main() { int n, i,j,m,t,k; scanf("%d%d%d",&n,&m,&k); scanf("%d",&n_key); for (i=0;i<n_key;i++) { // initial list of key scanf("%d",&mykey[i]); seen[mykey[i]] = 1; } for (i=0; i<n; i++) { for (j=0;j<k;j++) { scanf("%d",&t); keybox[t][nkbox[t]++] = i; // insert } } for (i=0; i<n; i++) { for (j=0;j<k;j++) { scanf("%d",&t); boxkey[i][j] = t; } } for (i=0;i<n;i++) deg[i]=k; while (n_key) { // check one of my key int ikey=mykey[--n_key]; // pop the last key for (int i=0; i< nkbox[ikey]; i++) { int b=keybox[ikey][i]; // a box need this key if (--deg[b] == 0) { // if just can be opened mybox++; for (int j=0;j<k;j++) { // obtain key if (seen[boxkey[b][j]]==1) continue; seen[boxkey[b][j]] = 1; mykey[n_key++] = boxkey[b][j]; // push into stack } } } } printf("%d\n",mybox); return 0; } ```