ACGO排位賽#9 - 題目解析
序言,這次排位賽絕對是最輕鬆的一次排位賽了(打了四次排位賽,終於上了白銀組別)。肉眼可見 ACGO 的使用者量在慢慢地增長(至少參賽人數變多了,是件好事)[狗頭]。
PS:在提供 Cpp 標準程式碼的時候,本文也會同時提供 Python 版本的程式碼。但 Python 的執行效率不高,同樣複雜度的程式碼 Python 的效率可能是 C++ 程式碼的十分之一。因此在使用 Python 程式碼做題的時候,需要時刻注意複雜度和常數,否則有可能出現超時的情況。
第一題 - A23466.捉迷藏
題目連結跳轉:A23466.捉迷藏
第一題算是一個簽到題,題目要求給定兩個數字 \(A, B\) 輸出在 \([0, 9]\) 區間內不是 \(A \times B\) 的任意數字。此題非常簡單,可以暴力破解,也可以使用玄學演算法。
暴力演算法很好想,嘗試 \([0, 9]\) 之間的所有數字即可,找到一個符合標準的結果就輸出。
#include <iostream>
#include <algorithm>
using namespace std;
int main(){
int a, b;
cin >> a >> b;
for (int i=0; i<=9; i++){
if (i != a * b){
cout << i << endl;
return 0;
}
}
return 0;
}
由於我們知道,\(0\) 乘上任何數字都為 \(0\),因此對於所有的測試點,只要 \(A \neq 0\) 且 \(B \neq 0\),那麼輸出 \(0\) 即可(畢竟無論如何答案也都不會是 \(0\))。反之,輸出任意一個非零的數字即可。玄學程式碼如下:
#include <iostream>
#include <algorithm>
using namespace std;
int main(){
int a, b;
cin >> a >> b;
cout << ((a != 0 && b != 0) ? 0 : 1) << endl;
return 0;
}
本題的 Python 程式碼如下(不得不說,用 Python 的程式碼量確實減少很多):
a, b = map(int, input().split())
print(0 if a != 0 and b != 0 else 1)
兩個程式碼的時間複雜度都是 \(O(1)\),但是第二個演算法的複雜度可以精確到 \(\Theta(1)\)。
第二題 - A23467.最長公共字首
題目連結跳轉:A23467.最長公共字首
這道題也非常的簡單。題目要求求出兩個數字的最長公共字首的長度。例如字串 interview
和 interrupt
的最長公共字首就是 inter
,所以應該輸出 \(5\)。
具體地,在遍歷的時候判斷兩個字串中同一個索引對應的兩個字元是否相等,如果相等就將答案長度增加,否則就停止迴圈就可以了。PS:在遍歷的過程中需要注意不要讓索引超限。
本題的 AC 程式碼如下:
#include <iostream>
#include <algorithm>
using namespace std;
int main(){
string a, b;
int lena, lenb, ans = 0;
cin >> a >> b;
lena = a.size(); lenb = b.size();
// 迴圈終止條件取兩個字串長度的最小值,放置索引超限。
for (int i=0; i<min(lena, lenb); i++){
if (a[i] == b[i]){
ans++;
} else break;
}
cout << ans << endl;
return 0;
}
本題的 Python 程式碼如下:
S = input(); T = input()
def longest_common_prefix_length(S, T):
min_length = min(len(S), len(T))
for i in range(min_length):
if S[i] != T[i]:
return i
return min_length
print(longest_common_prefix_length(S, T))
本演算法的時間複雜度約為 \(O(\min(\text{lena}, \text{lenb}))\)。其中,lena
與 lenb
分別代表讀入進來的兩個字串的長度。
第三題 - A23468.壞掉的數字鍵
題目連結跳轉:A23468.壞掉的數字鍵
根據題意,我們需要統計在 \(N\) 個字串中,有多少個字串不包括字元 \(D\)。迴圈遍歷一個一個匹配實在太麻煩了,直接使用 string
類自帶的 .find()
函式即可。直接用 Cpp 的內建函式即可(內建函式大法真好用)。
a.find(b)
表示在 a
中尋找 b
。如果找到了就返回 b
第一次在 a
中出現的位置,否則就返回 string::npos
。
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "Hello, world!";
// 查詢字元 'w'。
size_t found = str.find('w');
// 如果 found == string::npos 則代表沒找到,否則返回的第一個匹配的索引。
if (found != string::npos) cout << "'w' found at: " << found << endl;
else cout << "'w' not found" << endl;
return 0;
}
本題的 AC 程式碼如下:
#include <iostream>
#include <algorithm>
using namespace std;
int T, n, total;
string d, tmp;
int main(){
cin >> T;
while(T--){
cin >> n >> d;
total = 0;
for (int i=1; i<=n; i++){
cin >> tmp;
// 如果找不到,就說明這個字串可以被題目中的“鍵盤”給打出來。
if (tmp.find(d) == string::npos)
total++;
}
cout << total << endl;
}
return 0;
}
本題的 Python 程式碼如下:
T = int(input())
while T:
T -= 1
n, d = input().split()
arr = input().split()
ans: int = 0
for i in arr:
ans += 1 if i.find(d) == -1 else 0
print(ans)
本題的每個測試點有多個測試用例,對於每一個測試用例,本演算法時間複雜度約為 \(O(n \times d)\),其中 d
表示讀入進的字串長度。更進一步地,a.find(b)
函式的時間複雜度為 \(O(\text{lena} \times \text{lenb})\)。其中,lena
與 lenb
分別代表兩個字串的長度。對於本道題而言,lenb
的值永遠為 \(1\)。
第四題 - A23469.奇怪的次方
題目連結跳轉:A23469.奇怪的次方
根據題目要求,我們需要找到一個整數 \(X\),滿足 \(X^N = Y\)。根據數學的基本運演算法則,我們對等式兩遍同時開 \(N\) 次根即可得到 \(X = \sqrt[N]{Y}\)。最後判斷再校驗一邊答案即可。需要注意的是,本題涉及關於小數的運算,因此在實現過程中需要使用 double
資料型別,同時也要關注計算機在處理浮點數存在的誤差(使用 round
函式可以將一個數字四捨五入)。
本題的 AC 程式碼如下:
#include <iostream>
#include <cmath>
using namespace std;
int T;
double n, y;
int main(){
cin >> T;
while(T--){
cin >> n >> y;
// 計算答案。
double root = pow(y, 1/n);
// 校驗答案是否是一個整數。
if (pow(round(root), n) == y)
cout << round(root) << endl;
else cout << -1 << endl;
}
return 0;
}
本題的 Python 程式碼如下:
T = int(input())
while T:
T -= 1
n, y = map(float, input().split())
ans: float = pow(y, 1 / n)
print(round(ans) if pow(round(ans), n) == y else -1)
本演算法的時間複雜度約為 \(O(1)\) 級別。對於 pow
函式的時間複雜度無法被精確地推算,因為在 C++ 底層中該函式會使用多種不同的演算法來實現相同的功能,但一般情況是 \(log_2(n)\) 級別的。
第五題 - A23470.隱藏元素
題目連結跳轉:A23470.隱藏元素
先講一個運演算法則 ,如果 \(A_i \oplus A_j = k\),那麼 \(A_i \oplus k = A_j\)。因此,當我們知道了 \(A_1 = 1\) 的時候,我們就可以推出所有有關 \(A_1\) 的線索的另一個數字 \(A_j\) 的值。一旦確定了一個數字,就可以確定所有與之有關聯的數字(線索)。
為了方便起見,我們可以建立一個無向圖,這樣子就可以透過類似拓撲排序的方式按照順序一個一個地推斷出剩餘的未知數。如果有一條線索 \(A_i\ A_j\ K\),那麼我們就在 \(A_i\) 和 \(A_j\) 直接連線一條無向邊,權值為 \(k\),最後對這個圖從 \(1\) 點開始進行拓撲排序就可以了。
需要注意的是,一個點如果已經被訪問過的話,就不需要再被訪問了,用 vis
陣列記錄一下就可以了。本題的 AC 程式碼如下:
#include <iostream>
#include <queue>
#include <cstring>
#include <vector>
using namespace std;
const int N = 1e5 + 5;
int n, m;
struct node{
int to, k;
};
int ans[N];
vector<node> G[N];
void bfs(){
queue<int> que;
que.push(1);
// 廣度優先搜尋/拓撲排序
while(!que.empty()){
int t = que.front();
que.pop();
for (int i=0; i<G[t].size(); i++){
node tmp = G[t][i];
if (ans[tmp.to] != -1) continue;
ans[tmp.to] = ans[t] ^ tmp.k;
que.push(tmp.to);
}
}
return ;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for (int i=1; i<=m; i++){
int a, b, c;
cin >> a >> b >> c;
// 建圖:注意邊一定是雙向的。
// 由a和c可以推出b,同時也可以由b和c推出a。
G[a].push_back((node){b, c});
G[b].push_back((node){a, c});
}
memset(ans, -1, sizeof ans);
ans[1] = 1; bfs();
for (int i=1; i<=n; i++)
cout << ans[i] << " ";
return 0;
}
本題的 Python 程式碼如下:
import queue
n, m = map(int, input().split())
G = [[] for _ in range(n+1)]
ans = [-1 for i in range(n+1)]
ans[1] = 1
for i in range(m):
u, v, w = map(int, input().split())
G[u].append((v, w))
G[v].append((u, w))
que = queue.Queue()
que.put(1)
while que.qsize() > 0:
t = que.get()
for to in G[t]:
if ans[to[0]] != -1:
continue
ans[to[0]] = ans[t] ^ to[1]
que.put(to[0])
for i in range(1, n+1):
print(ans[i], end=" ")
以上演算法基於 廣度優先搜尋/拓撲排序 實現,該演算法的時間複雜度約為 \(O(M)\)。
第六題 - A23471.聖誕禮物
題目連結跳轉:A23471.聖誕禮物
一道很明顯的完全揹包問題,但需要加以最佳化。如果不最佳化的話,按照本題的資料量,在極端情況下程式會執行 \(10^5 \times 10^5 = 10^{10}\) 次,大概需要 \(2\) 分鐘時間(其實也不是很久)。因此我們需要在原本的完全揹包的基礎上加以最佳化:
透過觀察可以發現,雖然物品的數量有 \(10^5\) 種,但是每種物品的花銷卻極其的少。就算有一個幸運數字為 \(888888888\),那麼也只需要 \(63\) 個聖誕糖果就可以。假設有多個幸運數字都需要 \(10\) 個糖果,那按照貪心的思路:如果我們要選支出則 \(63\) 個聖誕糖果的話,我們可以獲得的最大收益就是所有需要 \(10\) 個糖果的幸運數字所對應的可以獲得的金幣數量的最大值。我們可以透過一個陣列來記錄對於每一種支付的糖果組合,可以獲得的最大金幣數量。
這樣子我們就將原本的物品數量成功壓縮成了 \(63\) 種物品,即可透過本題的所有測試點。本題的 AC 程式碼如下:
#include <iostream>
#include <algorithm>
#include <vector>
#define int long long
using namespace std;
const int N = 1e5 + 5;
const int K = 500;
int T, n, m;
int A[N], B[N], C[K], vis[K], dp[N];
int nums[] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
int calc(int num){
int total = 0;
while(num){
total += nums[num % 10];
num /= 10;
}
return total;
}
void solve(){
cin >> n >> m;
for (int i=1; i<=n; i++) cin >> A[i];
for (int i=1; i<=n; i++) cin >> B[i];
// 每次記得都要初始化(否則大禍臨頭)。
for (int i=1; i<=m; i++) dp[i] = 0;
for (int i=1; i<=100; i++) C[i] = vis[i] = 0;
for (int i=1; i<=n; i++){
int t = calc(A[i]);
vis[t] = 1;
// 貪心取最優的。
C[t] = max(C[t], B[i]);
}
int ans = 0;
for (int i=1; i<=105; i++){
if (vis[i] == 0) continue;
for (int j=i; j<=m; j++){
dp[j] = max(dp[j], dp[j-i] + C[i]);
}
}
cout << dp[m] << endl;
return ;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> T; while(T--) solve();
return 0;
}
本題的 Python 程式碼如下:
T = int(input())
nums = [6, 2, 5, 5, 4, 5, 6, 3, 7, 6]
def calc(num):
ans = 0
while num:
ans += nums[num % 10]
num //= 10
return ans
while T:
T -= 1
n, m = map(int, input().split())
A = list(map(int, input().split()))
B = list(map(int, input().split()))
C, vis = [0 for _ in range(105)], [0 for _ in range(105)]
dp = [0 for _ in range(m+1)]
for i in range(0, n):
t = calc(A[i])
vis[t] = 1
C[t] = max(C[t], B[i])
ans = 0
for i in range(1, 105):
if vis[i] != 1:
continue
for j in range(i, m+1):
dp[j] = max(dp[j], dp[j-i] + C[i])
print(dp[m])
本題的每個測試點有多個測試用例,對於每一個測試用例,本演算法時間的多項式時間複雜度約為 \(O(63M)\),經過化簡後的複雜度約為 \(O(M)\)。其中,數字 \(63\) 代表最多存在 \(63\) 中不同的“物品”。