长春集训 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 的使用,常数不小,过不去。卡常方案如下:

  1. 将所有 vector<vector<int>> 类废弃,改用一维数组存坐标,每个坐标 \((i,j)\) 有一个掩码,正好对应 \(10^7\) 量级个掩码,空间够存 (掩码的技巧非常实用)。

  2. 将所有 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;
}
posted @ 2026-07-29 08:01  L-Coding  阅读(2)  评论(0)    收藏  举报