第二十三届中国计量大学校 ACM 团队赛题解

2026 年 6 月 19 日

11212 字

56 分钟

算法竞赛题解

比赛名称:第二十三届中国计量大学校 ACM 团队赛暨《ACM 竞赛实训》期末考试 编写日期:2026-06-19
编写人:Abs1nthe

说明

本文档已根据题面补全 A-N 题标题、题型标签、难度分层与 Codeforces 风格估分。CF 估分为主观训练参考分,并非官方难度。

目录

比赛总览

本场比赛共 14 题,题号为 A 到 N。整体覆盖模拟、基础数论、构造、树形 DP、最短路、并查集、动态规划、组合博弈等知识点。

题目难度与算法标签

题号题目名称难度CF 估分算法标签备注
A硬币分类中等偏难1900交互、分类、决策策略自适应交互题
BAgent 执行计划树困难2100树形 DP、换根 DP、组合数学、逆元统计每个根的拓扑序数量
C鸟为什么会飞签到900数论、gcd、质因子、枚举找最小可用编号
DGTNH?中等1600最短路、Dijkstra、虚点建图同色跳跃可压成颜色点
E矩阵构造困难2300构造、异或排列、完全映射题面提示不建议考试作答
F生命因何而沉睡签到800模拟、相邻差、边界判断扫描重点时刻
GACMBTI String困难2200构造、子序列、状态压缩、搜索控制 16 个模式的出现性
H以此烈火,斩无不断困难+2400组合博弈、排列计数、数学先判胜负性质,再计数最优排列
I数世往回皆空虚签到800整除、基础输入输出输出 a / b
J雪融之后中等1700离线、并查集、网格连通反向加点处理水位上升
KTaibo 相信着 LanGod 的希望灯塔并偷偷将感染和被蛇咬加入他的卡组中等偏难1900动态规划、状态优化、博弈式决策n,m <= 50
L千万次初见简单1200前缀和、构造判定、非负数组判断是否可重排
M然后,向着明天简单1000构造、排列、逆操作、字典序分情况构造
N命运骰子签到800模拟、骰子状态维护维护六个面

A. 硬币分类

基本信息

  • 时间限制:5000 ms
  • 内存限制:524288 KB
  • 难度:中等偏难
  • CF 估分:1900
  • 标签:交互、分类、决策策略

题意简述

n 枚硬币,被分为 A、B、C 三类,三类质量严格递增且每类至少一枚。每次可以询问两枚硬币质量大小关系,要求在不超过 floor(3n/2) 次询问内确定所有硬币类别。本题为自适应交互题。

数据范围

  • 1 <= T <= 10^4
  • 3 <= n <= 30

解题思路

维护若干个“相等组”,每个组内的硬币已经通过若干次 = 比较证明属于同一类,用一个代表元表示整个组。

第一阶段不断从未处理代表集合 u 中取出两个代表 x,y 比较:

  • x = y,则合并两个相等组,新的代表继续放回 u
  • x < y,则 x 所在组一定不是 C 类,只可能是 A 或 B;y 所在组一定不是 A 类,只可能是 B 或 C。于是把 x 放入低端候选集合 l,把 y 放入高端候选集合 h
  • x > y,同理把 y 放入 l,把 x 放入 h

第一阶段结束后,最多剩下一个未被配对出去的相等组,记为 ex

接下来分别处理 lh。由于 l 中每个代表都只可能属于 A 或 B,因此只要用 l[0] 和其余所有元素比较,就能把 l 分成较轻的一类 l1 和较重的一类 l2。如果 l2 为空,说明 l 中所有代表属于同一类。对 h 同理,将其分成较轻的 h1 和较重的 h2。其中 h 中元素只可能属于 B 或 C。

分类时分情况讨论:

  • l2h2 都非空,则 l1=A,l2=B,h1=B,h2=C
  • 若只有 l2 非空,则 l1=A,l2=B,而三类均非空,所以 h1=C
  • 若只有 h2 非空,则 h1=B,h2=C,而三类均非空,所以 l1=A
  • 若存在剩余组 ex,且前面已经确定了某个 B 类代表,则再询问一次 ex 与该 B 类代表的关系即可确定 ex
  • 若存在剩余组 ex,且 l2,h2 都为空,则当前只有三类候选组:l1,h1,ex,并且必有 l1 < h1。此时比较 exl1,h1,即可判断 ex 是 A、B、C 中的哪一类。

最后将每个代表对应的整个相等组统一赋值即可。

正确性证明

首先证明第一阶段的不变式。每次比较两个相等组代表时,若回答为 =,则这两个组内所有硬币均与代表同类,因此合并后仍然是一个合法相等组。若回答为 <,较轻的一组不可能是 C,因为 C 是最重类;较重的一组不可能是 A,因为 A 是最轻类。因此较轻代表只可能属于 A 或 B,可以放入 l;较重代表只可能属于 B 或 C,可以放入 h。回答为 > 时同理。所以第一阶段结束后,l 中每个代表只可能是 A/B,h 中每个代表只可能是 B/C,ex 若存在则仍是一个合法相等组。

然后证明 split2 正确。对于 l,其中所有代表只来自 A/B 两类。选定 l[0] 后,将其他代表与它比较:等于 l[0] 的必然与它同类;不等于 l[0] 的必然属于另一类。由于 l 中只有两种可能类别,所有不等于 l[0] 的元素方向必然一致。因此 split2(l) 得到的两部分若都非空,则较轻部分一定是 A,较重部分一定是 B;若第二部分为空,则说明 l 中所有代表同类。对 h 完全同理,若 split2(h) 两部分都非空,则较轻部分一定是 B,较重部分一定是 C。

接着证明分类分支正确。若 l2h2 都非空,由上一段可知 l1=A,l2=B,h1=B,h2=C。若只有 l2 非空,则 l 中同时出现 A 和 B;其中 B 出现在 l 中,只可能来自某次 B 与 C 的不等比较,所以对应的 C 一定被放入了 h。又因为 h2 为空,h 中所有代表同类,因此 h1=C。若只有 h2 非空同理,h1=B,h2=C,l1=A。若存在 ex 且已经有 B 类代表,直接比较 ex 与 B:小于则为 A,等于则为 B,大于则为 C。最后一种情况中,l2,h2 均为空,说明 l1 是同一类,h1 是同一类,且由第一阶段的配对关系可知 l1<h1。因为三类均至少出现一次,剩余组 ex 必须是缺失的第三类,比较 exl1,h1 即可唯一确定三者分别为 A、B、C。故算法输出的每个硬币类别均正确。

询问次数证明

设第一阶段产生了 s 对不等关系,即 |l|=|h|=s;产生了 r 次相等合并;最后是否存在剩余组记为 e,其中 e 为 0 或 1。每次相等比较使未处理组数量减少 1,每次不等比较使未处理组数量减少 2,因此有:

n = r + 2s + e

第一阶段询问 r+s 次。两次 split2 最多各询问 s-1 次,共 2s-2 次。若 e=0,无需额外询问,总次数为:

r+s+2s-2 = r+3s-2 = n+s-2 <= floor(3n/2)

e=1,最后最多额外询问 2 次,总次数为:

