进阶计划 2025 摸底考试
摸底考试 复盘
A 冰雪聪明
题意
给定两个不重合的点 \(A(x_1,y_1),B(x_2,y_2)\),要求找到一个点 \(C(x_3,y_3)\),使得 \(\triangle ABC\) 为直角三角形。
\(|x_1|,|y_1|,|x_2|,|y_2|,|x_3|,|y_3|\le10^9\)。
解法
初中数学中的「两线一圆」搬到 OI 中来了?
容易发现,当 \(l_{AB}\) 为水平线或者为竖直线时,直线的 \(k\) 不存在或者为 \(0\)(\(k=0\) 也就意味着 \(k\) 的负倒数不存在),所以可以根据这种情况分类讨论:
-
\(l_{AB}\) 为水平线或者竖直线:
此时可以以 \(A,B\) 为直角顶点,另外一个直角边边长为 \(1\)。
显然 \(A,B\) 存在两边可以构造,必然存在一组解。
-
\(l_{AB}\) 为倾斜线:
显然我们可以提出如下的构造:

过 \(A,B\) 分别做水平线和竖直线,交点即为所求。
由于 \(l_{AB}\) 为倾斜线,所以交点必然不落在 \(AB\) 上。
那么我们取 \(C\) 为 \((x_1,y_2)\) 或 \((x_2,y_1)\),由于 \(x_1,x_2,y_1,y_2\) 均满足条件,任意组合后的结果也必然满足条件。
根据上述做法构造即可。
时间复杂度
可以直接输出 \(C\),故时间复杂度 \(O(1)\),可以通过。
代码
/**
* Problem: A
* Author: OIer_wst
* Date: 2025-07-07
*/
#include <bits/stdc++.h>
using lint = long long;
lint x_1, y_1, x_2, y_2;
bool inRange(lint x) { // 判断是否在界内
return std::abs(x) <= 1e9;
}
int main() {
std::cin >> x_1 >> y_1 >> x_2 >> y_2;
if (x_1 != x_2 && y_1 != y_2) { // 倾斜线
std::cout << x_1 << " " << y_2 << std::endl;
} else if (x_1 == x_2) { // 垂直线
if (inRange(x_1 + 1)) std::cout << x_1 + 1 << " " << y_1 << std::endl;
else std::cout << x_1 - 1 << " " << y_1 << std::endl;
} else { // 水平线
if (inRange(y_1 + 1)) std::cout << x_1 << " " << y_1 + 1 << std::endl;
else std::cout << x_1 << " " << y_1 - 1 << std::endl;
}
return 0;
}
B 金发女孩
题意
有 \(n\) 个物品和体积为 \(m\) 的背包,第 \(i\) 个物品已知体积 \(v_i\in[l_i,r_i]\),价值为 \(w_i\)。
要求构造一种序列 \(\{x_n\}\),使得在最坏情况下所获得的魔法书价值之和最大。
选取为从前往后取,如果 \(x_i=1\),如果可以装入就放入背包。
\(n,m\le10^3\)。
解法
容易猜出一个结论:将每个物品的体积设为 \(r_i\),然后跑 01 背包,此时获得的结果 \(w_0\) 就是答案。
证明一下这个结论。
证明没有方案的最坏情况收益可以超过 \(w_0\),相当于 \(w_0\) 是最大收益。
- 反证法,假设存在一个计划 \(X\),最坏收益收益 \(w_1>w_0\)。
- 由于 \(w_1\) 是最坏情况收益,那就说明无论每个物品是区间中的哪个体积,最终价值都大于 \(w_0\)。
- 考虑一种特殊情况,将 \(X\) 中所有物品的体积都设置为 \(r_i\)。根据 2,此时价值大于 \(w_0\)。
- 假设拿到的物品集合为 \(S\),那么 \(\sum_{i\in S}w_i>w_0\)。
- 又由于这些物品可以被拿到,所以 \(\sum_{i\in S}r_i\le m\)。
- 根据 4、5,可以得出 \(S\) 就是通过 01 背包求出来的最优解。
- 但是 \(w_0\) 就是 01 背包的最优解,所以必然 \(w_1\) 不大于 \(w_0\)。
- 假设不成立,证毕。
显然由于每个实际体积必然 \(\le r_i\),所以显然每一个物品都可以拿到,也就是说 \(w_0\) 可以取到。
根据这个结论直接跑 01 背包,再输出方案即可。
时间复杂度
同 01 背包的复杂度,\(O(nm)\),可以通过。
代码
/**
* Problem: B
* Author: OIer_wst
* Date: 2025-07-07
*/
#include <bits/stdc++.h>
using lint = long long;
const int N = 1e3 + 10;
int n, m, f[N][N], pre[N][N];
void print(int i, int j) {
if (i == 0) return ;
print(i - 1, pre[i][j]);
if (pre[i][j] == j) std::cout << "0 ";
else std::cout << "1 ";
}
int main() {
std::cin >> n >> m;
for (int i = 1, w, v; i <= n; ++i) {
std::cin >> w >> v >> v;
for (int j = 0; j <= m; ++j) {
f[i][j] = f[i - 1][j], pre[i][j] = j;
if (j >= v && f[i - 1][j - v] + w > f[i][j])
f[i][j] = f[i - 1][j - v] + w, pre[i][j] = j - v;
}
}
std::cout << f[n][m] << "\n";
print(n, m);
std::cout << std::endl;
return 0;
}
C 兰顿蚂蚁
题意
兰顿蚂蚁可以视为一个无穷大平面上的一个质点,平面上每个坐标对应于一个格子。兰顿蚂蚁初始位于 \((0, 0)\) 并面向北方(指向 \((0, 1)\) 方向)。它会按照如下规则行走:
- 若当前位于白色格子,向顺时针方向旋转 \(90^{\circ}\),接着把所在格子染黑,向前移动一格;
- 若当前位于黑色格子,向逆时针方向旋转 \(90^{\circ}\),接着把所在格子染白,向前移动一格。
有 \(T\) 次询问,每次询问给定步数 \(s\),你要求出从起点出发行进 \(s\) 步后,兰顿蚂蚁的坐标。
\(T\le10^4\),\(s\le10^9\)。
解法
感谢样例的图片:

