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)!\)

所以递推方程式为:

\[\begin{align*} f_i&=\sum_{j=1}^i(f_{i-j}{i-1\choose j-1}(j-1)!+(2j-1)(i-1)!) \\ &=\sum_{j=1}^i\frac{f_{i-j}(i-1)!}{(i-j)!}+(i-1)!\sum_{j=1}^i(2j-1) \\ &=(i-1)!i^2+\sum_{j=1}^i\frac{f_{i-j}(i-1)!}{(i-j)!} \\ &=i!i+\sum_{j=1}^i\frac{f_{i-j}(i-1)!}{(i-j)!} \end{align*} \]

这样就可以 \(O(n^2)\) 求了,但是没必要,这显然可以配凑成前缀和的形式。将等式左右两边同时除以 \(i!\)

\[\begin{align*} \frac{f_i}{i!}&=i+\frac 1i\sum_{j=1}^i\frac{f_{i-j}}{(i-j)!} \end{align*} \]

其实 \(F_i=\frac{f_i}{i!}\) 就是期望值,这说明可以不用算总和直接算期望(但是我太菜了),这样就可以维护前缀和 \(O(n)\) 递推了。

关于序列前缀和的递推式有一个很套路的做法,错位相减。

先将式子写成:

\[iF_i=i^2+\sum_{j=0}^{i-1}F_j \]

再列出 \(i-1\) 的式子:

\[(i-1)F_{i-1}=(i-1)^2+\sum_{j=0}^{i-2}F_j \]

上式减下式,得到:

\[iF_i-(i-1)F_{i-1}=2i-1+F_{i-1} \]

\[F_i-F_{i-1}=2-\frac 1i \]

将式子变成差分的形式,由于 \(f_0=0,f_1=1\),将这些等式对 \(i\) 累和,得到:

\[F_n-F_0=\sum_{i=1}^n(2-\frac 1i)=2n-\sum_{i=1}^n\frac 1i=F_n \]

所以这就得到了 \(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;
}
posted @ 2025-08-14 13:38  liruixiong0101  阅读(329)  评论(0)    收藏  举报