【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(3)
比赛链接
本文发布于博客园,会跟随补题进度实时更新,若您在其他平台阅读到此文,请前往博客园获取更好的阅读体验。
跳转链接:https://www.cnblogs.com/TianTianChaoFangDe/p/18786128
开题 + 补题情况
很菜的一把,就开了三个签到题,1001 Lucas 定理花了好久才看出来,明明前两周 CF div3 才遇到过,1010 那么明显的曼哈顿距离拆绝对值想了八百年才想出来,明明寒假集训才学过的,真的感觉自己基础太不牢固了,很多很基本的东西用不好。

1005 - 修复公路
非常明显的并查集,答案就是连通块个数 \(-1\)。
点击查看代码
#include <bits/stdc++.h>
#define inf32 1e9
#define inf64 2e18
#define ls o << 1
#define rs o << 1 | 1
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned int;
const int N = 3e5 + 9;
int a[N], fa[N];
int root(int x) {
return (fa[x] == x) ? x : fa[x] = root(fa[x]);
}
void merge(int u, int v) {
fa[root(u)] = root(fa[v]);
}
void solve()
{
int n;std::cin >> n;
for(int i = 1;i <= n;i ++) {
std::cin >> a[i];
}
for(int i = 1;i <= n;i ++) {
fa[i] = i;
}
for(int i = 1;i <= n;i ++) {
if(i - a[i] > 0) {
merge(i, i - a[i]);
}
if(i + a[i] <= n) {
merge(i, i + a[i]);
}
}
int sum = 0;
for(int i = 1;i <= n;i ++) {
sum += (root(i) == i);
}
std::cout << sum - 1 << '\n';
}
1001 - 数列计数
乘积为奇数,那就是每一项都是奇数,运用 Lucas 定理,一个组合数 \(C_n^m\) 为奇数,当且仅当 \(n \& m = m\)。
若我们先忽略掉题目给的 \(L_i\) 的限制,则答案为 \(2^{sum_i}\),\(sum_i\) 为 \(a_i\) 中为 \(1\) 的位数,很好理解,也就是对于这些位,\(b_i\) 可以取 \(0\) 也可以取 \(1\),总的组合数。
但是题目有一个 \(L_i\) 的限制,这个如何考虑呢?
- 如果 \(L_i \geq a_i\),则无需考虑。
- 如果 \(L_i < a_i\),那么我们从高位到低位考虑。
- 如果 \(a_i\) 这一位为 \(1\),但是 \(L_i\) 这一位为 \(0\),则我们减掉第 \(i\) 位右边所有 \(a_i\) 为 \(1\) 的位的组合数,相当于就是减掉了这一位取 \(1\) 的情况,因为此时大于了 \(L_i\),不合法;如果 \(a_i\) 这一位。
- 如果 \(a_i\) 这一位为 \(0\),但是 \(L_i\) 这一位为 \(1\),则我们直接跳出循环,因为后面的答案都是合法的。
对每一个数的答案乘起来就是最终答案了。
点击查看代码(省略了取模类)
const long long M = 998244353;
using Z = ModNum<M>;
const int N = 2e5 + 9;
void solve()
{
int n;std::cin >> n;
std::vector<i64> a(n), l(n);
for(auto &i : a)std::cin >> i;
for(auto &i : l)std::cin >> i;
Z ans(1);
Z t(2);
for(int i = 0;i < a.size();i ++) {
int sum = 0;
for(int j = 0;j < 32;j ++) {
if(a[i] & (1 << j)) {
sum ++;
}
}
Z tmp;
tmp = t.Pow(sum);
if(l[i] < a[i]) {
for(int j = 31;j >= 0;j --) {
if(a[i] & (1 << j))sum --;
int x = (a[i] >> j & 1);
int y = (l[i] >> j & 1);
if(x == 1 && y == 0) {
tmp -= t.Pow(sum);
}
if(x == 0 && y == 1) {
break;
}
}
}
ans *= tmp;
}
std::cout << ans << '\n';
}
1010 - 选择配送
寒假集训的时候才学过的东西,竟然想了这么久,真的不应该。
对曼哈顿距离拆掉绝对值,可以得到 \((x_1, y_1)\),\((x_2, y_2)\) 的曼哈顿距离为:
- \(\max((x_1 - y_1) - (x_2 - y_2),(x_1 + y_1) - (x_2 + y_2),(x_2 - y_2) - (x_1 - y_1),(x_2 + y_2) - (x_1 + y_1))\)。
对 \(x_2 - y_2\) 和 \(x_2 + y_2\) 各自维护一下最大最小值,然后对每个待选点查询一下即可。
点击查看代码
#include <bits/stdc++.h>
#define inf32 1e9
#define inf64 2e18
#define ls o << 1
#define rs o << 1 | 1
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned int;
const int N = 2e5 + 9;
struct Node {
i64 x, y;
};
void solve()
{
int n, m;std::cin >> n >> m;
std::vector<Node> a(n), b(m);
for(auto &[x, y] : a) {
std::cin >> x >> y;
}
for(auto &[x, y] : b) {
std::cin >> x >> y;
}
i64 admx = -inf64, admi = inf64, demx = -inf64, demi = inf64;
for(int i = 0;i < n;i ++) {
admx = std::max(admx, a[i].x + a[i].y);
admi = std::min(admi, a[i].x + a[i].y);
demx = std::max(demx, a[i].x - a[i].y);
demi = std::min(demi, a[i].x - a[i].y);
}
i64 ans = inf64;
for(auto &[x, y] : b) {
i64 tmp = -inf64;
tmp = std::max(tmp, x + y - admi);
tmp = std::max(tmp, admx - (x + y));
tmp = std::max(tmp, x - y - demi);
tmp = std::max(tmp, demx - (x - y));
ans = std::min(ans, tmp);
}
std::cout << ans << '\n';
}
1007 - 宝石商店(补题)
这题有一说一确实不难,赛时想到了正解的,但是因为时间不够没能码出来,感觉榜被开得有点歪。
首先题目给出的那个式子,枚举一下每一位的 \(0,1\) 情况不难发现,不就是异或吗,整这么一大坨来吓唬人。
那么问题就转化为了,对于给定的数,在给定区间里面找另一个数,和这个数构成一个最大异或值。
由于不带修改,我们可以考虑主席树,而对于这些数的二进制情况,可以使用 01trie 树来维护,那么,这个题的解题思路就出来了:把 01trie 树挂在主席树上,查找时贪心地查,尽可能让高位更大。
主要是没怎么写过 01trie 树挂主席树上面,所以最后没有及时码出来。
顺便复习一下 trie 树吧,感觉好久没写了不太熟悉了,trie 树上面每一个结点代表的是一个前缀,从根走到这个结点形成的前缀,因此,只要一个结点有出现次数,就代表这个前缀是存在的,那么我们在查找时,只需要尽可能贪心地往让当前位更大的那个结点走就行了。
(还尝试了一下莫队,但是莫队的复杂度是 \(O(n\sqrt n)\),而这个题的 \(n\) 的和达到了 \(10^6\),因此哪怕是给了 10s 也难以通过)
点击查看代码
#include <bits/stdc++.h>
#define inf32 1e9
#define inf64 2e18
#define ls o << 1
#define rs o << 1 | 1
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned int;
const int N = 2e5 + 9;
struct trie {
std::vector<std::array<int, 2>> tr;
std::vector<int> vis;
int tot;
void init(int n) {
tr = std::vector<std::array<int, 2>>(n * 32);
vis = std::vector<int>(n * 32);
tot = 0;
}
void clear() {
for(int i = 0;i <= tot;i ++) {
vis[i] = 0;
tr[i][0] = tr[i][1] = 0;
}
tot = 0;
}
void insert(int &p, int pre, int now, int x) {
p = ++ tot;
tr[p] = tr[pre];
vis[p] = vis[pre];
vis[p] ++;
if(now < 0)return;
int c = (x >> now & 1);
insert(tr[p][c], tr[pre][c], now - 1, x);
}
int query(int lp, int rp, int x) {
int res = 0;
for(int i = 30;i >= 0;i --) {
int c = (x >> i & 1 ^ 1);
if(vis[tr[rp][c]] - vis[tr[lp][c]]) {
res |= (1 << i);
lp = tr[lp][c];
rp = tr[rp][c];
} else {
lp = tr[lp][c ^ 1];
rp = tr[rp][c ^ 1];
}
}
return res;
}
}te;
void solve()
{
int n, q;std::cin >> n >> q;
std::vector<int> a(n + 1), rt(n + 1);
te.clear();
for(int i = 1;i <= n;i ++) {
std::cin >> a[i];
}
te.insert(rt[0], 0, 30, 0);
for(int i = 1;i <= n;i ++) {
te.insert(rt[i], rt[i - 1], 30, a[i]);
}
while(q --) {
int l, r, x;
std::cin >> l >> r >> x;
std::cout << te.query(rt[l - 1], rt[r], x) << '\n';
}
}
int main()
{
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
te.init(N);
int t = 1;std::cin >> t;
while(t --)solve();
return 0;
}
1009 - 部落冲突(补题)
听说是前几场 ABC 的原题,有点后悔没打 ABC 了。
对于这个题,若没有第三个操作,则很明显就是一个并查集,用并查集来维护一下各个部落的归属情况,查询的时候查一下野人对应的部落所在的连通块即可。
但是操作 3 让这个变得难了一点,操作 3 我们需要互换两个部落,若暴力求解显然会 TLE。
我们看一下上面没有操作 3 的思路,对于合并操作,我们并没有修改野人对应的部落,而是修改部落相互之间的归属关系,查询时直接查询即可。
那么类似的,我们也想一下能不能维护。
我们把这个问题转化一下,初始的时候有 \(n\) 个部落,放在 \(n\) 个位置中,那么对于操作 \(3\) 的交换,就可以翻译为“交换位置 \(a\) 和位置 \(b\) 的部落”,而对于最后的查询,我们也是查询一个野人所在部落的当前位置,这样就无需修改野人的位置信息了,而对于题目中的输入,我们输入的都是位置,而不是部落,因此此做法成立!
我们用 \(f_i\) 表示位置 \(i\) 的部落,用 \(g_i\) 表示部落 \(i\) 所在的位置,用 \(h_i\) 表示野人 \(i\) 所在的部落,那么就有以下解决方法:
- 操作 \(1\): 获取一下位置 \(a\) 和位置 \(b\) 当前的部落 \(f_a\) 和 \(f_b\),使用并查集进行合并。
- 操作 \(2\): 获取一下位置 \(b\) 对应的部落 \(f_b\),修改一下野人 \(a\) 当前的部落。
- 操作 \(3\): 获取一下位置 \(a\) 和位置 \(b\) 当前的部落 \(f_a\) 和 \(f_b\),同时交换 \(f_a,f_b\) 和 \(g_{f_a},g_{f_b}\),表示交换位置 \(a\) 和 位置 \(b\) 对应的部落,同时交换部落 \(f_a\) 和部落 \(f_b\) 对应的位置。
- 操作 \(4\): 由于我们对于野人存储的是部落信息 \(root_{h_a}\),但答案需要的是位置信息,因此应该输出 \(g_{root_{h_a}}\)。
很好的一个题吧,算是把离散数学的函数应用得淋漓尽致了。
点击查看代码
#include <bits/stdc++.h>
#define inf32 1e9
#define inf64 2e18
#define ls o << 1
#define rs o << 1 | 1
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned int;
const int N = 2e5 + 9;
struct BCJ {
std::vector<int> fa;
BCJ(int n) {
fa = std::vector<int>(n + 1);
std::iota(fa.begin() + 1, fa.begin() + n + 1, 1);
}
int root(int x) {
return (fa[x] == x) ? x : (fa[x] = root(fa[x]));
}
void merge(int u, int v) {
fa[root(u)] = root(fa[v]);
}
};
void solve()
{
int n, q;std::cin >> n >> q;
std::vector<int> f(n + 1), g(n + 1), h(n + 1);
std::iota(f.begin() + 1, f.begin() + n + 1, 1);
std::iota(g.begin() + 1, g.begin() + n + 1, 1);
std::iota(h.begin() + 1, h.begin() + n + 1, 1);
BCJ bcj(n);
while(q --) {
int op;std::cin >> op;
if(op == 1) {
int a, b;std::cin >> a >> b;
bcj.merge(f[b], f[a]);
} else if(op == 2) {
int a, b;std::cin >> a >> b;
h[a] = f[b];
} else if(op == 3) {
int a, b;std::cin >> a >> b;
a = f[a];
b = f[b];
std::swap(f[g[a]], f[g[b]]);
std::swap(g[a], g[b]);
} else {
int a;std::cin >> a;
std::cout << g[bcj.root(h[a])] << '\n';
}
}
}
1003 - 拼尽全力(补题)
充分体现出数据范围和时间复杂度分析的一道题。
这个题,首先 \(nm \leq 3 \times 10 ^ 6\),因此可以考虑 \(O(nm)\) 的做法或者再乘一个 \(\log\)。
思考一下,什么样的公司可以通过面试,很简单,题目已经说过了,所有能力都大于等于公司要求就能通过。
那么什么样的要求可以通过,大于等于公司要求的能力可以通过。
那么,要通过公司要求,需要做什么?需要让我们的能力值尽可能的大。
嗯,上面三句话看似都是题目直接告诉我们的废话(没错我赛时也是这么觉得的),但是却是我们此题贪心的关键!
我们把上面三句话倒过来看一遍呢?
我们要让能力值尽可能大,那就要在达不到的公司之前尽可能多地提升。
我们要想尽可能多地提升,我们就要尽可能多地通过一些公司来提升能力。
我们要想尽可能多地通过一些公司来提升能力,那就要尽可能把能通过的都通过了。
能通过的,一定是能力要求越小的,因为越容易通过。
那么,答案就出来了。
对于这 \(m\) 种能力,我们分别放进 \(m\) 个小根堆中,然后对于每一个堆,只要堆顶能打过,就打,并对这个公司的能力总数 \(-1\),表示通过了这一项能力,当一个公司所有能力都通过后,我们就可以用这个公司来提升自己,提升后再像上述进行贪心,直到公司全通过,那答案就是 YES,如果某一轮没有新通过的公司,那答案就是 NO。
时间复杂度 \(O(nm\log m)\),因为最坏的情况就是每一轮只通过一家公司,那一共就是 \(n\) 轮。
复盘一下,这个题,赛时过于关注题目所描述的“顺序”了,从而导致一直想着有没有一种排序方式,可以直接全部满足,而忽略了这一题的贪心性质,即能打则打。
点击查看代码
#include <bits/stdc++.h>
#define inf32 1e9
#define inf64 2e18
#define ls o << 1
#define rs o << 1 | 1
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned int;
const int N = 2e5 + 9;
struct Node {
int ix;
i64 val;
bool operator < (const Node &v) const {
return val > v.val;
}
};
void solve()
{
int n, m;std::cin >> n >> m;
std::vector<i64> a(m);
std::vector<std::vector<i64>> b(n, std::vector<i64>(m)), w(n, std::vector<i64>(m));
for(auto &i : a) {
std::cin >> i;
}
for(int i = 0;i < n;i ++) {
for(int j = 0;j < m;j ++) {
std::cin >> b[i][j];
}
for(int j = 0;j < m;j ++) {
std::cin >> w[i][j];
}
}
std::vector<std::priority_queue<Node>> pq(m);
for(int i = 0;i < n;i ++) {
for(int j = 0;j < m;j ++) {
pq[j].push({i, b[i][j]});
}
}
int sum = n;
std::vector<int> cnt(n);
while(true) {
int pre = sum;
std::vector<int> tmp;
for(int i = 0;i < m;i ++) {
while(pq[i].size() && pq[i].top().val <= a[i]) {
cnt[pq[i].top().ix] ++;
if(cnt[pq[i].top().ix] == m) {
tmp.push_back(pq[i].top().ix);
sum --;
}
pq[i].pop();
}
}
for(auto &i : tmp) {
for(int j = 0;j < m;j ++) {
a[j] += w[i][j];
}
}
if(sum == 0)break;
if(sum == pre) {
std::cout << "NO\n";
return;
}
}
std::cout << "YES\n";
}
1004 - 弯曲筷子(补题)
看到题很明显是一个 DP,但由于有必选的筷子,因此 DP 状态和转移方程不是很好想。
DP 其实有一个很好的思考方向,定义一个满足题目要求的状态,在状态转移的过程中,始终满足此要求,让所有 DP 值变得合法。
那么对于这个题,我们首先不管筷子是否是必选,对于筷子 \(i\),\(dp_{i, 0/1}\) 表示在 \(i\) 之前的必选筷子均已选的情况下,一个筷子是选用还是不选用的最优解。
首先不难想到,我们可以对筷子按照弯曲值进行排序,越靠近的一双筷子对答案的贡献肯定越小,那么一双筷子,要么间隔 \(0\) 根筷子,要么间隔 \(1\) 根筷子。
间隔 \(0\) 根筷子很好理解,那为什么可能间隔 \(1\) 根筷子呢?
这里简单定性分析理解一下,假设 \(x \leq a \leq b \leq c \leq y\),\(a\) 和 \(c\) 均为必选,不考虑间隔一根的情况,那么会出现以下三种选法:
- \(x, a\) 和 \(c, y\)。
- \(x, a\) 和 \(b, c\)。
- \(a, b\) 和 \(c, y\)。
若 \((a - x) ^ 2\) 特别大并且 \((y - c) ^ 2\) 特别大,那么以上三种分法均会有一项变得特别大而导致答案特别大,但如果 \((a - c) ^ 2\) 很小,比如 \(a - c = 0\),那么此时 \(a, c\) 一起选就会很优秀,因此可能会间隔 \(1\) 根筷子。
为什么不会间隔 \(2\) 根筷子?
官方题解没有证明,这里来证明一下。
该问题也就也就转化为证明:对于四个正整数 \(a \leq b \leq c \leq d\),一定有 $$(d - a) ^ 2 \geq (b - a) ^ 2 + (d - c) ^ 2$$设 \(x = b - a, y = c - b, z = d - a\),那么证明转化为:$$(x + y + z) ^ 2 \geq x ^ 2 + z ^ 2$$令 \(f = (x + y + z) ^ 2 - x ^ 2 + z ^ 2\),将平方展开可以得到:$$f = x ^ 2 + y ^ 2 + z ^ 2 + 2xy + 2yz + 2xz - x ^ 2 - z ^ 2$$继续消元可以得到:$$f = y ^ 2 + 2xy + 2yz + 2xz$$此时可以发现,\(f\) 中所有项均为非负整数,因此 \(f\) 为非负整数,当且仅当 \(a = b = c = d\) 时为 \(0\),因此:$$(x + y + z) ^ 2 \geq x ^ 2 + z ^ 2$$证毕。
那么对于 \(3\) 根及以上,自然也不可能了,因为还不如间隔 \(2\) 根。
得到了这个事实后,我们就可以用 DP 来维护答案了,对于第 \(i\) 根筷子:
- 如果第 \(i - 1\) 根筷子必选,那么:$$dp_{i, 0} = dp_{i - 1, 1}$$ $$dp_{i, 1} = dp_{i - 1, 0} + (c_i - c_{i - 1}) ^ 2$$
- 如果第 \(i - 1\) 根筷子非必选,那么:$$dp_{i, 0} = \min(dp_{i - 1, 0}, dp_{i - 1,1})$$ $$dp_{i, 1} = \min(dp_{i - 1, 0} + (c_i - c_{i - 1}) ^ 2, dp_{i - 2, 0} + (c_i - c_{i - 2}) ^ 2)$$
由于我们在 DP 过程中用到了 \(dp_{i - 2}\),因此我们要对前两项进行初始化:
- 对于第 \(1\) 根筷子,\(dp_{1, 0} = 0\),\(dp_{1, 1} = inf\),因为我们维护 DP 维护的是到第 \(i\) 根筷子为止的选法,但是第一根筷子是不能单选的,因此 \(dp_{1, 1} = inf\)。
- 对于第 \(2\) 根筷子,\(dp_{2, 1} = (c_2 - c_1) ^ 2\),如果第 \(1\) 根筷子必选,那么 \(dp_{2, 0} = inf\),否则 \(dp_{2, 0} = 0\),还是因为我们维护 DP 维护的是到第 \(i\) 根筷子为止的选法,而我们要满足 \([1, i - 1]\) 都是合法的选法,因此如果第一根筷子必选,则 \(dp_{2, 0}\) 不合法。
最后取答案,如果第 \(n\) 根筷子必选,则答案为 \(dp_{n, 1}\),否则答案为 \(\min(dp_{n, 1}, dp_{n, 0})\)。
点击查看代码(以 0 为下标起始点)
#include <bits/stdc++.h>
#define inf32 1e9
#define ls o << 1
#define rs o << 1 | 1
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned int;
const int N = 2e5 + 9;
const i64 inf64 = 2e18;
void solve()
{
int n, m;std::cin >> n >> m;
std::vector<i64> a(n);
std::vector<bool> vis(n);
std::vector<std::pair<i64, int>> c(n);
for(auto &i : a) {
std::cin >> i;
}
for(int i = 0;i < n;i ++) {
c[i] = {a[i], i};
}
sort(c.begin(), c.end());
for(int i = 1;i <= m;i ++) {
int x;std::cin >> x;
x --;
vis[x] = true;
}
std::vector<std::array<i64, 2>> dp(n, {inf64, inf64});
auto power = [](i64 x) -> i64 {
return x * x;
};
dp[0][0] = 0;
dp[1][1] = power(c[0].first - c[1].first);
if(!vis[c[0].second])dp[1][0] = 0;
for(int i = 2;i < n;i ++) {
if(vis[c[i - 1].second]) {
dp[i][0] = std::min(dp[i][0], dp[i - 1][1]);
dp[i][1] = std::min(dp[i][1], dp[i - 1][0] + power(c[i - 1].first - c[i].first));
} else {
dp[i][0] = std::min(dp[i][0], std::min(dp[i - 1][0], dp[i - 1][1]));
dp[i][1] = std::min(dp[i][1], dp[i - 1][0] + power(c[i - 1].first - c[i].first));
dp[i][1] = std::min(dp[i][1], dp[i - 2][0] + power(c[i - 2].first - c[i].first));
}
}
i64 ans = inf64;
if(vis[c[n - 1].second])ans = std::min(ans, dp[n - 1][1]);
else ans = std::min(ans, std::min(dp[n - 1][0], dp[n - 1][1]));
std::cout << ans << '\n';
}
作者: 天天超方的
出处: https://www.cnblogs.com/TianTianChaoFangDe
关于作者:ACMer,算法竞赛爱好者
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显醒目位置给出, 原文链接 如有问题, 可邮件(1005333612@qq.com)咨询.

浙公网安备 33010602011771号