2024 CCPC郑州邀请赛 F. 优秀字符串(模拟) 题意 如果给定字符串$s$的长度为5,第三个字符与第五个字符相同且前四个字符互不相同,认为此字符串为”优秀字符串”,求“优秀字符串”的数量.
思路 签到题,直接模拟即可.
Code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include<bits/stdc++.h> #define endl '\n' using i64 = long long; using namespace std; signed main(){ std::ios::sync_with_stdio(false);cin.tie(0); int n, ans = 0;cin >> n; set<char> st; for (int i = 1; i <= n;i++){ string str; cin >> str; if(str.size()==5&&str[2]==str[4]){ st.insert(str[0]),st.insert(str[1]); st.insert(str[2]),st.insert(str[3]); if(st.size()==4) ans++; } st.clear(); } cout << ans << endl; return 0; }
J. 排列与合数 (暴力枚举) 时间复杂度:$O(T \cdot 5! \cdot \sqrt n)$ 题意 有$t$组数据,每组给定一个恰好 $5$ 位且各个数位均不相同的十进制正整数$n$。可以任意排列 该数字,保证数字没有前导零。请问是否存在一个方案使得重新排列的整数为 合数 ,若存在,则输出任意一个排列得到的合数,反之,则输出“-1”.
数据范围
$1 \le T \le 10^5$
$10^4 \le n < 10^5$
思路 判断整数$x$是否为合数,只需满足$x$不是质数且$x \ne 1$ ,但本题保证各个数位均不相同,故只需判断是否为质数即可.
从暴力枚举的角度,$t$组数据,每组数据进行全排列枚举$O(5!)$,并对每个数进行朴素判质数$O(\sqrt n)$操作,那么复杂度为 $\colorbox{Yellow}{O(120Tn)}$,最坏情况的数值为$10^5 \times 120 \times \sqrt {10^5} \ge 10^9$ ,考虑 剪枝优化 ,当排列的整数为偶数时,也就是最后一位是偶数,那么一定是满足条件的合数 ,直接退出循环并输出 .
Note: 使用埃氏筛或者欧拉筛预处理可以将复杂度降为$O(120T)$ ! 全排列除了使用深搜回溯的方式,也可以使用函数$next\underline{~} \;permutation()$ 加以实现,非常实用
Code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include<bits/stdc++.h> #define endl '\n' using i64 = long long; using namespace std; char s[10]; signed main(){ std::ios::sync_with_stdio(false);cin.tie(0); int t; cin >> t; auto isPrime = [&](int x) -> bool { //判断质数 if(x==1) return 0; for (int i = 2; i <= x / i;i++) if(x%i==0) return 0; return 1; }; while(t--){ cin >> s + 1; vector<int> p(6); iota(p.begin(), p.end(), 0); bool flag = 0; do{ if (s[p[1]] == '0') //不含前导零 continue; int n = (s[p[1]] - '0') * 10000 + (s[p[2]] - '0') * 1000 + (s[p[3]] - '0') * 100 + (s[p[4]] - '0') * 10 + (s[p[5]] - '0'); if (!isPrime(n) && n != 1){ cout << n << endl; flag = 1; break; } } while (next_permutation(p.begin() + 1, p.end())); //全排列函数 if(!flag){ cout << "-1" << endl; } } return 0; }
B. 扫雷1 (贪心) 时间复杂度:$O(n \log n)$ 题意 进行$n$轮扫雷,已知每轮扫雷开始之前均会获得1枚金币,且后续不会被回收。在第$i$轮扫雷中,可以花费$c_i$枚金币购买一个地雷检测器,购买数量不限。请问在这$n$轮结束后最多能购买的地雷检测器的数量?
数据范围
$1 \le n \le 2 \times 10^5$
$1 \le c_i \le 10^9$
思路 设当前拥有的金币数为$cnt$,显然,每次的购买数量为$\dfrac{cnt}{c_i}$,为使得购买数量最大化,显然, 贪心地选择$c_i$最靠后的最小值一定是最优的 ,因为前面轮数积累的金币数固定,且$c_i$的值较小,故购买数量一定更大。
不难想到,依次找到最远的最小值、次小值、…… 、较大值,类似于单调栈 的操作,需要维护最值的最远下标 ,当找到当前的最值后统计数量并依次找次大值,保证该次大值的下标高于最大值,遍历即可。该维护最值的下标过程和遍历操作可使用优先队列或者map实现。
Code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include<bits/stdc++.h> #define endl '\n' using i64 = long long; using namespace std; signed main(){ std::ios::sync_with_stdio(false);cin.tie(0); int n; cin >> n; vector<int> c(n + 1); i64 ans = 0; map<int, int> mp; for (int i = 1; i <= n;i++){ cin >> c[i]; mp[c[i]] = i; } int cur = 0, sum = 0; for(auto i:mp){ if (i.second > cur){ sum += (i.second - cur); ans += sum / i.first; cur = i.second, sum = sum % i.first; //需记录当前遍历到的位置和所剩金币 } } cout << ans << endl; return 0; }
M. 有效算法(二分,线段相交) 时间复杂度:$O(T \times n \log n)$ 题意 给定长度为$n$的正整数序列$\{a_n\}$和$\{b_n\}$ ,对于每个$a_i$,进行恰好一次 转换:
将$a_i$ 变成满足$|a_i-x| \le k \times b_i$ 的任意整数$x$
请求出 最小 的非负整数$k$,使得存在至少一种方案使得$a_i$所有数均相等 .
数据范围
$1 \le T \le 1.5 \times 10^5$
$2 \le n \le 3 \times 10^5,\sum n \le 3 \times 10^5$
$1 \le a_i,b_i \le 10^9$
思路 题目所求是最小值,不难想到 二分答案 的操作。对$k$的值进行二分查找,所以需要找到二分函数判定的条件:
设$c=k \times b_i$,显然$c$是定值,拆开绝对值,即 $a_i-c \le x \le a_i+c$
存在一种方案使得转换成$x$ 的值完全一致,不难想到,将各个区间范围抽象成线段,只需要判断所有线段是否两两相交
设线段的左端点为$L$,右端点为$R$ ,那么线段相交的条件就是$L_{max} \le R_{min}$ .
tips:需要注意二分的上界,过小则范围不够,过大则爆long long !
Code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include<bits/stdc++.h> #define inf 1E10 #define endl '\n' using i64 = long long; using namespace std; int t, n; int a[300010], b[300010]; bool check(i64 x){ i64 L, R, lmax = -1E18, rmin = 1E18; for (int i = 1; i <= n;i++){ L = (i64)a[i] - x * b[i]; R = (i64)a[i] + x * b[i]; lmax = max(lmax, L), rmin = min(rmin, R); } return (lmax <= rmin); } void solve(){ cin >> n; for (int i = 1; i <= n;i++) cin >> a[i]; for (int i = 1; i <= n;i++) cin >> b[i]; i64 l = 0, r = inf, ans; while(l<=r){ i64 mid = l + r >> 1; if (check(mid)) r = mid - 1, ans = mid; else l = mid + 1; } cout << ans << endl; } signed main(){ std::ios::sync_with_stdio(false);cin.tie(0); cin >> t; while(t--) solve(); return 0; }
H. 随机栈 (概率论/逆元) 前置知识:逆元(除法取模) 题意 有一个初始为空的多元集合($multiset$),总共进行 $2n$ 次操作,其中$n$次插入$n$次取出。给定操作序列$a$,插入时$a_i \ge 0$,取出时$a_i = -1$。已知每次取出元素的事件相互独立,并保证每次取出时集合一定非空,将取出的数排成一个序列,请问这个序列递增(当$i \in [1,n), a_{i+1} \ge a_i$)的概率是多少? 由于答案可能不是整数,只需要输出这个值对$998244353$取模的结果 .
数据范围
$1 \le n \le 2 \times 10^5$
$-1 \le a_i \le n$
思路 题设条件是取出的数应当递增序列,不难想到,每次取数应当是当前集合内的最小值,同时,如果插入的数严格小于已经取出的元素(递增序列)的最大值,那么递增序列显然无效,最终的概率就是0 .
如若能构建递增序列,由于每次取元素的事件是独立的,设第$i$次取数的概率为$p_i$,所以最终的概率为$\prod \limits_{i=1}^n p_i$ ;设第$i$ 次取数时元素内最小值为$x$,且$x$的元素数量为$cnt$,那么$p_i = \dfrac{x}{cnt}$ .
综上,需要维护的信息是当前集合内的各个元素,集合内的最小值以及各个元素的数量,可以使用$STL$容器$multiset$ 直接模拟插入和删除,元素的数量则可以用 $map$ 加以维护 .
最后,需要运用到除法取模的相关数论知识(逆元),因$\dfrac{P}{Q}(mod\: 998244353)=P \cdot Q^{-1} (mod\: 998244353)$ ,根据费马定理,如果模数是一个质数,有$Q^{p-1} mod \:p=1$ ,则$Q^{-1}$的逆元的数值等于$Q^{p-2}$ ,最终的答案就是$P \times Q^{p-2} (mod \: 998244353)$ ,需要应用快速幂取模 .
Code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include<bits/stdc++.h> #define endl '\n' using i64 = long long; const i64 p = 998244353; using namespace std; int power_mod(i64 a, i64 b, i64 mod){ i64 res = 1LL; a = a % mod; while (b){ if (b & 1) res = (res * a) % mod; b /= 2; a = (a * a) % mod; } return res; } signed main(){ std::ios::sync_with_stdio(false);cin.tie(0); int n; cin >> n; n <<= 1; vector<int> a(n + 1); for (int i = 1; i <= n; i++) cin >> a[i]; int num = 0, mx = 0; // mx表示当前递增序列的最大值 i64 P = 1LL, Q = 1LL; map<int, int> cnt; multiset<int> st; bool flag = 1; for (int i = 1; i <= n;i++){ if(a[i]>=0){ if (a[i] < mx){ flag = 0; break; } st.insert(a[i]), cnt[a[i]]++; }else{ int temp = *st.begin(); (P *= cnt[temp]) %= p; (Q *= st.size()) %= p; st.erase(st.find(temp)); cnt[temp]--; mx = max(mx, temp); } } i64 inv = power_mod(Q, p - 2, p); // Q的逆元值 i64 ans = (P * inv) % p; cout << (flag ? ans : 0) << endl; return 0; }
L. Toxel 与 PCPC II (动态规划) 时间复杂度:$O(50m)$ 题意 需要调试一段共有$n$行的代码,已知其中有$m$行代码有$bug$,位置为$a_1,a_2,…,a_m$ .
每次调试可以选定一个整数$i$,使得程序从第$1$行开始运行到第$i$行,共需要时间$i$秒;同时,会对前$i$行所有的$bug$进行修复,假设现存的$bug$数为$x$,则需要$x^4$进行修复 .
请问最短需要多少秒才能完成$debug$ ,修复整个代码所有的漏洞?
数据范围
$1 \le m \le n \le 2 \times 10^5$
$1 \le a_i \le n$
思路 根据直觉,本题存在多状态且子问题有重叠,可使用动态规划进行求解 .
设$dp[i]$表示前$i$行代码被修复(含运行)的最短时间,其状态转移方程显然为:
$dp[i]=Max(dp[i],dp[j]+(i-j)^4+a[i])$,其中$1 \le j < i<=m$
总时间复杂度为$O(m^2)$ ,无法通过本题,所以应当对时间复杂度进行优化.
不难注意到,$x^4$的指数增长极快,$50^4 > 2 \times 10^5$ ,而代码位置的上界就是$2 \times 10^5$ , 粗略估计只需考虑$i-j \le 50$的范围即可(实际更小)。此时时间复杂度便降为$O(50m)$.
Code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include<bits/stdc++.h> #define endl '\n' using i64 = long long; using namespace std; signed main(){ std::ios::sync_with_stdio(false);cin.tie(0); int n, m; cin >> n >> m; vector<i64> a(m + 1), dp(m + 1, 1E15); dp[0] = 0; for (int i = 1; i <= m;i++) cin >> a[i]; for (int i = 1; i <= m; i++) for (int j = i - 1; j >= 0 && i - j <= 50; j--) dp[i] = min(dp[i], dp[j] + 1LL * (i - j) * (i - j) * (i - j) * (i - j) + a[i]); cout << dp[m] << endl; return 0; }