r+s+2s-2+2 = r+3s = n+s-1 <= floor(3n/2)

其中 s <= floor(n/2),当 e=1 时还有 s <= (n-1)/2。因此总询问次数一定不超过题目限制。

参考代码

#pragma GCC optimize(3, "Ofast", "inline", "unroll-loops")
#include<bits/stdc++.h>
using namespace std;
#define i64 long long
#define u64 unsigned long long
#define debug(x) cout<<x<<endl;
#define endl "\n"

char ask(int x, int y)
{
    cout << "? " << x << " " << y << endl;
    cout.flush();

    char ch;
    cin >> ch;
    if (!cin) exit(0);
    if (ch != '<' && ch != '>' && ch != '=') exit(0);
    return ch;
}

void put(const vector<int>& a, char c, vector<char>& ans, const vector<vector<int>>& g)
{
    for (int id : a)
    {
        for (int x : g[id]) ans[x] = c;
    }
}

pair<vector<int>, vector<int>> split2(const vector<int>& v)
{
    if (v.empty()) return {{}, {}};
    vector<int> x, y;
    x.push_back(v[0]);
    int dir = 0;
    for (int i = 1; i < (int)v.size(); i++)
    {
        char ch = ask(v[0], v[i]);
        if (ch == '=')
        {
            x.push_back(v[i]);
        }
        else
        {
            y.push_back(v[i]);
            if (!dir) dir = (ch == '<' ? -1 : 1);
        }
    }
    if (y.empty()) return {x, {}};
    if (dir == -1) return {x, y};
    return {y, x};
}

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

    vector<vector<int>> g(n + 1);
    for (int i = 1; i <= n; i++) g[i].push_back(i);

    vector<int> u, l, h;
    for (int i = 1; i <= n; i++) u.push_back(i);
    while ((int)u.size() >= 2)
    {
        int x = u.back(); u.pop_back();
        int y = u.back(); u.pop_back();

        char ch = ask(x, y);
        if (ch == '=')
        {
            for (int v : g[y]) g[x].push_back(v);
            u.push_back(x);
        }
        else if (ch == '<')
        {
            l.push_back(x);
            h.push_back(y);
        }
        else
        {
            l.push_back(y);
            h.push_back(x);
        }
    }

    int ex = -1;
    if (!u.empty()) ex = u[0];
    auto [l1, l2] = split2(l);
    auto [h1, h2] = split2(h);

    vector<char> ans(n + 1, '?');

    if (ex == -1)
    {
        if (!l2.empty() && !h2.empty())
        {
            put(l1, 'A', ans, g);
            put(l2, 'B', ans, g);
            put(h1, 'B', ans, g);
            put(h2, 'C', ans, g);
        }
        else if (!l2.empty())
        {
            put(l1, 'A', ans, g);
            put(l2, 'B', ans, g);
            put(h1, 'C', ans, g);
        }
        else
        {
            put(l1, 'A', ans, g);
            put(h1, 'B', ans, g);
            put(h2, 'C', ans, g);
        }
    }
    else
    {
        if (!l2.empty() || !h2.empty())
        {
            int b = -1;

            if (!l2.empty() && !h2.empty())
            {
                put(l1, 'A', ans, g);
                put(l2, 'B', ans, g);
                put(h1, 'B', ans, g);
                put(h2, 'C', ans, g);
                b = l2[0];
            }
            else if (!l2.empty())
            {
                put(l1, 'A', ans, g);
                put(l2, 'B', ans, g);
                put(h1, 'C', ans, g);
                b = l2[0];
            }
            else
            {
                put(l1, 'A', ans, g);
                put(h1, 'B', ans, g);
                put(h2, 'C', ans, g);
                b = h1[0];
            }

            char ch = ask(ex, b);
            for (int v : g[ex])
            {
                if (ch == '<') ans[v] = 'A';
                else if (ch == '=') ans[v] = 'B';
                else ans[v] = 'C';
            }
        }
        else
        {
            int x = l1[0], y = h1[0];

            char c1 = ask(ex, x);
            if (c1 == '<')
            {
                put(l1, 'B', ans, g);
                put(h1, 'C', ans, g);
                for (int v : g[ex]) ans[v] = 'A';
            }
            else
            {
                char c2 = ask(ex, y);
                if (c2 == '>')
                {
                    put(l1, 'A', ans, g);
                    put(h1, 'B', ans, g);
                    for (int v : g[ex]) ans[v] = 'C';
                }
                else
                {
                    put(l1, 'A', ans, g);
                    put(h1, 'C', ans, g);
                    for (int v : g[ex]) ans[v] = 'B';
                }
            }
        }
    }

    cout << "! ";
    for (int i = 1; i <= n; i++)
    {
        cout << ans[i] << (i == n ? '\n' : ' ');
    }
    cout.flush();
}

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

    int _;
    cin >> _;
    while (_--) sol();
    return 0;
}

B. Agent 执行计划树

基本信息

  • 时间限制:2000 ms
  • 内存限制:262144 KB
  • 难度:困难
  • CF 估分:2100
  • 标签:树形 DP、换根 DP、组合数学、逆元

题意简述

给定一棵 n 个点的树。对于每个点 r,将树以 r 为根,要求每个节点必须先于它的所有子节点执行,求合法执行序列数量,答案对 998244353 取模。

数据范围

  • 1 <= n <= 2 * 10^5

解题思路

对于一棵以 r 为根的树,合法执行日志要求每个节点必须在它的所有子节点之前执行。这个问题等价于统计这棵有根树的拓扑序数量。

设以 r 为根时,节点 u 的子树大小为 siz_u。有一个经典结论:

ans_r = n! / Π siz_u

原因可以理解为:根节点必须最先执行;之后各个子树内部要保持各自的合法顺序,但不同子树之间可以任意交错。递归展开后,所有组合数会相互抵消,最终得到上面的形式。

先随便选 1 为根,DFS 求出每个点的子树大小 siz[u]。那么:

ans[1] = n! * inv(siz[1]) * inv(siz[2]) * ... * inv(siz[n])

其中 siz[1]=n

接下来考虑换根。假设当前根为 u,现在把根从 u 换到它的儿子 v,并且在以 1 为根的 DFS 树中 vu 的儿子,令 s = siz[v]

换根前,乘积 Π siz_x 中与这条边相关的两个子树大小为:

u 的子树大小 = n
v 的子树大小 = s

换根到 v 后,只有 uv 的子树大小发生变化:

v 的子树大小 = n
u 的子树大小 = n - s

其他点的子树大小不变。因此:

ans[v] / ans[u] = (n * s) / (n * (n - s)) = s / (n - s)

所以换根转移为:

ans[v] = ans[u] * siz[v] / (n - siz[v])

在模 998244353 意义下,用逆元表示除法即可。

复杂度分析

  • 时间复杂度:O(n log mod)
  • 空间复杂度:O(n)

参考代码

#pragma GCC optimize(3, "Ofast", "inline", "unroll-loops")
#include<bits/stdc++.h>
using namespace std;
#define i64 long long
#define u64 unsigned long long
#define debug(x) cout<<x<<endl;
#define endl "\n"

const i64 mod = 998244353;

