正睿 24 NOIP十连测 Day7 赛后总结
这场模拟赛结束后 be like:
A. 水题 Mini
给定两个序列 \(a, b\)。\(a\) 的长度为 \(n\), \(b\) 的长度为 \(m\), 其中 \(n \geq m\)。
选择 \(a\) 的一个长度为 \(m\) 的子序列 \(c\), 并将 \(c\) 任意重排。求 \(\sum|b_i - c_i|\) 的最小值。
注意到对于递增的 \(b\),选择的 \(a\) 也肯定是递增的。所以将 \(a,b\) 排序,然后设 \(f_{i,j}\) 表示考虑了 \(a\) 的前 \(i\) 个,已经配对了 \(b\) 中的 \(j\) 个的最小代价,然后 DP 即可。
const int MAXN = 5e3 + 5;
int n, m, a[MAXN], b[MAXN], f[MAXN][MAXN];
void work() {
cin >> n >> m;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= m; ++i)
cin >> b[i];
sort(a + 1, a + 1 + n);
sort(b + 1, b + 1 + m);
memset(f, 0x3f, sizeof f);
f[0][0] = 0;
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= i; ++j) {
if (i) f[i][j] = min(f[i][j], f[i - 1][j]);
// if (j) f[i][j] = min(f[i][j], f[i][j - 1]);
if (i && j) f[i][j] = min(f[i][j], f[i - 1][j - 1] + abs(a[i] - b[j]));
}
}
cout << f[n][m] << endl;
}
B. 水题
有 \(n\) 个硬币,第 \(i\) 个硬币会在 \(t_i\) 时刻出现在数轴 \(x_i\) 处。
有 两 个人,他们可以独自行动。0 时刻他们可以任意选择自己的起始位置,每秒可以移动不超过 \(v\) 的距离 (限制 \(v\) 为整数)。
对于第 \(i\) 个硬币,要求 \(t_i\) 时刻至少有一个人处于 \(x_i\) 位置。求最小的 \(v\)。
数据保证有解。
场上只会 60 分的 \(O(n \log n\log V)\)。
\(O(n \log n\log V)\) 做法
首先二分这个 \(v\)。然后考虑怎么判定。如果当前有一个人完成了 \(i\) 这个任务,他想要完成 \(j\) 这个任务,则需要满足:
根据很经典的绝对值拆法,将绝对值拆为 \(\max\):
又因为这是 \(\max\) 小于某个数,于是我们可以将它拆成两个条件了(这里两个条件与上面的条件是完全等价的):
移项得到:
令 \(a_i=x_i+t_iv,b_i=x_i-t_iv\),则这是一个二维偏序的形式。我们有两个人,因此合法的条件就是这个偏序的最小链覆盖数量小于等于 \(2\)。根据 Dilworth 定理,最小链覆盖等于最长反链。
于是我们将所有点按 \(a_i\) 从大到小排序,然后即为要求 \(b_i\) 的最长上升子序列。
const int MAXN = 1e6 + 5;
int n, t[MAXN], p[MAXN];
struct _bit {
int tr[MAXN];
int lowbit(int x) { return x & (-x); }
int query(int x) {
if (x <= 0) return 0;
int ret = 0;
while (x) {
ret = max(ret, tr[x]);
x -= lowbit(x);
}
return ret;
}
void modify(int x, int v) {
while (x <= n) {
tr[x] = max(tr[x], v);
x += lowbit(x);
}
}
void clear() { fill(tr, tr + 1 + n, 0ll); }
} bit;
bool check(int v) {
vector<pair<int, int>> a; a.reserve(n);
vector<int> lsh; lsh.reserve(n);
for (int i = 1; i <= n; ++i) {
a.push_back(make_pair(p[i] + t[i] * v, p[i] - t[i] * v));
lsh.push_back(p[i] - t[i] * v);
}
sort(lsh.begin(), lsh.end());
lsh.erase(unique(lsh.begin(), lsh.end()), lsh.end());
for (int i = 0; i < n; ++i) {
a[i].second = lower_bound(lsh.begin(), lsh.end(), a[i].second) - lsh.begin() + 1;
}
sort(a.begin(), a.end(), [](auto x, auto y) {
if (x.first == y. first) return x.second <= y.second;
return x.first < y.first;
});
bit.clear();
for (int i = 0; i < n; ++i) {
int x = bit.query(a[i].second - 1);
if (x == 2) return false;
bit.modify(a[i].second, x + 1);
}
return true;
}
void work() {
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> t[i] >> p[i];
int l = -1, r = 1e9 + 1;
while (l + 1 < r) {
if (check(mid)) r = mid;
else l = mid;
}
cout << r << endl;
}
考场上就想到这里了。然后后面一直在想如何把这个 \(\log V\) 去掉(因为看起来里面的 \(\log n\) 不太好去掉,且这个东西关于 \(V\) 的性质比较强)。但是正解其实是换一种 check 方式把 \(\log n\) 去掉了。
\(O(n\log V)\) 做法
还是继续二分 \(v\)。接下来考虑怎么 check。
将所有任务按照 \(t_i\) 排序,然后考虑有三角形不等式:如果 \(i\) 可达 \(j\),\(j\) 又可达 \(k\),那么 \(i\) 可达 \(k\)。
所以说,如果存在两个点 \(i,j\) 不可达,那么一定存在一个 \(k\in[i+1,j]\) 满足 \(k-1\) 和 \(k\) 不可达。所以我们可以把所有 \(i-1\) 不可达 \(i\) 的点设为关键点,那么这些点的 \(i-1, i\) 一定要用两个人来占,也就是此时一定是一个人在 \(i-1\),一个人在 \(i\)。用 0/1 表示两个人,则所有 01 交界处就是关键点,他们之间是一个人就可以走完的地方:

