2025-08-14 模拟赛总结 🙂
预期:\(100+100+100+0=300\)。
实际:\(100+100+100+0=300\)。
排名:\(rk1/13\)。
比赛链接:https://htoj.com.cn/cpp/oj/contest/detail?cid=22465653729920&gid=22394193382400。
(这次模拟赛前三题代码长度 \(\le 30\),但是全是思维题。
A - 麦斯威尔的魔术帽 / magic:
题意:
有一个长度为 \(n\) 的序列 \(a\),你需要求出 \(\max_{1\le i<j\le n}\gcd(a_i,a_j)\)。
思路:
我们可以枚举公因数(并不要求最大),找到序列中有多少个数是其倍数,如果数量 \(>1\) 就是合法的,遍历一遍找到最大值就可以了。
枚举倍数的时间复杂度是调和级数,\(O(n\log n)\)。
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 1e6 + 5, kMaxV = 5e6 + 5;
int n, a[kMaxN], c[kMaxV], d[kMaxV];
int main() {
freopen("magic.in", "r", stdin);
freopen("magic.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i], c[a[i]]++;
}
for (int i = 1; i < kMaxV; i++) {
for (int j = 1; j * i < kMaxV; j++) {
d[i] += c[i * j];
}
}
for (int i = kMaxV - 1; i; i--) {
if (d[i] > 1) {
cout << i;
return 0;
}
}
return 0;
}
B - 织影者的中庭:
题意:
现在有 \(n\) 个按钮,编号分别为 \(1,2,\cdots,n\)。现在按钮被随机打乱,你可以摁下任意一个按钮,按钮被摁下后就会显示编号,你需要顺序激活每一个按钮,当你摁下按钮时,如果按钮编号是当前需要的,那么将其激活,否则不激活。
你需要求出你的操作次数的期望值。
思路:
我们可以按照一个一个按按钮,找到 \(1\),然后在从已经显示编号的按钮中顺序选择 \(2,3,\cdots,x\),然后在一个一个按按钮,直到找到 \(x\),以此类推,显然,这样是最优的。
接着,我们可以求出操作次数的总和,最后再除以 \(n!\) 得到期望。
我们考虑递推,设 \(f_i\) 表示 \(n=i\) 时的操作次数总和,因为先要找 \(1\),所以我们枚举 \(1\) 的位置 \(1\le j\le i\),那么这个 \(1\) 将按钮分成两段,\(1\) 之前的一段(包括 \(1\))需要 \(2j-1\) 此操作将其按下(这 \(2j-1\) 次操作不一定连续,可以摁完后面的按钮再回来按,但是总和一定是 \(2j-1\)),后面一段的操作次数就是 \(f_{i-j}\) 了,但是肯定不是直接 \(f_i\gets 2j-1+f_{i-j}\),因为是总和,还需要乘以一个组合数系数。
除掉 \(1\) 后还有 \(i-1\) 个数,需要往 \(i-1\) 前塞 \(j-1\) 个数,方案数为 \({i-1\choose j-1}\),\(f_{i,j}\) 没有考虑到 \(1\) 之前的方案,所以系数要乘以 \((j-1)!\),而 \(2j-1\) 没有 \(1\) 之前和之后都没有考虑到所以系数为 \({i-1\choose j-1}(j-1)!(i-j)!=(i-1)!\)。
所以递推方程式为:
这样就可以 \(O(n^2)\) 求了,但是没必要,这显然可以配凑成前缀和的形式。将等式左右两边同时除以 \(i!\)。
其实 \(F_i=\frac{f_i}{i!}\) 就是期望值,这说明可以不用算总和直接算期望(但是我太菜了),这样就可以维护前缀和 \(O(n)\) 递推了。
关于序列前缀和的递推式有一个很套路的做法,错位相减。
先将式子写成:
再列出 \(i-1\) 的式子:
上式减下式,得到:
将式子变成差分的形式,由于 \(f_0=0,f_1=1\),将这些等式对 \(i\) 累和,得到:
所以这就得到了 \(F_n\) 的通项,这样做也是 \(O(n)\) 的。
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 1e7 + 5, kM = 998244353;
int n, f[kMaxN], g[kMaxN], inv[kMaxN];
void Init() {
inv[1] = 1;
for (int i = 2; i <= n; i++) {
inv[i] = 1LL * (kM - kM / i) * inv[kM % i] % kM;
}
}
int main() {
freopen("open.in", "r", stdin);
freopen("open.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
cin >> n, Init();
f[1] = 1, g[1] = 1;
for (int i = 2; i <= n; i++) {
f[i] = (1LL * g[i - 1] * inv[i] + i) % kM;
g[i] = (g[i - 1] + f[i]) % kM;
}
cout << f[n] << '\n';
return 0;
}
C - 挖洞兔的巢穴 / rabbit:
题意:
给出一个 \(n\) 个点的凸包(输入为逆时针输入),你需要给这 \(n\) 个点两两连线,使得任意两条线段都相交,你需要给每条线段定向,定向的代价为 \(S_s+T_t\)(其中 \(s,t\) 分别为起点与终点),从起点开始往终点走,每个时刻可以从线段的每一个交点走到下一个交点(包括起点终点),你需要保证不存在两条线段从起点走到一个点相遇,请问线段长度总和与定向代价之和最小是多少。
数据保证没有 \(6\) 个点满足连线三线共点。
思路:
这道题是一个结论题,下面会介绍结论并证明。
结论 1:\(n\) 为奇数时无解。
结论 1 证明:显然,不然匹配不完。
结论 2:如果连线方式为 \(i\longleftrightarrow i+\frac n2(1\le i\le\frac n2)\),则一定两两相交,否则一定不两两相交。
结论 2 证明:前者显然;考虑后者,不妨设 \(i=1\),设 \(1\longleftrightarrow j\)(其中 \(j\neq 1+\frac n2\)),那么这条线段左右的点数不同,肯定存在另一条线段的两端在 \(1\longleftrightarrow j\) 的同一侧,那么就不满足线段两两相交的条件了。
结论 3:\(n\) 是 \(4\) 的倍数时无解。
结论 3 证明:如果 \(n\) 是 \(4\) 的倍数,那么肯定存在两个相邻的点同为起点,我们考虑这两条直线在交点之前的 V 字形的线段。因为这两个点相邻,所以如果有一条直线经过 V 的其中一条线,那么肯定经过另一条线,所以这两个交点数总是相同的,所以一定会相遇。
结论 4:定向肯定是交替定向。
结论 4 证明:不然肯定会出现两个相邻的点同时为起点,根据结论 3 的证明,这也会相遇。
结论 5:如果连线方式为 \(i\longleftrightarrow i+\frac n2(1\le i\le\frac n2)\),则会一定不会相遇。(结论 2 的完善)
结论 5 证明:根据结论 4 定向是交替定向,我们考虑两个同为起点的点,那么它们之间肯定有奇数个点往外连出的线段,同样考虑 V 字形的线段,发现它们之间的所有点只会给 V 的其中一边交点数增加 \(1\),由于增加的数量是奇数,那么 V 两边的交点数奇偶性肯定不同,那么交点数肯定也不同,所以肯定不会相遇。
有了这个结论,判完无解的情况就只剩两种不相遇的情况(就是两种交替定向的情况),将这两种情况取个 \(\min\) 就可以了。
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 1005;
int T, n, x[kMaxN], y[kMaxN], a[2][kMaxN];
long double ans, sum[2];
int main() {
freopen("rabbit.in", "r", stdin);
freopen("rabbit.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
for (cin >> T; T; T--, sum[0] = sum[1] = ans = 0) {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> x[i] >> y[i] >> a[0][i] >> a[1][i];
sum[0] += a[i % 2][i], sum[1] += a[1 - i % 2][i];
}
if (n % 2 || n % 4 == 0) {
cout << "-1\n";
continue;
}
for (int i = 1; i <= n / 2; i++) {
ans += sqrtl(powl(x[i] - x[i + n / 2], 2) + powl(y[i] - y[i + n / 2], 2));
}
cout << fixed << setprecision(8) << min(sum[0] + ans, sum[1] + ans) << '\n';
}
return 0;
}

浙公网安备 33010602011771号