i64 qpow(i64 a, i64 b)
{
    i64 res = 1;
    while (b)
    {
        if (b & 1) res = res * a % mod;
        a = a * a % mod;
        b >>= 1;
    }
    return res;
}

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

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

    vector<i64> fac(n + 1), inv(n + 1), siz(n + 1), ans(n + 1);
    fac[0] = 1;
    for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % mod;
    for (int i = 1; i <= n; i++) inv[i] = qpow(i, mod - 2);

    auto dfs1 = [&](auto self, int u, int fa) -> void
    {
        siz[u] = 1;
        for (int v : g[u])
        {
            if (v == fa) continue;
            self(self, v, u);
            siz[u] += siz[v];
        }
    };

    dfs1(dfs1, 1, 0);

    ans[1] = fac[n];
    for (int i = 1; i <= n; i++)
    {
        ans[1] = ans[1] * inv[siz[i]] % mod;
    }

    auto dfs2 = [&](auto self, int u, int fa) -> void
    {
        for (int v : g[u])
        {
            if (v == fa) continue;
            ans[v] = ans[u] * siz[v] % mod * inv[n - siz[v]] % mod;
            self(self, v, u);
        }
    };

    dfs2(dfs2, 1, 0);

    for (int i = 1; i <= n; i++)
    {
        cout << ans[i] << (i == n ? '\n' : ' ');
    }
}

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

    sol();
    return 0;
}

C. 鸟为什么会飞

基本信息

  • 时间限制:1000 ms
  • 内存限制:32768 KB
  • 难度:签到
  • CF 估分:900
  • 标签:数论、gcd、质因子、枚举

题意简述

给定 x,y,寻找最小的整数 v,满足 2 <= v <= max(x,y)gcd(v,x)=gcd(v,y)=1。若不存在则输出 -1

数据范围

  • 1 <= T <= 10^4
  • 1 <= x,y <= 2 * 10^5

解题思路

题目要求找到最小的可用编号 v。根据冲突定义,vx 不冲突等价于 gcd(v,x)=1vy 不冲突等价于 gcd(v,y)=1

因此可以从小到大枚举 v,范围为 [2,max(x,y)]。对于每个 v,检查:

gcd(v,x)=1 且 gcd(v,y)=1

第一个满足条件的 v 就是答案。若枚举完整个区间仍不存在合法编号,则输出 -1

复杂度分析

  • 时间复杂度:O(T * max(x,y) * log max(x,y))
  • 空间复杂度:O(1)

参考代码

//#pragma GCC optimize(3, "Ofast", "inline", "unroll-loops")
#include<bits/stdc++.h>
using namespace std;
#define i64 long long
#define u64 unsigned long long
#define debug(x) cout<<x<<endl;
#define endl "\n"
void sol()
{
    int x, y;
    cin >> x >> y;
    int limit = max(x, y);
    int ans = -1;
    for (int v = 2; v <= limit; v++)
    {
        if (__gcd(v, x) == 1 && __gcd(v, y) == 1)
        {
                ans = v;
                break;
        }
    }
    cout << ans << '\n';
}
signed main()
{
    //freopen("input.txt","r",stdin);
	//freopen("output.txt","w",stdout);
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int _=1;
    cin>>_;
    while(_--)sol();
    return 0;
}

D. GTNH?

基本信息

  • 时间限制:2000 ms
  • 内存限制:262144 KB
  • 难度:中等
  • CF 估分:1600
  • 标签:最短路、Dijkstra、虚点建图

题意简述

给定带权无向图,每个点有颜色。处在某个点时,可以花费该颜色对应代价跳到任意同色点。求从 1n 的最短路,不可达输出 -1

数据范围

  • 1 <= n,m,k <= 2 * 10^5
  • 边权、跳跃代价 <= 10^9

解题思路

这题很明显是最短路,关键在于如何处理“可以跳到任意同色点”这个操作。

如果直接在所有同色点之间连边,最坏情况下边数会达到 O(n^2),无法接受。注意到同色跳跃本质上是“从某个点进入这个颜色集合,再从这个颜色集合出来”。因此可以给每种颜色建立一个虚点。

对于颜色 j,建立虚点 n+j。若原图中的点 i 颜色为 c[i],则连两条边:

i -> n+c[i],边权为 a[c[i]]
n+c[i] -> i,边权为 0

这样从某个颜色为 j 的点 u 出发,先花费 a[j] 走到颜色虚点,再花费 0 走到任意一个同色点 v,就等价于执行一次同色跳跃。

原来的 m 条无向边照常双向连边。建完图后,直接从 1 跑 Dijkstra,答案就是到点 n 的最短距离。

复杂度分析

  • 点数:n+k
  • 边数:2m+2n
  • 时间复杂度:O((n+m+k) log(n+k))
  • 空间复杂度:O(n+m+k)

参考代码

#pragma GCC optimize(3, "Ofast", "inline", "unroll-loops")
#include<bits/stdc++.h>
using namespace std;
#define i64 long long
#define u64 unsigned long long
#define debug(x) cout<<x<<endl;
#define endl "\n"
//很显然的最短路,但是要考虑的是如何建图
//存在集合和集合之间的关系,可以考虑在集合之间多建一条路就行
//然后跑正常的dij即可
const i64 inf = (1LL << 62);
struct node
{
    int to;
    i64 w;
};
void sol()
{
    int n, m, k;
    cin >> n >> m >> k;
    vector<int> c(n + 1);
    for (int i = 1; i <= n; i++) cin >> c[i];
    vector<i64> a(k + 1);
    for (int i = 1; i <= k; i++) cin >> a[i];
    int tot = n + k;
    vector<vector<node>> g(tot + 1);
    for (int i = 1; i <= m; i++)
    {
        int u, v;
        i64 w;
        cin >> u >> v >> w;
        g[u].push_back({v, w});
        g[v].push_back({u, w});
    }
    for (int i = 1; i <= n; i++)
    {
        int id = n + c[i];
        g[i].push_back({id, a[c[i]]});
        g[id].push_back({i, 0});
    }
    vector<i64> dis(tot + 1, inf);
    priority_queue<pair<i64, int>, vector<pair<i64, int>>, greater<pair<i64, int>>> q;
    dis[1] = 0;
    q.push({0, 1});
    while (!q.empty())
    {
        auto [d, u] = q.top();
        q.pop();
        if (d != dis[u]) continue;
        for (auto [v, w] : g[u])
        {
            if (dis[v] > d + w)
            {
                dis[v] = d + w;
                q.push({dis[v], v});
            }
        }
    }
    if (dis[n] == inf) cout << -1 << endl;
    else cout << dis[n] << endl;
    return;
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int _ = 1;
    while (_--) sol();
    return 0;
}

E. 矩阵构造

基本信息

  • 时间限制:1000 ms
  • 内存限制:524288 KB
  • 难度:困难
  • CF 估分:2300
  • 标签:构造、异或排列、完全映射

题意简述

构造一个 3 * n 矩阵,使每一行都是 0n-1 的排列,并且每一列三个数的异或和为 0。若不存在输出 NO

数据范围

  • 1 <= T <= 20
  • 1 <= n <= 10^4

解题思路

本题解由 AI 辅助生成和整理,构造代码来自给定代码,建议赛后结合官方题解复核。

