COCI 2025/2026 做题记录
COCI 2025/2026 做题记录
#6 教室 / Učionica
复健第一题。写了个线段树调半天才过,然后看题解才发现有个非常好写的二分做法,呃呃。
首先对 \(a_{i, j}\) 的每一列取前缀 \(\max\):令
则第 \(l\) 个朋友可以坐在 \((i, j)\) 位置,当且仅当 \(a_{i, j} = 0\) 且 \(h_l > b_{i, j}\)。
考虑一段长度为 \(k\) 的连续空座位区间,其 \(b\) 值对应为 \((c_1, c_2, \cdots, c_k)\)。如何判定 \(k\) 个朋友能否坐在这段座位上,使得每人都有清晰的视线?
这个问题等价于,是否存在 \(h\) 的一种排列方式 \(h'\),使得 \(\forall 1 \le i \le k\),\(h'_i > c_i\)。
定义:给定两个长度均为 \(n\) 的数列 \(a\) 和 \(b\),记 \(a \prec b\) 表示 \(\forall 1 \le i \le n\),\(a_{i} < b_{i}\)。
则上面的问题可以记为:是否存在 \(h\) 的一种排列方式 \(h'\),使得 \(c \prec h'\)。
引理 1:给定两个长度均为 \(n\) 的数列 \(a\) 和 \(b\),以下两个条件等价:
- 存在 \(b\) 的重排方式 \(b'\),使得 \(a \prec b'\);
- 设 \(a'\) 和 \(b'\) 分别为 \(a\) 和 \(b\) 从小到大排列后的结果,\(a' \prec b'\)。
回到原问题的情景,这个引理就是说,让最矮的朋友去坐 \(b\) 值最小的座位,次矮的朋友去坐 \(b\) 值次小的座位……如果这种方案可以让每个朋友都有清晰的视线,那么这段空座位区间就满足条件,否则不满足。
可以用调整法简单证明:设\(a \prec b\),\(a\) 数列已经从小到大排序,\(b\) 数列存在逆序对 \((i, j)\)(即 \(i < j\) 且 \(b_i > b_j\))。由 \(a \prec b\) 得 \(b_i > a_i\) 且 \(b_j > a_j\)。那么
且
所以交换 \(b_i\) 和 \(b_j\) 后,\(a \prec b\) 仍然成立。重复这个步骤直到 \(b\) 不存在逆序对,即 \(b\) 也是有序的。证毕。
回到题目。对于每一行,用一个长度为 \(k\) 的“窗口”去框住一段座位。如果这一段座位有人坐,则不满足条件。否则设框到的座位 \(b\) 值为 \(c = (c_1, c_2, \cdots, c_k)\)。窗口从最左端向右移动,每移动一格,就从 \(c\) 中删去一个数,再增加一个数。现在我们的任务是,在有修改的条件下,维护 \(c \prec h\) 是否成立。
自然的想法是维护排序后的 \(c\) 数列,逐个判断 \(c_i < h_i\) 是否成立,但这个过程不好优化。这里要做一些转化。
引理 2:给定两个长度均为 \(n\) 的有序数列 \(a\) 和 \(b\),设 \(s_i\) 表示 \(a\) 中有多少数比 \(b_i\) 小,则
该引理显然成立。
这个转化有什么用呢?我们把 \(b\) 矩阵中的数按以下离散化:
如果 \(h_i \le x < h_{i + 1}\),则 \(x \gets i\)。(边界条件:令 \(h_0 = 0\),\(h_{k + 1} = +\infty\)。)
对于离散化后的一段连续座位,记 \(\operatorname{cnt}_{i}\) 表示 \(i\) 的数量,\(s\) 表示 \(\operatorname{cnt}\) 的前缀和,则 \(s_{i - 1}\) 就表示有多少个数小于 \(h_{i}\)。 那么这段座位满足条件,等价于 \(\forall 1 \le i \le k\),\(s_{i} \ge i + 1\)。这里我们就看到这样转换的好处了:删除或增添座位只修改 \(\operatorname{cnt}\) 的 \(1\) 个位置,对应 \(s\) 上的一段区间修改。于是我们只需判定修改后 \(s_{i} - i - 1\) 是否总不小于 \(0\)。这容易用一个支持区间加减和全局最值的数据结构(如线段树)维护。
易知时间复杂度为 \(O(nm \log k)\)。
参考代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
struct SGT {
#define lson (id << 1)
#define rson (id << 1 | 1)
const int n;
vector<int> info, tag;
void update(int id) {
info[id] = min(info[lson], info[rson]);
}
void set(int id, int x) {
info[id] += x;
tag[id] += x;
}
void pushdown(int id) {
set(lson, tag[id]), set(rson, tag[id]);
tag[id] = 0;
}
SGT(const vector<int> &init): n((int)init.size()), info(n << 2), tag(n << 2) {
function<void(int, int, int)> build = [&](int id, int l, int r) {
if(l + 1 == r) {
info[id] = init[l];
return;
}
int mid = (l + r) >> 1;
build(lson, l, mid), build(rson, mid, r);
update(id);
};
build(1, 0, n);
}
void add(int id, int l, int r, int L, int R, int x) {
if(l == L && r == R) {
set(id, x);
return;
}
int mid = (l + r) >> 1;
pushdown(id);
if(R <= mid) {
add(lson, l, mid, L, R, x);
} else if(L >= mid) {
add(rson, mid, r, L, R, x);
} else {
add(lson, l, mid, L, mid, x);
add(rson, mid, r, mid, R, x);
}
update(id);
}
void add(int L, int R, int x) {
add(1, 0, n, L, R, x);
}
int query() {
return info[1];
}
#undef lson
#undef rson
};
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n, m, k;
cin >> n >> m >> k;
vector<int> h(k + 1);
for(int i = 1; i <= k; i++) {
cin >> h[i];
}
vector<vector<int>> a(n, vector<int>(m));
for(int i = 0; i < n; i++) {
for(int j = 0; j < m; j++) {
cin >> a[i][j];
}
}
sort(h.begin(), h.end());
vector<vector<int>> b(a);
for(int i = 0; i < n; i++) {
for(int j = 0; j < m; j++) {
auto it = upper_bound(h.begin(), h.end(), b[i][j]);
int p = (int)distance(h.begin(), it);
b[i][j] = p - 1;
}
}
for(int j = 0; j < m; j++) {
for(int i = 1; i < n; i++) {
b[i][j] = max(b[i][j], b[i - 1][j]);
}
}
int ans = 0;
for(int i = 0; i < n; i++) {
vector<int> cnt(k + 1, -1);
cnt[k] = 0;
int num = 0;
for(int j = 0; j < k - 1; j++) {
cnt[b[i][j]]++;
num += a[i][j] > 0;
}
for(int j = 1; j <= k; j++) {
cnt[j] += cnt[j - 1];
}
SGT tr(cnt);
for(int j = k - 1; j < m; j++) {
if(j >= k) {
tr.add(b[i][j - k], k + 1, -1);
num -= (a[i][j - k] > 0);
}
tr.add(b[i][j], k + 1, 1);
num += (a[i][j] > 0);
if(tr.query() >= 0 && !num) {
ans++;
}
}
}
cout << ans << '\n';
return 0;
}

浙公网安备 33010602011771号