# APCS實作題2025年6月第4題:分組遊戲
> 日期:2025年6月27日
> 作者:王一哲
> [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=q839)
<br />
## 題目
### 問題描述
有 $n$ 個物品,編號為 $1$ 到 $n$。每對物品 $(i, j)$ 之間有一個正整數距離 $D[i][j]$,其中 $D[i][i] = 0$,且 $D[i][j] = D[j][i] > 0$。
你需要將這些物品分成恰好 $k$ 組,每組至少包含一個物品,且每個物品只能分在其中一組。
分組後,對於所有 $1 \leq i < j \leq n$,如果物品 $i$ 和 $j$ 分在同一組,則將 $D[i][j]$ 設為 $\infty$(無限大),否則保持原距離。
接下來,考慮整個距離矩陣中剩下的 $D[i][j]$ 值(即不同組的物品之間的距離),你要最大化其中的最小值。
以範例 $2$ 為例,分成 $\{1, 4\}, \{2, 5\}, 3$,會使得距離矩陣變為(以 $-$ 標記為 $\infty$)
```
0 5 6 - 3
5 0 3 4 -
6 3 0 4 7
- 4 4 0 5
3 - 7 5 0
```
最小值為 3
<br />
### 輸入格式
第一行包含兩個整數 $n$ 和 $k$ ($2 \leq k \leq n \leq 500$)。
接下來 $n$ 行,每行 $n$ 個整數,第 $i$ 行第 $j$ 個整數表示 $D[i][j] ~(1 \leq D[i][j] \leq 10^8)$。
子題配分
- 20%:$k = 2, 2 \leq n \leq 10$
- 80%:無額外限制
<br />
### 輸出格式
輸出一個整數,表示最大化後的最小組間距離。
<br />
### 範例一:輸入
```
3 2
0 2 1
2 0 3
1 3 0
```
### 範例一:正確輸出
```
2
```
### 範例二:輸入
```
5 3
0 5 6 1 3
5 0 3 4 2
6 3 0 4 7
1 4 4 0 5
3 2 7 5 0
```
### 範例二:正確輸出
```
3
```
<br /><br />
## Python 程式碼
使用時間約為 1 s,記憶體約為 22 MB,通過測試。
```python=
from collections import deque
def check(n, k, D, x):
"""
輸入參數:物品數量 n,分組數 k,距離 D,同組之間的最大距離 x
運作原則:小於 x 的資料放在同一組,分組後各組之間的距離至少為 x。
計算分組數量 cnt,如果 cnt 大於等於題目要求的分組數 k,
可以將小組合併,符合題目的要求。
回傳值:cnt >= k 回傳 True;反之回傳 False。
"""
visited = [False]*n # 走訪狀態
cnt = 0 # 分組數
for i in range(n): # 掃過物品 0 ~ n-1
if not visited[i]: # 還沒有走訪 i
visited[i] = True # 已走訪 i
cnt += 1 # 分組數加 1
### 用 BFS 找這個小組的成員 ###
que = deque([i]) # 待走訪佇列,先放入 i
while que: # 如果 que 還有資料繼續執行
u = que.popleft() # 取出 que 最前面的資料
for v in range(n): # 掃過物品 0 ~ n-1
# 如果還沒有走訪 v,而且 u, v 距離小於 x,分在同一組
if not visited[v] and D[u][v] < x:
visited[v] = True # 已走訪 v
que.append(v) # v 加到 que
### End of BFS ###
return cnt >= k # 回傳 cnt 是否大於等於 k
""" 讀取資料 """
n, k = map(int, input().split()) # 物品數量 n,分組數 k
D = [list(map(int, input().split())) for _ in range(n)] # 讀取物品之間的距離
dis_set = set() # 所有的距離集合
for i in range(n):
for j in range(i+1, n):
dis_set.add(D[i][j])
dis_list = sorted(dis_set) # 排序好的所有距離串列
""" 二分搜尋法找答案 """
low, high = 0, len(dis_list)-1 # 從 dis_list 找答案的索引值下限、上限
while low <= high:
mid = low + (high - low) // 2
if check(n, k, D, dis_list[mid]): # 至少可以分成 k 組
low = mid + 1 # 提升下限,試著用更大的距離分組
else: # 反之,降低上限,試著用更小的距離分組
high = mid - 1
# 結束時 dis_list[low] 是無法分成 k 組的組間距離最小值
print(dis_list[low-1]) # 答案是 dis_list[low-1]
```
<br /><br />
## C++ 程式碼
使用時間約為 0.1 s,記憶體約為 7.4 MB,通過測試。
```cpp=
#include <cstdio>
#include <queue>
#include <vector>
#include <set>
using namespace std;
bool check(int, int, vector<vector<int>>&, int);
int main() {
/* 讀取資料 */
int n, k; scanf("%d %d", &n, &k); // 物品數量 n,分組數 k
vector<vector<int>> D (n, vector<int> (n, 0)); // 物品之間的距離
set<int> dis_set; // 所有的距離集合
for(int i=0; i<n; i++) { // 讀取距離
for(int j=0; j<n; j++) {
int d; scanf("%d", &d);
D[i][j] = d;
D[j][i] = d;
dis_set.insert(d);
}
}
vector<int> dis_vec (dis_set.begin(), dis_set.end()); // 排序好的所有距離陣列
/* 二分搜尋法找答案 */
int low = 0, high = (int)dis_vec.size()-1; // 從 dis_vec 找答案的索引值下限、上限
while(low <= high) {
int mid = low + (high - low) / 2;
if (check(n, k, D, dis_vec[mid])) { // 至少可以分成 k 組
low = mid + 1; // 提升下限,試著用更大的距離分組
} else { // 反之,降低上限,試著用更小的距離分組
high = mid - 1;
}
} // 結束時 dis_vec[low] 是無法分成 k 組的組間距離最小值
printf("%d\n", dis_vec[low-1]); // 答案是 dis_vec[low-1]
return 0;
}
bool check(int n, int k, vector<vector<int>>& D, int x) {
/* 輸入參數:物品數量 n,分組數 k,距離 D,同組之間的最大距離 x
運作原則:小於 x 的資料放在同一組,分組後各組之間的距離至少為 x。
計算分組數量 cnt,如果 cnt 大於等於題目要求的分組數 k,
可以將小組合併,符合題目的要求。
回傳值:cnt >= k 回傳 True;反之回傳 False。 */
vector<bool> visited (n, false); // 走訪狀態
int cnt = 0; // 分組數
for(int i=0; i<n; i++) { // 掃過物品 0 ~ n-1
if (!visited[i]) { // 還沒有走訪 i
visited[i] = false; // 已走訪 i
cnt++; // 分組數加 1
/* 用 BFS 找這個小組的成員 */
queue<int> que; // 待走訪佇列,先放入 i
que.push(i);
while(!que.empty()) { // 如果 que 還有資料繼續執行
int u = que.front(); // 取出 que 最前面的資料
que.pop();
for(int v=0; v<n; v++) { // 掃過物品 0 ~ n-1
/* 如果還沒有走訪 v,而且 u, v 距離小於 x,分在同一組 */
if (!visited[v] && D[u][v] < x) {
visited[v] = true; // 已走訪 v
que.push(v); // v 加到 que
}
}
}
/* End of BFS */
}
}
return cnt >= k;
}
```
<br /><br />
---
###### tags:`APCS`、`C++`、`Python`