我们可以固定第一行为:

0, 1, 2, ..., n-1

若第二行为一个排列 p,那么为了让每一列异或和为 0,第三行必须为:

q_i = i xor p_i

因此问题转化为:构造一个排列 p,使得 q_i = i xor p_i 也是 0n-1 的排列。

先看无解条件。三行分别都是 0n-1 的排列,所以每一行的整体异或和都等于:

S = 0 xor 1 xor 2 xor ... xor (n-1)

又因为每一列异或和为 0,所以整个矩阵所有数的异或和也应为 0。但三行整体异或起来是:

S xor S xor S = S

因此必须有 S=0。而 0 xor 1 xor ... xor (n-1)=0 当且仅当 n=1n4 的倍数。所以只有这两类情况有解。

构造部分比较复杂。代码中的核心是构造一个长度为 n 的排列 p,并输出:

第一行:i
第二行:p_i
第三行:i xor p_i

辅助函数 gao2(len) 会构造 len 个二元组 (x_i,y_i),满足两个关键性质:

  1. 所有 x_i,y_i 合起来,每个 0..len-1 中的数恰好出现两次。
  2. 所有 i xor x_i, i xor y_i 合起来,每个 0..len-1 中的数也恰好出现两次。

这两个性质由 gao1gao2 的递归构造保证。gao1 处理长度为二的幂的部分,并保证左右两个坐标各自为排列,同时把异或值分布到固定半区;gao2 每次取不超过 n 的最大二的幂 N,将 [0,N) 与剩余 [N,n) 拼接起来,递归保持“端点出现两次”和“异或值出现两次”的不变量。

n4 的倍数时,令 len=n/4。先用 gao2(len) 得到 len 个二元组。由于每个数在端点中出现两次,可以把这些二元组看成一张每个点度数为 2 的图。代码中的 DFS 只是给每条边定向,使得:

pre[i].first 形成 0..len-1 的排列
pre[i].second 也形成 0..len-1 的排列

然后每个二元组 (x,y) 扩展成四个位置:

p[4i+0] = 4x+0
p[4i+1] = 4x+2
p[4i+2] = 4y+1
p[4i+3] = 4y+3

因为 xy 分别都是排列,所以第二行 p 一定覆盖所有形如 4t+0,4t+1,4t+2,4t+3 的数,即第二行是排列。

再看第三行。对位置 4i+r,有:

(4i+r) xor (4z+s) = 4 * (i xor z) + (r xor s)

也就是说,高位由 i xor z 决定,低两位由 r xor s 决定。由 gao2 的第二个不变量,每个高位值会出现两次。代码用 vis 检查第三行是否已经出现过同一个高位的某一组低位;如果已经出现,则把当前相邻两个位置的低两位从 {0,3} 调整为 {1,2}。这样每个高位最终都对应低两位 0,1,2,3 各一次,因此第三行也是排列。

所以当 n=14|n 时可以构造出合法矩阵,其余情况无解。

复杂度分析

  • 时间复杂度:O(n log n)
  • 空间复杂度:O(n)

参考代码

#include <bits/stdc++.h>
using namespace std;

vector<pair<int,int>> gao1(int N,int k){
	if (N<=2){
		vector<pair<int,int>> ans;
		for (int i=0;i<N;i++) ans.emplace_back(i,i^(N-1));
		return ans;
	}
	int mid=N/2;
	vector<pair<int,int>> ans(N);
	for (int i=0;i<N;i++) ans[i].second=i^(i/2+mid);

	if (k<mid){
		vector<pair<int,int>> pre=gao1(mid,k);
		for (int i=0;i<mid;i++){
			ans[i].first=pre[i].first;
			ans[i+mid].first=pre[i].second^mid;
		}
	}
	else{
		vector<pair<int,int>> pre=gao1(mid,k-mid);
		for (int i=0;i<mid;i++){
			ans[i].first=pre[i].second;
			ans[i+mid].first=pre[i].first^mid;
		}
	}
	return ans;
}

vector<pair<int,int>> gao2(int n){
	if (n<=2){
		vector<pair<int,int>> ans;
		for (int i=0;i<n;i++) ans.emplace_back(i,i^(n-1));
		return ans;
	}
	int N=1;
	for (;(N<<1)<=n;) N<<=1;
	int k=n-N;
	vector<pair<int,int>> ans(n);
	vector<pair<int,int>> pre1=gao1(N,k);
	vector<pair<int,int>> pre2=gao2(k);

	for (int i=0;i<k;i++) ans[i].first=pre2[i].first^N;
	for (int i=k;i<N;i++) ans[i].first=pre1[i].first;
	for (int i=0;i<k;i++) ans[i+N].first=pre2[i].second;

	for (int i=0;i<N;i++) ans[i].second=pre1[i].second;
	for (int i=0;i<k;i++) ans[i+N].second=pre1[i].first^N;

	return ans;
}

vector<int> gao3(int n){
	if (n==1){
		return {0};
	}
	assert((n>0) && (n%4==0));

	int len=n/4;
	vector<int> ans(n);
	auto pre=gao2(len);

	{
		vector<vector<int>> pos(len);
		for (int i=0;i<len;i++){
			pos[pre[i].first].push_back(i);
			pos[pre[i].second].push_back(i);
		}
		vector<int> vis(len);
		function<void(int,int)> dfs=[&](int x,int p){
			if (vis[x]) return;
			vis[x]=1;
			if (pre[p].first!=x) swap(pre[p].first,pre[p].second);
			int y=pre[p].second;
			int nxtp=pos[y][0]+pos[y][1]-p;
			if (!vis[y]) dfs(y,nxtp);
		};
		for (int i=0;i<len;i++) if (!vis[i]) dfs(i,pos[i][0]);
	}
	{
		for (int i=0;i<len;i++){
			ans[i*4+0]=pre[i].first*4+0;
			ans[i*4+1]=pre[i].first*4+2;
			ans[i*4+2]=pre[i].second*4+1;
			ans[i*4+3]=pre[i].second*4+3;
		}
		vector<int> vis(n);
		for (int i=0;i<n;i+=2){
			if (vis[i^ans[i]]){
				ans[i]+=2;
				ans[i+1]-=2;
			}
			vis[i^ans[i]]++;
			vis[(i+1)^ans[i+1]]++;
		}
	}
	return ans;
}

int main(){
	ios_base::sync_with_stdio(false);
	cin.tie(0);
	int Tcase=1;
	cin >> Tcase;
	for (;Tcase--;){
		int n;
		cin >> n;
		if (n==1 || (n>0 && n%4==0)){
			cout << "YES" << "\n";
			auto ans=gao3(n);

            vector<int> v;
			for (int i=0;i<n;i++) cout << i << " \n"[i==n-1];
			for (int i=0;i<n;i++) {cout << ans[i] << " \n"[i==n-1];v.push_back(ans[i]^i);}
            for(int i:v) cout<<i<<" ";
            cout<<endl;
		}
		else{
			cout << "NO" << "\n";
		}
	}
	return 0;
}

F. 生命因何而沉睡

基本信息

  • 时间限制:1000 ms
  • 内存限制:32768 KB
  • 难度:签到
  • CF 估分:800
  • 标签:模拟、相邻差、边界判断

