模拟赛 5 讲评课件
普及模拟赛 5 讲评课件
T1 舞蹈机器人(dance)
题目大意
机器人在二维平面原点,朝上下左右之一出发。每移动一次后必须左转或右转 \(90^\circ\)(即:若本次是上下移动,下次必须左右移动;反之亦然)。移动 \(n\) 步后,可到达多少个不同的终点?方向不计。
思路分析
每两次移动构成一组「斜向」移动:先水平再垂直(或先垂直再水平),效果是向对角线方向走了一步。
-
当 \(n\) 为偶数时,所有终点构成一个边长为 \(\frac{n}{2}+1\) 的正方形。
答案公式:\((\frac{n}{2}+1)^2\) -
当 \(n\) 为奇数时,可到达的点构成两个长为 \(\frac{n}{2}+2\)、宽为 \(\frac{n}{2}+1\) 的矩形。
答案公式:\(2 \times (\frac{n}{2}+1) \times (\frac{n}{2}+2)\)
时间复杂度:\(O(1)\)
AC 代码
#include <iostream>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n; cin >> n;
if (n % 2 == 0) cout << (n / 2 + 1) * (n / 2 + 1) << "\n";
else cout << (1 + (n + 1) / 2) * (n + 1) << "\n";
return 0;
}
T2 狗是啥呀(dog)
题目大意
有 \(n\) 种武器,每种每次可击毙 \(d_i\) 个头,之后生物会长出 \(h_i\) 个头。武器可无限次使用。生物初始有 \(x\) 个头,头数 \(\le 0\) 时被击败。求最少使用次数,或输出 \(-1\)。
思路分析
分情况讨论:
- 一次击败:若存在 \(d_i \ge x\),则答案为 \(1\)。
- 永远杀不死:若对所有武器都有 \(d_i \le h_i\)(即每次攻击净效果 \(\le 0\)),且不存在 \(d_i \ge x\),则输出 \(-1\)。
- 一般情况:类似「蜗牛爬井」的贪心策略。
- 选择「一次净减少」最大的武器 \(d_i - h_i\) 用于前若干轮(每轮头先减少再增长)。
- 最后一轮选择 \(d_i\) 最大的武器,一击致命。
- 设 \(\text{maxd} = \max d_i\),\(\text{maxk} = \max(d_i - h_i)\)。
- 若 \(x > \text{maxd}\),先使用 \(\lceil \frac{x - \text{maxd}}{\text{maxk}} \rceil\) 次「磨血」武器,再使用一次「斩杀」武器。
- 总次数 \(= \lceil \frac{x - \text{maxd}}{\text{maxk}} \rceil + 1\)。
时间复杂度:\(O(n)\)
AC 代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, x; scanf("%d%d", &n, &x);
int maxd = 0, maxk = -2e9;
for (int i = 1; i <= n; i++) {
int d, h; scanf("%d%d", &d, &h);
maxd = max(maxd, d);
maxk = max(maxk, d - h);
}
if (maxk <= 0) {
if (maxd >= x) puts("1");
else puts("-1");
} else {
if (x <= maxd) puts("1");
else printf("%d\n", (x - maxd + maxk - 1) / maxk + 1);
}
return 0;
}
T3 枢纽(junction)
题目大意
给定 \(n\) 个点 \(m\) 条边的连通无向图,以及两个特殊点 \(a,b\)。统计满足以下条件的二元组 \((u,v)\) 个数:
- \(1 \le u < v \le n\)
- \(u,v \neq a,b\)
- 所有从 \(u\) 到 \(v\) 的路径都经过 \(a\) 和 \(b\)。
核心结论
若去掉 \(a\) 后 \(u,v\) 不连通,且去掉 \(b\) 后 \(u,v\) 也不连通,则 \(u \to v\) 的所有路径必然同时经过 \(a\) 和 \(b\)。
利用这个性质,可设计如下「割点阻断」方法:
将 \(b\) 标记为已访问,从 \(a\) 出发 BFS/DFS,未访问的点即为「\(b\) 右侧的点」,记其数量为 \(s_1\)。
将 \(a\) 标记为已访问,从 \(b\) 出发 BFS/DFS,未访问的点即为「\(a\) 左侧的点」,记其数量为 \(s_2\)。
答案为 \(s_1 \times s_2\)。
20% 数据:\(n \le 10,\ m \le 15\)
思路:直接枚举所有可能的 \((u,v)\) 对。对于每一对,分别检查去掉 \(a\) 后 \(u,v\) 是否连通、去掉 \(b\) 后 \(u,v\) 是否连通。若两者都不连通则计数。
时间复杂度:\(O(n^2 \cdot (n+m))\)
#include <bits/stdc++.h>
using namespace std;
const int N = 15;
vector<int> g[N];
bool vis[N];
void dfs(int u, int block, int target) {
vis[u] = true;
if (u == target) return;
for (int v : g[u]) {
if (v == block) continue;
if (!vis[v]) dfs(v, block, target);
}
}
int main() {
int n, m, a, b; cin >> n >> m >> a >> b;
for (int i = 0; i < m; i++) {
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
int ans = 0;
for (int u = 1; u <= n; u++) {
if (u == a || u == b) continue;
for (int v = u + 1; v <= n; v++) {
if (v == a || v == b) continue;
fill(vis, vis + N, false);
dfs(u, a, v);
bool needA = !vis[v];
fill(vis, vis + N, false);
dfs(u, b, v);
bool needB = !vis[v];
if (needA && needB) ans++;
}
}
cout << ans << endl;
return 0;
}
40% 数据:\(n \le 50,\ m \le 200\)
思路:此时直接使用 BFS 阻断法即可高效求解——标记一个特殊点为已访问,从另一个出发遍历全图,统计未访问点数。
时间复杂度:\(O(n+m)\)
#include <bits/stdc++.h>
using namespace std;
const int N = 55, M = 405;
int h[N], e[M], ne[M], cnt;
bool fl[N];
int n, m, a, b;
void add(int u, int v) {
e[++cnt] = v; ne[cnt] = h[u]; h[u] = cnt;
e[++cnt] = u; ne[cnt] = h[v]; h[v] = cnt;
}
void bfs(int s) {
queue<int> q;
q.push(s); fl[s] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int k = h[u]; k; k = ne[k]) {
int v = e[k];
if (!fl[v]) { fl[v] = 1; q.push(v); }
}
}
}
int main() {
cin >> n >> m >> a >> b;
for (int i = 0; i < m; i++) {
int u, v; cin >> u >> v;
add(u, v);
}
fl[b] = 1; bfs(a);
int s1 = 0;
for (int i = 1; i <= n; i++) if (!fl[i]) s1++;
memset(fl, 0, sizeof(fl));
fl[a] = 1; bfs(b);
int s2 = 0;
for (int i = 1; i <= n; i++) if (!fl[i]) s2++;
cout << 1LL * s1 * s2 << endl;
return 0;
}
另外 20% 数据:\(m = n-1\)(树)
思路:在树中,任意两点之间有且仅有一条简单路径。
以 \(a\) 为根进行 DFS,找出 \(b\) 的子树大小 \(\text{sz}[b]\),以及 \(a\) 的哪个儿子 \(c\) 的子树包含 \(b\)(记其大小为 \(\text{sz}[c]\))。
- 在 \(b\) 的子树中(不含 \(b\))的点,必须经过 \(b\) 才能到达 \(a\) 方向的节点:\(\text{sz}[b] - 1\)。
- 在 \(c\) 的子树之外(不含 \(a\))的点,必须经过 \(a\) 才能到达 \(b\) 方向的节点:\(n - \text{sz}[c] - 1\)。
答案为 \((\text{sz}[b] - 1) \times (n - \text{sz}[c] - 1)\)。
时间复杂度:\(O(n)\)
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
vector<int> g[N];
int sz[N], parent[N];
void dfs(int u, int p) {
parent[u] = p;
sz[u] = 1;
for (int v : g[u]) {
if (v == p) continue;
dfs(v, u);
sz[u] += sz[v];
}
}
int main() {
int n, m, a, b; scanf("%d%d%d%d", &n, &m, &a, &b);
for (int i = 0; i < m; i++) {
int u, v; scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(a, 0);
int cur = b;
while (parent[cur] != a) cur = parent[cur];
long long ans = 1LL * (sz[b] - 1) * (n - sz[cur] - 1);
printf("%lld\n", ans);
return 0;
}
100% 数据:\(n \le 2 \times 10^5,\ m \le 5 \times 10^5\)
思路:BFS/DFS 阻断法,与 40% 做法完全相同,仅放大数组至足够容纳所有点与双向边。
- 建立邻接表,加双向边。
- BFS 从 \(a\) 出发,将 \(b\) 标记为已访问(即禁止通过 \(b\)),统计未访问点数 \(s_1\)。
- 重置标记,BFS 从 \(b\) 出发,将 \(a\) 标记为已访问,统计未访问点数 \(s_2\)。
- 答案 \(= s_1 \times s_2\)。
时间复杂度:\(O(n+m)\)
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
const int M = 1000010;
int h[N], e[M], ne[M], cnt;
bool fl[N];
int n, m, a, b;
void add(int u, int v) {
e[++cnt] = v; ne[cnt] = h[u]; h[u] = cnt;
e[++cnt] = u; ne[cnt] = h[v]; h[v] = cnt;
}
void bfs(int s) {
queue<int> q;
q.push(s); fl[s] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int k = h[u]; k; k = ne[k]) {
int v = e[k];
if (!fl[v]) { fl[v] = 1; q.push(v); }
}
}
}
int main() {
scanf("%d%d%d%d", &n, &m, &a, &b);
for (int i = 0; i < m; i++) {
int u, v; scanf("%d%d", &u, &v);
add(u, v);
}
fl[b] = 1; bfs(a);
long long s1 = 0;
for (int i = 1; i <= n; i++) if (!fl[i]) s1++;
memset(fl, 0, sizeof(fl));
fl[a] = 1; bfs(b);
long long s2 = 0;
for (int i = 1; i <= n; i++) if (!fl[i]) s2++;
printf("%lld\n", s1 * s2);
return 0;
}
T4 魔法药水(potion)
题目大意
有 \(n\) 种材料,第 \(i\) 种魔力为 \(a_i\)。选择若干种混合成药水:
- 选 \(k\) 种材料,药水初始魔力 = \(\sum a_i\),每秒增加 \(k\) 魔力。
- 从 \(0\) 时刻开始,问最早在什么时刻魔力值恰好为 \(m\)?
核心思路
设选取了 \(s\) 种材料,魔力和为 \(\text{sum}\)。在 \(t\) 时刻的魔力值为:
移项得 \(t = \frac{m - \text{sum}}{s}\),需满足 \(m \equiv \text{sum} \pmod s\) 且 \(t \ge 0\)。
要使 \(t\) 最小,在固定 \(s\) 时应使 \(\text{sum}\) 尽可能大。于是问题转化为:
对于每个 \(s\ (1 \le s \le n)\),在 \(n\) 个材料中选恰好 \(s\) 个,使得总和对 \(s\) 取模的值为 \(m \bmod s\),并最大化总和。
20% 数据:\(n \le 2\)
思路:材料极少(最多 2 个),直接枚举所有非空子集(最多 \(2^2 - 1 = 3\) 种),对每种计算最小 \(t\) 取最小值即可。
时间复杂度:\(O(2^n \cdot n)\)
#include <bits/stdc++.h>
using namespace std;
#define int long long
signed main() {
int n, m; cin >> n >> m;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
int ans = m;
for (int mask = 1; mask < (1 << n); mask++) {
int sum = 0, cnt = 0;
for (int i = 0; i < n; i++) {
if (mask >> i & 1) {
sum += a[i];
cnt++;
}
}
if ((m - sum) >= 0 && (m - sum) % cnt == 0)
ans = min(ans, (int)((m - sum) / cnt));
}
cout << ans << endl;
return 0;
}
50% 数据:\(n \le 20\)
思路:仍然枚举子集,但 \(n\) 可到 \(20\),总子集数 \(2^{20} = 1\,048\,576\),枚举所有子集可行。
时间复杂度:\(O(2^n \cdot n)\)
#include <bits/stdc++.h>
using namespace std;
#define int long long
signed main() {
int n, m; cin >> n >> m;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
int ans = m;
for (int mask = 1; mask < (1LL << n); mask++) {
int sum = 0, cnt = 0;
for (int i = 0; i < n; i++) {
if (mask >> i & 1) {
sum += a[i];
cnt++;
}
}
if ((m - sum) >= 0 && (m - sum) % cnt == 0)
ans = min(ans, (int)((m - sum) / cnt));
}
cout << ans << endl;
return 0;
}
70% 数据:\(n \le 50\)
思路:不能用指数级枚举了,需要用动态规划。
设 \(dp[i][j][k]\) 表示在前 \(i\) 个材料中恰好选 \(j\) 个,且选出的材料魔力之和 \(\bmod\ s = k\) 时,能达到的最大 \(sum\) 值。
转移方程:
其中第二项仅在 \(dp[i-1][j-1][(k - a_i) \bmod s] \neq -1\)(即该状态可达)时考虑。
对每个 \(s\) 独立做一次 DP,最后答案为:
时间复杂度:\(O(n^4)\),当 \(n=50\) 时约 \(2 \times 10^6\) 次操作,非常快。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 55;
int a[N], dp[N][N][N];
signed main() {
int n, m; cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
int ans = m;
for (int s = 1; s <= n; s++) {
memset(dp, -1, sizeof(dp));
dp[0][0][0] = 0;
for (int i = 1; i <= n; i++) {
dp[i][0][0] = 0;
for (int j = 1; j <= s; j++) {
for (int k = 0; k < s; k++) {
dp[i][j][k] = dp[i - 1][j][k];
int pre = ((k - a[i]) % s + s) % s;
if (dp[i - 1][j - 1][pre] != -1)
dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - 1][pre] + a[i]);
}
}
}
if (dp[n][s][m % s] != -1)
ans = min(ans, (m - dp[n][s][m % s]) / s);
}
cout << ans << endl;
return 0;
}
100% 数据:\(n \le 100\)
思路:与 70% 做法完全相同,仅将数组大小开至 \(105\),\(O(n^4)\) 在 \(n=100\) 时约为 \(3.3 \times 10^7\) 次操作,C++ 在 O2 优化下可轻松通过。
时间复杂度:\(O(n^4)\)
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 105;
int a[N], dp[N][N][N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m; cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
int ans = m;
for (int s = 1; s <= n; s++) {
memset(dp, -1, sizeof(dp));
dp[0][0][0] = 0;
for (int i = 1; i <= n; i++) {
dp[i][0][0] = 0;
for (int j = 1; j <= s; j++) {
for (int k = 0; k < s; k++) {
dp[i][j][k] = dp[i - 1][j][k];
int pre = ((k - a[i]) % s + s) % s;
if (dp[i - 1][j - 1][pre] != -1)
dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - 1][pre] + a[i]);
}
}
}
if (dp[n][s][m % s] != -1)
ans = min(ans, (m - dp[n][s][m % s]) / s);
}
cout << ans << endl;
return 0;
}
以上就是本次模拟赛的完整讲评。祝大家 CSP-J 取得好成绩!
浙公网安备 33010602011771号