长春集训 Day 3 - 模拟赛
长春集训 Day 3 - 模拟赛
模拟赛:14+10+0+0,暴露出很多问题,需要重新反思一下。
T1 - 平衡的白鸽
推开一扇门 还有万千重门
Description
给你一个长度为 \(2n\) 的数组,要求在数组中选出 \(n\) 对无序数对 \((x,y)\),每个数只能被选用一次。
给定常数 \(A\) 和 \(B\)。称一个配对是好的,当且仅当 \(|x-y| \le B\);一个配对是一般的,当且仅当 \(B \lt |x-y| \le A\);一个配对是坏的,当且仅当 \(|x-y| \gt A\)。
试问是否存在一种选数对的方案,使得方案中不存在坏数对,且好数对的数量尽可能多。输出最多的好数对数量或报告无解。
多测,\(1 \le T \le 20,\ 1 \le n \le 5 \times 10^5\)。时限 \(4 \sec\)。
Reflection
(我修改码风后,有将代码神秘化的趋势。一个调整方案是,将一些整块的东西合并起来。)
重演一下场上的过程。
首先注意到了选邻项一定不劣,然后滋生了判断无解的算法。这一步是容易的。
后续发现选数对的方式可分为两种,即 (奇, 偶) 或 (偶, 奇)。这两类互不交叉。此外,可以通过一种跨越多个数对的选数对方案(我们称之为“桥”)使桥内与桥外的选数对类别不同。
据此看出了嵌套问题,即是说,嵌套层数大于 2 一定更劣。因为层数大于 2 的完全可以转化为更优的层数小于等于 2 的方案。
然后思维开始错乱。看了一下右边的电脑,发现 hkb 写了个反贪(但是是错的),瞬间慌了,因为我对反贪很不熟练。这启示我们,场上一定不要受外界因素干扰,相信自己的做法。
我先想到,对于每个点,二分找到距离它最远的可行匹配,中间的全用另外一种选数对方案来选。这样就把每个点抽象成一个线段,每个线段有对应的贡献(可以前缀和 \(O(n)\) 预处理、\(O(1)\) 查询),这样问题转化为选择数轴上若干不交的线段,使总贡献最大,这是一个明显的 DP 问题。后来意识到,这样做肯定是错的,因为不一定这个点到最远的点的所有配对都是最优的。然后就不知道咋做了。
其实顺着这个思路可以继续往前。
Solution
我们深入思考一下。一种配对显然可以分为“算入贡献的”和“不算入贡献的”两类。
至于那些“不算入贡献的”配对,它们之所以不被算入贡献,正是因为连默认的“选择相邻数”的最优方案,都无法产生“算入贡献的”匹配方案。
那么我们考虑,如果使用原来“选择相邻数”的选数方案产生了“不算入贡献的”匹配方案,那么可知“选择相邻数”的限制是完全无效的——因为相邻的数都产生不了贡献,其它数也产生不了贡献。换言之,我们的限制浪费了一些潜在的选数方案。既然这样的匹配方案已经算不了贡献了,不妨将这种差的匹配方案联合起来。这即是说,遇到一个无法产生贡献的数,就将它打上标记;每两个被打上标记的数自动按顺序凑成一对,保证了“不算入贡献的”选数方案得以被最佳利用。
P.S. 这种“充分利用某种性质”的思维在其他贪心题中也有体现,如 NOIP 2025 T1 糖果店。
具体地,我们从头开始遍历,对每个数尝试与相邻位置配对。
-
如果这对配对能产生贡献,则使用这对配对;
-
如果不能产生贡献,则将这个数标记。每当产生两个有标记的数时,将其合并成一对。
- 特别地,如果当前存在一个有标记的数,且遍历指针指向了一个位置,使得这个位置与该数产生了非法配对,那么必须停止。这即是说,我们需要找到一个位置,使得这个位置恰好是最大的能与该数产生合法配对的位置。这很好找,边扫边找即可。
注意边界条件。
算法时间复杂度 \(O(n \log n)\),瓶颈在排序。
Code
#include <bits/stdc++.h>
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 1e6+5;
int T, n, b[N], a[N];
int q, A, B;
bool vis[N];
void solve() {
memset(vis, 0, sizeof vis);
cin >> q;
for (int i = 1; i <= q; i++) {
int x, v; cin >> x >> v;
b[x] = v;
}
cin >> A >> B;
for (int i = 1; i <= 2*n; i++) {
a[i] = b[i];
}
sort(a+1, a+1+2*n);
// 特判无解
for (int i = 1; i <= 2*n-1; i += 2) {
if (a[i+1] - a[i] > A) {
cout << -1 << '\n';
return;
}
}
int pre = -1, ans = 0;
for (int i = 1; i <= 2*n; i++) {
if (vis[i]) continue;
vis[i] = 1;
if (pre != -1 and i+2 <= 2*n and a[i+2] - a[pre] > A) {
pre = -1; // 越界了,赶紧合并
} else {
if (i+1 <= 2*n and a[i+1] - a[i] <= B) {
ans++, vis[i+1] = 1;
} else {
if (pre == -1) pre = i;
else pre = -1;
}
}
}
cout << ans << '\n';
}
signed main() {
cin.tie(0) -> sync_with_stdio(0);
// file(dove);
cin >> T >> n;
for (int i = 1; i <= 2*n; i++) {
cin >> b[i];
}
while (T--) {
solve();
}
return 0;
}
T2 - 你也在这里
Description
有一个 \(n \times m\) 的方格平面,其中 \(k\) 个方格是障碍,第 \(i\) 个障碍的坐标是 \((x_i, y_i)\)。
可以选择一个正整数 \(x\),使得存在一个 \(x \times x\) 的正方形,使得它能被合法地放置在方格图中,并沿水平或竖直方向移动。全过程中正方形不能碰任何障碍。
正方形移动过程中覆盖过的方格视为已标记的方格。试求最大的 \(x\),使得存在一种有限的移动方式,能使的所有方格都被标记,或报告无解。
记 \(S = \sum nm\),则 \(1 \le n,m \le 3 \times 10^6,\ 0 \le k \lt nm \le 3 \times 10^6,\ 1 \le T,S \le 10^7,\ \sum k \le 3 \times 10^6\)。时限 \(5 \sec\)。
Reflection
赛时先看了 A、B、C 特殊性质,感觉都很好打,做几个特判和分讨即可。
然后并没有注意到答案的单调性,认为是一个悬线问题,按照这个思路考虑了很久,未果。
那么我们考虑,如何注意到这个答案的单调性呢?我们会采用顺序的思考方式,即从小的 \(x\) 思考到大的 \(x\),如果这时结合答案的合法性去考虑,很容易想到这点。
总之,如果答案难于直接计算,那么可以考虑二分答案。其他算法都是用某种结构计算答案;但二分是一种极其关键的,直接对答案出手的方法,与其他算法本质不同。
Solution
注意到答案具有单调性,二分即可出结果。考虑 check 函数。
对于答案 \(len\) 的检验,可以直接广搜。每次搜索需要遍历所有网格,时间复杂度带一个 \(S\);对于正方形覆盖,如果直接暴力则需要再带一个 \(S\)。暴力复杂度是 \(O(S^2 \log S)\) 的。
至于代码实现,可以用一个 map<pair<int, int>, bool> b 存储障碍位置(注意这样会带一个小 \(\log\)),也可以构建哈希表,根据横纵坐标的值来计算坐标的掩码(因为总的格数在 \(10^7\) 左右,一维数组可以直接存下)。
考虑优化。每次正方形的覆盖是一个重复性的问题,最后统计所有方格是否全被覆盖也是高度重复的。前者可以差分,后者可以前缀和。值得一提的是,这里代码实现不太好弄。用 map 类比较麻烦。可以用 vector<vector<int>> 类。原因仍然是总元素数比较少,空间不会爆。
值得注意的是,vector<vector<int>> 类初始化不会给你开那么大空间,需要根据题目 \(n,m\) 的值来现场界定。方法是:
vector<vector<int>> diff;
diff.assign(n+5, vector<int>(m+5, 0));
这样的话,第二个 \(S\) 被砍掉了,时间复杂度降到 \(O(S \log S)\)。由于各类 stl 的使用,常数不小,过不去。卡常方案如下:
-
将所有
vector<vector<int>>类废弃,改用一维数组存坐标,每个坐标 \((i,j)\) 有一个掩码,正好对应 \(10^7\) 量级个掩码,空间够存 (掩码的技巧非常实用)。 -
将所有
map<pair<int, int>, bool>类同上处理。
Code
#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 3e6+5, dir[4][2] = {{1,0},{0,1},{-1,0},{0,-1}};
int c, T, n, m, k;
vector<bool> b, used;
vector<int> sum, diff;
inline int msk(int x, int y) {
return x * (m+5) + y;
}
inline bool in_range(int x, int y, int len) {
return x >= 1 and y >= 1 and x+len-1 <= n and y+len-1 <= m;
}
inline long long query(int x1, int y1, int x2, int y2) {
return sum[msk(x2,y2)] - sum[msk(x1-1,y2)] - sum[msk(x2,y1-1)] + sum[msk(x1-1,y1-1)];
}
void BFS(int sx, int sy, int len) {
queue<pair<int, int>> q;
q.push({sx, sy});
used[msk(sx,sy)] = 1;
diff[msk(sx,sy)] += 1;
diff[msk(sx+len,sy)] -= 1;
diff[msk(sx,sy+len)] -= 1;
diff[msk(sx+len,sy+len)] += 1;
while (!q.empty()) {
auto [x, y] = q.front(); q.pop();
for (int d = 0; d < 4; ++d) {
int dx = x + dir[d][0], dy = y + dir[d][1];
if (!in_range(dx, dy, len)) continue;
int nxt = msk(dx, dy);
if (used[nxt]) continue;
if (query(dx, dy, dx+len-1, dy+len-1) == 0) {
used[nxt] = 1;
q.push({dx, dy});
diff[msk(dx,dy)] += 1;
diff[msk(dx+len,dy)] -= 1;
diff[msk(dx,dy+len)] -= 1;
diff[msk(dx+len,dy+len)] += 1;
}
}
}
}
bool check(int len) {
diff.assign((n+5)*(m+5), 0);
used.assign((n+5)*(m+5), 0);
int sx = -1, sy = -1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (b[msk(i, j)]) continue;
if (query(i, j, i+len-1, j+len-1) > 0) continue;
sx = i; sy = j;
break;
}
if (sx != -1) break;
}
if (sx == -1) return 0;
BFS(sx, sy, len);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
diff[msk(i,j)] += diff[msk(i-1,j)] + diff[msk(i,j-1)] - diff[msk(i-1,j-1)];
if (b[msk(i,j)] == 0 and diff[msk(i,j)] == 0) return 0;
}
}
return 1;
}
void solve() {
cin >> n >> m >> k;
b.assign((n+5)*(m+5), 0);
sum.assign((n+5)*(m+5), 0);
for (int i = 1; i <= k; ++i) {
int x, y; cin >> x >> y;
b[msk(x,y)] = sum[msk(x,y)] = 1;
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
sum[msk(i,j)] += sum[msk(i-1,j)] + sum[msk(i,j-1)] - sum[msk(i-1,j-1)];
}
}
int l = 1, r = min(n, m), ans = -1;
while (l <= r) {
int mid = (l + r) >> 1;
if (check(mid)) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
cout << ans << '\n';
}
signed main() {
cin.tie(0) -> sync_with_stdio(0);
cin >> c >> T;
while (T--) {
solve();
}
return 0;
}

浙公网安备 33010602011771号