题意简述

课程持续 T 分钟,若连续 k 分钟没有重点内容就会睡着。给出重点时刻,求最多能坚持到第几分钟,若不睡着则输出 T

数据范围

  • 1 <= q <= 10^4
  • 1 <= n <= 10^5
  • 1 <= k <= T <= 10^9
  • 所有测试数据 sum n <= 10^5

解题思路

维护上一次听到重点内容的时刻 now,初始为 0

依次扫描所有重点时刻 a[i]

  • 如果 a[i] - now > k,说明从 now 之后开始,连续 k 分钟内都没有新的重点,开拓者会在 now+k 时刻睡着,直接输出。
  • 否则,说明他可以坚持到这次重点内容出现,于是令 now=a[i],继续扫描。

所有重点时刻扫描完后,如果最后一次重点之后还能坚持到下课,则答案是 T;否则会在 now+k 睡着。因此输出:

min(T, now+k)

注意判断条件是 a[i]-now>k,不是 >=k。如果刚好第 k 分钟讲到重点,他不会睡着,会被重新唤醒。

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

参考代码

#pragma GCC optimize(3, "Ofast", "inline", "unroll-loops")
#include<bits/stdc++.h>
using namespace std;
#define i64 long long
#define u64 unsigned long long
#define debug(x) cout<<x<<endl;
#define endl "\n"
void sol()
{
    int n,t,k;cin>>n>>t>>k;
    int now=0;
    vector<int>a(n+1);
    for(int i=1;i<=n;++i)cin>>a[i];
    for(int i=1;i<=n;++i)
    {
        if(a[i]-now>k)
        {
            cout<<now+k<<endl;
            return;
        }
        now=a[i];
    }
    cout<<min(t,now+k)<<endl;
    return;
}
signed main()
{
    //freopen("input.txt","r",stdin);
	//freopen("output.txt","w",stdout);
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int _=1;
    cin>>_;
    while(_--)sol();
    return 0;
}

G. ACMBTI String

基本信息

  • 时间限制:2000 ms
  • 内存限制:524288 KB
  • 难度:困难
  • CF 估分:2200
  • 标签:构造、子序列、状态压缩、搜索

题意简述

有 16 个固定的 ACMBTI 串。给定长度为 16 的 01 串 X,构造一个大写字符串 Y,使得第 i 个 ACMBTI 串是 Y 的子序列当且仅当 X_i=1。若不存在输出 NO

数据范围

  • T <= 10^4
  • 若存在答案,可证明存在长度不超过 100 的答案

解题思路

本题解由 AI 辅助生成和整理,建议赛后结合官方题解复核。

一共有 16 个目标串,每个串长度都是 4。我们可以把一个字符串 Y 对这 16 个目标串的匹配情况压成一个 16 位二进制数 mask:如果第 i 个 ACMBTI 串是 Y 的子序列,则 mask 的第 i 位为 1。

考虑对任意当前构造出的字符串 Y,维护一个状态 state

state[i] = 第 i 个目标串目前最多已经匹配到的前缀长度

因为目标串长度为 4,所以 state[i] 的取值只可能是 0..4。当我们在 Y 后面追加一个字符 ch 时,对于每个目标串,如果它当前需要匹配的下一个字符正好是 ch,就让 state[i]++

这样,state[i]=4 就代表第 i 个 ACMBTI 串已经成为当前字符串的子序列。

由于只有 16 个目标串、8 个有效字符 EINSFTJP,可以从空串开始 BFS 枚举所有可达状态。每到达一个状态,就根据 state[i]==4 得到当前字符串对应的 mask,如果这个 mask 还没有答案,就记录当前 BFS 字符串。BFS 结束后,对于每个询问字符串 X,把它转成 16 位 mask,若预处理时有答案就输出,否则输出 NO

空集合比较特殊:如果 X 全是 0,可以直接输出 "A",因为 A 不会匹配任何 ACMBTI 串。

复杂度分析

  • 预处理状态数约为一万级,转移字符数为 8。
  • 单次询问时间复杂度:O(16)
  • 空间复杂度:O(可达状态数 + 2^16)

参考代码

#pragma GCC optimize(3, "Ofast", "inline", "unroll-loops")
#include<bits/stdc++.h>
using namespace std;
#define i64 long long
#define u64 unsigned long long
#define debug(x) cout<<x<<endl;
#define endl "\n"

vector<string> pat;
vector<string> ans(1 << 16, "");

u64 encode(const array<int, 16>& a)
{
    u64 res = 0;
    for (int i = 15; i >= 0; i--)
    {
        res = res * 5 + a[i];
    }
    return res;
}

array<int, 16> decode(u64 x)
{
    array<int, 16> a{};
    for (int i = 0; i < 16; i++)
    {
        a[i] = x % 5;
        x /= 5;
    }
    return a;
}

int get_mask(const array<int, 16>& a)
{
    int mask = 0;
    for (int i = 0; i < 16; i++)
    {
        if (a[i] == 4) mask |= (1 << i);
    }
    return mask;
}

void init()
{
    string s1 = "EI";
    string s2 = "NS";
    string s3 = "FT";
    string s4 = "JP";

    for (char a : s1)
    {
        for (char b : s2)
        {
            for (char c : s3)
            {
                for (char d : s4)
                {
                    string s;
                    s += a;
                    s += b;
                    s += c;
                    s += d;
                    pat.push_back(s);
                }
            }
        }
    }

    ans[0] = "A";

    string alpha = "EINSFTJP";
    queue<u64> q;
    unordered_map<u64, string> mp;

    array<int, 16> st{};
    u64 start = encode(st);
    q.push(start);
    mp[start] = "";

    while (!q.empty())
    {
        u64 cur = q.front();
        q.pop();

        array<int, 16> a = decode(cur);
        string now = mp[cur];

        int mask = get_mask(a);
        if (ans[mask].empty())
        {
            ans[mask] = now;
        }

        for (char ch : alpha)
        {
            array<int, 16> b = a;
            for (int i = 0; i < 16; i++)
            {
                if (b[i] < 4 && pat[i][b[i]] == ch)
                {
                    b[i]++;
                }
            }

            u64 nxt = encode(b);
            if (!mp.count(nxt))
            {
                mp[nxt] = now + ch;
                q.push(nxt);
            }
        }
    }
}

void sol()
{
    string x;
    cin >> x;

    int mask = 0;
    for (int i = 0; i < 16; i++)
    {
        if (x[i] == '1') mask |= (1 << i);
    }

    if (ans[mask].empty())
    {
        cout << "NO" << endl;
    }
    else
    {
        cout << "YES" << endl;
        cout << ans[mask].size() << endl;
        cout << ans[mask] << endl;
    }
}

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

    init();

    int _=1;
    cin>>_;
    while(_--)sol();
    return 0;
}

H. 以此烈火,斩无不断

基本信息

  • 时间限制:1000 ms
  • 内存限制:32768 KB
  • 难度:困难+
  • CF 估分:2400
  • 标签:组合博弈、排列计数、数学

题意简述

对一个排列的每个连续子区间视作一个公平组合博弈状态。每次可以选择任意个正数元素同时减一,无法操作者输。定义先手必胜区间为有效区间,求使有效区间数量达到最大值的排列个数,答案对 1e9+7 取模。

