• C++
  • CSP-J历年复赛解析

  • @ 2025-7-17 15:11:54

历年CSP-J组第二轮题目完整解析

[CSP-J2019] 数字游戏

题目链接:P5660 [CSP-J2019] 数字游戏

解题代码

// 题意:统计一个长度为8的01字符串中'1'的个数。

#include<bits/stdc++.h>

using namespace std;
using ll=long long;

int sol() {
    string s;
    cin >> s;
    
    int ans = 0;
    
    // 遍历字符串中的每个字符
    for (char c : s) {
        if (c == '1') {
            ans++;
        }
    }
    
    cout << ans << endl;
    
    return 0;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    
    sol();
    
    return 0;
}

[CSP-J2019] 公交换乘

题目链接:P5661 [CSP-J2019] 公交换乘

解题代码

// 题意概括:
// 模拟地铁和公交的计费过程。
// 坐地铁会得到一张有时效(45分钟)和票价限制(公交票价不高于地铁票价)的优惠票。
// 坐公交时,如果有多张可用优惠票,优先使用获得时间最早的那一张。
// 求总花费。

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

queue<int> q[1005]; // q[i] 存储价格为 i 的地铁票的乘车时间
ll ans;
int n;

void sol() {
    cin >> n;

    for (int i = 0; i < n; ++i) {
        int op, p, t;
        cin >> op >> p >> t;

        if (op == 0) { // 地铁
            ans += p;
            q[p].push(t);
        } else { // 公交
            int fnd = -1; // 记录找到的可用票对应的价格
            int get = 2e9; // 记录找到的可用票中最早的出发时间

            // 从 p 到 1000 遍历所有价格足够的票
            for (int j = p; j <= 1000; ++j) {
                // 清除该价格队列中过期的票
                while (!q[j].empty() && t - q[j].front() > 45) {
                    q[j].pop();
                }

                // 如果该价格有票,且其时间戳比当前找到的更早,则更新
                if (!q[j].empty() && q[j].front() < get) {
                    get = q[j].front();
                    fnd = j;
                }
            }

            if (fnd != -1) { // 找到了可用的票
                q[fnd].pop(); // 使用这张票
            } else { // 未找到
                ans += p; // 付费
            }
        }
    }

    cout << ans << endl;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    sol();

    return 0;
}

[CSP-J2019] 纪念品

题目链接:P5662 [CSP-J2019] 纪念品

解题代码

#include<bits/stdc++.h>

using namespace std;
using ll=long long;

// 题意:T天,N种物品,初始有M元钱。
// 每天可以无限次买卖任何物品。
// 求T天后最多能有多少钱。
// 这可以看作一个每日的完全背包问题。
// 对于第 i 天,我们拥有 m 元,考虑如何投资能在第 i+1 天获得最多的钱。
// 将第 i 天的价格视为物品的“花费”,第 i+1 天的价格视为物品的“价值”。
// dp[k] 表示用 k 元钱在第 i 天买入,在第 i+1 天卖出后能拥有的最大金币数。

int t, n, m;
int p[105][105];
int dp[10005];

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    cin >> t >> n >> m;

    for (int i = 0; i < t; ++i) {
        for (int j = 0; j < n; ++j) {
            cin >> p[i][j];
        }
    }

    // 从第一天循环到倒数第二天
    for (int i = 0; i < t - 1; ++i) {
        
        // 初始化dp数组,不买任何东西的话,钱不变
        for (int k = 0; k <= m; ++k) {
            dp[k] = k;
        }

        // 对每一种纪念品进行完全背包计算
        for (int j = 0; j < n; ++j) {
            // k 代表当前拥有的钱(背包容量)
            for (int k = p[i][j]; k <= m; ++k) {
                dp[k] = max(dp[k], dp[k - p[i][j]] + p[i+1][j]);
            }
        }
        
        // 用今天所有钱m能换到的最大价值,更新为明天的钱
        m = dp[m];
    }

    cout << m << endl;

    return 0;
}

[CSP-J2019] 加工零件

题目链接:P5663 [CSP-J2019] 加工零件

解题代码

#include<bits/stdc++.h>

using namespace std;
using ll=long long;

// 题意:一个n个点m条边的无向图。
// q次询问,每次问a点生产L阶段零件,1号点是否需要提供原材料。
// 分析可知,a点生产L阶段零件,需要所有存在一条长度为L的路径可达的节点提供原材料。
// 这里的“路径”指的可以经过重复点和边的"walk"。
// 存在长度为L的walk,等价于 最短路d<=L 且 d和L奇偶性相同。
// 这是因为我们总可以在一条边上来回走,每次使路径长度增加2。
// 因此问题转化为,求1号点到各点a的最短奇数长路径和最短偶数长路径。
// 用一个BFS即可解决。BFS队列中的状态为(当前节点, 路径长度奇偶性)。

const int N = 100005;

int n, m, q;
vector<int> g[N];
int de[N];   // 1号点到各点的偶数最短路
int doo[N];  // 1号点到各点的奇数最短路

void bfs() {
    memset(de, -1, sizeof(de));
    memset(doo, -1, sizeof(doo));
    queue<pair<int, int>> qu;

    de[1] = 0;
    qu.push({1, 0}); // {节点, 奇偶性}, 0代表偶数

    while (!qu.empty()) {
        auto [u, p] = qu.front();
        qu.pop();

        int cst;
        if (p == 0) cst = de[u];
        else cst = doo[u];
        
        for (int v : g[u]) {
            if (p == 0) { // 从偶数路径点u出发,到达v的路径为奇数
                if (doo[v] == -1) {
                    doo[v] = cst + 1;
                    qu.push({v, 1});
                }
            } else { // 从奇数路径点u出发,到达v的路径为偶数
                if (de[v] == -1) {
                    de[v] = cst + 1;
                    qu.push({v, 0});
                }
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    cin >> n >> m >> q;
    for (int i = 0; i < m; ++i) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    bfs();

    while (q--) {
        int a;
        ll l;
        cin >> a >> l;

        bool ok = false;
        if (l % 2 == 0) { // 需要偶数长度的路径
            if (de[a] != -1 && de[a] <= l) {
                ok = true;
            }
        } else { // 需要奇数长度的路径
            if (doo[a] != -1 && doo[a] <= l) {
                ok = true;
            }
        }

        if (ok) {
            cout << "Yes\n";
        } else {
            cout << "No\n";
        }
    }

    return 0;
}

[CSP-J2019 江西] 面积

题目链接:P5681 [CSP-J2019 江西] 面积

解题代码

// 题意:比较边长为a的正方形和长宽为b,c的矩形的面积大小。

#include<bits/stdc++.h>

using namespace std;
using ll=long long;

void sol() {
    ll a, b, c;
    cin >> a >> b >> c;
    
    // 由于a, b, c最大可达10^9,面积会超过int范围,需要使用long long
    ll s_a = a * a;
    ll s_b = b * c;
    
    if (s_a > s_b) {
        cout << "Alice" << endl;
    } else {
        cout << "Bob" << endl;
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    
    sol();
    
    return 0;
}

[CSP-J2019 江西] 次大值

题目链接:P5682 [CSP-J2019 江西] 次大值

解题代码

/*
 * 题意:给定n个正整数,计算所有 a_i % a_j (i!=j) 的值,去重后求严格次大值。
 *
 * 思路:
 * 对输入数组去重并排序,得到递增序列 u, 大小为 m。
 * 如果 m <= 1, 结果数量不足两个,输出 -1。
 *
 * 所有 a_i % a_j 的结果中:
 * 1. 当 a_i < a_j 时,结果为 a_i。这部分能产生的最大值是 u[m-2] (通过 u[m-2] % u[m-1] 得到)。
 * 2. 当 a_i > a_j 时,结果为 a_i % a_j,该值一定小于 a_j,从而小于 u[m-1]。
 * 同时,a_i % a_j < a_j <= u[m-2]。
 * 综上,所有结果中的最大值就是 u[m-2]。
 *
 * 寻找次大值,它由两类候选者决定:
 * 1. a_i < a_j 情况下的次大值,即 u[m-3] (前提是 m >= 3)。
 * 2. a_i > a_j 情况下的最大值。为使 a_i % a_j 最大,a_i 应取最大,即 u[m-1]。
 * 所以需要计算 max(u[m-1] % u[i]) 对于所有 i < m-1。
 *
 * 最终答案就是这两类候选者的最大值。
 */
#include <bits/stdc++.h>

using namespace std;
using ll = long long;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n;
    cin >> n;

    vector<int> a(n);
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
    }

    sort(a.begin(), a.end());
    a.erase(unique(a.begin(), a.end()), a.end()); // 排序去重

    int m = a.size();

    if (m <= 1) {
        cout << -1 << endl;
        return 0;
    }

    int res = 0;

    // 候选1: 来自 a_i < a_j 的次大值
    if (m >= 3) {
        res = a[m - 3];
    }

    // 候选2: 来自 a_i > a_j 的最大值
    int M = a[m - 1];
    for (int i = 0; i < m - 1; ++i) {
        res = max(res, M % a[i]);
    }

    cout << res << endl;

    return 0;
}

[CSP-J2019 江西] 道路拆除

题目链接:P5683 [CSP-J2019 江西] 道路拆除

解题代码

// 题意:给定一个n点m边的无权无向图,以及两个目标点s1, s2和对应的最大距离t1, t2。
// 问最多可以拆除多少条边,使得首都1到s1和s2的最短路分别不超过t1和t2。
// 这等价于求满足条件所需的最少保留边数,然后用总边数m减去它。

#include<bits/stdc++.h>

using namespace std;
using ll=long long;

const int N = 3005;
const int INF = 1e9;

int n, m;
int s1, t1, s2, t2;
vector<int> adj[N];
int d0[N], da[N], db[N]; // 分别存储从1, s1, s2出发的距离

// 广度优先搜索,计算单源最短路
void bfs(int s, int d[]) {
    for (int i = 1; i <= n; ++i) {
        d[i] = INF;
    }
    
    queue<int> q;
    q.push(s);
    d[s] = 0;

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int v : adj[u]) {
            if (d[v] == INF) {
                d[v] = d[u] + 1;
                q.push(v);
            }
        }
    }
}