容易发现,蚂蚁一开始的运动是毫无规律的,但是在足够多的步数之后,蚂蚁的运动突然有了规律。
这就启示我们对于小数据可以打表预处理,然后对于大的数据可以找规律求解。
本题做法主要偏技术,并没有运用什么算法,所以会直接结合代码讲解。
首先我们要打表。由于我们打表不仅用于预处理小数据,还可以用来找大数据的规律,所以表的大小至少要达到 \(10000\) 以上,这里我们可以选择打到 \(10^5\sim10^6\)。
pii ans[N];
std::set<pii> black;
const int dir[][2] = {0, 1, 1, 0, 0, -1, -1, 0};
void init(int ed) { // 打表函数,ed 表示运动步数
int x = 0, y = 0, direction = 0;
for (int i = 1; i <= ed; ++i) {
if (black.find({x, y}) == black.end()) {
(direction += 1) %= 4;
black.insert({x, y});
} else {
(direction += 3) %= 4;
black.erase({x, y});
}
x += dir[direction][0], y += dir[direction][1];
ans[i] = {x, y};
}
}
表打出来后,应该怎么找规律呢?对于这么多的数字,肉眼一定看不出来,所以可以利用程序来帮我们找规律。
注意到图形左下部分都是一个图案平移的结果,所以我们只需要判断两段数是否差值为常数即可。
/**
* Problem: C
* Author: OIer_wst
* Date: 2025-07-08
*/
#include <bits/stdc++.h>
using lint = long long;
const int N = 1e7 + 10;
int x[N], y[N];
int main() {
freopen("a.out", "r", stdin); // a 为用打表程序输出的结果
int n = 0, xx, yy;
while (std::cin >> xx >> yy) ++n, x[n] = xx, y[n] = yy;
int st = 11000;
for (int len = 2; len <= 1000000; ++len) { // 枚举周期长度
int disx = x[st + len] - x[st], disy = y[st + len] - y[st]; // x,y 的差值
bool flag = true;
for (int i = st, j = st + len; i < st + len; ++i, ++j) { // 枚举两段
if (x[j] - x[i] != disx || y[j] - y[i] != disy) {
flag = false;
break;
}
}
if (flag) { // 找到最小周期
std::cout << len << std::endl;
std::cout << disx << " " << disy << std::endl;
return 0;
}
}
return 0;
}
然后运行,可以发现周期长度为 \(104\),并且每个周期偏移量为 \((-2,-2)\)。
那么做法就呼之欲出了:
- 小范围数据直接查表;
- 大范围数据先将蚂蚁移到 \(11000+((s-11000)\bmod 104)\),然后根据周期去偏移即可。
时间复杂度
每次询问时间复杂度 \(O(1)\),总时间复杂度 \(O(T)\),可以通过。
代码
/**
* Problem: C
* Author: OIer_wst
* Date: 2025-07-07
*/
#include <bits/stdc++.h>
using lint = long long;
using pii = std::pair<int, int>;
const int N = 2e4 + 10;
const int dir[][2] = {0, 1, 1, 0, 0, -1, -1, 0};
int T, s;
pii ans[N];
std::set<pii> black;
void init(int ed) {
int x = 0, y = 0, direction = 0;
for (int i = 1; i <= ed; ++i) {
if (black.find({x, y}) == black.end()) {
(direction += 1) %= 4;
black.insert({x, y});
} else {
(direction += 3) %= 4;
black.erase({x, y});
}
x += dir[direction][0], y += dir[direction][1];
ans[i] = {x, y};
}
}
int main() {
int mx = 12000;
init(mx);
for (std::cin >> T; T; --T) {
std::cin >> s;
if (s <= mx) std::cout << ans[s].first << " " << ans[s].second << "\n"; // 表内,直接输出
else {
int r = (s - 11000) % 104;
int x = ans[11000 + r].first, y = ans[11000 + r].second;
x += -2 * ((s - 11000) / 104), y += -2 * ((s - 11000) / 104);
std::cout << x << " " << y << "\n";
}
}
return 0;
}
D 原野幻想
题意
给定一张 \(n\) 个点 \(m\) 条边的带权有向图。
可以选择两个点 \(a,b\),连接一条 \(a\overset{0}{\to}b\) 的边。
连边后选择一个起点 \(s\),最小化起点到其它点最短路的最大值。
\(n\le500,m\le250000\)。
解法
先枚举 \(a,b\) 的话时间复杂度必然不低,所以做一个转化,考虑枚举起点 \(s\)。
假设已知了 \(a,b\),那么将 \(s\leadsto t\) 的路径分为两类:
- 经过 \(a\to b\) 这条边,这样这条路径最短长度为 \(d_{b,t}\)。
- 不经过 \(a\to b\) 这条边,那么这条路径长度显然为 \(d_{s,t}\)。
对于第一种情况,容易发现令 \(a=x\) 就可以让长度取到 \(d_{b,t}\)。
由此可知 \(a\) 必然与终点重合。
那么做法就呼之欲出了:
- 用 Floyd 求出全源最短路。
- 枚举 \(s\) 和 \(b\),选择连一条 \(s\overset{0}{\to}b\) 的边。接着统计 \(s\) 到每个点 \(i\) 的距离 \(\min\{d_{s,i},d_{b,i}\}\)。
- 对所有结果取 \(\min\) 即可获得答案。
时间复杂度
- Floyd,\(O(n^3)\)。
- 枚举 \(s,b\),\(O(n^2)\)。
- 计算 \(s\) 到每个点的距离,\(O(n)\)。
时间复杂度 \(O(n^3)\),可以通过。
代码
/**
* Problem: D
* Author: OIer_wst
* Date: 2025-07-07
*/
#include <bits/stdc++.h>
using lint = long long;
const int N = 5e2 + 10;
int n, m;
lint ans = 1e18, g[N][N];
void Floyd() {
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
g[i][j] = std::min(g[i][j], g[i][k] + g[k][j]);
}
int main() {
std::cin >> n >> m;
memset(g, 0x3f, sizeof(g));
for (int i = 1; i <= n; ++i) g[i][i] = 1;
for (int i = 0, u, v, w; i < m; ++i) {
std::cin >> u >> v >> w;
g[u][v] = std::min(g[u][v], 1ll * w);
}
Floyd();
for (int s = 1; s <= n; ++s) {
for (int b = 1; b <= n; ++b) {
lint mx = 0;
for (int k = 1; k <= n; ++k)
mx = std::max(mx, std::min(g[s][k], g[b][k]));
ans = std::min(ans, mx);
}
}
std::cout << ans << std::endl;
return 0;
}
E 最强妖精 I
题意
给定一个序列 \(\{a_n\}\),对于一个下标序列 \(\{p_m\}\),定义其为合法的要求为:
- \(1\le p_1<p_2<\cdots<p_m\le n\);
- 对于 \(i\in[1,m-1]\),有 \(p_{i+1}-p_i=i\)。
60 分解法
枚举每一个子序列的起点 \(i\),然后依次往后添加下标,同时维护当前子序列的和并且计入答案。
时间复杂度
先给个结论:最长子序列长度为 \(O(\sqrt{n})\)。
证明:
显然最长子序列必然从 \(a_1\) 开始,那么就可以递推得到后面的下标。
\[\begin{aligned} p_1&=1\\ p_2&=1+1\\ p_3&=1+1+2\\ p_4&=1+1+2+3\\ \vdots&\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\vdots\\ p_m&=1+1+2+\cdots+(m-1) \end{aligned} \]对 \(p_m\) 进行变形可得
\[\begin{aligned} p_m&=1+1+2+\cdots+(m-1)\\ &=1+\sum\limits_{i=1}^{m-1}i\\ &=1+\dfrac{m(m-1)}{2}\le n \end{aligned} \]所以 \(m=O(\sqrt{n})\)。
- 枚举起点,\(O(n)\)。
- 往后遍历,长度不超过 \(O(\sqrt{n})\)。
时间复杂度 \(O(n\sqrt{n})\),预期得分 \(60\)。
100 分解法
原来的做法不好优化的,故考虑统计每个数对答案的贡献。
假设我们已知子序列长度为 \(l\),那么每个数的贡献应该如何统计呢?