数据范围

  • 1 <= t <= 10^4
  • 1 <= n <= 3 * 10^5

解题思路

先分析区间对应的博弈。

一个状态由若干个正整数构成。每次可以选择任意个仍然大于 0 的数,并让它们同时减 1。结论是:

当前状态为必败态,当且仅当所有数都是偶数。

证明思路很简单:

  • 如果所有数都是偶数,那么无论先手怎么操作,被选择的那些数都会变成奇数,所以操作后一定存在奇数。
  • 如果存在奇数,那么先手可以选择所有奇数并让它们减 1,这样所有奇数都变成偶数,原本的偶数不动,局面变成全偶数。

因此先手必胜当且仅当区间中至少存在一个奇数。

于是对于一个排列 p,有效区间数量就是:

总区间数 - 全偶数区间数

总区间数固定,所以要让有效区间数量最大,就要让全偶数区间数量最少。

设排列中偶数个数为:

even = n / 2

奇数个数为:

odd = n - even

每个偶数单独作为长度为 1 的区间时,一定是全偶数区间,所以全偶数区间至少有 even 个。若两个偶数相邻,就会额外产生长度大于 1 的全偶数区间。因此最优条件就是:任意两个偶数不能相邻。

先摆放所有奇数,它们会形成 odd+1 个空隙:

_ odd _ odd _ ... odd _

为了让偶数不相邻,需要从这 odd+1 个空隙中选择 even 个放偶数,方案数为:

C(odd+1, even)

选好奇偶位置后,奇数之间可以任意排列,偶数之间也可以任意排列,所以答案为:

C(odd+1, even) * odd! * even!

1e9+7 取模即可。

复杂度分析

  • 预处理阶乘和逆元:O(N)
  • 单组询问:O(1)
  • 空间复杂度:O(N)

参考代码

#pragma GCC optimize(3, "Ofast", "inline", "unroll-loops")
#include<bits/stdc++.h>
using namespace std;
#define i64 long long
#define u64 unsigned long long
#define debug(x) cout<<x<<endl;
#define endl "\n"

const int mod = 1000000007;
const int N = 300000 + 5;

i64 fac[N], ifac[N];

i64 qpow(i64 a, i64 b)
{
    i64 res = 1;
    while (b)
    {
        if (b & 1) res = res * a % mod;
        a = a * a % mod;
        b >>= 1;
    }
    return res;
}

i64 C(int n, int m)
{
    if (m < 0 || m > n) return 0;
    return fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}

void init()
{
    fac[0] = 1;
    for (int i = 1; i < N; i++) fac[i] = fac[i - 1] * i % mod;
    ifac[N - 1] = qpow(fac[N - 1], mod - 2);
    for (int i = N - 2; i >= 0; i--) ifac[i] = ifac[i + 1] * (i + 1) % mod;
}

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

    int even = n / 2;
    int odd = n - even;

    i64 ans = C(odd + 1, even);
    ans = ans * fac[odd] % mod;
    ans = ans * fac[even] % mod;

    cout << ans << endl;
}

signed main()
{
    //freopen("input.txt","r",stdin);
    //freopen("output.txt","w",stdout);
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);

    init();

    int _ = 1;
    cin >> _;
    while (_--) sol();

    return 0;
}

I. 数世往回皆空虚

基本信息

  • 时间限制:1000 ms
  • 内存限制:32768 KB
  • 难度:签到
  • CF 估分:800
  • 标签:整除、基础输入输出

题意简述

签到题,略。

数据范围

  • 1 <= a,b <= 10^9

解题思路

签到题,略。


J. 雪融之后

基本信息

  • 时间限制:2000 ms
  • 内存限制:262144 KB
  • 难度:简单
  • CF 估分:1000
  • 标签:离线、并查集、网格连通

题意简述

给定 n*m 网格海拔和按水位非降序给出的询问。第 k 个询问判断两个格子在海拔严格大于水位的可通行格子中是否连通。

数据范围

  • n*m <= 2 * 10^5
  • q <= 2 * 10^5
  • 海拔、水位 <= 10^9
  • 询问水位满足 w_1 <= w_2 <= ... <= w_q

解题思路

要判断网格连通,容易想到 DFS/BFS 或并查集。由于有很多询问,如果每次询问都重新扫一遍网格,复杂度会达到 O(qnm),无法接受。

注意到一个格子在水位为 w 时可通行,当且仅当:

h > w

也就是说,水位越低,可通行的格子越多;水位越高,可通行的格子越少。这个过程具有单调性。我们可以把询问按水位 w 从大到小排序,同时把所有格子也按海拔 h 从大到小排序。

处理某个询问 w 时,将所有满足 h>w 且还没有加入过的格子加入并查集。每加入一个格子,就和上下左右已经加入的邻居合并。这样当前并查集恰好表示水位为 w 时所有可通行格子的连通关系。

最后对于询问中的两个位置:

  • 如果任意一个位置没有加入,说明它被淹没,答案为 NO
  • 否则检查它们是否在同一个并查集里。

由于每个格子最多加入一次,每条邻接关系最多检查常数次,所以整体复杂度可以接受。

复杂度分析

  • 排序复杂度:O(nm log(nm) + q log q)
  • 并查集处理复杂度:O(nm α(nm))
  • 空间复杂度:O(nm + q)

参考代码

#pragma GCC optimize(3, "Ofast", "inline", "unroll-loops")
#include<bits/stdc++.h>
using namespace std;
#define i64 long long
#define u64 unsigned long long
#define debug(x) cout<<x<<endl;
#define endl "\n"
//要判联通,首先可能是dsu/dfs之类的
//但是还存在一个动态的过程,其实也不算动态
//如果是dsu,我们要知道在k天的时候有哪几个点是可以走的
//如果每次这样硬扫一遍就是qnm肯定不行,那我们要优化下找路的这个过程
//可以咋优化呢
//对于这个水流的增长,很显然是有单调性的,那我们直接从最后到前面去加点就行了
//然后这样就是nm+q,应该没啥问题
//剩下就是代码实现了,注意下w的排序和dsu实现
int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
struct DSU
{
    vector<int> f, sz;

    void init(int n)
    {
        f.resize(n + 1);
        sz.assign(n + 1, 1);
        iota(f.begin(), f.end(), 0);
    }

    int find(int x)
    {
        if (f[x] == x) return x;
        return f[x] = find(f[x]);
    }

