2026“钉耙编程”中国大学生算法设计春季联赛(1)题解
2026“钉耙编程”中国大学生算法设计春季联赛(1)题解
Problem 1001. 氟化钙
小远和小涛从下界挖来了 \(998244353^{1145141919810}\) 颗萤石,由于实在是太多了,他们决定拿这些萤石来玩个游戏。
小远和小涛决定玩 \(t\) 次游戏,每次游戏给定长度为 \(n\) 的数组 \(a\)。
定义 \(f(i,j,k) = a_i \cdot k^{a_i+a_j} + a_j\)。
每次游戏,小远和小涛将随机选择两个正整数 \((i,j) (1 \le i,j \le n)\),取 \(l=\min(i,j), r=\max(i,j), k = \max_{x=l}^r a_x\)。小远和小涛会将 \(f(i,j,k)\) 颗萤石堆成一堆,轮流从堆中取萤石,每次取出 \(1\) 到 \(k\) 之间任意颗,取到最后一颗萤石的人获胜,小远先手,两人都采取最佳策略。
小远想知道,有多少对 \((i,j)\) 可以使他获胜。
数据范围:\(1 \le t \le 10, 1 \le n \le 10^5, 2 \le a_i \le 10^9\)。
这个题考的前置知识比较多,但是本身不是很难:
首先是比较经典的 \(Bash\) 游戏结论:
假设手上有 \(n\) 个石子,双方可以轮流任取 \(1~k\) 颗石子,先手必败当且仅当 \(n \equiv 0 \pmod {k + 1}\)。
由于先手必胜的情况太多,于是直接考虑先手必败的情况再相减得到答案。
于是可以知道:当拥有 \(f(i, j, k)\) 个石子时,由于 \(k \equiv -1 \pmod {k + 1}\),原式可得:\(f(i, j, k) \equiv a_i(-1)^{a_i + a_j}+ a_j\pmod{k + 1}\)。
有简单的分类讨论:
-
如果 \(a_i + a_j\) 为奇数,则要求 \(-a_i + a_j = 0 \pmod{k + 1}\) 时先手必败。此时当且仅当两数相等时可以成立,但是与前提矛盾,故不存在。
-
如果 \(a_i + a_j\) 为偶数,则要求 \(a_i + a_j = k + 1\) 时先手必败,此时 \(k\) 一定为奇数。
于是可以很自然的考虑到一个情况:枚举区间最大值 \(a_p\),然后统计 \(a_p\) 成为最大值的区内有多少对 \((i, j)\) 满足上述条件。
统计 \(a_p\) 在哪些区间内可以成为最大值是一个很经典的单调栈/笛卡尔树建树问题,这里不再赘述。处理完后可以得到左右边界 \(L_p, R_p\)。
区间会被 \(p\) 分成两个部分,我们枚举较小的那个部分的值 \(a_i\)、然后查询另外一个区间内满足 \(a_j = a_p + 1 - a_i\) 的数字有多少。这个问题数据结构的做法就是写主席树(逃),或者用 \(vector\) 记录下每个数字的下标序列,然后利用二分求出边界内的下标数量即可。
统计完后这个题就做完了。
枚举 \(a_p\) 较小区间的复杂度是 \(n\log n\),然后二分又自带一个 \(\log\),所以最后复杂度是 \(n \log^2 n\)
#include <algorithm>
#include <map>
#include <iostream>
#include <random>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int n;
int a[kMaxN];
void solve() {
cin >> n;
int cnt = 0;
std::map<int, int> mp;
std::vector<int> a(n + 1), L(n + 1), R(n + 1);
std::vector<std::vector<int>> pos(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i], mp[a[i]] = 0;
for (auto& [key, val] : mp) val = ++cnt;
std::vector<int> stk;
for (int i = 1; i <= n; i++) {
pos[mp[a[i]]].push_back(i);
while (!stk.empty() && a[stk.back()] <= a[i]) {
R[stk.back()] = i - 1, stk.pop_back();
}
if (stk.empty()) L[i] = 1;
else L[i] = stk.back() + 1;
stk.push_back(i);
}
for (auto& i : stk) R[i] = n;
auto calc = [&](int l, int r, int val) -> long long {
if (l > r || !mp.count(val)) return 0;
int p = mp[val];
auto L = std::lower_bound(pos[p].begin(), pos[p].end(), l);
auto R = std::upper_bound(pos[p].begin(), pos[p].end(), r);
return R - L;
};
int ans = 0;
for (int i = 1; i <= n; i++) {
if (!(a[i] & 1)) continue;
int llen = i - L[i], rlen = R[i] - i;
if (llen <= rlen) {
for (int j = L[i]; j < i; j++) {
ans += 2 * calc(i + 1, R[i], a[i] + 1 - a[j]);
}
} else {
for (int j = i + 1; j <= R[i]; j++) {
ans += 2 * calc(L[i], i - 1, a[i] + 1 - a[j]);
}
}
}
ans = n * n - ans;
cout << ans << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
return 0;
}
Problem 1002. 数字王国的警报密码
居然一血的一道题!
在数字王国中,国王最近遇到一个棘手的难题。他需要为宝库设置一系列长度为n且严格递增的密码数字。然而,王国的古老魔法有一个特殊的禁忌:任何一个连续密码段(至少包含一个数字)的总和,都绝对不能是某个神秘数字k的倍数。一旦违反,宝库的警报就会响起,后果不堪设想。国王将此重任交给了你,他的首席密码师。你需要为每次任务设计符合要求的密码序列。作为最优秀的密码师,你不仅要构造出满足条件的序列,还要确保它是最优的:在所有可能的合法序列中,你需要找出那个字典序最小的序列。
什么是字典序最小?我们比较两个长度相同的序列 \(A = [a_1, a_2, \dots, a_n]\) 和 \(B = [b_1, b_2, \dots, b_n]\):
- 从第一个位置开始比较
- 找到第一个满足 \(a_i \neq b_i\) 的位置 \(i\)
- 如果 \(a_i < b_i\),则称序列 \(A\) 的字典序小于序列 \(B\)
- 如果所有对应位置的元素都相等,则两个序列字典序相同
你将面对多次挑战(多次测试数据)。每次挑战会给你两个关键数字:n:你需要设置的密码序列的长度 k:那个神秘的危险数字
你的任务是构造一个严格单调递增的正整数序列 \(a_1, a_2, \dots, a_n\),使得该序列中任意一个长度至少为1的连续子序列(即连续几个密码)的数字之和,都不能被k整除,并且使它的字典序最小。
定义前缀和数组 \(S\),特别的 \(S_0 = 0\)。则根据题目定义,要求不存在 \((i, j)\) 使得 \(S_i - S_j \equiv 0 \pmod k\),也就是不存在 \(S_i \equiv S_j \pmod k\)。
这个等价条件抽象出来以后就很好做了:
- 不存在解当且仅当 \(n \ge k\),根据抽屉原理很好得到。
- 构造过程中贪心地选择最小的数字,使得前缀和是独一无二的。
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int n, k;
void solve() {
cin >> n >> k;
if (n >= k) return cout << -1 << endl, void();
std::vector<int> ans(n);
std::vector<bool> used(k + 1);
used[0] = true;
for (int i = 0, j = 1, cur = 0; i < n; i++) {
while (1) {
int nxt = (cur + j++) % k;
if (!used[nxt]) {
used[nxt] = true, cur = nxt, ans[i] = j - 1;
break;
}
}
}
for (auto i : ans) cout << i << ' ';
cout << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
return 0;
}
Problem 1003. 回文串
好像有人说这个题其实可以做到 \(n\log n\)?
\(yc\) 不懂喵…… \(yc\) 只会 \(n^2\)……
小木最近在学回文串,他很快就掌握了如何求出一个字符串内有多少个回文串,但是...如果是在两个字符串内找呢?
现给定两个只包含小写字母的字符串\(S\)和\(T\),长度均为\(n\),你可以在\(S\)和\(T\)中分别选取子串\(S'\)和\(T'\),并将\(T'\)拼接在\(S'\)后面组成新字符串。
问有多少种选法,使得组成的新字符串是个回文串。小木被难住了,来请求你帮助他解决这个问题。
注:回文串是指一个字符串,其正读和反读都一样。例如,"abba" 和 "aba" 都是回文串。而"abc"不是回文串。注意空串不是回文串。
首先将 \(T\) 翻转(翻转谁都一样)。那么你挑选的子串要满足如下要求:
- 前缀相同
- 多出来的非前缀部分要构成一个回文
记录 \(cntS_i\),表示 \(S\) 中 \(S_i\) 作为第一个字符有多少个回文。\(cntT\) 同理。这个记数可以用哈希来解决。
考虑枚举 \(i, j, k\),表示枚举 \(S[i...k]\) 和 \(T[j...k]\) 作为子串同时要满足相等,然后累加 \(cntS_{k + 1}\) 和 \(cntT_{k + 1}\)。
但是事实上从固定起点开始最长能走多远实际上就是 \(lcp\) 问题,而这个奇怪的小东西是可以通过 \(dp\) 提前处理出来的。
记 \(lcp[i][j]\) 为 \(S\) 从 \(i\) 开始、\(T\) 从 \(j\) 开始的最长前缀。处理完这个以后 \(cntS\) 和 \(cntT\) 就是一个前缀和能解决的了。
#include <iostream>
#include <algorithm>
#include <random>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
using ull = unsigned long long;
ull haS[kMaxN], haT[kMaxN];
ull rhaS[kMaxN], rhaT[kMaxN];
ull t[kMaxN];
ull qry(int l, int r, ull* hash) {
return hash[r] - hash[l - 1] * t[r - l + 1];
}
ull qryr(int l, int r, ull* hash) {
return hash[l] - hash[r + 1] * t[r - l + 1];
}
void solve() {
std::string S, T;
cin >> S >> T;
int n = S.size();
std::reverse(T.begin(), T.end());
std::vector<int> cntS(n + 2, 0), cntT(n + 2, 0);
std::vector<std::vector<int>> lcp(n + 2, std::vector<int>(n + 2));
S = '#' + S, T = '#' + T;
rhaS[n + 1] = rhaT[n + 1] = 0;
for (int i = 1; i <= n; i++) {
haS[i] = haS[i - 1] * 131 + S[i];
haT[i] = haT[i - 1] * 131 + T[i];
}
for (int i = n; i >= 1; i--) {
rhaS[i] = rhaS[i + 1] * 131 + S[i];
rhaT[i] = rhaT[i + 1] * 131 + T[i];
}
// 由于后面某个地方会稍微溢出一位,所以这里要多算一次
for (int i = 1; i <= n + 1; i++) {
for (int j = i; j <= n; j++) {
cntS[i] += qry(i, j, haS) == qryr(i, j, rhaS);
cntT[i] += qry(i, j, haT) == qryr(i, j, rhaT);
}
cntS[i] += cntS[i - 1], cntT[i] += cntT[i - 1];
}
for (int i = n; i >= 1; i--) {
for (int j = n; j >= 1; j--) {
lcp[i][j] = S[i] == T[j] ? lcp[i + 1][j + 1] + 1 : 0;
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
int t = lcp[i][j];
// S' 多一点点,T' 多一点点以及两个完全相同
ans += cntS[i + t] - cntS[i] + cntT[j + t] - cntT[j] + t;
}
}
cout << ans << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
t[0] = 1;
for (int i = 1; i < kMaxN; i++) t[i] = t[i - 1] * 131;
int t;
cin >> t;
while (t--) solve();
return 0;
}
但是神秘 gemini 写了一个线性做法,我看不懂.jpg:
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 1e6;
// Suffix Automaton (后缀自动机) 结构
struct Node {
int len, link;
int next[26];
long long cnt; // 该状态在原串中的出现次数
} st[MAXN * 2];
int sz, last;
// 初始化 SAM
void sam_init() {
st[0].len = 0;
st[0].link = -1;
memset(st[0].next, 0, sizeof(st[0].next));
st[0].cnt = 0;
sz = 1;
last = 0;
}
// 向 SAM 中添加字符
void sam_extend(char c) {
int cur = sz++;
st[cur].len = st[last].len + 1;
st[cur].cnt = 1; // 初始叶子节点计数为 1
memset(st[cur].next, 0, sizeof(st[cur].next));
int p = last;
while (p != -1 && !st[p].next[c - 'a']) {
st[p].next[c - 'a'] = cur;
p = st[p].link;
}
if (p == -1) {
st[cur].link = 0;
} else {
int q = st[p].next[c - 'a'];
if (st[p].len + 1 == st[q].len) {
st[cur].link = q;
} else {
int clone = sz++;
st[clone].len = st[p].len + 1;
memcpy(st[clone].next, st[q].next, sizeof(st[q].next));
st[clone].link = st[q].link;
st[clone].cnt = 0; // 克隆节点不代表新前缀,初始 cnt 为 0
while (p != -1 && st[p].next[c - 'a'] == q) {
st[p].next[c - 'a'] = clone;
p = st[p].link;
}
st[q].link = st[cur].link = clone;
}
}
last = cur;
}
// 预处理字符串 S,获取每个位置作为起点的【非空回文串个数】
vector<int> getPalStarts(const string& s) {
int n = s.length();
string t = "#";
for (char c : s) {
t += c;
t += "#";
}
int m = t.length();
vector<int> p(m, 0); // 回文半径数组
int C = 0, R = 0;
// 1. 标准 Manacher 算法
for (int i = 0; i < m; i++) {
int i_mirror = 2 * C - i;
if (R > i) {
p[i] = min(R - i, p[i_mirror]);
} else {
p[i] = 0;
}
while (i - 1 - p[i] >= 0 && i + 1 + p[i] < m && t[i - 1 - p[i]] == t[i + 1 + p[i]]) {
p[i]++;
}
if (i + p[i] > R) {
C = i;
R = i + p[i];
}
}
// 2. 利用差分数组统计回文起点
vector<int> diff(n + 2, 0);
for (int i = 0; i < m; i++) {
if (p[i] > 0) {
// 将 `#` 串的中心映射回原串的起始位置
int L = (i - p[i] + 1) / 2;
int count = (p[i] + 1) / 2; // 该中心贡献的回文串个数
if (count > 0) {
diff[L]++;
diff[L + count]--; // 差分区间操作
}
}
}
// 3. 求前缀和得到真实的起点数量
vector<int> palStarts(n + 2, 0);
int cur = 0;
for (int i = 0; i <= n; i++) {
cur += diff[i];
palStarts[i] = cur;
}
return palStarts;
}
// 核心求解函数
// 计算在 B 中寻找 A 的子串,并乘以回文串权重的总方案数
long long Solve(string A, string B, int c_val) {
int n = A.length();
int m = B.length();
// 1. Manacher 求 A 中每个位置起点的非空回文串数量
vector<int> palStarts = getPalStarts(A);
// 2. 为 B 建立后缀自动机
sam_init();
for (char c : B) {
sam_extend(c);
}
// 3. 拓扑排序计算 SAM 节点的实际 cnt 和 父树上的累加前缀和 G
vector<int> c(m + 1, 0);
vector<int> order(sz);
for (int i = 0; i < sz; i++) c[st[i].len]++;
for (int i = 1; i <= m; i++) c[i] += c[i - 1];
for (int i = 0; i < sz; i++) order[--c[st[i].len]] = i;
// 自底向上累加 cnt
for (int i = sz - 1; i > 0; i--) {
int u = order[i];
if (st[u].link != -1) {
st[st[u].link].cnt += st[u].cnt;
}
}
// 自顶向下计算 Parent Tree 上的匹配总数前缀和 G
vector<long long> G(sz, 0);
for (int i = 1; i < sz; i++) {
int u = order[i];
int p = st[u].link;
if (p != -1) {
G[u] = G[p] + 1LL * (st[u].len - st[p].len) * st[u].cnt;
}
}
// 4. 将 A 在 B 的 SAM 上运行匹配
long long ans = 0;
int u = 0, l = 0; // u 为当前状态,l 为当前最大匹配长度
for (int i = 0; i < n; i++) {
int ch = A[i] - 'a';
while (u != -1 && !st[u].next[ch]) {
u = st[u].link;
if (u != -1) l = st[u].len;
}
if (u == -1) {
u = 0;
l = 0;
} else {
u = st[u].next[ch];
l++;
}
long long matches = 0;
if (l > 0) {
int p = st[u].link;
// 当前节点生效的长度部分 + 所有祖先节点的总长度贡献
matches = 1LL * (l - st[p].len) * st[u].cnt + G[p];
}
// i 是当前匹配的结束位置,对应的下一个回文串起点是 i + 1
ans += matches * (palStarts[i + 1] + c_val);
}
return ans;
}
void solve_test_case() {
string S, T;
cin >> S >> T;
string TR = T;
reverse(TR.begin(), TR.end());
// Case 1: |S'| >= |T'|. c_val = 1 代表可接"空回文串"
long long ans1 = Solve(S, TR, 1);
// Case 2: |S'| < |T'|. c_val = 0 代表必须接"非空回文串"
long long ans2 = Solve(TR, S, 0);
cout << ans1 + ans2 << "\n";
}
int main() {
// 优化 C++ 标准 I/O 的速度
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int T_cases;
if (cin >> T_cases) {
while (T_cases--) {
solve_test_case();
}
}
return 0;
}
Problem 1004. 炼金术士的配方
炼金术士小 A 正在尝试提炼一种传说中的“贤者之石”。根据古老的炼金术手稿记载,提炼过程需要达成一种特定的“元素共鸣”。小 A 的实验台上摆放着一排由 \(N\) 瓶炼金材料组成的序列,序列记为 \(A\),其中第 \(i\) 瓶材料的能量值为 \(a[i]\)。此外,他还有一个作为参考标准的“目标样本”,其能量值为 \(X\)。为了定义共鸣,我们引入物质的“核心本质”概念。对于任意正整数 \(v\),其“核心本质” \(f(v)\) 的定义如下:将 \(v\) 进行标准质因数分解 \(v = p_1^{k_1} \times p_2^{k_2} \times \dots \times p_m^{k_m}\),则 \(f(v)\) 等于所有指数 \(k_i\) 为奇数的质因数 \(p_i\) 的乘积。例如:
- 对于数字 12,分解为 \(2^2 * 3^1\),指数为奇数的质因数是 3,故 \(f(12) = 3\)。
- 对于数字 90,分解为 \(2^1 * 3^2 * 5^1\),指数为奇数的质因数是 2 和 5,故 \(f(90) = 10\)。
- 对于数字 36,分解为 \(2^2 * 3^2\),没有指数为奇数的质因数,故 \(f(36) = 1\)。
- 对于数字 27,分解为 \(3^3\),唯一质因数 3 的指数是奇数,故 \(f(27) = 3\)。
小 A 需要在原材料序列 \(A\) 中找到一个连续子区间 \([l, r]\),使得该子区间内所有材料能量值的乘积,其“核心本质”与目标样本 \(X\) 的“核心本质”完全相同。请你计算,有多少对下标 \((l, r)\) 满足 \((1 <= l <= r <= N)\) 满足以下条件:
\[f\left(\prod_{i=l}^{r} a_i\right) = f(X) \]
核心算法就一个字:异或哈希……
嗯,就这么点。如果你想到为每一个质数赋予一个随机数作为权值的话,那么这个题的正确做法就呼之欲出了。
感觉异或哈希是个很抽象的东西,一句两句说不清楚啊()
直接看代码吧(
#include <iostream>
#include <map>
#include <random>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int map[kMaxN];
std::vector<int> prime;
int a[kMaxN];
int n, x;
std::map<int, int> cnt;
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
for (int i = 0; i < kMaxN; i++) map[i] = -1;
map[1] = 0;
for (int i = 2; i < kMaxN; i++) {
if (map[i] == -1) prime.push_back(i), map[i] = rand(1, 1e18);
for (auto j : prime) {
if (i * j >= kMaxN) break;
map[i * j] = map[i] ^ map[j];
if (i % j == 0) break;
}
}
cin >> n >> x;
int tar = map[x], ans = 0;
cnt[0] = 1;
for (int i = 1; i <= n; i++) {
cin >> a[i], a[i] = a[i - 1] ^ map[a[i]];
ans += cnt[tar ^ a[i]];
cnt[a[i]]++;
}
cout << ans << endl;
return 0;
}
Problem 1005. 大户爱的开根
一个简单的问题,给定 \(N, K\),求 \(\lfloor \sqrt[K]{N} \rfloor\)。大户爱曾经用 C++ 自带的函数求解,代码为 floor(pow(N,1.0/K)),但这样写会造成精度丢失,事实上有一定概率不能求得正确答案,于是大户爱请求你来解决这个问题。题目保证 \(2 \leq K \leq N \leq 10^9\)。
其实直接开根就可以了……
开根以后虽然会丢失精度,但是一般差的也就一两个,直接暴力加一加就以了。
#include <cmath>
#include <iostream>
#include <random>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
void solve() {
int n, k, ans;
cin >> n >> k;
if (k > 40) {
return cout << 1 << endl, void();
}
long double p = (int)std::pow((long double)n, 1.0 / k);
while (std::pow(p + 1, k) <= n) p++;
cout << (int)p << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
return 0;
}
复杂度 \(O(1)\)。
但是这么写会比较危险,还是写二分会比较安全。
一般来说当 \(k\) 比较大的时候无论如何就是 \(1\) 了,大概就是 \(30\) 左右。
判掉之后在进行暴力二分,如果不想写的很麻烦直接写 \(O(k)\) 的次幂来计算。复杂度大概就是 \(O(min\{30, k\}\log n)\)。
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
void solve() {
int n, k;
cin >> n >> k;
if (k >= 30) return cout << 1 << endl, void();
int l = 1, r = n, ans = 0;
while (l <= r) {
int mid = (l + r) >> 1;
bool flag = true;
long long now = 1;
for (int i = 1; i <= k; i++) {
now *= mid;
if (now > n) {
flag = false;
break;
}
}
if (flag) {
ans = mid, l = mid + 1;
} else {
r = mid - 1;
}
}
cout << ans << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
return 0;
}
Problem 1006. 巧克力工厂
小黄来到了他一直梦寐以求的巧克力工厂,这里有各种各样的巧克力等待品尝。工厂里只有一台巧克力加工机,所有巧克力必须加工完成后才能食用。巧克力并非一开始就全部加工好,而是会在一天中的不同时间陆续加工完成。不同的巧克力,小黄吃完它所需要的时间也不同。只会吃的小黄完全不懂算法,你能帮他想想办法,让他尽早吃完所有巧克力吗?
其实没什么好说的,直接排序后模拟就可以了。
#include <iostream>
#include <algorithm>
#include <random>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int n;
int s[kMaxN], d[kMaxN];
int id[kMaxN];
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> s[i] >> d[i], id[i] = i;
std::sort(id + 1, id + 1 + n, [](int i, int j) {
if (s[i] == s[j]) return d[i] < d[j];
return s[i] < s[j];
});
int cur = 0;
for (int i = 1; i <= n; i++) {
int p = id[i];
if (cur < s[p]) cur = s[p];
cur += d[p];
}
cout << cur << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
return 0;
}
Problem 1007. 流沙
给出一棵包含 \(N\) 个节点的有根树,根节点为 1。初始时,每个节点 \(i\) 上存放着 \(A_i\) 粒沙子。
沙子具有一种向根流动的特性:你可以随时将任意节点上的 1 粒沙子移动到它的直接父节点上。此操作可以进行任意次。定义一个函数 \(f[u]\):假设此时只有以节点 \(u\) 为根的子树存在(即沙子绝对不能移出子树 \(u\)),在最优操作下,子树 \(u\) 中所有节点的沙子数量的最小值,最大能达到多少?
你需要为每一个节点 \(u \in [1, N]\) 计算出 \(f[u]\) 的值,并输出这 \(N\) 个值。
- \(1 \le T \le 10^5\)(测试用例组数)
- \(1 \le N \le 10^6\)
- \(0 \le A_i \le 10^9\)
- 给出的是合法的树结构。
- 保证所有测试用例中 \(N\) 的总和不超过 \(10^6\)。
仔细思考可以约束 \(f[u]\) 的一共有两种:
- 子树中最小的 \(f[u]\)。
- 沙子数量的平均值。
一个比较显然的事情是 \(f[u]\) 从叶子到根一定是单调不增的,其上界一定会被平均值严格卡死。
好像想明白这两个事情以后这个题还挺好写的。
Problem 1008. 大户爱的gcd
大户爱有一个长度为 \(N\) 的正整数数组 \(a\),初始你不知道每个位置的数字具体是多少,但你得到了 \(M\) 个提示,每个提示形如 \(x\) \(y\) \(g\),表示 \(a_x\) 和 \(a_y\) 的最大公因数为 \(g\)。然后你要回答 \(Q\) 个问题,每个问题形如 \(x\) \(y\),你需要回答 \(a_x\) 和 \(a_y\) 的最大公因数最小可能是多少。数据范围:\(g \le 30, N, M, Q \le 2 \times 10^5\)。题目保证提示是不矛盾的。
怎么说呢,很微妙的一个题目:题目保证了提示是互不矛盾的。
同时你需要贪心使得数字尽可能小……
所以你直接对着题目给出的条件求 \(lcm\) 和 \(gcd\) 就……就结束了。
#include <iostream>
#include <random>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int n, m, q;
void solve() {
cin >> n >> m >> q;
std::vector<int> val(n + 1, 1);
for (int i = 1, x, y, g; i <= m; i++) {
cin >> x >> y >> g;
val[x] = std::lcm(val[x], g);
val[y] = std::lcm(val[y], g);
}
for (int i = 1, x, y; i <= q; i++) {
cin >> x >> y;
cout << std::gcd(val[x], val[y]) << endl;
}
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
int t;
cin >> t;
while (t--) solve();
return 0;
}
Problem 1009. 力的平衡
在物理实验室中,Alice 正在研究力的平衡问题。
实验室配备了 \(k\) 种力发生器,第 \(i\) 种发生器可以产生力向量 \((F_{x_i}, F_{y_i})\) 牛顿。其中,正值表示向右/向上的力,负值表示向左/向下的力。
为了达到完美的力学平衡状态,Alice 需要选择若干个力发生器(每种可以使用任意多次),使得所有力的合力恰好为零向量 \((0, 0)\)。
由于实验经费有限,每次启动力发生器都需要消耗电能。Alice 希望启动力发生器的次数尽量少来达到平衡。
注意: Alice 必须至少启动一次力发生器,即答案至少为 1。
请帮助 Alice 计算最少需要启动多少次力发生器。如果无论如何都无法达到平衡,输出 -1。
Input
第一行一个整数 \(t\),表示测试数据的组数。
对于每组测试数据:
- 第一行一个整数 \(k\),表示力发生器的种类数。
- 接下来 \(k\) 行,每行两个整数 \(F_{x_i}, F_{y_i}\),表示第 \(i\) 种力发生器产生的力向量。
对于所有测试数据:
- \(1 \le t \le 10\)
- \(1 \le k \le 500\)
- \(-100 \le F_{x_i}, F_{y_i} \le 100\)
- 保证 \(\sum k \le 1000\)
这个题主要还是猜结论……这里提供一个 hack 数据可以叉爆一部分范围开错了的人:
1
3
100 99
-99 100
-100 -100
这组数据在计算过程中绝对值上限会达到 149,但是有相当一部分人只开到了 200。
Problem 1010. MEX
歪歪小朋友不喜欢计数,但她很喜欢 MEX。
歪歪首先给出了本题中排列的定义:对于一个长度为 \(n\) 的序列 \(P\),若其中的数字 \(p_i\) 都在 \([0, n-1]\) 的范围内,并且每个数字都只出现一次,我们称其为排列。如 \([1, 4, 2, 3, 0]\) 就是一个长度为 5 的排列。
她再定义了函数 \(MEX_P[l, r]\):在长度为 \(n\) 的排列 \(P\) 中,有 \(1 \le l \le r \le n\),其中 \([a_l, a_{l+1} \dots a_r]\) 组成的子数组中未出现的最小非负整数值即为 \(MEX_P[l, r]\)。如排列 \(P\) 为 \([2, 4, 3, 1, 0]\),\(l = 3, r = 5\),组成的子数组为 \([3, 1, 0]\),未出现的最小非负整数值即为 2,故 \(MEX_P[3, 5] = 2\)。
歪歪小朋友有一个长度为 \(n\) 的排列 \(A\),还有一个长度为 \(n\) 的 \(B\) 序列,其中对于 \(1 \le i \le n\) 都有 \(b_i = MEX_A[i, n]\)。如果通过给出排列 \(A\) 的来求序列 \(B\) 是什么,歪歪小朋友觉得这太简单了,她马上就会了。但是如果给出序列 \(B\) 来求 \(A\) 呢?歪歪会告诉你序列 \(B\) 中 \(m\) 项的值,即从形式上来讲会给出 \(m\) 对 \([x_i, y_i]\),表示 \(b_{x_i} = y_i\)。很显然,满足条件的排列 \(A\) 可能有很多种,或者一种都没有,她想让你告诉他有多少种排列 \(A\) 满足条件。答案可能很大,输出对 998244353 取模之后的值。
数据保证:\(T \le 10^5, 1 \le m \le n \le 10^5, \sum_{i=1}^T m_i \le 10^5, 1 \le x_i \le n, 0 \le y_i \le n\),在同一组数据中的 \(x_i\) 都是两两不同的。
注意:本题没有对 \(n\) 的总和作出限制!
一个看上去很难但是拆开以后非常简单的……数数题。没写其实亏了。
首先需要知道,对于一个合法的 \(B\) 序列,它一定是单调不增的。这样可以直接特判掉一定的不合法情况。
另外,一个隐含的天然边界是:整个完整排列的 MEX 必然是 \(n\)(即 \(B_1 = n\)),而在数组末尾之后(我们可以设一个虚拟位置 \(n+1\))的 MEX 必然是 \(0\)(即 \(B_{n+1} = 0\))。先把这两个边界条件加进我们的条件集合里,保证两端不漏算。
考虑 \(B_x = y\) 这个条件究竟约束了排列 \(A\) 中的什么?既然后缀 \(A[x \dots n]\) 的 MEX 是 \(y\),这就意味着:
- 所有小于 \(y\) 的数字(\(0 \dots y-1\))必须全部出现在这个后缀中,即它们的下标必须 \(\ge x\)。
- 数字 \(y\) 本身绝对不能出现在这个后缀中,即它的下标必须 \(< x\)。
如果同一个 MEX 值 \(y\) 被给出了多次,我们只需要记录它要求的最小限制位置 \(xmin_y\) 和最大限制位置 \(xmax_y\)。
接下来,我们将所有真正出现过的 MEX 值从小到大排序并去重。假设当前枚举到相邻的两个值 \(lst\) 和 \(now\)(注意 \(lst < now\)):
-
对于数字 \(lst\) 应该填在哪: 由于 \(lst < now\),它必须满足 \(now\) 的下界约束,也就是位置必须 \(\ge xmax_{now}\);同时受限于自身的约束,它的位置必须 \(< xmin_{lst}\)。因此 \(lst\) 这个数值只能放在区间 \([xmax_{now}, xmin_{lst} - 1]\) 里。这一步的放置方案数即为区间长度 \(xmin_{lst} - xmax_{now}\)。如果算出来 \(\le 0\),说明限制发生了物理冲突,直接判 \(0\) 即可。
-
对于中间没有显式约束的“自由变量”填在哪: 在值域 \((lst, now)\) 之间的数字共有 \(now - lst - 1\) 个。它们唯一的要求就是跟随着一起排在 \(\ge xmax_{now}\) 的位置。
那么在 \(\ge xmax_{now}\) 的范围内还剩多少个“空坑位”呢?总共有 \(n - xmax_{now} + 1\) 个位置,但在这些位置里,必定有 \(lst + 1\) 个坑位要被 \(\le lst\) 的数字无情占有。所以剩下真正可用的空位是 \(n - xmax_{now} - lst\) 个。
我们从这些空位中选出位置给自由变量并全排列,这部分的方案数就是一个简单的排列数:\(A_{n - xmax_{now} - lst}^{now - lst - 1}\)(也可以写成组合数乘上阶乘的形式)。
直接把每一段的这两部分乘起来,乘法原理一波带走。
题目中有一个感叹号的提示:“本题没有对 \(n\) 的总和作出限制”。按照上面的做法,我们只对给出的 \(m\) 个点进行排序和分段计算,中间跨度极大的自由数字直接用 \(O(1)\) 的组合数学计算,单次时间复杂度严格控制在了 \(O(m \log m)\),极其优美地跨过了 \(O(n)\) 的陷阱。
#include <iostream>
#include <algorithm>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
const int MOD = 998244353;
struct modint {
int val;
modint(long long v = 0) {
if (v < 0) v = v % MOD + MOD;
if (v >= MOD) v %= MOD;
val = (int)v;
}
explicit operator int() const { return val; }
friend modint qpow(modint a, long long p) {
modint ans(1);
while (p > 0) {
if (p & 1) ans *= a;
a *= a, p >>= 1;
}
return ans;
}
modint inv() const { return qpow(*this, MOD - 2); }
modint& operator+=(modint b) {
val += b.val;
if (val >= MOD) val -= MOD;
return *this;
}
modint& operator-=(modint b) {
val -= b.val;
if (val < 0) val += MOD;
return *this;
}
modint& operator*=(modint b) {
val = (int)(1LL * val * b.val % MOD);
return *this;
}
modint& operator/=(modint b) { return *this *= b.inv(); }
friend modint operator+(modint a, modint b) { return a += b; }
friend modint operator-(modint a, modint b) { return a -= b; }
friend modint operator*(modint a, modint b) { return a *= b; }
friend modint operator/(modint a, modint b) { return a /= b; }
};
modint fac[kMaxN], inv[kMaxN];
modint invf[kMaxN];
int n, m;
int x[kMaxN], y[kMaxN];
int xmax[kMaxN], xmin[kMaxN];
modint C(int n, int m) {
if (m > n) return 0;
return fac[n] * invf[m] * invf[n - m];
}
void solve() {
cin >> n >> m;
std::vector<std::pair<int, int>> v(m);
std::vector<int> vals;
for (auto& [x, val] : v) cin >> x >> val;
v.push_back({1, n}), v.push_back({n + 1, 0});
for (auto [x, val] : v) upmax(xmax[val], x), upmin(xmin[val], x), vals.push_back(val);
std::sort(v.begin(), v.end());
for (int i = 1; i < v.size(); i++) { if (v[i - 1].second < v[i].second) {
cout << 0 << endl;
for (auto val : vals) xmax[val] = -1, xmin[val] = 2e9;
return;
}
}
std::sort(vals.begin(), vals.end());
vals.erase(std::unique(vals.begin(), vals.end()), vals.end());
modint ans = 1;
for (int i = 1; i < vals.size(); i++) {
int lst = vals[i - 1], now = vals[i];
// lst 的值一定要出现在[xmax[now], xmin[lst])
modint cnt = xmin[lst] - xmax[now];
// 对于其余没有约束的变量,也就是now - lst - 1,可以自由选择后面,也就是 n - xmax[now] + 1 - (lst + 1) = n - xmax[now] - lst 个位置
cnt *= C(n - xmax[now] - lst, now - lst - 1) * fac[now - lst - 1];
ans *= cnt;
}
cout << static_cast<int>(ans) << endl;
for (auto [x, val] : v) xmax[val] = -1, xmin[val] = 2e9;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
for (int i = 0; i < kMaxN; i++) xmax[i] = -1, xmin[i] = 2e9;
fac[0] = 1;
for (int i = 1; i < kMaxN; i++) fac[i] = fac[i - 1] * i;
invf[kMaxN - 1] = 1 / fac[kMaxN - 1];
for (int i = kMaxN - 2; i >= 0; i--) invf[i] = invf[i + 1] * (i + 1);
for (int i = 1; i < kMaxN; i++) inv[i] = invf[i] * fac[i - 1];
int t;
cin >> t;
while (t--) solve();
return 0;
}
其实最后一篇题解根据AI生成

浙公网安备 33010602011771号