模拟赛 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\)

思路分析

分情况讨论:

  1. 一次击败:若存在 \(d_i \ge x\),则答案为 \(1\)
  2. 永远杀不死:若对所有武器都有 \(d_i \le h_i\)(即每次攻击净效果 \(\le 0\)),且不存在 \(d_i \ge x\),则输出 \(-1\)
  3. 一般情况:类似「蜗牛爬井」的贪心策略。
    • 选择「一次净减少」最大的武器 \(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% 做法完全相同,仅放大数组至足够容纳所有点与双向边。

  1. 建立邻接表,加双向边。
  2. BFS 从 \(a\) 出发,将 \(b\) 标记为已访问(即禁止通过 \(b\)),统计未访问点数 \(s_1\)
  3. 重置标记,BFS 从 \(b\) 出发,将 \(a\) 标记为已访问,统计未访问点数 \(s_2\)
  4. 答案 \(= 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\) 时刻的魔力值为:

\[\text{sum} + s \times t = m \]

移项得 \(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][j][k] = \max(dp[i-1][j][k],\ dp[i-1][j-1][(k - a_i) \bmod s] + a_i) \]

其中第二项仅在 \(dp[i-1][j-1][(k - a_i) \bmod s] \neq -1\)(即该状态可达)时考虑。

对每个 \(s\) 独立做一次 DP,最后答案为:

\[\min_{1 \le s \le n} \frac{m - dp[n][s][m \bmod s]}{s} \]

时间复杂度\(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 取得好成绩!

posted @ 2026-07-14 14:25  surprise_ying  阅读(27)  评论(0)    收藏  举报