    void merge(int x, int y)
    {
        x = find(x), y = find(y);
        if (x == y) return;
        if (sz[x] < sz[y]) swap(x, y);
        f[y] = x;
        sz[x] += sz[y];
    }
};
struct node
{
    int h, id;
    bool operator < (const node &a) const
    {
        return h > a.h;
    }
};
struct node1
{
    int w, a, b, c, d, id;
    bool operator < (const node1 &a) const
    {
        return w > a.w;
    }
};
void sol()
{
    int n,m,q;cin>>n>>m>>q;
    vector<int>h(n*m+1);
    vector<node>p;
    p.reserve(n * m);
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=m;++j)
        {
            int x;cin>>x;
            int now=(i-1)*m+j;
            h[now]=x;
            p.push_back({x,now});
        }
    }
    vector<node1>tmp(q);
    for(int i=0;i<q;++i)
    {
        cin>>tmp[i].w>>tmp[i].a>>tmp[i].b>>tmp[i].c>>tmp[i].d;
        tmp[i].id=i;
    }
    sort(p.begin(),p.end());
    sort(tmp.begin(),tmp.end());

    DSU check;
    check.init(n*m);
    vector<int>vis(n*m+1,0);
    vector<string>ans(q);

    int j=0;
    for(auto &qq:tmp)
    {
       while(j<n*m&&p[j].h>qq.w)
       {
         int id=p[j].id;
         vis[id]=1;
         int x = (id - 1) / m + 1;
         int y = (id - 1) % m + 1;
         for (int k = 0; k < 4; k++)
         {
            int nx = x + dx[k];
            int ny = y + dy[k];
            if (nx < 1 || nx > n || ny < 1 || ny > m) continue;
            int nid =(nx-1)*m+ny;
            if (vis[nid]) check.merge(id, nid);
         }
         j++;
       }
         int u=(qq.a-1)*m+qq.b;
         int v=(qq.c-1)*m+qq.d;
        if (vis[u] && vis[v] && check.find(u) == check.find(v)) ans[qq.id] = "YES";
        else ans[qq.id] = "NO";
    }
    for (int i = 0; i < q; i++) cout << ans[i] << endl;
    return;
}
signed main()
{
    //freopen("input.txt","r",stdin);
	//freopen("output.txt","w",stdout);
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int _=1;
    //cin>>_;
    while(_--)sol();
    return 0;
}

K. Taibo 相信着 LanGod 的希望灯塔并偷偷将感染和被蛇咬加入他的卡组

基本信息

  • 时间限制:4000 ms
  • 内存限制:524288 KB
  • 难度:中等偏难
  • CF 估分:1900
  • 标签:动态规划、状态优化、决策最优化

题意简述

n 张毒雾和 m 张触媒。每回合先按当前毒雾层数叠毒,再打一张未出的牌,最后触发 c+1 次中毒伤害。求通过安排出牌顺序能造成的最大总伤害。

数据范围

  • 1 <= T <= 50
  • 1 <= n,m <= 50

解题思路

这题很难直接贪心,因为打出毒雾会增加之后每回合叠毒量,打出触媒会增加之后每回合结算次数,两者互相影响。

由于 n,m <= 50,可以直接做动态规划。

设:

dp[i][j][p]

表示已经打出了 i 张毒雾、j 张触媒,且当前回合开始前敌人有 p 层中毒时,最多已经造成的伤害。

对于当前状态,回合开始时会先叠加 i 层毒,所以当前可结算的中毒层数为:

q = p + i

接下来有两种选择。

如果打出一张毒雾,那么毒雾层数变成 i+1,触媒层数仍为 j。回合结束时一共触发 j+1 次中毒,所以实际触发次数为:

t = min(q, j+1)

造成的伤害为:

q + (q-1) + ... + (q-t+1)
= t * (2q - t + 1) / 2

剩余中毒层数为:

np = q - t

于是转移:

dp[i+1][j][np] = max(dp[i+1][j][np], dp[i][j][p] + damage)

如果打出一张触媒,那么触媒层数会先增加到 j+1,回合结束时总共触发 (j+1)+1 = j+2 次中毒,所以:

t = min(q, j+2)

类似地转移到:

dp[i][j+1][np]

当前中毒层数的上界可以粗略取:

n*m + n*(n-1)/2

表示毒雾叠毒可能产生的最大累计中毒量。最后在所有 dp[n][m][p] 中取最大值即可。

复杂度分析

  • 状态数:O(n*m*lim)
  • 转移复杂度:O(1)
  • 其中 lim = n*m + n*(n-1)/2
  • 空间复杂度:O(n*m*lim)

参考代码

#pragma GCC optimize(3, "Ofast", "inline", "unroll-loops")
#include <bits/stdc++.h>
using namespace std;
#define i64 long long
#define u64 unsigned long long
#define debug(x) cout << x << endl;
#define endl "\n"
// 很显然的dp,无论是哪一个的变化都无法直接贪心
// dp[i][j][p]
// 本轮攻击设为t,那tot=t(2q-t+1)/2 t=min(c+1,q)
// 如果i>1 t=min(q,j+1) nq=q-t tot=.. dp[i+1][j][np]=max(dp[i+1][j][np],dp[i][j][p]+tot)
// 如果j>1 t=min(q,j+2) dp[i][j+1][np] = max(dp[i][j+1][np], dp[i][j][p] + s);
//然后p的上界是n*m+(n-1)*n/2
void sol()
{
    int n, m;
    cin >> n >> m;
    int lim = n * m + n * (n - 1) / 2;
    const i64 inf = -(1LL << 60);
    vector<vector<vector<i64>>> dp(n + 1, vector<vector<i64>>(m + 1, vector<i64>(lim + 1, inf)));
    dp[0][0][0] = 0;
    for (int i = 0; i <= n; i++)
    {
        for (int j = 0; j <= m; j++)
        {
            for (int p = 0; p <= lim; p++)
            {
                if (dp[i][j][p] == inf)
                    continue;
                int q = p + i;
                if (i < n)
                {
                    int t = min(q, j + 1);
                    int np = q - t;
                    i64 s = 1LL * t * (2LL * q - t + 1) / 2;
                    dp[i + 1][j][np] = max(dp[i + 1][j][np], dp[i][j][p] + s);
                }
                if (j < m)
                {
                    int t = min(q, j + 2);
                    int np = q - t;
                    i64 s = 1ll * t * (2ll * q - t + 1) / 2;
                    dp[i][j + 1][np] = max(dp[i][j + 1][np], dp[i][j][p] + s);
                }
            }
        }
    }
    i64 ans = 0;
    for (int p = 0; p <= lim; p++)
        ans = max(ans, dp[n][m][p]);
    cout << ans << endl;
    return;
}
signed main()
{
    // freopen("input.txt","r",stdin);
    // freopen("output.txt","w",stdout);
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int _ = 1;
    cin >> _;
    while (_--)
        sol();
    return 0;
}

L. 千万次初见

基本信息

  • 时间限制:2000 ms
  • 内存限制:262144 KB
  • 难度:简单
  • CF 估分:1200
  • 标签:前缀和、构造判定、非负数组

题意简述

给定非负数组 a 和目标 S,可以任意重排数组。若重排后每个位置 R 都存在某个 L <= R,使得区间 [L,R] 的和为 S,则输出 YES,否则输出 NO

数据范围

  • 1 <= T <= 10^5
  • 1 <= n <= 10^5
  • 0 <= S,a_i <= 10^9
  • 所有测试数据 sum n <= 3 * 10^5

解题思路

由于数组元素都是非负数,区间和具有很强的限制。

S=0,那么一个区间和为 0 当且仅当区间内所有数都是 0。要让每个位置 R 都能作为某个和为 0 的区间右端点,所有元素都必须是 0。因此 S=0 时,判断是否所有元素都是 0 即可。

下面考虑 S>0

如果存在一个元素 a_i 满足:

a_i != 0 且 a_i != S

