正睿 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\) 这个任务,则需要满足:

\[|x_i-x_j|\le (t_j-t_i)v \]

根据很经典的绝对值拆法,将绝对值拆为 \(\max\)

\[\max(x_i-x_j,x_j-x_i)\le (t_j-t_i)v \]

又因为这是 \(\max\) 小于某个数,于是我们可以将它拆成两个条件了(这里两个条件与上面的条件是完全等价的):

\[x_i-x_j \le (t_j-t_i)v\\ x_j-x_i \le (t_j-t_i)v \]

移项得到:

\[x_i+t_iv\le x_j+t_jv\\ x_i-t_iv\ge x_j-t_jv \]

\(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 交界处就是关键点,他们之间是一个人就可以走完的地方:

image-20251124170242654

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

image-20251124170326286

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

image-20251124170521825

但是我们可以证明,最多只需要钦定一个地方使得他们交换即可(也就是图中这种交换两次的情况不可能发生)。理由如下:

如果出现了两次及以上的交换,我们可以将任意相邻的两个交换删掉,原先合法的仍然合法(条件只会变宽不会变严)。不妨用上图举例。中间的这一段 1 全部改为 0后,如下:

image-20251124170723670

其中,原先红色连接的四个 \(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)\)

posted @ 2025-11-24 19:12  小蛐蛐awa  阅读(8)  评论(0)    收藏  举报