# APCS實作題2016年10月第3題:定時K彈
> 第1版:2023年2月10日
> 第2版:2023年6月12日,加上 C++ 程式碼
> 作者:王一哲
> 題目來源:[105年10月29日實作題第3題](https://apcs.csie.ntnu.edu.tw/wp-content/uploads/2020/11/1051029APCSImplementation.pdf)
> [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=c296)
<br />
## 題目
### 問題描述
「定時 K 彈」是一個團康遊戲,N 個人圍成一個圈,由 1 號依序到 N 號,從 1 號開始依序傳遞一枚玩具炸彈,炸彈每次到第 M 個人就會爆炸,此人即淘汰,被淘汰的人要離開圓圈,然後炸彈再從該淘汰者的下一個開始傳遞。遊戲之所以稱 K 彈是因為這枚炸彈只會爆炸 K 次,在第 K 次爆炸後,遊戲即停止,而此時在第 K 個淘汰者的下一位遊戲者被稱為幸運者,通常就會被要求表演節目。例如 N=5 , M=2 ,如果 K=2 ,炸彈會爆炸兩次,被爆炸淘汰的順序依序是 2 與 4(參見下圖),這時 5 號就是幸運者。如果 K=3,剛才的遊戲會繼續,第三個淘汰的是 1 號,所以幸運者是 3 號。如果K=4,下一輪淘汰 5 號,所以 3 號是幸運者。此題輸入 N、M 與 K,請你計算出誰是幸運者。
<br />
<img height="80%" width="80%" src="https://i.imgur.com/Xp5Xk4q.png" style="display: block; margin-left: auto; margin-right: auto;"/>
<br />
### 輸入格式
輸入只有一行包含三個正整數,依序為 N、M 與 K,兩數中間有一個空格分開。其中 $1 \leq K < N$。
<br />
### 輸出格式
請輸出幸運者的號碼,結尾有換行符號。
<br />
### 範例一:輸入
```
5 2 4
```
### 範例一:正確輸出
```
3
```
(說明)被淘汰的順序是 2、4、1、5,此時 5 的下一位是 3,也是最後剩下的,所以幸運者是 3。
<br />
### 範例二:輸入
```
8 3 6
```
### 範例二:正確輸出
```
4
```
(說明)被淘汰的順序是 3、6、1、5、2、8,此時 8 的下一位是 4,所以幸運者是 4。
<br />
### 評分說明
輸入包含若干筆測試資料,每一筆測試資料的執行時間限制(time limit)均為 1 秒,依
正確通過測資筆數給分。其中:
- 第 1 子題組 20 分,$1 \leq N \leq 100$,且 $1 \leq M \leq 10$,$K = N-1$。
- 第 2 子題組 30 分,$1 \leq N \leq 10,000$,且 $1 \leq M \leq 1,000,000$,$K = N-1$。
- 第 3 子題組 20 分,$1 \leq N \leq 200,000$,且 $1 \leq M \leq 1,000,000$,$K = N-1$。
- 第 4 子題組 30 分,$1 \leq N \leq 200,000$,且 $1 \leq M \leq 1,000,000$,$1 \leq K < N$。
<br />
## 參考答案
### 方法1
比較直接的作法,用串列 (list) 儲存資料,於迴圈中模擬整個運作過程,但是在 ZeroJudge #4 測資會超時。
```python=
N, M, K = map(int, input().split())
num = N
count = 0
idx = 0
data = [i for i in range(N)]
while num > N - K:
count += 1
idx += 1
if idx >= num: idx -= num
if count % M == 0:
del(data[idx])
idx -= 1
num -= 1
count = 0
idx += 1
if idx >= num: idx -= num
print(data[idx])
```
<br />
1. 第5行:産生長度為N的一維串列,元素為 0、1、2、…、N-1。
2. 第7 ~ 15行:while 迴圈
1. 在串列長度(人數) num > N - K 時繼續執行 while 迴圈
2. 迴圈中先將次數 count 加1、索引值 idx 加1
3. 如果 idx >= num,則 idx 要回到串列最前方,將 idx 值減去 num。
4. 如果 count % M == 0,炸彈爆炸,此人出局,del(data[idx])。下次執行 while 迴圈時,炸彈傳到下一個人手上,次數為1,所以先將 idx 減1,退回上一個人。人數 num 減1。次數 count 歸0。
3. 第17 ~ 19行:先將 idx 加1,再檢查 idx 是否要回到串列最前方,最後印出幸運者的編號。
<br /><br />
### 方法2
比較直接的作法,用串列 (list) 儲存資料,用佇列的概念運作,於迴圈中模擬整個運作過程,但是在 ZeroJudge #4 測資會超時。
```python=
N, M, K = map(int, input().split())
men = [i+1 for i in range(N)]
count = 0
#man = men.pop(0)
#men.append(man)
while count < K:
for i in range(M):
man = men.pop(0)
if i == M-1: count += 1
else: men.append(man)
#print(men)
print(men[0])
```
<br /><br />
即使改用 C++ 及佇列 (queue),仍然在 ZeroJudge #4 測資會超時。
```cpp=
#include <iostream>
#include <queue>
using namespace std;
int main() {
int N, M, K; cin >> N >> M >> K;
queue<int> men;
for(int i=1; i<=N; i++) men.push(i);
int count = 0;
while(count < K) {
for(int i=0; i<M; i++) {
int man = men.front(); men.pop();
if (i == M-1) count++;
else men.push(man);
}
}
cout << men.front() << endl;
return 0;
}
```
<br /><br />
### 方法3
自訂類別 Noed,再用 Noed 産生環狀串列 (linked list) 儲存資料,於迴圈中模擬整個運作過程,但是在 ZeroJudge #4 測資會超時。
```python=
class Node(object):
def __init__(self, value):
self.value = value
self.next = None
def creatLinkedList(n):
head = Node(1)
pre = head
for i in range(2, n+1):
newNode = Node(i)
pre.next = newNode
pre = newNode
pre.next = head
return head
N, M, K = map(int, input().split())
head = creatLinkedList(N)
pre = None
cur = head
for i in range(K):
for j in range(M-1):
pre = cur
cur = cur.next
#print(cur.value)
pre.next = cur.next
cur.next = None
cur = pre.next
print(cur.value)
```
<br />
1. 第1 ~ 4行:自訂類別 Node,使用 Node 産生物件時,輸入編號 value,先將下一個物件 self.next 指定為 None。
2. 第6 ~ 14行:自訂函式 creatLinkedList 産生環狀串列,呼叫時輸入數量 n。
1. 第7行:産生環狀串列開頭 head = Node(1)
2. 第8行:環狀串列前一個物件先指定為開頭 pre = head
3. 第9 ~ 12行:使用 for 迴圈産生編別 2、3、…、n 的物件,每次執行 for 迴圈時,先將編號為i新的物件命名為 newNode = Node(i),再將 newNode 指定為 pre 的下一個物件 pre.next = newNode,最後將 pre 重新定義為 newNode。
4. 第13行:接回開頭處,完成環狀串列。 pre.next = head
5. 第14行:回傳環狀串列的開頭 head
3. 第17行:呼叫 creatLinkedList,産生共有N個物件的環狀串列。
4. 第18行:環狀串列中的前一個物件 pre 暫時指定為 None
5. 第19行:環狀串列中目前的物件 cur 暫時指定為 head
6. 第20 ~ 27行:於 for 迴圈中模擬運作過程
1. 第20行:共有K人出局,此迴圈運作K次。
2. 第21 ~ 24行:炸彈每傳M次就爆炸,此迴圈運作 M-1 次,迴圈結束時 cur 為出局者。如果想要印出出局者,可以移除第26行的註解符號。
3. 第25 ~ 27行:刪除出局者,於環狀串列中跳過 cur,連接到 cur.next。
7. 印出幸運者的編號。
<br /><br />
### 方法4
這個方法的出處為 [http://yuihuang.com/zj-c296/](http://yuihuang.com/zj-c296/), 適合算數學程度較好的同學,從最後的贏家逆推𠩤來的編號,最後印出時再加1。可以通過 ZeroJudge 的測試,但是最後一筆測資費時 0.2 s,測資數量似乎很大。
```python=
N, M, K = map(int, input().split())
idx = 0
num = N-K
for i in range(1, K+1):
idx = (idx + M) % (num + i)
print(idx+1)
```
<br />
1. 為了方便運算,成員的編號由0開始,最後輸出結果時再加1。
2. 先假設幸運者的編號 $idx_{0} = 0$,最後剩下的人數 $num = N - K$。
3. 第 $K$ 次(最後1次)運作時,參與的人數為 $num + 1$,幸運者原來的編號為 $idx_{k-1} = M \% (num + 1)$。
4. 第 $K-1$ 次(倒數第2次)運作時,參與的人數為 $num + 2$,幸運者原來的編號為 $idx_{k-2} = (idx_{k-1} + M) \% (num + 2)$。
5. 第 $K-2$ 次(倒數第3次)運作時,參與的人數為 $num + 3$,幸運者原來的編號為 $idx_{k-3} = (idx_{k-2} + M) \% (num + 3)$。
6. 若 $i = 1、2、\dots、K$,則第 $K-i+1$ 次(倒數第$i$次)運作時,參與的人數為 $num + i$,幸運者原來的編號為 $idx_{i} = (idx_{i-1} + M) \% (num + i)$。
7. 輸出幸運者的編號,由於題目的編號從1開始,需要將 idx 加1。
<br /><br />
按照同樣的想法,也可以寫成以下這樣,於 ZeroJudge 測試,花費時間為 80 ms,使用記憶體 3.3 MB。
```python=
N, M, K = map(int, input().split())
R = 0
for i in range(N-K+1, N+1): R = (R+M)%i
print(R+1)
```
<br />
如果用 C++ 解題,於 ZeroJudge 測試,花費時間為 8 ms,使用記憶體 332 kB。
```cpp=
#include <iostream>
using namespace std;
int main(int argc, char* argv[]) {
int N, M, K, R = 0;
cin >> N >> M >> K;
for(int i=N-K+1; i<=N; i++) R = (R+M)%i;
cout << R+1 << endl;
return 0;
}
```
<br /><br />
---
###### tags:`APCS`、`C++`、`Python`