void sol() {
    cin >> n >> m;
    for (int i = 0; i < m; ++i) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    cin >> s1 >> t1 >> s2 >> t2;

    // 预处理出三个源点的最短路
    bfs(1, d0);
    bfs(s1, da);
    bfs(s2, db);
    
    int res = INF;

    // 遍历所有点i,假设它是1到s1和1到s2路径的汇合点
    for (int i = 1; i <= n; ++i) {
        ll p1 = (ll)d0[i] + da[i]; // 路径1->i->s1的长度
        ll p2 = (ll)d0[i] + db[i]; // 路径1->i->s2的长度

        if (p1 <= t1 && p2 <= t2) {
            // 如果两条路径都满足时间限制,更新所需的最少边数
            int cnt = d0[i] + da[i] + db[i];
            res = min(res, cnt);
        }
    }

    if (res == INF) {
        cout << -1 << endl;
    } else {
        cout << m - res << endl;
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    sol();

    return 0;
}

[CSP-J2019 江西] 非回文串

题目链接:P5684 [CSP-J2019 江西] 非回文串

解题代码

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

/*
 * 题意:
 * 给定 n 个小写字母,问有多少种排列方案,使得组成的字符串不是回文串。
 * 方案不同指的是排列的下标序列不同。
 *
 * 解法:
 * 正难则反。总方案数减去回文串的方案数。
 *
 * 1. 总方案数:
 * n 个字符,下标有 1~n。对下标进行全排列,共有 n! 种不同的方案。
 *
 * 2. 回文串方案数:
 * 首先,判断是否可能构成回文串。
 * - 如果 n 是偶数,所有字符的出现次数必须都是偶数。
 * - 如果 n 是奇数,有且仅有一个字符的出现次数是奇数。
 * 如果不满足上述条件,则无法构成回文串,回文方案数为 0。
 *
 * 如果满足条件,我们来计算能构成回文串的方案数。
 * 这个问题的核心是将问题分解为两部分:
 * a. 可以构成多少种不同的回文串?
 * b. 对于每一种回文串,有多少种下标排列方案可以得到它?
 *
 * 对于 a):
 * 一个回文串由其前一半决定。设 h = n / 2。
 * 前一半的字符串,是由每种字符数量的一半构成的。
 * 例如,总共有 cnt['a'] 个 'a',那么前一半就需要 cnt['a']/2 个 'a'。
 * 所以,前一半是一个长度为 h 的多重集排列。
 * 不同的前一半字符串数量为 h! / ( (cnt['a']/2)! * (cnt['b']/2)! * ... )。
 * 这就是不同回文串的数量。
 *
 * 对于 b):
 * 对于一个确定的目标串 S (比如 "abccba"),我们要从原始的 n 个字符 c_1, ..., c_n 中,
 * 选择对应的字符来填充。假设 S 需要 k_a 个 'a', k_b 个 'b' ...
 * 我们原始字符中,'a' 有 cnt['a'] 个,'b' 有 cnt['b'] 个 ...
 * 将 cnt['a'] 个 'a' 安排到 S 中 k_a 个 'a' 的位置上,有 cnt['a']! 种方案。
 * 所以总共有 cnt['a']! * cnt['b']! * ... 种方案。
 *
 * 因此,回文串方案数 = (不同回文串的数量) * (每种串的构成方案数)。
 * P_pal = (h! / (Π (cnt[c]/2)!)) * (Π cnt[c]!)
 *
 * 3. 最终答案:
 * (n! - P_pal) mod (10^9 + 7)。
 * 所有计算都在模意义下进行,需要预处理阶乘和阶乘逆元。
 */

const int N = 2005;
const int mod = 1e9 + 7;

ll fac[N];
ll ifac[N];
int cnt[26];

// 快速幂
ll qpo(ll a, ll b) {
    ll res = 1;
    a %= mod;
    while (b) {
        if (b & 1) res = (res * a) % mod;
        a = (a * a) % mod;
        b >>= 1;
    }
    return res;
}

// 预处理阶乘和逆元
void pre(int n) {
    fac[0] = 1;
    for (int i = 1; i <= n; i++) {
        fac[i] = (fac[i - 1] * i) % mod;
    }

    ifac[n] = qpo(fac[n], mod - 2);
    for (int i = n - 1; i >= 0; i--) {
        ifac[i] = (ifac[i + 1] * (i + 1)) % mod;
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n;
    cin >> n;
    string s;
    cin >> s;

    for (char c : s) {
        cnt[c - 'a']++;
    }

    pre(n);

    int odd = 0;
    for (int i = 0; i < 26; i++) {
        if (cnt[i] % 2 != 0) {
            odd++;
        }
    }

    ll pal = 0;
    bool ok = false;
    // 检查是否可能构成回文串
    if (n % 2 == 0) {
        if (odd == 0) ok = true;
    } else {
        if (odd == 1) ok = true;
    }

    if (ok) {
        int h = n / 2;
        ll p_s = fac[h]; // 计算不同回文串数量
        for (int i = 0; i < 26; i++) {
            p_s = (p_s * ifac[cnt[i] / 2]) % mod;
        }

        ll ways = 1; // 计算每种串的构成方案数
        for (int i = 0; i < 26; i++) {
            ways = (ways * fac[cnt[i]]) % mod;
        }

        pal = (p_s * ways) % mod;
    }

    ll tot = fac[n];
    ll ans = (tot - pal + mod) % mod;
    cout << ans << endl;

    return 0;
}

[CSP-J2020] 优秀的拆分

题目链接:P7071 [CSP-J2020] 优秀的拆分

解题代码

// 题意概括:
// 将一个正整数 n 拆分成若干个不同的 2 的正整数次幂之和。
// 如果 n 是奇数,其二进制表示必然包含 2^0=1,不满足“正整数次幂”的要求,无解。
// 如果 n 是偶数,其唯一的二进制表示就是答案。
// 从大到小输出拆分出的数。

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

int n;
vector<int> ans;

void sol() {
    cin >> n;

    if (n % 2 != 0) { // 奇数必须包含 2^0,不符合题意,无解
        cout << -1 << endl;
        return;
    }
    
    // 从大到小贪心选取 2 的幂
    for (int p = 1 << 24; p >= 2; p /= 2) {
        if (n >= p) {
            ans.push_back(p);
            n -= p;
        }
    }

    for (int i = 0; i < ans.size(); ++i) {
        cout << ans[i] << (i == ans.size() - 1 ? "" : " ");
    }
    cout << endl;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    sol();

    return 0;
}

[CSP-J2020] 直播获奖

题目链接:P7072 [CSP-J2020] 直播获奖

解题代码

// 题意简述:
// 逐一给出n个选手的成绩,在每次给出成绩后,计算当前的获奖分数线并输出。
// 获奖率为w%,获奖人数为 max(1, floor(p * w/100)),p为当前已公布成绩的人数。
// 分数线为排名为获奖人数的选手的成绩。

// 解题思路:
// 选手成绩范围为0到600,值域很小。
// 可以用一个大小为601的桶(数组)来记录每个分数出现的次数。
// 每次读入新成绩,对应桶的计数加一。
// 然后计算出当前获奖人数k。
// 从高分到低分(600到0)遍历桶,累加人数,
// 当累计人数第一次大于等于k时,当前分数即为分数线。
// 时间复杂度为O(n * 分数范围),可以通过此题。

#include<bits/stdc++.h>

using namespace std;
using ll=long long;

const int S = 601;

int n, w;
int cnt[S];

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    cin >> n >> w;

    for (int i = 1; i <= n; ++i) {
        int s;
        cin >> s;
        cnt[s]++;

        int k = max(1, i * w / 100);
        int sum = 0;

        for (int j = 600; j >= 0; --j) {
            sum += cnt[j];
            if (sum >= k) {
                cout << j << " ";
                break; // 找到分数线
            }
        }
    }

    cout << endl;

    return 0;
}

[CSP-J2020] 表达式

题目链接:P7073 [CSP-J2020] 表达式

解题代码

/*
 * 题意: 给定一个后缀表达式和变量初值,多次询问翻转某个变量后表达式的值。
 *
 * 思路:
 * 暴力地对每次询问都重新计算表达式的值会超时。
 * 核心思想是预处理出每个变量的改变是否会影响最终结果。
 * 1. 建树 + 初值计算: 后缀表达式可以很方便地建成一棵表达式树。
 * 在建树的同时,利用栈进行计算,求出每个子表达式(节点)的初始值。
 * 2. 影响传递分析: 从根节点开始进行一次深度优先搜索(DFS)。
 * 根节点的结果就是最终结果,所以根节点的改变一定会影响最终结果(标记为有影响)。
 * 对于一个有影响的父节点P,分析其子节点C的影响:
 * - 若P是 '!',则C的改变必然导致P的改变,所以C也有影响。
 * - 若P是 '&' 或 '|',C的影响取决于其兄弟节点S的值。
 * 例如,对于 A&B,若B为1,则A的改变会影响结果;若B为0,则结果恒为0,A无影响。
 * 或运算同理。
 * 这样一次DFS就可以预处理出所有变量节点的影响情况。
 * 3. 回答询问: 对于每个询问,查询对应变量节点的影响标记。
 * 若有影响,输出与初始表达式结果相反的值;否则输出初始结果。
 * 每次询问的时间复杂度为O(1)。
 */
#include <bits/stdc++.h>

using namespace std;
using ll = long long;

// 表达式树节点
struct N {
    int op; // 0:变量, 1:!, 2:&, 3:|
    int v;  // 节点的值
    bool f; // 改变此节点是否影响最终结果
    int l, r; // 左右子节点索引
    int vid; // 如果是变量节点,其下标
};

vector<N> t; // 存储表达式树
map<int, int> vtn; // 变量下标 -> 树中节点索引

// DFS计算每个节点的影响
void dfs(int u) {
    if (t[u].l == -1) { // 叶子节点
        return;
    }

    if (t[u].f) { // 只有当父节点有影响时,子节点才可能产生影响
        int c1 = t[u].l;
        int c2 = t[u].r;

        if (t[u].op == 1) { // '!'
            t[c1].f = true;
        } else if (t[u].op == 2) { // '&'
            if (t[c2].v == 1) t[c1].f = true; // a&1, a有影响
            if (t[c1].v == 1) t[c2].f = true; // 1&b, b有影响
        } else if (t[u].op == 3) { // '|'
            if (t[c2].v == 0) t[c1].f = true; // a|0, a有影响
            if (t[c1].v == 0) t[c2].f = true; // 0|b, b有影响
        }
    }

    dfs(t[u].l);
    if (t[u].r != -1) {
        dfs(t[u].r);
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    string s;
    getline(cin, s);

    vector<string> tok;
    string tmp;
    for (char c : s) {
        if (c == ' ') {
            if (!tmp.empty()) tok.push_back(tmp);
            tmp.clear();
        } else {
            tmp += c;
        }
    }
    if (!tmp.empty()) tok.push_back(tmp);

    int n;
    cin >> n;
    vector<int> iv(n + 1);
    for (int i = 1; i <= n; ++i) cin >> iv[i];

    stack<int> st;
    for (const auto& T : tok) {
        if (T[0] == 'x') {
            int id = stoi(T.substr(1));
            t.push_back({0, iv[id], false, -1, -1, id});
            vtn[id] = t.size() - 1;
            st.push(t.size() - 1);
        } else {
            int c1 = st.top(); st.pop();
            if (T[0] == '!') {
                t.push_back({1, !t[c1].v, false, c1, -1, 0});
            } else {
                int c2 = st.top(); st.pop();
                swap(c1, c2); // 后缀表达式先弹出的是右操作数
                if (T[0] == '&') {
                    t.push_back({2, t[c1].v & t[c2].v, false, c1, c2, 0});
                } else { // '|'
                    t.push_back({3, t[c1].v | t[c2].v, false, c1, c2, 0});
                }
            }
            st.push(t.size() - 1);
        }
    }

    int rt = st.top();
    int ir = t[rt].v;

    t[rt].f = true; // 根节点总是有影响的
    dfs(rt);

    int qn;
    cin >> qn;
    while (qn--) {
        int i;
        cin >> i;
        cout << (t[vtn[i]].f ? !ir : ir) << "\n";
    }

    return 0;
}

[CSP-J2020] 方格取数

题目链接:P7074 [CSP-J2020] 方格取数

解题代码

/*
 * 题意:
 * 在一个 n x m 的方格图中,从左上角 (1,1) 走到右下角 (n,m)。
 * 每一步可以向右、向上或向下,不能重复经过同一个格子。
 * 求经过格子中的整数之和的最大值。
 *
 * 思路:
 * 动态规划。dp[i][j][k] 表示到达 (i,j) 的最大和。
 * k=0: 从左边 (i, j-1) 到达。
 * k=1: 从上边 (i-1, j) 到达。
 * k=2: 从下边 (i+1, j) 到达。
 * 按列计算,防止回头路。
*/

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

const int MAX = 1005;
const ll NINF = -1e18; // 代表负无穷

int n, m;
int a[MAX][MAX];
ll dp[MAX][MAX][3];

void slv() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            cin >> a[i][j];
        }
    }

    // 初始化dp数组
    for (int i = 0; i <= n + 1; ++i) {
        for (int j = 0; j <= m + 1; ++j) {
            dp[i][j][0] = dp[i][j][1] = dp[i][j][2] = NINF;
        }
    }

    // 虚拟起点在 (1,0),分数为0
    dp[1][0][0] = 0;

    for (int j = 1; j <= m; ++j) {
        // k=0: 从左边过来
        for (int i = 1; i <= n; ++i) {
            ll p = max({dp[i][j - 1][0], dp[i][j - 1][1], dp[i][j - 1][2]});
            if (p > NINF) {
                dp[i][j][0] = p + a[i][j];
            }
        }

        // k=1: 从上面过来 (向下走)
        for (int i = 1; i <= n; ++i) {
            ll p = max(dp[i - 1][j][0], dp[i - 1][j][1]);
            if (p > NINF) {
                dp[i][j][1] = p + a[i][j];
            }
        }

        // k=2: 从下面过来 (向上走)
        for (int i = n; i >= 1; --i) {
            ll p = max(dp[i + 1][j][0], dp[i + 1][j][2]);
            if (p > NINF) {
                dp[i][j][2] = p + a[i][j];
            }
        }
    }
    
    // 终点 (n,m) 不可能从下方(n+1,m)到达
    ll ans = max(dp[n][m][0], dp[n][m][1]);

    cout << ans << endl;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    slv();

    return 0;
}

