【题目全解】ACGO排位赛#9
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\) 中不同的“物品”。