观察上面这张图,我们可以发现,在 \(A\) 序列平移到 \(B\) 序列时,\([1,pos]\) 之间的数都对答案产生了贡献。
同理第二个数 \([p_2,p_2+pos-1]\) 也对答案产生了贡献。
以此类推,我们可以得到所有长度为 \(l\) 的子序列对答案的贡献。
那么我们就可以枚举长度,然后用差分维护贡献。
时间复杂度
- 当长度为 \(1\) 时,需要遍历 \(1\) 个位置。
- 当长度为 \(2\) 时,需要遍历 \(2\) 个位置。
- 依次类推,当长度为 \(\sqrt{n}\) 时,需要遍历 \(\sqrt{n}\) 个位置。
故时间复杂度为 \(O\left(\sum_{i=1}^{\sqrt{n}}i\right)=O(n)\),预期得分 \(100\),可以通过。
代码
/**
* Problem: E
* Author: OIer_wst
* Date: 2025-07-07
*/
#include <bits/stdc++.h>
using lint = long long;
const int N = 2e6 + 10;
int n;
lint ans, a[N], b[N]; // b 为差分数组
int main() {
std::cin.tie(0)->sync_with_stdio(0);
std::cin >> n;
for (int i = 1; i <= n; ++i) std::cin >> a[i];
for (int len = 1; ; ++len) { // 枚举子序列长度
int lst = 1 + len * (len - 1) / 2;
if (lst > n) break;
int delta = n - lst + 1, pos = 1;
++b[pos], --b[pos + delta];
for (int i = 1; i < len; ++i) {
pos += i;
if (i & 1) --b[pos], ++b[pos + delta];
else ++b[pos], --b[pos + delta];
}
}
for (int i = 1; i <= n; ++i) ans += a[i] * (b[i] += b[i - 1]);
std::cout << ans << std::endl;
return 0;
}
F 爷是恋恋
题意
给定一颗以 \(1\) 为根结点,共有 \(n\) 个结点的树,点有点权。每个点有状态 \(s_i\in\{0,1\}\)。
有 \(m\) 次操作,每次操作给出一个结点的编号 \(x\):
- 如果 \(s_x=1\),设 \(s_x\) 为 \(0\)。
- 如果 \(s_x=0\),设 \(s_x\) 为 \(1\),同时将 \(x\) 的所有状态为 \(0\) 的儿子结点状态设置为 \(1\),不断递归下去。
在每次操作前后输出
特别地,如果不存在状态为 \(1\) 的结点,输出 \(-1\)。
解法
考虑一种很暴力的做法。
用 std::set 维护所有状态为 \(1\) 的结点,还有所有结点中状态为 \(0\) 的儿子编号。
- 当 \(s_x=1\) 时,在父亲的 \(0\) 儿子集合中加入 \(x\),在全局 \(1\) 儿子集合中删除 \(x\)。
- 当 \(s_x=0\) 时,在父亲的 \(0\) 儿子集合中删除 \(x\),并且直接暴力递归下去。
时间复杂度
上述做法时间复杂度貌似很高,特别是当 \(s_x=0\) 时,最坏情况下需要遍历 \(O(n)\) 个点,那么总体时间复杂度是否为 \(O(nm)\) 呢?
将代码交上去,发现 AC 了,说明上述分析是错误的。
下面正确地分析时间复杂度(具体结合代码理解):
- 输入,由于需要将状态为 \(1\) 的结点加入
id中,\(O(n\log n)\)。 dfs1,\(O(n\log n)\)。
下面分析 \(m\) 次操作:
- \(s_x=1\),需要操作
set,单次操作 \(O(\log n)\),总时间复杂度 \(O(m\log n)\)。 - \(s_x=0\):
- 这里是分析的重点。我们不能孤立地看单词操作,而是用均摊的眼光来看。
- 显然结点 \(u\) 被
dfs2访问依次,就必然发生了 \(0\to 1\) 的转变。我们需要统计 \(0\to 1\) 的转变发生了多少次:- 原来初始最多有 \(O(n)\) 个状态为 \(0\) 的结点。
- 一个结点 \(u\) 的状态 \(1\to 0\) 只可能通过操作进行修改,最多发生 \(O(m)\) 次。
- 因此一个结点 \(0\to 1\) 的上界为 \(O(n+m)\)。
- 现在来考虑这个操作的总成本:
id.insert(u):每次dfs2访问 \(u\) 都会执行依次,\(O((n+m)\log n)\)。for循环和son0[u].clear(),\(O((n+m)\log n)\)。son0[fa[x]].erase(x),\(O(m\log n)\)。
综上,时间复杂度实际上为 \(O((n+m)\log n)\),可以通过本题。
代码
/**
* Problem: F
* Author: OIer_wst
* Date: 2025-07-07
*/
#include <bits/stdc++.h>
using lint = long long;
const int N = 2e5 + 10;
std::vector<int> g[N];
int n, m, w[N], s[N], fa[N];
struct cmp {
bool operator()(int x, int y) const {
return w[x] == w[y] ? x < y : w[x] > w[y];
}
};
std::set<int, cmp> id;
std::set<int> son0[N];
void dfs1(int u, int f) {
for (auto v : g[u]) {
if (v == f) continue;
if (s[v] == 0) son0[u].insert(v);
dfs1(v, u), fa[v] = u;
}
}
void dfs2(int u) {
s[u] = 1, id.insert(u);
for (auto v : son0[u]) dfs2(v);
son0[u].clear();
}
int main() {
std::cin.tie(0)->sync_with_stdio(0);
std::cin >> n >> m;
for (int i = 1, u, v; i < n; ++i) {
std::cin >> u >> v;
g[u].emplace_back(v), g[v].emplace_back(u);
}
for (int i = 1; i <= n; ++i) std::cin >> w[i];
for (int i = 1; i <= n; ++i) {
std::cin >> s[i];
if (s[i]) id.insert(i);
}
dfs1(1, 0);
if (id.empty()) std::cout << "-1\n";
else std::cout << w[*id.begin()] << "\n";
for (int x; m; --m) {
std::cin >> x;
if (s[x]) {
s[x] = 0, id.erase(x);
if (x != 1) son0[fa[x]].insert(x);
} else {
dfs2(x);
if (x != 1) son0[fa[x]].erase(x);
}
if (id.empty()) std::cout << "-1\n";
else std::cout << w[*id.begin()] << "\n";
}
std::cout.flush();
return 0;
}
收获技巧
- 对于规律题可以使用程序找规律。
- 用 \(1\sim n\) 组成的最长等差子序列长度为 \(O(\sqrt{n})\)。
- 要经常考虑将答案转化为每一个数的贡献。
- 自定义集合比较函数时必须十分严谨。

浙公网安备 33010602011771号