[CSP-J 2021] 分糖果

题目链接:P7909 [CSP-J 2021] 分糖果

解题代码

// 题意概括:
// 在 [L, R] 区间内选择一个整数 k,使得 k % n 的值最大。
// k % n 的值随 k 的增加而周期性地从 0 递增到 n-1。
// 如果 [L, R] 区间跨越了 n 的某个倍数,那么这个区间内一定可以取到余数为 n-1 的 k。
// 否则,该区间内 k % n 的值是单调递增的,最大值在 k=R 时取到。
// 判断是否跨越 n 的倍数,只需比较 L/n 和 R/n 的整数商是否相等。

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

ll n, l, r;

void sol() {
    cin >> n >> l >> r;

    // 如果 L 和 R 除以 n 的整数商相同,说明它们在同一个长度为 n 的区间内
    // 在这种情况下,余数是单调递增的,因此最大余数在 k=R 时取得
    if (l / n == r / n) {
        cout << r % n << endl;
    } else {
        // 如果商不同,说明区间 [L, R] 至少跨越了一个 n 的倍数
        // 这意味着区间内必然存在一个数 k,使得 k % n = n - 1,这是最大可能的余数
        cout << n - 1 << endl;
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    sol();

    return 0;
}

[CSP-J 2021] 插入排序

题目链接:P7910 [CSP-J 2021] 插入排序

解题代码

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

const int N = 8005;
const int B = 450; // 块大小

int n, q;
ll a[N];

struct Op {
    int t, x;
    ll v;
};

struct Elem {
    ll v;
    int p;

    bool operator<(const Elem& o) const {
        if (v != o.v) return v < o.v;
        return p < o.p;
    }
};

Elem sa[N];
Op ops[200005];

void sol() {
    for (int i = 0; i < q; i += B) {
        int end = min(i + B, q);

        for (int j = 1; j <= n; ++j) {
            sa[j] = {a[j], j};
        }
        sort(sa + 1, sa + n + 1);

        for (int j = i; j < end; ++j) {
            if (ops[j].t == 2) {
                int qp = ops[j].x;
                ll qv = a[qp];
                
                // 确定查询时的真实值
                for (int k = i; k < j; ++k) {
                    if (ops[k].t == 1 && ops[k].x == qp) {
                        qv = ops[k].v;
                    }
                }
                
                Elem qe = {qv, qp};

                // 在块开始时的数组中计算排名
                auto it = lower_bound(sa + 1, sa + n + 1, qe);
                ll ans = (it - (sa + 1));
                
                map<int, ll> chg;
                
                // 根据块内修改进行修正
                for (int k = i; k < j; ++k) {
                    if (ops[k].t == 1) {
                        int p = ops[k].x;
                        ll old_v, new_v;
                        
                        new_v = ops[k].v;

                        // 找到修改前的旧值
                        if (chg.count(p)) {
                            old_v = chg[p];
                        } else {
                            old_v = a[p];
                        }
                        
                        if ((Elem{old_v, p}) < qe) {
                            ans--;
                        }
                        if ((Elem{new_v, p}) < qe) {
                            ans++;
                        }
                        
                        chg[p] = new_v;
                    }
                }
                cout << ans + 1 << "\n";
            }
        }
        
        // 应用本块的全部修改
        for (int j = i; j < end; ++j) {
            if (ops[j].t == 1) {
                a[ops[j].x] = ops[j].v;
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    cin >> n >> q;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
    }
    
    for (int i = 0; i < q; ++i) {
        cin >> ops[i].t >> ops[i].x;
        if (ops[i].t == 1) {
            cin >> ops[i].v;
        }
    }
    
    sol();

    return 0;
}

[CSP-J 2021] 网络连接

题目链接:P7911 [CSP-J 2021] 网络连接

解题代码

/*
 * 题意:
 * 模拟 n 个网络连接操作。计算机分为服务机(Server)和客户机(Client)。
 * 每个操作提供一个类型和一个地址串。
 * 需要先验证地址串 `a.b.c.d:e` 的格式是否规范:
 * 1. a,b,c,d 在 [0, 255] 内, e 在 [0, 65535] 内。
 * 2. 均不能有前导零 (除非是单个0)。
 * 3. 格式严格,不多不少3个'.'和1个':',且'.'在':'之前。
 *
 * 如果地址串非法,输出 ERR。
 *
 * 如果地址合法:
 * - Server:
 * - 若地址未被占用,则成功建立,输出 OK,记录该地址和编号。
 * - 若地址已被占用,则失败,输出 FAIL。
 * - Client:
 * - 若地址存在已成功的Server,则连接成功,输出该Server的编号。
 * - 若地址不存在对应Server,则连接失败,输出 FAIL。
 *
 * 解法:
 * 这是一个纯模拟题。核心是编写一个函数来验证地址串的合法性。
 * 我们可以用一个 map<string, int> 来存储已成功建立连接的服务机地址及其编号。
 * 循环 n 次,对每个操作:
 * 1. 调用地址验证函数。若非法,输出 ERR。
 * 2. 若合法,根据是 Server 还是 Client 进行处理:
 * - Server: 查 map。若存在,输出 FAIL。否则,输出 OK 并将地址和编号存入 map。
 * - Client: 查 map。若存在,输出其编号。否则,输出 FAIL。
 *
 * 地址验证函数可以手动解析字符串,查找3个'.'和1个':'的位置,分割出5个数字字符串,
 * 然后对每个数字字符串检查前导零、纯数字、范围等。
 */

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

// 验证地址串的一个部分
bool vpc(string s, int lim) {
    if (s.empty()) return false;
    for (char c : s) {
        if (!isdigit(c)) return false;
    }
    if (s.length() > 1 && s[0] == '0') return false;
    
    ll val = 0;
    for (char c : s) {
        val = val * 10 + (c - '0');
        if (val > lim) return false; // 防止溢出和超限
    }

    return val <= lim;
}

// 验证整个地址串
bool vld(string ad) {
    vector<int> dts;
    int cln = -1;

    for (int i = 0; i < ad.length(); ++i) {
        if (ad[i] == '.') {
            dts.push_back(i);
        } else if (ad[i] == ':') {
            if (cln != -1) return false; // 多个冒号
            cln = i;
        }
    }

    if (dts.size() != 3 || cln == -1) return false;

    // 保证分隔符顺序正确
    if (!(dts[0] < dts[1] && dts[1] < dts[2] && dts[2] < cln)) return false;
    
    string s1 = ad.substr(0, dts[0]);
    string s2 = ad.substr(dts[0] + 1, dts[1] - dts[0] - 1);
    string s3 = ad.substr(dts[1] + 1, dts[2] - dts[1] - 1);
    string s4 = ad.substr(dts[2] + 1, cln - dts[2] - 1);
    string s5 = ad.substr(cln + 1);

    if (!vpc(s1, 255)) return false;
    if (!vpc(s2, 255)) return false;
    if (!vpc(s3, 255)) return false;
    if (!vpc(s4, 255)) return false;
    if (!vpc(s5, 65535)) return false;

    return true;
}

void sol(int n) {
    map<string, int> srv;

    for (int i = 1; i <= n; ++i) {
        string op, ad;
        cin >> op >> ad;

        if (!vld(ad)) {
            cout << "ERR\n";
            continue;
        }

        if (op[0] == 'S') { // Server
            if (srv.count(ad)) {
                cout << "FAIL\n";
            } else {
                srv[ad] = i;
                cout << "OK\n";
            }
        } else { // Client
            if (srv.count(ad)) {
                cout << srv[ad] << "\n";
            } else {
                cout << "FAIL\n";
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n;
    cin >> n;
    sol(n);

    return 0;
}

[CSP-J 2021] 小熊的果篮

题目链接:P7912 [CSP-J 2021] 小熊的果篮

解题代码

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

const int MAX = 2e5 + 5;

int n;
int v[MAX];
int l[MAX], r[MAX];
bool b[MAX];

void sol() {
    vector<int> c;
    v[0] = -1; // 哨兵, 方便处理边界
    for (int i = 1; i <= n; ++i) {
        l[i] = i - 1;
        r[i] = i + 1;
        if (v[i] != v[i - 1]) {
            c.push_back(i);
        }
    }
    r[n] = 0;

    while (!c.empty()) {
        sort(c.begin(), c.end());
        for (int i = 0; i < c.size(); ++i) {
            cout << c[i] << (i == c.size() - 1 ? "" : " ");
        }
        cout << "\n";

        for (int p : c) {
            b[p] = true;
        }

        vector<int> x;
        for (int p : c) {
            int R_p = r[p];
            if (R_p != 0 && !b[R_p]) {
                int L_p = l[p];
                // 找到 R_p 实际上的左邻居
                while (L_p != 0 && b[L_p]) {
                    L_p = l[L_p];
                }
                
                if (L_p == 0 || v[L_p] != v[R_p]) {
                    x.push_back(R_p);
                }
            }
        }
        
        // 更新链表,将被拿走的水果从链表中移除
        for (int p : c) {
            int L_p = l[p];
            int R_p = r[p];
            
            if (L_p != 0) {
                r[L_p] = R_p;
            }
            if (R_p != 0) {
                l[R_p] = L_p;
            }
        }
        
        // 清理标记数组
        for (int p : c) {
            b[p] = false;
        }

        sort(x.begin(), x.end());
        x.erase(unique(x.begin(), x.end()), x.end());

        c = x;
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> v[i];
    }
    
    sol();

    return 0;
}

[CSP-J 2022] 乘方

题目链接:P8813 [CSP-J 2022] 乘方

解题代码

/*
 * 题意:
 * 计算 a^b,如果结果大于 10^9,则输出 -1,否则输出其值。
 *
 * 解法:
 * 由于 b 的值可能很大(最高可达 10^9),直接循环 b 次进行乘法会超时。
 * 因此,我们需要使用 O(log b) 复杂度的快速幂算法。
 *
 * 快速幂的核心思想是利用 b 的二进制表示来加速计算。
 * 例如 a^9 = a^(1001_2) = a^(8+1) = a^8 * a^1。
 * 我们在计算过程中迭代地将底数平方(a -> a^2 -> a^4 -> a^8 ...),
 * 同时根据 b 的二进制位来决定是否将当前的底数乘入结果。
 *
 * 在本题中,我们不需要取模,而是要计算真实值。关键在于,在每一步乘法之前,
 * 都要检查结果是否会超过 10^9 这个上限。
 *
 * - 在 ans = ans * a 之前,检查 ans > 10^9 / a 是否成立。
 * - 在 a = a * a 之前,检查 a > 10^9 / a 是否成立。
 *
 * 如果任何一步乘法将导致结果超过 10^9,我们就可以提前判断并输出 -1。
 * 此外,需要特别处理 a = 1 的情况,此时结果永远是 1。
 */

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

const ll lim = 1e9;

void sol() {
    ll a, b;
    cin >> a >> b;

    if (a == 1) {
        cout << 1 << endl;
        return;
    }

    ll ans = 1;

    while (b > 0) {
        if (b & 1) {
            // 检查 ans * a 是否会超过上限 lim
            if (ans > lim / a) {
                cout << -1 << endl;
                return;
            }
            ans *= a;
        }

        b >>= 1;

        // 如果循环还未结束,则需要计算 a*a
        if (b > 0) {
            // 检查 a * a 是否会超过上限 lim
            if (a > lim / a) {
                cout << -1 << endl;
                return;
            }
            a *= a;
        }
    }

    cout << ans << endl;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    sol();

    return 0;
}

[CSP-J 2022] 解密

题目链接:P8814 [CSP-J 2022] 解密

解题代码

/*
 * 题意:
 * 给定 k 组 n, e, d,求正整数 p, q 满足:
 * 1. n = p * q
 * 2. e * d = (p - 1)(q - 1) + 1
 *
 * 解法:
 * 这是一个数学问题。我们可以通过化简给定的两个方程来求解。
 *
 * 对第二个方程进行展开:
 * e * d = (p - 1)(q - 1) + 1
 * e * d = pq - p - q + 1 + 1
 * e * d = pq - (p + q) + 2
 *
 * 将第一个方程 n = pq 代入上式:
 * e * d = n - (p + q) + 2
 *
 * 移项得到 p + q 的表达式:
 * p + q = n - e * d + 2
 *
 * 题目中给了一个提示性的定义 m = n - e * d + 2。
 * 所以我们有了一个关于 p 和 q 的方程组:
 * 1. p + q = m
 * 2. p * q = n
 *
 * 这是经典的韦达定理应用场景。p 和 q 可以看作是某个一元二次方程 x^2 - (p+q)x + pq = 0 的两个根。
 * 代入 m 和 n,方程为:
 * x^2 - mx + n = 0
 *
 * 我们可以用求根公式解这个方程:
 * x = (m ± sqrt(m^2 - 4n)) / 2
 *
 * 令判别式 Δ = m^2 - 4n。
 * 要使 p, q 有正整数解,需要满足几个条件:
 * 1. Δ 必须大于等于 0,否则方程无实数解。
 * 2. Δ 必须是一个完全平方数,否则解是无理数。
 * 3. (m ± sqrt(Δ)) 必须是偶数,才能被2整除得到整数解。(实际上,如果Δ是整数的完全平方数,这个条件自动满足,因为 m 和 sqrt(Δ) 的奇偶性必然相同)
 *
 * 算法步骤:
 * 对于每组查询:
 * 1. 计算 m = n - e * d + 2。
 * 2. 计算判别式 Δ = m*m - 4*n。
 * 3. 如果 Δ < 0,无解。
 * 4. 计算 Δ 的整数平方根 s = round(sqrt(Δ))。如果 s*s != Δ,说明 Δ 不是完全平方数,无解。
 * 5. 否则,解存在,p = (m - s) / 2, q = (m + s) / 2。
 * 6. 由于要求 p <= q,这个解的顺序正好。输出 p 和 q 即可。
 *
 * 注意数据范围,n 和 e*d 可能很大,需要使用 long long。
 */

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

void sol() {
    ll n, e, d;
    cin >> n >> e >> d;

    ll ed = e * d;
    ll m = n - ed + 2;

    // 判别式 delta = m^2 - 4n
    ll del = m * m - 4 * n;

    if (del < 0) {
        cout << "NO\n";
        return;
    }

    // 求整数平方根,使用 long double 保证精度
    ll s = round(sqrtl((long double)del));

    // 检查 s 是否为正确的整数平方根
    if (s * s != del) {
        cout << "NO\n";
        return;
    }

    // 求解 p 和 q
    // m 和 s 奇偶性相同, m-s 一定是偶数
    ll p = (m - s) / 2;
    ll q = (m + s) / 2;

    // 验证解是否正确,p,q为正整数且乘积为n
    if (p > 0 && q > 0 && p * q == n) {
        cout << p << " " << q << "\n";
    } else {
        cout << "NO\n";
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int k;
    cin >> k;
    while (k--) {
        sol();
    }

    return 0;
}

[CSP-J 2022] 逻辑表达式

题目链接:P8815 [CSP-J 2022] 逻辑表达式

解题代码

// 题意概括:
// 对一个包含 '0','1','&','|','(',')' 的逻辑表达式求值,并统计短路次数。
// 运算规则: `()` 优先级最高,然后是 `&`,最后是 `|`,同级从左到右。
// 短路规则:
// 1. a & b: 若 a 的值为 0,则 b 不再计算,这计为一次 '&' 短路。
// 2. a | b: 若 a 的值为 1,则 b 不再计算,这计为一次 '|' 短路。
// 3. 如果一个表达式因外层短路而未被计算,其内部的潜在短路也不被统计。
//
// 这是一个经典的表达式求值问题,带有副作用(短路计数)。
// 此类问题最自然的解法是递归下降分析法。
// 我们可以根据 `expr -> term -> factor` 的语法结构进行递归解析。
// 在解析 `a|b` 和 `a&b` 时,先求出左操作数 a 的值,
// 然后根据 a 的值判断是否发生短路,再决定是否继续求右操作数 b。

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

// Res 结构体用于封装一个子表达式的计算结果
// v: 表达式的值 (0 或 1)
// a: & 短路次数
// o: | 短路次数
struct Res {
    int v, a, o;
};

string s;
int pos;

// 函数前向声明,因为 expr, term, fact 存在相互递归调用
Res expr();
Res term();
Res fact();

// 解析一个因子 (factor)
// factor 的定义: '0', '1', 或被括号包裹的表达式
// factor ::= '0' | '1' | '(' expr ')'
Res fact() {
    char c = s[pos];

    if (c == '(') {
        pos++; // 跳过 '('
        Res res = expr();
        pos++; // 跳过 ')'
        return res;
    }

    pos++; // 跳过 '0' 或 '1'
    return {c - '0', 0, 0};
}

// 解析一个项 (term),由 '&' 连接
// term ::= factor { '&' factor }
Res term() {
    Res res = fact();

    while (pos < s.length() && s[pos] == '&') {
        pos++; // 跳过 '&'
        
        if (res.v == 0) { // 发生 '&' 短路
            res.a++;
            fact(); // 需调用解析函数来正确跳过右侧表达式
        } else {
            Res rhs = fact();
            res.v &= rhs.v;
            res.a += rhs.a;
            res.o += rhs.o;
        }
    }
    return res;
}

// 解析一个表达式 (expr),由 '|' 连接
// expr ::= term { '|' term }
Res expr() {
    Res res = term();

    while (pos < s.length() && s[pos] == '|') {
        pos++; // 跳过 '|'
        
        if (res.v == 1) { // 发生 '|' 短路
            res.o++;
            term(); // 需调用解析函数来正确跳过右侧表达式
        } else {
            Res rhs = term();
            res.v |= rhs.v;
            res.a += rhs.a;
            res.o += rhs.o;
        }
    }
    return res;
}

void sol() {
    cin >> s;
    pos = 0;
    Res ans = expr();

    cout << ans.v << endl;
    cout << ans.a << " " << ans.o << endl;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    sol();

    return 0;
}

[CSP-J 2022] 上升点列

题目链接:P8816 [CSP-J 2022] 上升点列

解题代码

/*
 * 题意:
 * 给定n个点和k个可自由添加的点,找一个x,y坐标均单调不减的序列,求其最大长度。
 * 序列中相邻点欧几里得距离为1,即只能向右或向上移动一格。
 *
 * 思路:
 * 动态规划。将点排序后,dp[i][j]表示以p_i结尾、用掉j个自由点的最长路径。
 * 路径可以从一个自由点序列开始,也可以从另一个给定点p_m转移而来。
 * 转移时,连接p_m和p_i的成本是曼哈顿距离减1,路径长度增加曼哈顿距离。
 * 最后,对于一个算出的路径,剩余的自由点都可以加在末尾。
*/

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

struct Pt {
    ll x, y;
};

// 按x, y坐标排序
bool cmp(const Pt& a, const Pt& b) {
    if (a.x != b.x) return a.x < b.x;
    return a.y < b.y;
}

int n;
ll k;
Pt p[505];
ll dp[505][105];

void slv() {
    cin >> n >> k;
    for (int i = 1; i <= n; ++i) {
        cin >> p[i].x >> p[i].y;
    }

    sort(p + 1, p + n + 1, cmp);

    // 初始化dp数组
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= k; ++j) {
            // 路径可以由j个自由点开始,最后连接到p_i,长度为j+1
            dp[i][j] = j + 1;
        }
    }

    for (int i = 1; i <= n; ++i) {
        for (int m = 1; m < i; ++m) {
            // p[m].x <= p[i].x 已由排序保证
            if (p[m].y > p[i].y) {
                continue;
            }

            ll dx = p[i].x - p[m].x;
            ll dy = p[i].y - p[m].y;
            ll d = dx + dy;

            // 连接 p_m 和 p_i 需要 d-1 个自由点
            if (d > 0) {
                ll cst = d - 1;
                if (cst <= k) {
                    for (int j = cst; j <= k; ++j) {
                        ll pk = j - cst; // 到达p_m时使用的自由点数
                        dp[i][j] = max(dp[i][j], dp[m][pk] + d);
                    }
                }
            }
        }
    }

    ll ans = 0;
    
    // 包含至少一个给定点的路径
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= k; ++j) {
            // 路径长度dp[i][j],剩余k-j个自由点可以加在末尾
            ans = max(ans, dp[i][j] + k - j);
        }
    }

    cout << ans << endl;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    
    slv();
    
    return 0;
}

[CSP-J2022 山东] 植树节

题目链接:P11853 [CSP-J2022 山东] 植树节

解题代码

// 题意:给出 n 个区间 [a, b],求在所有整数点上,被区间覆盖次数的最大值。
// 这是一个经典的差分问题。

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

// 定义一个足够大的数组来做差分,坐标最大到 10^6
// d[b+1] 需要访问到 10^6 + 1,所以数组大小至少为 10^6 + 2
const int N = 1000005;
int d[N];

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n;
    cin >> n;

    int mxb = 0; // 记录最大的右端点,可以优化遍历范围

    for (int i = 0; i < n; ++i) {
        int a, b;
        cin >> a >> b;
        
        d[a]++;
        d[b + 1]--; // 区间[a,b]的影响,在b+1处消除

        mxb = max(mxb, b);
    }

    int ans = 0;
    int cur = 0;

    // 遍历求前缀和,得到每个点的实际覆盖次数,同时找出最大值
    for (int i = 0; i <= mxb; ++i) {
        cur += d[i];
        ans = max(ans, cur);
    }

    cout << ans << endl;

    return 0;
}

[CSP-J2022 山东] 宴会

题目链接:P11854 [CSP-J2022 山东] 宴会

解题代码

/*
 * 题意概括:
 * n个人位于数轴上不同的点x_i,每个人需要t_i的时间打扮。
 * 他们要选择一个宴会地点x_0,使得所有人到达所需时间的最大值最小。
 * 第i个人到达x_0的总时间为 |x_i - x_0| + t_i。
 * 求最优的x_0。
 *
 * 解法:
 * 设宴会开始时间为T,则对任意一个人i,必须满足 |x_i - x_0| + t_i <= T。
 * 变形得 x_i + t_i - T <= x_0 <= x_i - t_i + T。
 * 为了让x_0有解,所有人的可行区间必须有交集。
 * 即 max(x_i + t_i - T) <= min(x_j - t_j + T)。
 * 令 ms = max(x_i + t_i), md = min(x_j - t_j)。
 * 得 ms - T <= md + T,即 T >= (ms - md) / 2。
 * 最小的T在等号成立时取到,此时x_0的区间缩为一个点。
 * x_0 = ms - T = ms - (ms - md) / 2 = (ms + md) / 2。
 * 所以,我们只需遍历所有人,求出ms和md,即可算出唯一的x_0。
 */
#include <bits/stdc++.h>

using namespace std;
using ll = long long;

void sol() {
    int n;
    cin >> n;

    vector<int> x(n), p(n);
    for (int i = 0; i < n; ++i) cin >> x[i];
    for (int i = 0; i < n; ++i) cin >> p[i];

    // ms存储max(x_i + t_i), md存储min(x_i - t_i)
    ll ms = (ll)x[0] + p[0];
    ll md = (ll)x[0] - p[0];

    for (int i = 1; i < n; ++i) {
        ll s = (ll)x[i] + p[i];
        ll d = (ll)x[i] - p[i];
        if (s > ms) ms = s;
        if (d < md) md = d;
    }

    // 计算并输出 x0 = (ms + md) / 2
    ll sm = ms + md;

    if (sm % 2 == 0) {
        cout << sm / 2 << "\n";
    } else {
        // 如果和为奇数,结果是.5,按要求格式化输出
        cout << fixed << setprecision(1) << (double)sm / 2.0 << "\n";
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int tcs;
    cin >> tcs;
    while (tcs--) {
        sol();
    }

    return 0;
}

[CSP-J2022 山东] 部署

题目链接:P11855 [CSP-J2022 山东] 部署

解题代码

/*
 * 题意概括:
 * 给定一棵以1为根的n个节点的树,每个节点有初始兵力。
 * 存在两种操作:
 * 1. 对指定节点x及其整个子树的兵力增加y。
 * 2. 对指定节点x及其所有直接邻居(父节点和子节点)的兵力增加y。
 * 经过m次操作后,回答q次询问,每次询问一个节点的最终兵力。
 *
 * 解法:
 * 采用离线处理的思想,先记录所有修改,最后计算结果并回答询问。
 *
 * 对于操作1 (子树加):
 * 使用数组 b1 记录。对于操作(1 x y),执行 b1[x] += y。
 * 完成所有记录后,通过一次从根节点开始的DFS,将父节点的b1值累加到子节点上。
 * 这样,每个节点u的b1值就等于路径(root -> u)上所有节点的初始b1值之和,即为操作1对u的总贡献。
 *
 * 对于操作2 (邻居加):
 * 使用数组 b2 记录。对于操作(2 x y),执行 b2[x] += y。
 *
 * 最终计算:
 * 1. DFS建树,确定父子关系。
 * 2. 读入m个操作,填充b1和b2数组。
 * 3. DFS传递b1的贡献。
 * 4. 将初始兵力a与b1的贡献相加。
 * 5. 遍历所有节点i,若b2[i]非零,将其值加到i, fa[i]及i的所有子节点的兵力上。
 * 6. 回答q次询问。
 *
 * 兵力总和可能很大,需使用long long。
 * 整体复杂度为 O(N+M+Q)。
 */
#include <bits/stdc++.h>

using namespace std;
using ll = long long;

const int N = 1000005;

int n;
int m;
int q;
ll a[N];      // 存储兵力
vector<int> e1[N]; // 初始邻接表
vector<int> e2[N]; // 子节点邻接表
int fa[N];     // 父节点数组
ll b1[N];     // 操作1的贡献
ll b2[N];     // 操作2的贡献

// 建树,确定父子关系
void bld(int u, int p) {
    fa[u] = p;
    for (int v : e1[u]) {
        if (v == p) continue;
        e2[u].push_back(v);
        bld(v, u);
    }
}

// DFS传递操作1的贡献
void prop(int u) {
    for (int v : e2[u]) {
        b1[v] += b1[u]; // 子节点继承父节点的累积贡献
        prop(v);
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
    }

    for (int i = 0; i < n - 1; ++i) {
        int u, v;
        cin >> u >> v;
        e1[u].push_back(v);
        e1[v].push_back(u);
    }

    bld(1, 0); // 以1为根建树

    cin >> m;
    while (m--) {
        int p, x;
        ll y;
        cin >> p >> x >> y;
        if (p == 1) {
            b1[x] += y;
        } else {
            b2[x] += y;
        }
    }

    prop(1); // 传递操作1的子树贡献

    // 合并操作1的贡献
    for (int i = 1; i <= n; ++i) {
        a[i] += b1[i];
    }

    // 合并操作2的贡献
    for (int i = 1; i <= n; ++i) {
        if (b2[i] == 0) continue;
        
        a[i] += b2[i]; // 对中心点i

        if (fa[i] != 0) {
            a[fa[i]] += b2[i]; // 对父节点
        }

        for (int v : e2[i]) {
            a[v] += b2[i]; // 对所有子节点
        }
    }

    cin >> q;
    while (q--) {
        int x;
        cin >> x;
        cout << a[x] << "\n";
    }

    return 0;
}

[CSP-J2022 山东] 吟诗

题目链接:P11856 [CSP-J2022 山东] 吟诗

解题代码

// 题意: 统计长度为N(元素1-10)的序列中, 存在三段连续子数组和分别为X,Y,Z的方案数.
#include<bits/stdc++.h>

using namespace std;
using ll=long long;

const int p = 998244353;

int n, m, x, y, z, e;
int f[41][1 << 17];
ll ans = 1;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    cin >> n >> x >> y >> z;

    // 核心思想: 正难则反. 总方案数(10^N)减去不满足条件的方案数.
    // 我们用状压DP来统计不满足条件的方案数.

    // f[i][s]: 长度为i的序列, 其所有后缀和的集合由位掩码s表示的方案数.
    // s的第k位为1, 表示存在一个后缀, 其和为 k+1.
    
    // 我们只需要关心和最大为 x+y+z 的后缀, 所以用 m 进行按位与操作来限制状态.
    m = (1 << (x + y + z)) - 1;

    // "妙手"出现的条件是: 存在i<j<k<l, 使sum(i..j-1)=X, sum(j..k-1)=Y, sum(k..l-1)=Z.
    // 这等价于在l-1位置, 同时存在和为 Z, Y+Z, X+Y+Z 的后缀.
    // e就是这个目标状态的掩码.
    e = (1 << (z - 1)) | (1 << (y + z - 1)) | (1 << (x + y + z - 1));

    f[0][0] = 1; // 初始条件: 长度为0的序列, 后缀和集合为空, 方案数为1.

    // i: 枚举序列长度
    for (int i = 1; i <= n; i++, ans = ans * 10ll % p) { // ans顺便计算总方案数
        // j: 枚举上一轮的状态 (长度为i-1时的后缀和集合)
        for (int j = 0; j <= m; j++) {
            if (!f[i - 1][j]) continue; // 从可达状态转移

            // k: 枚举新添加的数字 (1到10)
            for (int k = 1; k <= 10; k++) {
                // 计算新状态s:
                // 1. (j << k): 原有后缀和S都变为S+k, 对应掩码左移k位.
                // 2. | (1 << (k-1)): 新增一个值为k的后缀, 对应掩码第k-1位置1.
                int s = ((j << k) | (1 << (k - 1))) & m;

                // (s | e) != s 等价于 (s & e) != e, 检查s是否包含了e的所有位.
                // 如果s中尚未凑齐目标状态e, 说明这个长为i的序列还不算"妙手", 计入f[i][s].
                if ((s | e) != s) {
                    f[i][s] = (f[i][s] + f[i - 1][j]) % p;
                }
            }
        }
    }

    // 遍历所有最终状态, f[n][i]里存储的都是长度为n但未达成"妙手"的方案数.
    // 从总方案ans里减去这些不满足条件的方案.
    for (int i = 0; i <= m; i++) {
        ans = (ans + p - f[n][i]) % p;
    }

    cout << ans << '\n';

    return 0;
}

[CSP-J 2023] 小苹果

题目链接:P9748 [CSP-J 2023] 小苹果

解题代码

// 题意: n个苹果, 每天取走位置为1,4,7...的, 剩下的重排. 求总天数与编号n被取走的天数.
#include<bits/stdc++.h>

using namespace std;
using ll=long long;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    ll n;
    cin >> n;

    // --- 计算总天数 ---
    ll cur = n;
    int d1 = 0;
    while (cur > 0) {
        d1++;
        // 每一天, 数量为cur的苹果会被拿走 ceil(cur / 3.0) 个
        // 在整数计算中, (cur + 2) / 3 等价于 ceil(cur / 3.0)
        cur -= (cur + 2) / 3;
    }

    // --- 计算编号为n的苹果被取走的天数 ---
    ll pos = n;
    int d2 = 0;
    while (true) {
        d2++;
        // 如果当前位置是3k+1, 则在这一天被取走
        if (pos % 3 == 1) {
            break;
        }
        // 如果没被取走, 计算它在下一天新队列中的位置
        // 它前方被取走的苹果数量为 (pos + 1) / 3
        pos -= (pos + 1) / 3;
    }

    cout << d1 << " " << d2 << '\n';

    return 0;
}

[CSP-J 2023] 公路

题目链接:P9749 [CSP-J 2023] 公路

解题代码

/*
 * 题意:
 * 开车从1号站到n号站,每段路长v[i],i号站油价a[i],每升油跑d公里。
 * 求最小花费。
 *
 * 思路:
 * 贪心。在当前站点i,找到下一个油价更低的站点j。
 * 若有,则在i只买恰好能跑到j的油;若无,则在i买足跑到终点的油。
 * 用单调栈O(n)预处理出每个站点右边第一个更便宜的站点。
 * 然后模拟跳跃过程,计算总花费。
*/
#include <bits/stdc++.h>

using namespace std;
using ll = long long;

const int MAX = 100005;

int n;
ll d;
ll v[MAX];
ll a[MAX];
ll pre[MAX]; // v的前缀和
int nxt[MAX]; // 下一个更小油价站点的下标

void slv() {
    cin >> n >> d;
    // v_i 是站点 i 到 i+1 的距离
    for (int i = 1; i < n; ++i) cin >> v[i];
    for (int i = 1; i <= n; ++i) cin >> a[i];

    // 计算距离前缀和
    pre[0] = 0;
    for (int i = 1; i < n; ++i) {
        pre[i] = pre[i - 1] + v[i];
    }
    // 从 i 到 j (j>i) 的距离是 pre[j-1] - pre[i-1]

    // 单调栈预处理下一个更便宜的油站
    stack<int> s;
    for (int i = n; i >= 1; --i) {
        while (!s.empty() && a[s.top()] >= a[i]) {
            s.pop();
        }
        if (s.empty()) {
            nxt[i] = n + 1; // 哨兵,表示后面没有更便宜的了
        } else {
            nxt[i] = s.top();
        }
        s.push(i);
    }

    ll ans = 0;
    ll rge = 0; // 当前油量能跑的公里数
    int cur = 1;

    while (cur < n) {
        int tar = nxt[cur];
        if (tar > n) { // 后面没有更便宜的油站
            tar = n;
        }

        // 计算从 cur 到 tar 的距离
        ll dis = pre[tar - 1] - pre[cur - 1];

        // 如果当前油量不够跑到tar
        if (rge < dis) {
            ll buy = dis - rge;
            // 计算需要买多少升油,向上取整
            ll ltr = (buy + d - 1) / d;

            ans += ltr * a[cur];
            rge += ltr * d;
        }

        rge -= dis;
        cur = tar;
    }

    cout << ans << endl;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    slv();

    return 0;
}

[CSP-J 2023] 一元二次方程

题目链接:P9750 [CSP-J 2023] 一元二次方程

解题代码

// 题意: 求解一元二次方程并按格式化输出.
#include<bits/stdc++.h>

using namespace std;
using ll=long long;

// 计算最大公约数
ll gcd(ll x, ll y) {
    return y == 0 ? x : gcd(y, x % y);
}

// 按要求格式化输出有理数 p/q
void put(ll p, ll q) {
    if (p == 0) {
        cout << 0;
        return;
    }
    ll g = gcd(abs(p), abs(q));
    p /= g;
    q /= g;

    if (q < 0) {
        p = -p;
        q = -q;
    }

    if (q == 1) {
        cout << p;
    } else {
        cout << p << "/" << q;
    }
}

// 单次求解过程
void slv() {
    ll a, b, c;
    cin >> a >> b >> c;

    ll del = 1ll * b * b - 4ll * a * c;

    if (del < 0) {
        cout << "NO";
        return;
    }

    ll sd = round(sqrt(del));
    if (sd * sd == del) { // 有理根
        ll p = -b;
        // 根据a的符号决定取+sqrt(del)还是-sqrt(del)得到较大根
        if (a > 0) p += sd;
        else p -= sd;
        ll q = 2 * a;
        put(p, q);
    } else { // 无理根
        // 将 sqrt(del) 化简为 k*sqrt(r)
        ll k = 1;
        ll r = del;
        for (ll i = 2; i * i <= r; ++i) {
            while (r % (i * i) == 0) {
                k *= i;
                r /= (i * i);
            }
        }
        
        // 输出有理部分 q1 = -b / 2a
        if (b != 0) {
            put(-b, 2 * a);
            cout << "+";
        }
        
        // 输出无理部分 q2*sqrt(r)
        // q2 = k / |2a|
        ll p2 = k;
        ll q2 = abs(2 * a);
        ll g = gcd(p2, q2);
        p2 /= g;
        q2 /= g;

        if (p2 == 1 && q2 == 1) { // q2 = 1
            cout << "sqrt(" << r << ")";
        } else if (q2 == 1) { // q2 为整数
            cout << p2 << "*sqrt(" << r << ")";
        } else if (p2 == 1) { // q2 = 1 / 整数
            cout << "sqrt(" << r << ")/" << q2;
        } else { // q2 为一般分数
            cout << p2 << "*sqrt(" << r << ")/" << q2;
        }
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int t, m;
    cin >> t >> m;

    while (t--) {
        slv();
        cout << "\n";
    }

    return 0;
}

[CSP-J 2023] 旅游巴士

题目链接:P9751 [CSP-J 2023] 旅游巴士

解题代码

/*
 * 题意:
 * 在一个n点m边的有向图中,从1到n找一条路径。
 * 每条边通过时间为1,且有一个开放时间a,必须在不早于a的时刻出发。
 * 不能停留。要求进入1号点和离开n号点的时间都是k的倍数。求最早离开时间。
 *
 * 思路:
 * 路径长度L必须是k的倍数。设出发时间为T_start,则T_start >= max(a_i - (i-1))。
 * 目标是最小化 T_end = ceil(max(a_i-(i-1))/k)*k + L。
 * 这是一个带有复杂代价函数的最短路问题。
 *
 * 使用Dijkstra,状态为 (u, r),表示到达u点,路径长度模k为r。
 * d[u][r] 记录最优路径的{长度L, 约束值M}。
 * 优先队列按T_end排序,寻找最优解。
*/
#include <bits/stdc++.h>

using namespace std;
using ll = long long;

const int N = 10004;
const int K = 101;
const ll INF = 1e18;

int n, m, k;
vector<pair<int, int>> adj[N];

pair<ll, ll> D[N][K]; // 存储最优路径的 {L, M}

struct St {
    ll cst, l, m; // cost (T_end), length, M_val
    int u, r;     // node, remainder

    bool operator>(const St& o) const {
        if (cst != o.cst) return cst > o.cst;
        return l > o.l;
    }
};

// 计算T_end
ll cal(ll l, ll m, int kk) {
    if (l >= INF) return INF;
    ll st = 0;
    if (m > 0) {
        st = (m + kk - 1) / kk * kk;
    }
    return st + l;
}

void slv() {
    cin >> n >> m >> k;

    // 用map处理平行边,只保留a最小的
    map<pair<int, int>, int> E;
    for (int i = 0; i < m; ++i) {
        int u, v, a;
        cin >> u >> v >> a;
        if (E.find({u, v}) == E.end()) {
            E[{u, v}] = a;
        } else {
            E[{u, v}] = min(E[{u, v}], a);
        }
    }
    for (auto const& [p, a] : E) {
        adj[p.first].push_back({p.second, a});
    }

    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j < k; ++j) {
            D[i][j] = {INF, INF};
        }
    }

    priority_queue<St, vector<St>, greater<St>> q;

    D[1][0] = {0, -INF};
    q.push({0, 0, -INF, 1, 0});

    while (!q.empty()) {
        St cur = q.top();
        q.pop();

        ll cst = cur.cst;
        ll l = cur.l;
        ll M = cur.m;
        int u = cur.u;
        int r = cur.r;

        if (cst > cal(D[u][r].first, D[u][r].second, k) ||
           (cst == cal(D[u][r].first, D[u][r].second, k) && l > D[u][r].first)) {
            continue;
        }

        for (auto& edg : adj[u]) {
            int v = edg.first;
            int a = edg.second;

            ll nl = l + 1;
            ll nm = max(M, (ll)a - l);
            int nr = nl % k;
            
            ll nc = cal(nl, nm, k);
            
            if (nc < cal(D[v][nr].first, D[v][nr].second, k) ||
               (nc == cal(D[v][nr].first, D[v][nr].second, k) && nl < D[v][nr].first)) {
                D[v][nr] = {nl, nm};
                q.push({nc, nl, nm, v, nr});
            }
        }
    }

    ll ans = cal(D[n][0].first, D[n][0].second, k);

    if (ans >= INF) {
        cout << -1 << endl;
    } else {
        cout << ans << endl;
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    slv();

    return 0;
}

[CSP-J 2024] 扑克牌

题目链接:P11227 [CSP-J 2024] 扑克牌

解题代码

/*
 * 题意:
 * 一副完整的扑克牌有 52 张 (4 种花色 x 13 种点数)。
 * 现有 n 张牌,可能包含重复的牌。
 * 问至少需要再借多少张牌,才能凑齐一副完整的扑克牌。
 *
 * 思路:
 * 统计给定的 n 张牌中,有多少种不同的牌。
 * 用一个集合 (set) 来存储已有的牌,集合能自动去重。
 * 遍历 n 张牌,将代表牌的字符串插入集合。
 * 最终,52 减去集合的大小,即为所需借的牌数。
*/
#include <bits/stdc++.h>

using namespace std;
using ll = long long;

void slv() {
    int n;
    cin >> n;

    set<string> s;

    for (int i = 0; i < n; ++i) {
        string c;
        cin >> c;
        s.insert(c);
    }

    cout << 52 - s.size() << endl;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    slv();

    return 0;
}

[CSP-J 2024] 地图探险

题目链接:P11228 [CSP-J 2024] 地图探险

解题代码

// 题意概括:
// 在一个 n x m 的网格地图上模拟一个机器人的移动。地图上有空地和障碍物。
// 机器人有位置(x,y)和朝向d(0-3分别代表E,S,W,N)。它将执行k次操作。
// 每次操作,机器人尝试向当前朝向前进一格:
// - 如果目标格子在地图内且是空地,则移动到该格,方向不变。
// - 否则,位置不变,方向右转90度 (d = (d+1)%4)。
// 任务是计算k次操作后,机器人总共访问过多少个不同的格子(包括起点)。
//
// 核心思路:
// 这是一个直接的模拟问题。由于操作次数 k (<= 10^6) 和地图大小 (<= 1000x1000)
// 都在可接受的范围内,我们可以精确地模拟机器人的每一步操作。
// 使用一个二维布ール数组 `vis` 来记录访问过的格子。
// 循环 k 次,在每一步中,根据当前状态 (x,y,d) 计算出下一步的目标位置 (nx,ny)。
// 判断 (nx,ny) 是否可达(在边界内且非障碍)。
// 如果可达,更新机器人位置,并检查 `vis[nx][ny]`,如果未访问过则计数器加一并标记。
// 如果不可达,则更新机器人朝向。
// 模拟结束后,计数器的值即为答案。
// 考虑到 k 可能大于总状态数(n*m*4),理论上存在循环。但由于 k 的值相对较小,
// 不需要实现复杂的循环检测,直接模拟即可通过。

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

int n, m;
ll k;
int x, y, d;
vector<string> g;
vector<vector<bool>> vis;

// 方向向量: 0:E, 1:S, 2:W, 3:N
int dx[] = {0, 1, 0, -1};
int dy[] = {1, 0, -1, 0};

void sol() {
    cin >> n >> m >> k;
    cin >> x >> y >> d;

    x--; 
    y--;

    g.assign(n, string(m, ' '));
    for (int i = 0; i < n; ++i) {
        cin >> g[i];
    }

    vis.assign(n, vector<bool>(m, false));
    
    vis[x][y] = true;
    int cnt = 1;

    for (ll i = 0; i < k; ++i) {
        int nx = x + dx[d];
        int ny = y + dy[d];

        bool ok = true;
        if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
            ok = false;
        } else if (g[nx][ny] == 'x') {
            ok = false;
        }

        if (ok) {
            x = nx;
            y = ny;
            if (!vis[x][y]) {
                vis[x][y] = true;
                cnt++;
            }
        } else {
            d = (d + 1) % 4;
        }
    }

    cout << cnt << "\n";
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int t;
    cin >> t;
    while (t--) {
        sol();
    }

    return 0;
}