然后我们相当于是要问,类似于这种蓝色与蓝色点之间是否可达(因为红色之间都是可达的,否则它就会成为一个新的关键点):

为了使得蓝色之间可达,我们需要钦定中间的一些地方,让两个人交换:

但是我们可以证明,最多只需要钦定一个地方使得他们交换即可(也就是图中这种交换两次的情况不可能发生)。理由如下:
如果出现了两次及以上的交换,我们可以将任意相邻的两个交换删掉,原先合法的仍然合法(条件只会变宽不会变严)。不妨用上图举例。中间的这一段 1 全部改为 0后,如下:

其中,原先红色连接的四个 \(1\),现在只需要首部的 \(1\) 连向尾部的 \(1\),因为原先可达,现在也可达(传递性)。
然后其中的绿色的 \(0\),因为中间这两次交换是我们钦定出来的关键点,所以他们原先就 \(i-1\) 可达 \(i\),所以改了之后仍然可达。
因此,我们按照 \(i-1\) 是否可达 \(i\),将序列分成若干段。然后对于每一段,我们枚举钦定哪个点交换,由于段与段之间互不影响,所以最终检查一次的时间复杂度为 \(O(n)\)。
#include <bits/stdc++.h>
using namespace std;
// Options Start
// #define NETWORK_FLOW
// #define SEGMENT_TREE
// #define FILE_IO
// #define MULTI_TESTS
// Options End
#ifdef LOCAL_TEST
bool __mem_begin;
#endif
#define int long long
#define mid ((l + r) >> 1)
#ifndef LOCAL_TEST
#define endl '\n'
#endif
#ifdef SEGMENT_TREE
#define lson (p << 1)
#define rson (p << 1 | 1)
#endif
#ifdef NETWORK_FLOW
#define rev(p) (p ^ 1)
#endif
const int MAXN = 1e6 + 5;
int n;
struct _node {
int x, t;
bool operator < (const _node b) const {
return t < b.t;
}
} a[MAXN];
bool check(int v) {
auto ok = [&](auto i, auto j) {
if (i < 1 || i > n) return true;
if (j < 1 || j > n) return true;
return abs(a[i].t - a[j].t) * v >= abs(a[i].x - a[j].x);
};
vector<pair<int, int>> seg;
int l = 1;
for (int i = 2; i <= n; ++i) {
if (!ok(i - 1, i)) {
if (l != 1) seg.push_back(make_pair(l, i - 1));
l = i;
}
}
for (auto [L, R]:seg) {
if (ok(L - 1, R + 1)) continue;
bool flg = false;
for (int j = L + 1; j <= R; ++j) {
if (ok(L - 1, j) && ok(j - 1, R + 1)) {
flg = true; break;
}
}
if (!flg) return false;
}
return true;
}
void work() {
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i].t >> a[i].x;
sort(a + 1, a + 1 + n);
int l = -1, r = 1e9 + 1;
while (l + 1 < r) {
if (check(mid)) r = mid;
else l = mid;
}
cout << r << endl;
}
#ifdef LOCAL_TEST
bool __mem_end;
#endif
signed main(void) {
#ifdef FILE_IO
#ifndef LOCAL_TEST
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
#endif
#endif
ios::sync_with_stdio(false); cin.tie(NULL);
srand(time(nullptr));
#ifdef MULTI_TESTS
int T = 1; cin >> T; T--;
while (T--) work();
#endif
work();
return 0;
}
C. 水题 Plus
给定 \(n\) 个数 \(a_1, a_2, \dots, a_n\)。
定义 \(f(x, k)\) 为 \(a_1 \oplus x, a_2 \oplus x, \dots, a_n \oplus x\) 中的第 \(k\) 小值,其中 \(\oplus\) 为按位异或。
给定 \(m\) 组询问,每次给定 \(l, r, k\),你需要求出
\[\sum_{x=l}^r f(x, k) \]答案对 \(998244353\) 取模。
没补,题解:
建立 Trie,令 \(dp_{u,k}\) 表示仅考虑 \(u\) 的子树时 \(\sum_{x=0}^{2^d-1} f(x, k)\) 的值。其中 \(d\) 为 \(u\) 子树深度。
因为 Trie 上所有子树大小和为 \(O(n \log V)\),所以状态数量为 \(O(n \log V)\)。
转移时讨论当前的最高位,并从两个子树的 \(dp\) 值中将答案合并上来即可。
在每次询问中,我们先将 \([l, r]\) 划分为 \(O(\log V)\) 个形如 \([p \times 2^d, (p + 1) \times 2^d)\) 的区间,依次处理求和。
只需先用类似于全局第 \(k\) 小的方式定位到 Trie 上子树深度为 \(d\) 的某个节点 \(u\),访问 \(dp_{u,k}\) 即可得到后 \(d\) 位的贡献。过程中容易统计更高位的贡献。
总时间复杂度 \(O(n \log^2 V)\) 或 \(O(n \log V)\)。
D. 水题 Pro Max
有一个长度为 \(n\) 的序列,每个数均为 \([1, m]\) 中的整数,求所有情况下不含数出现次数之和。
答案对给定的质数 \(p\) 取模。
考虑对于每个 \(k\) 求出所有数出现次数均不超过 \(k\) 的方案数。
容易,钦定其中一部分数出现了 \(> k\) 次,每钦定一个就要产生 \(-1\) 的系数。
对于某一种数 \(x\),若它被钦定出现 \(> k\) 次,则我们将它的后 \(k + 1\) 次出现单独拿出来。
相当于有两种操作:
- 往当前第一个还未填的位置填一个之前未被钦定的数。
- 选择后面的 \(k + 1\) 个位置填一种之前未被钦定的数。这种数在这次操作中被钦定。
令 \(f_{i,j}\) 表示当前共填了 \(i\) 数,其中有 \(j\) 种被钦定的方案数。
状态数为 \(O(n^2 / k)\),转移复杂度为 \(O(1)\)。

浙公网安备 33010602011771号