這裡的題目與高中生程式解讀系統的題目敘述會有落差,但基本都是相同的 這裡提供由AA競程做的解題系統: P1:https://apcs.aacpoj.com/problem/apcs202611mh1 P2:https://apcs.aacpoj.com/problem/apcs202611mh2 P3:https://apcs.aacpoj.com/problem/apcs202611mh3
高中生程式解讀系統連結:https://zerojudge.tw/ShowContest?contestid=27726
有任何問題歡迎傳Email:edychie0323@gmail.com給我!
P1 營火競賽
題目敘述
營隊要決定隔天由哪一隊負責營地建設,因此舉辦了一場火把競賽。
共有 n 個人參加,分屬 k 支隊伍。所有人面向營火圍成一圈,依順時針方向編號為 1, 2, …, n。第 i 個人所屬的隊伍為 tᵢ,隊伍編號介於 1 到 k 之間。
由於所有人都面向圓心,每個人的左手邊就是順時針方向的下一個人。因此,第 i 個人的左手邊是第 i+1 個人,第 n 個人的左手邊則是第 1 個人。
比賽開始時,每個人的火把都已點燃,滅火器由第 1 個人持有。持有滅火器的人稱為「行動者」,每一回合依照下列規則進行:
-
熄滅一支火把
行動者從自己左手邊的下一個人開始,沿順時針方向依序尋找。遇到火把已熄滅的人,或與行動者同隊的人,都直接跳過。找到第一位「火把仍點燃,且與行動者不同隊」的人後,熄滅他的火把。火把熄滅的人,之後不會再成為行動者。
-
判斷比賽是否結束
如果所有火把仍點燃的人都屬於同
-
一隊,比賽立刻結束,並由該隊負責隔天的營地建設。
-
傳遞滅火器
如果比賽尚未結束,行動者再次從自己左手邊的下一個人開始,沿順時針方向尋找,將滅火器交給第一位火把仍點燃的人,作為下一回合的行動者。這個人可以與原行動者同隊。
注意:尋找下一位行動者時,是從原行動者的左手邊開始,而不是從剛被熄滅火把的人的左手邊開始。
沿順時針方向尋找時,經過第 n 個人後,會回到第 1 個人繼續尋找。
如果一開始所有人就屬於同一隊,也就是 k = 1,比賽不進行任何回合便結束,熄滅的火把數為 0。
請計算比賽結束時,總共有幾支火把被熄滅,以及最後由哪一隊負責營地建設。
子題配分
本題共分為以下兩個子題:
| 子題 | 配分 | 額外限制 |
|---|---|---|
| 1 | 40% | n < 1000 |
| 2 | 60% | 無額外限制,符合原題限制 1 ≤ k ≤ n ≤ 2000 |
輸入說明
第一行有兩個整數 n 與 k,分別代表參加人數及隊伍數。
第二行有 n 個整數 t₁, t₂, …, tₙ,其中 tᵢ 代表第 i 個人所屬的隊伍。
資料範圍:
- 1 ≤ k ≤ n ≤ 2000。
- 1 ≤ tᵢ ≤ k。
- 第 1 隊到第 k 隊,每一隊都至少有一個人。
輸出說明
輸出一行,包含兩個整數 x 與 w,中間以一個空白隔開。
- x:比賽結束時,被熄滅的火把總數。
- w:最後負責營地建設的隊伍編號,也就是所有火把仍點燃的人所屬的隊伍。
範例一
輸入
6 3
2 2 1 3 1 2
輸出
4 2
範例一說明
| 回合 | 行動者 | 被熄滅火把的人 |
|---|---|---|
| 1 | 第 1 個人(第 2 隊) | 第 3 個人(第 1 隊) |
| 2 | 第 2 個人(第 2 隊) | 第 4 個人(第 3 隊) |
| 3 | 第 5 個人(第 1 隊) | 第 6 個人(第 2 隊) |
| 4 | 第 1 個人(第 2 隊) | 第 5 個人(第 1 隊) |
詳細過程如下:
- 第 1 個人屬於第 2 隊。他向左依序遇到第 2 個人(同隊,跳過)及第 3 個人(不同隊),因此熄滅第 3 個人的火把。接著由第 2 個人成為行動者。
- 第 2 個人屬於第 2 隊。他向左依序跳過火把已熄滅的第 3 個人,接著找到第 4 個人(第 3 隊),因此熄滅第 4 個人的火把。接著由第 5 個人成為行動者。
- 第 5 個人屬於第 1 隊。他向左找到第 6 個人(第 2 隊),因此熄滅第 6 個人的火把。接著沿圓圈繞回第 1 個人,由第 1 個人成為行動者。
- 第 1 個人屬於第 2 隊。他向左跳過同隊的第 2 個人,以及火把已熄滅的第 3、4 個人,接著找到第 5 個人(第 1 隊),因此熄滅第 5 個人的火把。
此時只有第 1、2 個人的火把仍然點燃,且兩人都屬於第 2 隊,因此競賽結束。總共熄滅 4 支火把,最後由第 2 隊負責隔日的營地建設。
範例二
輸入
7 3
1 2 1 3 2 3 2
輸出
6 2
範例二說明
| 回合 | 行動者 | 被熄滅火把的人 |
|---|---|---|
| 1 | 第 1 個人(第 1 隊) | 第 2 個人(第 2 隊) |
| 2 | 第 3 個人(第 1 隊) | 第 4 個人(第 3 隊) |
| 3 | 第 5 個人(第 2 隊) | 第 6 個人(第 3 隊) |
| 4 | 第 7 個人(第 2 隊) | 第 1 個人(第 1 隊) |
| 5 | 第 3 個人(第 1 隊) | 第 5 個人(第 2 隊) |
| 6 | 第 7 個人(第 2 隊) | 第 3 個人(第 1 隊) |
第 4 回合中,第 7 個人沿順時針方向繞回圓圈前端,找到第 1 個人。
第 6 回合結束後,只剩第 7 個人的火把仍然點燃,他屬於第 2 隊,因此競賽結束。總共熄滅 6 支火把,最後由第 2 隊負責隔日的營地建設。
解題思路:環狀雙向鏈結串列模擬
n 最多只有 2000,可以直接模擬,每回合尋找第一位不同隊伍的人。為了方便跳過火把已熄滅的人,使用兩個陣列表示環狀雙向鏈結串列:
- nxt[i]:從第 i 個人往順時針方向走,第一位火把仍點燃的人。
- prv[i]:從第 i 個人往逆時針方向走,第一位火把仍點燃的人。
- count[t]:第 t 隊目前還有幾個人的火把點燃。
- remaining_teams:目前還有火把點燃的隊伍數。
程式內的人員索引從 0 開始,因此 actor = 0 代表題目的第 1 個人;隊伍編號仍維持從 1 開始。
為何要這樣做
使用一般陣列刪除元素時,後方元素可能需要搬移。這裡只要更新兩條連結,就能把被熄滅的人從圈子中移除,之後不會再走到他。
每回合先從 nxt[actor] 出發,跳過同隊的人。找到 target 後,令 target 的前一位與後一位互相連接,再更新該隊人數。若某隊人數變成 0,就把剩餘隊伍數減 1。
最容易寫錯的是下一位行動者:必須在刪除 target 之後,設定 actor = nxt[actor]。這表示從「原行動者」向前找,而非從被熄滅的人向前找。若剛好熄滅緊鄰的人,更新後的連結也會自動跳過他。
正確性說明
一開始鏈結包含所有人,且順序和題目一致。假設某回合開始時,鏈結恰好包含所有火把仍點燃的人,actor 也正確。
沿著 nxt 走,等同跳過所有已熄滅的人;再跳過同隊的人,找到的 target 就是題目指定的第一位敵隊成員。刪除 target 後,鏈結仍依原順時針順序保留所有仍點燃的人。此時 nxt[actor] 正是題目規定的下一位行動者,因此下一回合也保持正確。
每回合更新隊伍人數,remaining_teams 恰好代表尚存隊伍數。當它等於 1 時立即停止,輸出的熄滅數與最後隊伍因此正確。若 k = 1,迴圈一次也不執行,會輸出 0 和原本的隊伍。
複雜度
每回合最多搜尋 n 個位置,最多進行 n − 1 回合,因此時間 O(n²),空間 O(n + k),在 k ≤ n 下也可寫成 O(n)。
Python 程式碼
import sys
def solve(n, k, team):
count = [0] * (k + 1)
for t in team:
count[t] += 1
# Circular doubly linked list of people whose torches are still lit.
nxt = [(i + 1) % n for i in range(n)]
prv = [(i - 1) % n for i in range(n)]
remaining_teams = k
actor = 0
extinguished = 0
while remaining_teams > 1:
target = nxt[actor]
while team[target] == team[actor]:
target = nxt[target]
# Remove the target before choosing the next actor.
nxt[prv[target]] = nxt[target]
prv[nxt[target]] = prv[target]
t = team[target]
count[t] -= 1
if count[t] == 0:
remaining_teams -= 1
extinguished += 1
actor = nxt[actor]
return extinguished, team[actor]
def main():
data = list(map(int, sys.stdin.buffer.read().split()))
n, k = data[:2]
print(*solve(n, k, data[2:2 + n]))
if __name__ == "__main__":
main()
C++17 程式碼
#include <iostream>
#include <vector>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<int> team(n), count(k + 1), nxt(n), prv(n);
for (int &t : team) {
cin >> t;
++count[t];
}
for (int i = 0; i < n; ++i) {
nxt[i] = (i + 1) % n;
prv[i] = (i + n - 1) % n;
}
int remainingTeams = k, actor = 0, extinguished = 0;
while (remainingTeams > 1) {
int target = nxt[actor];
while (team[target] == team[actor]) target = nxt[target];
nxt[prv[target]] = nxt[target];
prv[nxt[target]] = prv[target];
if (--count[team[target]] == 0) --remainingTeams;
++extinguished;
actor = nxt[actor];
}
cout << extinguished << ' ' << team[actor] << '\n';
}
P2 火星探測資料
題目敘述
探測車在火星表面進行了 k 次探測,每次的結果都記錄為一個長度為 n、只包含小寫英文字母的字串。研究團隊希望將這些資料整合成一個字串 S,以減少傳回地球時所需的能源。
整合後的字串 S 必須包含每一筆探測結果,且每筆結果都必須是 S 的「連續子字串」。不同探測結果可以共用部分字母,在 S 中出現的順序也不必與輸入順序相同。如果有多筆相同的探測結果,只需要在 S 中出現一次即可。
例如,rock 和 ocks 可以整合成 rocks,因為這兩個字串都是 rocks 的連續子字串。
請找出符合條件且長度最短的字串 S。如果有多個長度最短的答案,請輸出其中字典序最小的一個。
連續子字串
是指從一個字串中取出一段連續的字元,並保持原本的順序。例如,lan 是 planet 的連續子字串,但 pnt 不是。
字典序
的比較方式為:由左至右逐一比較兩個字串的字母,在第一個不同的位置,字母較前面的字串,其字典序較小。例如,rocker 的字典序比 rocket 小,因為第一個不同的字母是 r 和 t,而 r 在字母表中排在 t 前面。
輸入說明
第一行包含兩個整數 n、k(1 ≤ n ≤ 8,1 ≤ k ≤ 9),分別代表每筆探測結果的長度與探測結果的筆數。
接下來 k 行,每行包含一個長度恰好為 n、僅由小寫英文字母組成的字串,代表一筆探測結果。
輸出說明
輸出一行字串 S。S 必須包含所有輸入字串作為連續子字串,且長度必須最短。若答案不唯一,輸出其中字典序最小者。
範例一
輸入
4 2
aaaa
bbbb
輸出
aaaabbbb
範例一說明
aaaa 的非空後綴無法與 bbbb 的前綴相同,反過來也一樣,因此兩個字串無法重疊。任何可行答案的長度至少為 8。
aaaabbbb 與 bbbbaaaa 都包含這兩筆探測結果,且長度都是 8。比較字典序時,第一個字元分別為 a 與 b,因此 aaaabbbb 較小,應輸出 aaaabbbb。
範例二
輸入
4 6
aaaa
aaab
aaba
aabc
abaa
baaa
輸出
aabaaaabc
範例二說明
在輸出字串 aabaaaabc 中,各筆探測結果的位置如下(位置由 1 開始計算):
| 探測結果 | 在輸出字串中的位置 |
|---|---|
aaba | 第 1 至第 4 個字元 |
abaa | 第 2 至第 5 個字元 |
baaa | 第 3 至第 6 個字元 |
aaaa | 第 4 至第 7 個字元 |
aaab | 第 5 至第 8 個字元 |
aabc | 第 6 至第 9 個字元 |
因此,aabaaaabc 的確包含全部六筆探測結果。
由於六個輸入字串彼此不同,而長度為 L 的字串只有 L-4+1 個長度為 4 的連續子字串位置,因此要容納六個不同的輸入字串,S 的長度至少為 9。
將六筆資料依照 aaba、abaa、baaa、aaaa、aaab、aabc 的順序排列時,每一筆的後三個字元都與下一筆的前三個字元相同,所以它們可以依序重疊,得到長度為 9 的 aabaaaabc。
這個結果達到前述長度下限,因此不可能再縮短。經比較所有長度為 9 的可行字串後,aabaaaabc 的字典序最小,所以答案為 aabaaaabc。
解題思路:狀態壓縮動態規劃
k 最多為 9,因此可以用一個二進位整數記錄哪些字串已經選入排列。先移除重複字串,令剩下的不同字串數為 r。所有字串長度都相同,去重後不會有某字串完整包含另一個不同字串的情況。
先計算兩個字串的最大重疊
對每對字串 i、j,從大到小嘗試重疊長度,找出「i 的後綴」等於「j 的前綴」的最大長度。
例如 abcd 與 cdef 的最大重疊是 cd,接上第二個字串時只需追加 ef。程式的 add[i][j] 就記錄需要追加的部分。
不能每次只選眼前重疊最多的一對:這個決定可能破壞後續更好的銜接,所以需要考慮所有字串排列。
DP 狀態與轉移
dp[mask][i] 表示:排列中已選入 mask 標記的字串,最後一個選入的是 i 時,能得到的最佳合併字串。「最佳」依序比較長度、字典序。
mask 的第 j 個位元是 1,表示第 j 個字串已選入排列。這是排列的紀錄,不要求未選入的字串一定尚未作為子字串出現。
初始時,只選一個字串 i,所以 dp[1 << i][i] = words[i]。
對每個尚未選入的 j,候選答案為:
new_mask = mask | (1 << j)
candidate = dp[mask][i] + add[i][j]
若 candidate 比 dp[new_mask][j] 短,或長度相同但字典序更小,就更新該狀態。最後從全部字串皆選入的狀態中,選出最佳答案。
Python 用 None 表示尚未到達的狀態;C++ 用空字串表示,因為本題的有效答案一定非空。
為何每個狀態只留一個字串就夠
相同的 mask 和最後一個字串 i,接下來可選的字串完全相同,追加的內容也只由 i 和下一個 j 決定。
如果目前答案比較短,後面追加相同內容後仍然比較短;如果長度相同而字典序較小,追加相同內容後也仍然較小。因此較差的狀態不可能在未來反超,可以安全捨棄。
正確性說明
任一最短可行答案,都能為每個不同輸入字串選定一個出現位置,依照起點由左到右排序。因為所有輸入字串等長且互不相同,它們的起點與終點都依相同順序增加,不會互相包含。
這個排列至少有一種把字串依序接起來的方式。相鄰字串若有重疊,該重疊不會超過我們預先算出的最大重疊;若有未被覆蓋的空隙,刪掉空隙也不影響包含所有字串。因此用最大重疊依序合併,不會比原答案更長。
如果原答案已最短,合併後便不能再短,所以它對應的相鄰間距必須已經達到最大重疊。這表示所有最短答案都能由某個字串排列的最大重疊合併產生。
DP 考慮所有起點與所有下一個字串,涵蓋全部排列;每個狀態又保留長度與字典序最好的字串,所以最後必定得到最短、且同長度中字典序最小的答案。
複雜度
令 L = r × n,這是答案長度的上界,最多為 72。
目前程式透過切片/子字串比較計算重疊,預處理時間為 O(r² × n²)。DP 有 O(2^r × r) 個狀態,每個嘗試 r 種下一個字串;考慮字串複製與比較,時間為 O(2^r × r² × L),空間為 O(2^r × r × L + r² × n)。
Python 程式碼
import sys
def solve(words):
words = sorted(set(words))
r = len(words)
n = len(words[0])
# add[i][j] is the part of word j not covered by word i.
add = [[""] * r for _ in range(r)]
for i in range(r):
for j in range(r):
overlap = 0
for length in range(n, 0, -1):
if words[i][-length:] == words[j][:length]:
overlap = length
break
add[i][j] = words[j][overlap:]
dp = [[None] * r for _ in range(1 << r)]
for i, word in enumerate(words):
dp[1 << i][i] = word
for mask in range(1, 1 << r):
for i in range(r):
current = dp[mask][i]
if current is None:
continue
for j in range(r):
if mask & (1 << j):
continue
new_mask = mask | (1 << j)
candidate = current + add[i][j]
old = dp[new_mask][j]
if old is None or (len(candidate), candidate) < (len(old), old):
dp[new_mask][j] = candidate
return min((s for s in dp[-1] if s is not None), key=lambda s: (len(s), s))
def main():
data = sys.stdin.buffer.read().split()
k = int(data[1])
print(solve([s.decode() for s in data[2:2 + k]]))
if __name__ == "__main__":
main()
C++17 程式碼
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
using namespace std;
bool better(const string &a, const string &b) {
return b.empty() || a.size() < b.size() ||
(a.size() == b.size() && a < b);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<string> words(k);
for (auto &s : words) cin >> s;
sort(words.begin(), words.end());
words.erase(unique(words.begin(), words.end()), words.end());
int r = static_cast<int>(words.size());
vector<vector<string>> add(r, vector<string>(r));
for (int i = 0; i < r; ++i) {
for (int j = 0; j < r; ++j) {
int overlap = 0;
for (int length = n; length >= 1; --length) {
if (words[i].substr(n - length) == words[j].substr(0, length)) {
overlap = length;
break;
}
}
add[i][j] = words[j].substr(overlap);
}
}
// An empty string denotes an unreachable state; valid answers are nonempty.
vector<vector<string>> dp(1 << r, vector<string>(r));
for (int i = 0; i < r; ++i) dp[1 << i][i] = words[i];
for (int mask = 1; mask < (1 << r); ++mask) {
for (int i = 0; i < r; ++i) {
if (dp[mask][i].empty()) continue;
for (int j = 0; j < r; ++j) {
if (mask & (1 << j)) continue;
int newMask = mask | (1 << j);
string candidate = dp[mask][i] + add[i][j];
if (better(candidate, dp[newMask][j]))
dp[newMask][j] = candidate;
}
}
}
string answer;
for (const auto &s : dp.back()) {
if (!s.empty() && better(s, answer)) answer = s;
}
cout << answer << '\n';
}
P3 考拉茲距離
題目敘述
考拉茲操作會把一個正整數 n 換成另一個數,換法依 n 的奇偶而定:
- 如果 n 是奇數,n 變成 (3n + 1) / 2。
- 如果 n 是偶數,n 變成 n / 2。
當 n 是奇數時,3n + 1 是偶數,所以換出來的數仍是正整數。例如,21 是奇數,做一次操作後變成 (3 × 21 + 1) / 2 = 32;32 是偶數,再做一次操作後變成 16。
給定目標數字 t 和操作次數的上限 m。如果從正整數 x 開始連續做考拉茲操作,在 m 次以內(可以是 0 次)就會變成 t,就說 x 是「可行的數」。做 0 次的意思是 x 本身就等於 t,所以 t 一定是可行的數。
接著有 q 筆詢問,每筆詢問給一個正整數 a_i。對每筆詢問,請在所有可行的數 x 之中找出距離 a_i 最近的,輸出 |a_i − x| 的最小值。每筆詢問分開計算,互不影響。
輸入說明
第一行有三個整數 t、m、q,依序代表目標數字、操作次數的上限、詢問的筆數。
第二行有 q 個正整數 a_1、a_2、…、a_q,代表每筆詢問給的數。
資料範圍:
- 1 ≤ t ≤ 10^9
- 0 ≤ m < 32
- 1 ≤ q ≤ 10^4
- 1 ≤ a_i ≤ 10^18
輸出說明
輸出 q 行,第 i 行是第 i 筆詢問的答案,也就是 |a_i − x| 在所有可行的數 x 之中的最小值。
範例一
輸入
11 3 3
16 88 200
輸出
2
0
112
範例一說明
所有能在不超過 3 次考拉茲操作後變成 11 的正整數為:
| 考拉茲操作次數 | 可行的起始數字 | 操作過程 |
|---|---|---|
| 0 | 11 | 11 |
| 1 | 7,22 | 7→11;22→11 |
| 2 | 14,44 | 14→7→11;44→22→11 |
| 3 | 9,28,29,88 | 9→14→7→11;28→14→7→11;29→44→22→11;88→44→22→11 |
- 對 16 而言,最接近的可行數字是 14,所以將 16 減少 2 即可,答案為 2。
- 88 本身就是可行數字,不需要調整,答案為 0。
- 對 200 而言,最接近的可行數字是 88,所以需要減少 200-88=112,答案為 112。
範例二
輸入
5 4 5
1 9 20 27 100
輸出
2
1
0
1
20
範例二說明
可以從目標數字 5 反向列出所有可行的起始數字。若目前數字為 y,則 2y 一定能在一次操作後變成 y;此外,只有當 (2y-1)/3 是正奇數時,它才是另一個合法的前一步數字。
所有能在不超過 4 次考拉茲操作後變成 5 的正整數如下:
| 考拉茲操作次數 | 可行的起始數字 |
|---|---|
| 0 | 5 |
| 1 | 3,10 |
| 2 | 6,20 |
| 3 | 12,13,40 |
| 4 | 24,26,80 |
各詢問的計算如下:
| 詢問 | 最接近的可行數字 | 加減次數 | 考拉茲操作過程 |
|---|---|---|---|
| 1 | 3 | 2 | 3 → 5 |
| 9 | 10 | 1 | 10 → 5 |
| 20 | 20 | 0 | 20 → 10 → 5 |
| 27 | 26 | 1 | 26 → 13 → 20 → 10 → 5 |
| 100 | 80 | 20 | 80 → 40 → 20 → 10 → 5 |
所以五個詢問依序輸出 2,1,0,1,20。
解題思路:反向搜尋可行起點,再用二分搜尋
直接對每個詢問嘗試調整 x 不可行,因為 a 最大為 10^18。我們改從共同的目標 t 出發,一次找出所有能在 m 步以內抵達 t 的起點,再重複利用這個集合回答詢問。
如何反推前一步
假設操作後的數字為 y:
- 若前一步 x 是偶數,x / 2 = y,所以 x = 2y。這個前一步永遠合法。
- 若前一步 x 是奇數,(3x + 1) / 2 = y,所以 x = (2y − 1) / 3。必須確認它是正奇數。
第二種前一步恰好在 y % 3 == 2 時成立。寫成 y = 3b + 2,便得到 x = 2b + 1,一定是正奇數,因此不需要額外檢查奇偶性。
例如 y = 5 的前一步有 10 和 3;y = 3 只有 6,不能直接把 (2×3−1)/3 當成整數除法取整。
為何要分層搜尋與去重
第 0 層先放 t,代表允許操作 0 次。再向外反推 m 層,並把每層找到的數字加入 seen。
frontier 只放本層首次發現的數字。因為按層搜尋,每個數字第一次被找到時,就是它反向抵達 t 的最少步數;較晚再次到達同一個數字不會增加可用的剩餘步數,因此不需要重複展開。
這點也能處理 t = 1 等循環,例如 1 與 2 之間的重複走訪。不能只保留第 m 層,因為題目允許的是「不超過 m 次」,所以必須保留所有已發現的數字。
如何回答詢問
將 seen 排序成 values。對詢問 a,使用二分搜尋找出第一個大於或等於 a 的位置 pos。
最近的可行數字只可能是 values[pos] 或 values[pos − 1],也就是 a 左右最靠近的鄰居。更左邊或更右邊的值,距離一定不會更小。程式會先檢查索引是否存在,再計算兩者距離的較小值。
若 a 就在集合中,右側候選等於 a,答案自然為 0。
正確性說明
反向搜尋加入的每條邊,都是一次合法考拉茲操作的反向,所以第 d 層找到的數字能用 d 步抵達 t,不會加入不合法的起點。
反過來,任何能用不超過 m 步抵達 t 的起點,都有一條合法的正向操作路徑。把路徑反過來,會由 t 沿著上述兩種反向邊走到該起點,因此一定會被搜尋找到。分層去重不會漏掉路徑,因為最早發現同一數字時,剩餘可搜尋的深度最多。
因此 seen 正好是所有可行起點。排序後檢查 a 左右鄰居,可以得到集合中與 a 距離最小的數字,所以每個詢問的答案正確。
整數範圍與複雜度
每次反向操作得到的數字最多是原本的兩倍,所以所有可行起點都不超過 t × 2^m。依本題限制,上界為 10^9 × 2^31 = 2,147,483,648,000,000,000,小於有號 64 位元整數上限;C++ 使用 long long 即可,Python 的整數則會自動擴充。
注意可行起點可能大於 10^18,不能因為詢問上限是 10^18 就直接丟棄它們。
令 V 為搜尋得到的不同數字總數。使用雜湊集合時,搜尋的平均時間為 O(V),排序 O(V log V),q 個詢問共 O(q log V),空間 O(V)。這不是只需 O(m) 的演算法:反向搜尋會分支,V 也取決於 t、m;粗略最壞樹狀上界為 2^(m+1) − 1,實際受到合法奇數前驅條件與去重限制,不能把每層當成必有兩個分支。本題 m 最多為 31。
Python 程式碼
import sys
from bisect import bisect_left
def reachable(t, m):
seen = {t}
frontier = [t]
for _ in range(m):
next_frontier = []
for y in frontier:
# The even predecessor always exists.
x = 2 * y
if x not in seen:
seen.add(x)
next_frontier.append(x)
# If y % 3 == 2, (2*y-1)/3 is a positive odd integer.
if y % 3 == 2:
x = (2 * y - 1) // 3
if x not in seen:
seen.add(x)
next_frontier.append(x)
frontier = next_frontier
if not frontier:
break
return sorted(seen)
def distance(values, a):
pos = bisect_left(values, a)
answer = abs(a - values[0])
if pos < len(values):
answer = min(answer, values[pos] - a)
if pos > 0:
answer = min(answer, a - values[pos - 1])
return answer
def main():
data = list(map(int, sys.stdin.buffer.read().split()))
t, m, q = data[:3]
values = reachable(t, m)
print("\n".join(str(distance(values, a)) for a in data[3:3 + q]))
if __name__ == "__main__":
main()
C++17 程式碼
#include <algorithm>
#include <iostream>
#include <iterator>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
using int64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int64 t;
int m, q;
cin >> t >> m >> q;
unordered_set<int64> seen;
seen.insert(t);
vector<int64> frontier{t};
for (int depth = 0; depth < m; ++depth) {
vector<int64> nextFrontier;
for (int64 y : frontier) {
int64 x = 2 * y;
if (seen.insert(x).second) nextFrontier.push_back(x);
if (y % 3 == 2) {
x = (2 * y - 1) / 3;
if (seen.insert(x).second) nextFrontier.push_back(x);
}
}
frontier = std::move(nextFrontier);
if (frontier.empty()) break;
}
vector<int64> values(seen.begin(), seen.end());
sort(values.begin(), values.end());
while (q--) {
int64 a;
cin >> a;
auto it = lower_bound(values.begin(), values.end(), a);
int64 answer = (a >= values[0] ? a - values[0] : values[0] - a);
if (it != values.end()) answer = min(answer, *it - a);
if (it != values.begin()) answer = min(answer, a - *prev(it));
cout << answer << '\n';
}
}