[CSP-J 2024] 小木棍

题目链接:P11229 [CSP-J 2024] 小木棍

解题代码

// 题目:P11229 [CSP-J 2024] 小木棍
// 题意:用 n 根小木棍,拼成一个最小的正整数。
// 思路:
// 答案的位数越少,数就越小。
// 所以从小到大枚举答案的位数 len。
// 对于第一个能拼出数字的 len,用贪心构造出字典序最小的那个数,即为答案。
// 构造时,从高到低每一位都尝试最小的数字,并检查剩余木棍是否在剩余位数的表示范围内。

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

int cst[] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6}; // 数字0-9的代价

void sol() {
    int n;
    cin >> n;

    bool fnd = false;

    // 枚举答案的位数
    for (int len = 1; len <= n / 2 + 2 && len < 100000; ++len) {
        // 剪枝:len位数字的代价范围是[2*len, 7*len]
        if (n > len * 7 || n < len * 2) {
            continue;
        }

        int rem = n;
        string ans = "";
        bool psl = true;

        // 从高到低确定每一位
        for (int pos = 1; pos <= len; ++pos) {
            int rln = len - pos; // 剩余位数
            bool dgt = false;

            // 尝试当前位填入的最小数字
            for (int d = (pos == 1 ? 1 : 0); d <= 9; ++d) {
                int c = cst[d];
                if (rem >= c) {
                    // 检查剩余木棍数是否能被剩余位数表示
                    if (rem - c >= rln * 2 && rem - c <= rln * 7) {
                        ans += to_string(d);
                        rem -= c;
                        dgt = true;
                        break;
                    }
                }
            }

            if (!dgt) {
                psl = false;
                break;
            }
        }

        if (psl) {
            cout << ans << endl;
            fnd = true;
            break;
        }
    }

    if (!fnd) {
        cout << -1 << endl;
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int t;
    cin >> t;
    while (t--) {
        sol();
    }

    return 0;
}

[CSP-J 2024] 接龙

题目链接:P11230 [CSP-J 2024] 接龙

解题代码

/*
 * 题意概括:
 * n个人,每人有一个词库。进行r轮接龙游戏。
 * 规则:
 * 1. 每轮由一人 p 从其词库 Sp 中选一个长度为 [2, k] 的连续子序列 A。
 * 2. 第一轮 A 的开头必须是 1。
 * 3. 后续轮次 A 的开头必须是上一轮 A 的结尾。
 * 4. 相邻两轮不能是同一个人。
 * q 个任务,每个任务问 r 轮后,接龙序列的最后一个元素能否是 c。
 *
 * 解法:
 * 预处理动态规划。
 * 定义 own[i][j] 表示第 i 轮后,以数字 j 结尾的接龙是由谁完成的。
 * own[i][j] 的值有三种情况:
 * - 0: 表示无法达成。
 * - p (p > 0): 表示只能由第 p 个人完成。
 * - -1: 表示可以由多于一个人完成。
 *
 * 状态转移:
 * 从第 1 轮到第 100 轮(题目 r 的上限)进行迭代。
 * 对第 r 轮:
 * 1. 首先确定哪些数字可以作为本轮接龙序列的开头 (hed)。
 * - r=1 时,只有数字 1 可以作为开头。
 * - r>1 时,数字 s[i] 能作为开头,当且仅当它在 r-1 轮是一个合法的结尾,
 * 并且完成者不是当前持有 s[i] 的人。如果 r-1 轮的结尾有多个完成者,则任何人都可以接。
 * 2. 使用前缀和 (hsm) 优化查找过程。
 * 3. 遍历所有词库中的每个位置 i,判断它能否成为一个合法的结尾 (tal)。
 * - 这要求在 [i-k+1, i-1] 范围内,且在当前主人词库内,存在一个合法的开头。
 * 4. 根据所有合法的结尾,更新 own[r] 数组。
 * 所有查询均可 O(1) 回答。
 */

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

const int MXN = 100005;
const int MXR = 105;
const int MXL = 200005;
const int MXS = 200005;
const ll MUL = -1; // 代表多人可完成

int s[MXL], per[MXL];
bool hed[MXR][MXL], tal[MXR][MXL];
ll own[MXR][MXS];
int n, k, q;
int len[MXN], ed[MXN];
int hsm[MXL + 1];

void slv() {
    cin >> n >> k >> q;

    ed[0] = 0;
    int tot = 0;
    for (int i = 1; i <= n; ++i) {
        cin >> len[i];
        for (int j = 0; j < len[i]; ++j) {
            cin >> s[tot];
            per[tot] = i;
            tot++;
        }
        ed[i] = tot;
    }

    for (int r = 1; r <= 100; r++) {
        // 计算本轮哪些位置可以作为龙头
        if (r == 1) {
            for (int i = 0; i < tot; i++) {
                hed[r][i] = (s[i] == 1);
            }
        } else {
            for (int i = 0; i < tot; i++) {
                ll pown = own[r - 1][s[i]];
                if (pown == 0) {
                    hed[r][i] = false;
                } else if (pown == MUL) {
                    hed[r][i] = true; // 多人可达,任何人都可以接
                } else {
                    hed[r][i] = (pown != per[i]); // 不能自己接自己的
                }
            }
        }
        
        // 前缀和优化,快速判断一个区间内有无合法龙头
        hsm[0] = 0;
        for (int i = 0; i < tot; i++) {
            hsm[i + 1] = hsm[i] + hed[r][i];
        }
        
        // 计算本轮哪些位置可以作为龙尾
        for (int i = 0; i < tot; i++) {
            int p = per[i];
            int lft = max(ed[p - 1], i - k + 1); // 合法开头的搜索窗口左边界
            int rgt = i; // 搜索窗口右边界(开)

            if (lft < rgt && hsm[rgt] - hsm[lft] > 0) {
                tal[r][i] = true;
            } else {
                tal[r][i] = false;
            }
        }
        
        // 更新本轮的 owner 状态
        fill(own[r], own[r] + MXS, 0);
        for (int i = 0; i < tot; i++) {
            if (tal[r][i]) {
                if (own[r][s[i]] == 0) {
                    own[r][s[i]] = per[i];
                } else if (own[r][s[i]] != per[i]) {
                    own[r][s[i]] = MUL;
                }
            }
        }
    }

    while (q--) {
        int r, c;
        cin >> r >> c;
        if (r > 100 || c >= MXS) {
             cout << 0 << "\n";
        } else {
             cout << (own[r][c] != 0) << "\n";
        }
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int t;
    cin >> t;
    while (t--) {
        slv();
    }

    return 0;
}

0 条评论

目前还没有评论...