因为所有数非负,无论把它放在哪里,它所在位置作为右端点时,想凑出和为 S 的区间都会遇到问题:单独取它不等于 S,继续向左扩展只会让区间和变大或不变,无法保证正好得到 S。因此这样的元素不能存在。

也就是说,所有元素只能是 0S

如果至少存在一个 S,就可以把数组重排成:

S, 0, 0, ..., 0, S, 0, ...

事实上只要所有元素都是 0S,并且至少有一个 S,每个位置 R 都可以选择离它最近的、且不超过它的那个 S 作为左端点,中间只有若干个 0,区间和就是 S

所以 S>0 时的充要条件是:

  1. 每个元素都等于 0S
  2. 至少存在一个元素等于 S

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

参考代码

#pragma GCC optimize(3, "Ofast", "inline", "unroll-loops")
#include<bits/stdc++.h>
using namespace std;
#define i64 long long
#define u64 unsigned long long
#define debug(x) cout<<x<<endl;
#define endl "\n"
void sol()
{
    int n;
    i64 S;
    cin >> n >> S;
    vector<i64> a(n);
    bool ok = true;
    int cntS = 0, cnt0 = 0;
    for (int i = 0; i < n; i++)
    {
        cin >> a[i];
        if (a[i] == S) cntS++;
        if (a[i] == 0) cnt0++;
    }
    if (S == 0)
    {
        cout << (cnt0 == n ? "YES" : "NO") << endl;
        return;
    }
    for (int i = 0; i < n; i++)
    {
        if (a[i] != 0 && a[i] != S)
        {
            ok = false;
            break;
        }
    }
    if (cntS == 0) ok = false;
    cout << (ok ? "YES" : "NO") << endl;
    return;
}

signed main()
{
    //freopen("input.txt","r",stdin);
    //freopen("output.txt","w",stdout);
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int _=1;
    cin>>_;
    while(_--)sol();
    return 0;
}

M. 然后,向着明天

基本信息

  • 时间限制:1000 ms
  • 内存限制:32768 KB
  • 难度:简单
  • CF 估分:1000
  • 标签:构造、排列、逆操作、字典序

题意简述

需要构造字典序最大的初始排列,使其经过恰好 M 次“选择一个非末尾元素并移到末尾”的操作后变为 1,2,...,N

数据范围

  • 1 <= T <= 10^5
  • 1 <= N <= 10^5
  • 0 <= M <= 10^9
  • N=1,保证 M=0
  • 所有测试数据 sum N <= 10^5

解题思路

这题可以直接构造。

操作是选择一个不是最后一个位置的元素,把它移动到最后。为了让初始排列字典序尽量大,我们希望越大的数越靠前。

分情况讨论:

如果 n=1,没有操作空间,答案只能是 1

如果 n=2,排列只有两种:

  • m 为偶数时,输出 1 2
  • m 为奇数时,输出 2 1

因为一次操作会让两个数交换位置,所以只需要看奇偶性。

下面考虑 n>=3

如果 m < n-1,为了让字典序最大,可以把最大的 m 个数依次放在最前面:

n, n-1, ..., n-m+1

剩下的数保持升序:

1, 2, ..., n-m

这样前 m 个位置已经尽可能大,并且可以通过恰好 m 次操作把这些被提前的数依次移到末尾,最终恢复成升序。

如果 m >= n-1,字典序最大的排列显然是完全倒序:

n, n-1, ..., 1

对于 n>=3,倒序排列可以在至少 n-1 次操作后变回升序,多余的操作可以通过循环移动元素来消耗,因此所有 m>=n-1 都可以取倒序作为答案。

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

参考代码

#pragma GCC optimize(3, "Ofast", "inline", "unroll-loops")
#include<bits/stdc++.h>
using namespace std;
#define i64 long long
#define u64 unsigned long long
#define debug(x) cout<<x<<endl;
#define endl "\n"

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

    vector<int> ans;

    if (n == 1)
    {
        ans.push_back(1);
    }
    else if (n == 2)
    {
        if (m % 2 == 0)
        {
            ans.push_back(1);
            ans.push_back(2);
        }
        else
        {
            ans.push_back(2);
            ans.push_back(1);
        }
    }
    else
    {
        if (m < n - 1)
        {
            for (int x = n; x >= n - m + 1; x--)
            {
                ans.push_back(x);
            }

            for (int x = 1; x <= n - m; x++)
            {
                ans.push_back(x);
            }
        }
        else
        {
            for (int x = n; x >= 1; x--)
            {
                ans.push_back(x);
            }
        }
    }

    for (int i = 0; i < n; i++)
    {
        if (i) cout << ' ';
        cout << ans[i];
    }
    cout << endl;
}

signed main()
{
    //freopen("input.txt","r",stdin);
	//freopen("output.txt","w",stdout);
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);

    int _ = 1;
    cin >> _;
    while (_--) sol();

    return 0;
}

N. 命运骰子

基本信息

  • 时间限制:1000 ms
  • 内存限制:262144 KB
  • 难度:签到
  • CF 估分:800
  • 标签:模拟、骰子状态维护

题意简述

标准六面骰子初始上、前、右分别为 1,2,3。给定 L/R/F/B 操作串,模拟骰子滚动,输出最终上面的点数。

数据范围

  • 1 <= n <= 2 * 10^5

解题思路

直接维护骰子的六个面:

top, bottom, front, back, left, right

初始时:

top = 1
front = 2
right = 3
bottom = 6
back = 5
left = 4

每次根据操作更新六个面的相对位置:

  • L:向左滚,右面变成上面。
  • R:向右滚,左面变成上面。
  • F:向前滚,后面变成上面。
  • B:向后滚,前面变成上面。

所有操作结束后输出 top 即可。

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

参考代码

#pragma GCC optimize(3, "Ofast", "inline", "unroll-loops")
#include<bits/stdc++.h>
using namespace std;
#define i64 long long
#define u64 unsigned long long
#define debug(x) cout<<x<<endl;
#define endl "\n"

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

    int top = 1, bottom = 6;
    int front = 2, back = 5;
    int left = 4, right = 3;

    for (char ch : s)
    {
        if (ch == 'L')
        {
            int ntop = right;
            int nright = bottom;
            int nbottom = left;
            int nleft = top;
            top = ntop;
            right = nright;
            bottom = nbottom;
            left = nleft;
        }
        else if (ch == 'R')
        {
            int ntop = left;
            int nleft = bottom;
            int nbottom = right;
            int nright = top;
            top = ntop;
            left = nleft;
            bottom = nbottom;
            right = nright;
        }
        else if (ch == 'F')
        {
            int ntop = back;
            int nback = bottom;
            int nbottom = front;
            int nfront = top;
            top = ntop;
            back = nback;
            bottom = nbottom;
            front = nfront;
        }
        else
        {
            int ntop = front;
            int nfront = bottom;
            int nbottom = back;
            int nback = top;
            top = ntop;
            front = nfront;
            bottom = nbottom;
            back = nback;
        }
    }

    cout << top << endl;
}

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

    int _ = 1;
    while (_--) sol();

    return 0;
}
第二十三届中国计量大学校 ACM 团队赛题解
发布时间
2026 年 6 月 19 日
目录

输入关键词开始搜索