CSU-ACM2025 春季训练赛-第一场 题解
写在前面
以下按个人向难度排序。
题目名称来自专辑:鳥船遺跡 ~ Trojan Green Asteroid(Trojan Green Asteroid) 与 卯酉東海道 ~ Retrospective 53 minutes(East-West Tokaido ~ Retrospective 53 minutes)
本场比赛涉及的套路有:
- div2C,div1B:一些关于无向图的特殊形态的小结论;
- div2D:大力枚举分段函数;
- div2E,div1C:魔改最短路;
- div1D:二分答案中位数,预处理答案 \(O(1)\) 回答询问;
- div2G,div1F:差分和递推可以相互转化;
- div1G:切比雪夫距离转曼哈顿距离,通过一些数学转换将两维独立处理。
div2A 洛谷 P5788
【模板】单调栈。
div2B 洛谷 P3370
【模板】字符串哈希
div2C、div1B CodeForces 1581B
CF1581B 图论,构造,小讨论 1200 5:人类智慧题
妈的被 div2B 1200 硬控十分钟,什么图论小结论整合版妈的。
先判断边数对于简单无向连通图是否过少或过多。
然后讨论直径长度:
- 小于 0 则无解;
- 小于 1 当且仅当 \(n=1\) 有解;
- 小于 2 当且仅当恰好构成完全图有解;
- 小于 3 则可以先构造一个菊花图,然后随便加边,则一定有解。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
LL n, m, k; std::cin >> n >> m >> k;
if (m < n - 1 || m > n * (n - 1) / 2ll) {
std::cout << "NO\n";
continue;
}
if (k <= 1) {
std::cout << "NO" << "\n";
} else if (k == 2) {
std::cout << (n == 1 ? "YES" : "NO") << "\n";
} else if (k <= 3) {
std::cout << (m == (n * (n - 1) / 2ll) ? "YES" : "NO") << "\n";
} else {
std::cout << "YES" << "\n";
}
}
return 0;
}
div2D CodeForces 1891D
数学,典中典之枚举分段函数各段(类似数论分块) 1900 2.套路,trick
一眼题,发现 \(f\) 和 \(g\) 都是有非常显然的分段性质,\(f\) 至多只有 \(\log_2 v\) 段,在此基础上可知 \(g\) 至多只有 \(\log^2 v\) 段,大力枚举很容易就能处理出每一段的边界。
\(q=10^5\) 但是只有 1s,\(O(q\log^2 v)\) 不好跑,考虑预处理一下 \(1\sim 2^k - 1\) 的答案,则每次询问仅需考虑最后至多 \(\log v\) 段即可。
总时间复杂度 \(O\left(\log^2 v + q\log v\right)\) 级别。
会爆 LL 呃呃调红温了。
我的做法可能算是比较麻烦的,建议在参考下网上其他人题解。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define i128 __int128
const LL p = 1e9 + 7;
const i128 kInf = 1e21;
const int kN = 65;
//=============================================================
i128 ans[kN];
std::vector<i128> pw[kN];
//=============================================================
void init() {
for (int i = 2; i <= 60; ++ i) {
i128 l = (1ll << i), r = (1ll << (i + 1)) - 1;
LL L = 0, R = 0;
for (i128 j = 1, k = 0; j <= kInf; ++ k) {
pw[i].push_back(j % p);
if (j >= l && !L) L = (j == l ? k : k - 1);
if (j <= r) R = k;
j *= i;
}
ans[i] = ans[i - 1];
if (L == R) {
ans[i] += (r % p - l % p + 1 + p) % p * L % p, ans[i] %= p;
} else {
ans[i] += (pw[i][L + 1] % p - l % p + p) % p * L % p, ans[i] %= p;
for (LL k = L + 1; k <= R - 1; ++ k) {
ans[i] += (pw[i][k + 1] % p - pw[i][k] % p + p) % p * k % p, ans[i] %= p;
}
ans[i] += (r % p - pw[i][R] % p + 1 + p) % p * R % p, ans[i] %= p;
}
}
}
LL query(LL p_) {
if (p_ <= 3) return 0;
LL x = 0;
for (LL i = 0; i <= 60; ++ i) {
LL j = (1ll << i);
if (j >= p_) {
x = (j == p_ ? i : i - 1);
break;
}
}
LL ret = ans[x - 1];
LL l = (1ll << x), r = p_, L = 0, R = 0;
for (i128 j = 1, k = 0; j <= kInf; ++ k) {
if (j >= l && !L) L = (j == l ? k : k - 1);
if (j <= r) R = k;
j *= x;
}
if (L == R) {
ret += (r % p - l % p + 1 + p) % p * L % p, ret %= p;
} else {
ret += (pw[x][L + 1] % p - l % p + p) % p * L % p, ret %= p;
for (LL k = L + 1; k <= R - 1; ++ k) {
ret += (pw[x][k + 1] % p - pw[x][k] % p + p) % p * k % p, ret %= p;
}
ret += (r % p - pw[x][R] % p + 1 + p) % p * R % p, ret %= p;
}
return ret;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
init();
int q; std::cin >> q;
while (q --) {
LL l, r; std::cin >> l >> r;
LL ret = (query(r) - query(l - 1) + p) % p;
std::cout << ret << "\n";
}
return 0;
}
/*
1
8589934593 36028797018963969
*/
div2E、div1C CodeForces 2000G
CF2000G 二分答案,最短路 2100 2.trick 4.须魔改某一算法的题
显然可以二分答案枚举最早何时出发。
发现经过的路径可以分为三个阶段:\(t_1\) 之前可以坐车可以步行,\(t_1\sim t_2\) 只能步行,\(t_2\) 之后坐车最优,于是考虑记 \(\operatorname{dis}_{u, i}(0\le i\le 2)\) 表示到达节点 \(u\),且当前位于阶段 \(0\sim 2\) 时的最短路径长度,显然可以跑 Dijkstra 转移,根据当前状态枚举到达邻接点的下一状态即可。
实现时并不需要显示地记录上述三种状态,可以直接根据当前最短路长度判断所处状态。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 1e5 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
int n, m, t[3];
std::vector<pr <int, int> > edge[2][kN];
int dis[kN][3];
bool vis[kN][3];
//=============================================================
bool dijkstra(int mid_) {
for (int i = 1; i <= n; ++ i) {
for (int j = 0; j < 3; ++ j) {
dis[i][j] = kInf;
vis[i][j] = 0;
}
}
std::priority_queue<pr<int, pr<int, int> > > q;
if (mid_ < t[0]) dis[1][0] = mid_, q.push(mp(-mid_, mp(0, 1)));
else if (mid_ < t[1]) dis[1][1] = mid_, q.push(mp(-mid_, mp(-1, 1)));
else dis[1][2] = mid_, q.push(mp(-mid_, mp(-2, 1)));
while (!q.empty()) {
int u = q.top().second.second, type = -q.top().second.first;
q.pop();
if (vis[u][type]) continue;
vis[u][type] = 1;
if (type == 0) {
dis[u][1] = t[0], q.push(mp(-t[0], mp(-1, u)));
dis[u][2] = t[1], q.push(mp(-t[1], mp(-2, u)));
for (auto [v, w]: edge[0][u]) {
if (dis[u][0] + w <= t[0] &&
dis[u][0] + w <= dis[v][0]) {
dis[v][0] = dis[u][0] + w;
q.push(mp(-dis[v][0], mp(-0, v)));
}
}
for (auto [v, w]: edge[1][u]) {
for (int i = 1; i <= 2; ++ i) {
if (t[i - 1] <= dis[u][0] + w &&
dis[u][0] + w <= t[i] &&
dis[u][0] + w <= dis[v][i]) {
dis[v][i] = dis[u][0] + w;
q.push(mp(-dis[v][i], mp(-i, v)));
}
}
}
continue;
}
if (type == 1) {
dis[u][2] = t[1], q.push(mp(-t[1], mp(-2, u)));
for (auto [v, w]: edge[1][u]) {
for (int i = 1; i < 3; ++ i) {
if (t[i - 1] <= dis[u][1] + w &&
dis[u][1] + w <= t[i] &&
dis[u][1] + w <= dis[v][i]) {
dis[v][i] = dis[u][1] + w;
q.push(mp(-dis[v][i], mp(-i, v)));
}
}
}
continue;
}
for (auto [v, w]: edge[0][u]) {
if (t[1] <= dis[u][2] + w &&
dis[u][2] + w <= t[2] &&
dis[u][2] + w <= dis[v][2]) {
dis[v][2] = dis[u][2] + w;
q.push(mp(-dis[v][2], mp(-2, v)));
}
}
}
return dis[n][2] < kInf;
}
void init() {
for (int i = 1; i <= n; ++ i) edge[0][i].clear(), edge[1][i].clear();
for (int i = 1; i <= m; ++ i) {
int u, v, l1, l2; std::cin >> u >> v >> l1 >> l2;
edge[0][u].push_back(mp(v, l1)), edge[0][v].push_back(mp(u, l1));
edge[1][u].push_back(mp(v, l2)), edge[1][v].push_back(mp(u, l2));
}
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n >> m;
std::cin >> t[2] >> t[0] >> t[1];
init();
int ans = -1;
for (int l = 0, r = t[2]; l <= r; ) {
int mid = (l + r) >> 1;
if (dijkstra(mid)) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
std::cout << ans << "\n";
}
return 0;
}
/*
1
5 5
100 20 80
1 5 30 100
1 2 20 50
2 3 20 50
3 4 20 50
4 5 20 50
1
3 3
100 80 90
1 2 1 10
2 3 10 50
1 3 20 21
*/
不是哥们,上面你的写法太大粪了,有没有更优美的做法?有的兄弟有的,官方题解的做法更简单:
考虑找出每个顶点的最大时间 \(ans_i\) ,在该时间内,可以从该顶点出发,在时间 \(t_0\) 到达顶点 \(n\) 。为了找到这个值,我们将从最后一个顶点开始运行 Dijkstra 算法。
在处理下一条边时,我们将检查在 \(ans_v - l_{i1}\) 到 \(ans_v\) 的时间间隔内是否可以乘坐公交车。如果可能,我们将乘坐公交车;否则,我们将步行或在该顶点等待,然后乘坐公交车。
#include <iostream>
#include <vector>
#include <set>
using namespace std;
void solve() {
int n, m;
cin >> n >> m;
int t0, t1, t2;
cin >> t0 >> t1 >> t2;
vector<vector<vector<int>>> g(n);
for (int i = 0; i < m; ++i) {
int u, v, l1, l2;
cin >> u >> v >> l1 >> l2;
u--, v--;
g[u].push_back({v, l1, l2});
g[v].push_back({u, l1, l2});
}
set<pair<int, int>> prq;
prq.insert({t0, n - 1});
for (int i = 0; i + 1 < n; ++i) {
prq.insert({-1e9, i});
}
vector<int> dist(n, -1e9);
dist[n - 1] = t0;
while (!prq.empty()) {
auto p = *prq.rbegin();
prq.erase(p);
int d = p.first, u = p.second;
for (auto e: g[u]) {
int v = e[0], l1 = e[1], l2 = e[2];
int d1 = (d - l1 >= t2 || d <= t1) ? d - l1 : d - l2;
if(d1 == d - l2) d1 = max(d1, t1 - l1);
if (dist[v] < d1) {
prq.erase({dist[v], v});
dist[v] = d1;
prq.insert({d1, v});
}
}
}
cout << (dist[0] >= 0 ? dist[0] : -1) << '\n';
}
int main() {
int t;
cin >> t;
for (int i = 0; i < t; ++i) {
solve();
}
return 0;
}
div1D CodeForces 2008H
CF2008H 枚举,二分,调和级数 2100 2.trick
显然对于任意的 \(x\),最优的操作是将每个数调整为 \(a_i\bmod x\)。
考虑二分答案 \(\operatorname{mid}\),问题变为检查是否 \(a_i\bmod x \le \operatorname{mid}\) 的 \(a_i\) 的数量不小于 \(\left\lfloor\frac{n}{2}\right\rfloor + 1\),即检查权值区间 \([0, \operatorname{mid}], [x, x + \operatorname{mid}], [2x, 2x + \operatorname{mid}], \cdots\) 中数的个数。
发现枚举区间的数量是个调和级数的形式,于是考虑枚举所有 \(x\in [1, n]\) 按上述算法二分预处理即可。
总时间复杂度 \(O(n\log n\ln n + q)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, q, cnt[kN], ans[kN];
//=============================================================
bool check(int x_, int mid_) {
int need = n / 2 + 1, c = 0;
for (int i = 0; i <= n; i += x_) {
int j = std::min(i + mid_, n);
if (i) c += cnt[j] - cnt[i - 1];
else c += cnt[j];
}
return c >= need;
}
void solve() {
for (int x = 1; x <= n; ++ x) {
for (int l = 0, r = x - 1; l <= r; ) {
int mid = (l + r) >> 1;
if (check(x, mid)) {
ans[x] = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
}
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n >> q;
for (int i = 1; i <= n; ++ i) cnt[i] = 0;
for (int i = 1; i <= n; ++ i) {
int a; std::cin >> a;
++ cnt[a];
}
for (int i = 1; i <= n; ++ i) cnt[i] += cnt[i - 1];
solve();
while (q --) {
int x; std::cin >> x;
std::cout << ans[x] << " ";
}
std::cout << "\n";
}
return 0;
}
div1E CodeForces 1919D
CF1919D 实现,模拟 2100
好玩题,并且学到了维护技巧。
发现相邻的两个数 \(a_i, a_{i - 1}\) 有可能代表同一个父亲的两个叶节点,如果这两个数之差为 1 那么一定是同一个父亲的叶节点,则可以把 \(\min\{a_i, a_{i - 1}\}\) 删去,相当于把这两个点删去并使它们的父节点变为新的叶节点,再考虑子问题即可。不断重复上述过程后,若最后删得只剩一个数(即根节点),并且 \(\min\{ a_i \} = 0\)(存在一条全 0 链)说明有解,否则无解。
然后考虑怎么实现上述过程。发现显然应当按照 \(a_i\) 降序进行删除操作,若过程中无法继续删下去说明无解。想到用优先队列维护所有节点的 \(\operatorname{a}\),用链表模拟删除;但注意到 \(\operatorname{a}\) 为最大值的点可能有多个且并非其中所有都可以删去,不能直接一股脑扔到队列里。但注意到某个位置之前不能删之后可以删说明删掉旁边一些位置后有了相邻的满足条件的位置,即之后被删的位置要么一开始就能删,要么是某次删数之后相邻的位置,于是先遍历数列将一开始就满足条件的位置扔进优先队列,然后删除某个位置时检查相邻的位置是否满足条件即可。
这个技巧在上次 edu 见过呃呃,还是 suzt 大神传授给我的,太屌了 suzt 大神
总时间复杂度 \(O(n\log n)\) 级别。
另外有线性做法。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 2e5 + 10;
//=============================================================
int n, a[kN], pre[kN], next[kN];
bool inqueue[kN];
//=============================================================
bool check(int pos_) {
if (pos_ < 1 || pos_ > n) return 0;
return ((a[pos_] - a[pre[pos_]]) == 1) || ((a[pos_] - a[next[pos_]]) == 1);
}
bool Solve() {
std::priority_queue <pr <int, int> > q;
for (int i = 1; i <= n; ++ i) {
if (check(i)) q.push(mp(a[i], i)), inqueue[i] = 1;
}
while (!q.empty()) {
int p = q.top().second; q.pop();
next[pre[p]] = next[p];
pre[next[p]] = pre[p];
if (!inqueue[pre[p]] && check(pre[p])) {
inqueue[pre[p]] = 1;
q.push(mp(a[pre[p]], pre[p]));
}
if (!inqueue[next[p]] && check(next[p])) {
inqueue[next[p]] = 1;
q.push(mp(a[next[p]], next[p]));
}
}
int num = 0, mina = kN;
for (int i = 1; i <= n; ++ i) {
num += !inqueue[i];
mina = std::min(mina, a[i]);
}
return num == 1 && mina == 0;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n;
a[0] = a[n + 1] = kN;
for (int i = 1; i <= n; ++ i) {
std::cin >> a[i];
inqueue[i] = 0;
pre[i] = i - 1;
next[i] = i + 1;
}
std::cout << (Solve() ? "YES\n" : "NO\n");
}
return 0;
}
div2F、div1A 洛谷 P3373
【模板】线段树2
div2G、div1F CodeForces 1634F
CF1634F 对差分的本质理解 2700 2.trick 4.须魔改某一算法的题 5.人类智慧
https://codeforces.com/contest/1634/problem/F
好玩题。
先令 \(c_i = a_i - b_i\),则仅需检查 \(c\) 是否全为 0。
发现这题带 \(\log\) 不好跑,又是区间修改,只得考虑差分。但是又要动态检查,这如何是好?
考虑差分的本质。我们可以将区间加 \([l, r]\) 转化为差分数组上的 \(d_l\) 加 \(d_r\) 减,是因为修改的增量 \(\operatorname{delta}\) 满足 \(\operatorname{delta}_i = \operatorname{delta}_{i - 1} (l < i \le r)\),于是可以构建差分数组:\(d_1 = a_1\),\(d_i = a_{i} - a_{i - 1}(i\ge 2)\),则通过维护差分数组即可在修改完成后通过 \(a_i = a_{i - 1} + d_i\) 来递推出 \(a_i\)。
同理,区间加斐波那契数列也存在这样的递推关系:\(\operatorname{delta}_{i} = \operatorname{delta}_{i - 1} + \operatorname{delta}_{i - 2}(l + 1 <i \le r)\),则考虑构建差分数组:\(d_1 = c_1\),\(d_2 = c_2 - c_1\),\(d_i = c_{i} - c_{i - 1} - c_{i - 2}(i\ge 3)\),则区间加 \([l, r]\) 可转化为三次单点修改:\(d_{l}\leftarrow d_{l} + 1\),\(d_{r + 1}\leftarrow d_{r + 1} + f_{r - l + 2}\),\(d_{r + 2}\leftarrow d_{r + 1} + f_{r - l + 1}\), 则同理有:\(c_{i} = c_{i - 1} + c_{i - 2} + d_i\)。
发现 \(c\) 全为 0 当且仅当差分数组 \(d\) 全为 0,于是在单点修改 \(d\) 的过程中维护 \(d\) 有多少个位置为 0 即可。
总时间复杂度 \(O(n + q)\) 级别,注意随时取模。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 3e5 + 10;
//=============================================================
int n, q, num;
LL p, f[kN], c[kN], d[kN];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Init() {
n = read(), q = read(), p = read();
f[1] = f[2] = 1;
for (int i = 3; i <= n; ++ i) f[i] = (f[i - 1] + f[i - 2]) % p;
for (int i = 1; i <= n; ++ i) c[i] = read() % p;
for (int i = 1; i <= n; ++ i) c[i] = (c[i] - read() % p + p) % p;
d[1] = c[1] % p, d[2] = (c[2] - c[1] + p) % p;
for (int i = 3; i <= n; ++ i) {
d[i] = (c[i] - c[i - 1] - c[i - 2] + 2 * p) % p;
}
for (int i = 1; i <= n; ++ i) num += (!d[i]);
}
void modify(int pos_, int val_) {
if (pos_ > n) return ;
num -= (!d[pos_]);
d[pos_] = (d[pos_] + val_ + p) % p;
num += (!d[pos_]);
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
Init();
while (q --) {
char s[5]; scanf("%s", s + 1);
int l = read(), r = read();
int type = ((s[1] == 'A') ? 1 : -1), ntype = ((s[1] == 'B') ? 1 : -1);
modify(l, type);
modify(r + 1, ntype * f[r - l + 1 + 1]);
modify(r + 2, ntype * f[r - l + 1]);
printf("%s\n", (num == n) ? "YES" : "NO");
}
return 0;
}
学到了灵活差分。
差分的本质是递推。
div1G AtCoder abc221_g
曼哈顿距离转切比雪夫距离,bitset 优化背包,构造 紫题 2.trick 4.须魔改某一算法的题 5.人类智慧
发现对于原操作,每次操作只会对某一维产生贡献,则需要同时考虑两维,比较麻烦。
一个套路是考虑曼哈顿距离转切比雪夫距离,终点变为 \(X = A - B, Y = A + B\),则四种操作变为:
发现所有的操作每一维都是 \(\plusmn d\),考虑将每一维都平移 \(\sum d\),则终点变为 \(X = A - B + \sum d, Y = A + B + \sum d\),进一步地操作变为:
发现此时对于一次操作,可以任意选择不产生贡献、仅对某一维产生贡献、对两维同时产生贡献,则此时可将两维看做独立的问题。
对于一维的子问题,问题等价于有 \(n\) 个物品,选择第 \(i\) 个物品产生 \(d\) 的贡献,要求构造选择方案使得贡献之和恰好为 \(X\) 或 \(Y\)。显然的 01 背包问题,考虑到 \(n\) 和坐标的值域不太大,考虑 bitset 优化即可。然后根据背包逆序还原操作方案即可。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2010;
const int kM = 3e6 + 6e5 + 10;
const char opt[5] = "LDUR";
//=============================================================
int n, x, y, a, b, sumd, d[kN];
std::bitset<kM> f[kN];
int ans[kN];
//=============================================================
bool solve() {
if (abs(a) > sumd || abs(b) > sumd) return 0;
if ((a + sumd) % 2 || (b + sumd) % 2) return 0;
a = (a + sumd) / 2, b = (b + sumd) / 2;
f[0][0] = 1;
for (int i = 1; i <= n; ++ i) f[i] = f[i - 1] | (f[i - 1] << d[i]);
if (f[n][a] == 0 || f[n][b] == 0) return 0;
for (int i = n; i; -- i) {
if (!f[i - 1][a]) a -= d[i], ans[i] += 1;
if (!f[i - 1][b]) b -= d[i], ans[i] += 2;
}
return 1;
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> x >> y;
a = x - y, b = x + y;
for (int i = 1; i <= n; ++ i) std::cin >> d[i], sumd += d[i];
if (!solve()) {
std::cout << "No" << "\n";
return 0;
}
std::cout << "Yes" << "\n";
for (int i = 1; i <= n; ++ i) std::cout << opt[ans[i]];
return 0;
}

浙公网安备 33010602011771号