梦熊NOIP2025模拟赛4 赛后总结

A. 【MX-S12-T1】取模

给定一个长度为 \(n\) 的非负整数序列 \(a_1, \ldots, a_n\)。请你选取一个正整数 \(p\),然后将 \(a\) 中的每个数除以 \(p\) 得到的余数放进一个新数组 \(b\) 中,即 \(b_i = a_i \bmod p\),你的得分就是 \(b\) 中最大值与最小值的差。

求出你能得到的最大得分。

\(a\) 排序并去重,因为重复的数取模之后还是一样。

如果我们选的 \(p>a_n\),所有数取模之后都不变,此时得分是 \(a_n-a_1\)

如果我们选了一个 \(p\le a_n\),不妨设 \(<p\) 的最大数是 \(a_k\),则最后的得分最大不超过 \(a_k\),且能在 \(p\) 取到 \(a_{k+1}\) 的时候取到得分 \(a_k\)。所以我们可以取 \(p=a_n\),此时得分为 \(a_{n-1}\)

于是所有情况的最大得分是 \(\max(a_{n-1}, a_n-a_1)\)

void work() {
	int n; cin >> n;
	vector<int> a(n);
	for (int i = 0; i < n; ++i)
		cin >> a[i];
	sort(a.begin(), a.end());
	a.erase(unique(a.begin(), a.end()), a.end());
	n = a.size();
	if (n == 1) return cout << 0 << endl, void();
	int ans = max(a[n - 1] - a[0], a[n - 2]);
	cout << ans << endl;
}

B. 【MX-S12-T2】区间

给定三个长度为 \(n\) 的正整数序列:颜色序列 \(c\),权值序列 \(v\),代价序列 \(f\)。序列的下标均由 \(1\) 开始标号。保证代价序列单调不减,即 \(f_i \le f_{i + 1}\)

对一个区间 \([l, r]\)\(1 \le l \le r \le n\))做如下定义:

  1. 称区间 \([l, r]\) 合法,当且仅当:不存在一种颜色 \(x\) 在区间内外均出现过,即不存在颜色 \(x\) 和下标 \(i, j\) 满足 \(c_i = c_j = x\)\(i \in [l, r]\)\(j \notin [l, r]\)
  2. 区间 \([l, r]\)价值定义为 \(\displaystyle \sum_{i = l}^{r} (v_i \cdot f_{i - l + 1})\)

找出一个价值最小的合法区间,你只需要求出该最小价值。

首先一个关键的性质是,如果两个区间 \([l_1,r_1],[l_2,r_2]\) 满足 \([l_2, r_2]\subset [l_1,r_1]\),则显然 \([l_2,r_2]\) 一定比 \([l_1,r_1]\) 优,这是由 \(v_i,f_i\) 非负且 \(f_i\) 单调不降保证的:

\([l_1,r_1]\) 的价值是 \(\sum\limits_{i=l_1}^{r_1} (v_i\times f_{i-l_1+1})\)\([l_2,r_2]\) 的价值是 \(\sum\limits_{i=l_2}^{r_2} (v_i\times f_{i-l_2+1})\)。由于 \([l_2,r_2]\) 被包含在了 \([l_1,r_1]\) 中,把 \(i\in [l_2,r_2]\)\(f_{i-l_2+1}\) 换为 \(f_{i-l_1+1}\),这一步操作价值一定不降(因为 \(l_2\ge l_1\),所以 \(f_{i-l_2+1}\ge f_{i-l_1+1}\))。此时,\([l_2,r_2]\) 的价值就严格不低于 \([l_1,r_1]\)。故原先的 \([l_2,r_2]\) 一定比 \([l_1,r_1]\) 小。

然后我们通过一个哈希来确定所有好区间:对于每个颜色 \(i\) 我们随机一个权值 \(w_i\)。然后对于每一个位置 \(x\),如果位置 \(x\) 是他的颜色里第一次出现,则将这个位置的权值赋为 \(-(cnt_{c_x}-1)\times w_{c_x}\),否则将他的权值赋为 \(w_{c_x}\)。然后做一个前缀和 \(t_{i}=\sum\limits_{j=1}^i w_i\)

然后对于每一个位置 \(i\),找到上一个 \(t_j=t_i\) 的位置,则 \([j+1,i]\) 就是一个合法的区间。我们把所有这样的区间都拿出来,然后扫描线 \(r\) 用树状数组去个重。最后剩下来的区间一定是互不相交的(否则一定有一个颜色被两个区间包含,要么不优要么不合法),所以直接暴力算贡献即可。

最终时间复杂度 \(O(n \log n)\)

using ull = unsigned long long;
const int MAXN = 1e6 + 5;
int c[MAXN], v[MAXN], f[MAXN], n;
int cnt[MAXN], fr[MAXN];
ull t[MAXN], w[MAXN];

struct _bit {
	int tr[MAXN];
	int lowbit(int x) { return (x & (-x)); }
	void modify(int x, int k) {
		while (x <= n) {
			tr[x] += k;
			x += lowbit(x);
		}
	}
	int query(int x) {
		int ret = 0;
		while (x) {
			ret += tr[x];
			x -= lowbit(x);
		}
		return ret;
	}
} bit;

void work() {
	mt19937_64 rnd(11451419);

	cin >> n;
	for (int i = 1; i <= n; ++i)
		cin >> c[i];
	for (int i = 1; i <= n; ++i)
		cin >> v[i];
	for (int i = 1; i <= n; ++i)
		cin >> f[i];
	for (int i = 1; i <= n; ++i)
		w[i] = rnd();
	for (int i = 1; i <= n; ++i) {
		cnt[c[i]]++;
		if (!fr[c[i]]) fr[c[i]] = i;
	}
	for (int i = 1; i <= n; ++i) {
		if (i == fr[c[i]]) t[i] = w[c[i]] * (cnt[c[i]] - 1);
		else t[i] = -w[c[i]];
	}
	for (int i = 1; i <= n; ++i)
		t[i] += t[i - 1];
	map<int, int> mp;
	mp[0] = 0;
	vector<pair<int, int>> v1;
	for (int i = 1; i <= n; ++i) {
		if (mp.find(t[i]) != mp.end()) {
			v1.push_back(make_pair(mp[t[i]] + 1, i));
		}
		mp[t[i]] = i;
	}
	sort(v1.begin(), v1.end(), [](auto x, auto y) {
		if (x.second == y.second) return x.first > y.first;
		return x.second < y.second;
	});
	vector<pair<int, int>> v2;
	for (auto [l, r]:v1) {
		if (bit.query(n) - bit.query(l - 1) > 0) continue;
		bit.modify(l, 1);
		v2.push_back(make_pair(l, r));
	}
	int ans = LLONG_MAX;
	for (auto [l, r]:v2) {
		int tmp = 0;
		for (int i = l; i <= r; ++i) {
			tmp += v[i] * f[i - l + 1];
		}
		ans = min(ans, tmp);
	}
	cout << ans << endl;
}

C. 【MX-S12-T3】排列

求出有多少个 \(1 \sim n\) 的排列 \(a_1, \ldots, a_n\) 满足以下条件:

对于每个下标 \(i\)\(1 \le i \le n\)),

  • \(a_i < \min_{j < i} a_j\)
  • \(a_i > \max_{j < i} a_j\)
  • \(a_i < \min_{j > i} a_j\)
  • \(a_i > \max_{j > i} a_j\)

对每个下标 \(i\),其满足四种情况中的哪一种由输入给出。答案对 \(998244353\) 取模。

注:约定 \(0\) 个数的 \(\min\)\(+\infty\)\(0\) 个数的 \(\max\)\(-\infty\)

将所有限制分为两类,一种是对前缀的限制,一种是对后缀的限制。发现这两种限制自己之内的顺序是确定的,我们现在要计数他们之间的顺序情况。

不妨设限制是前缀的点之间的大小关系是 \(a_1,a_2,\cdots a_n\),后缀的点之间的大小关系为 \(b_1,b_2,\cdots b_n\)。我们现在需要在他们之间填数。

然后设 \(f_{i,j}\) 表示填了 \(a\) 的前 \(i\) 个和 \(b\) 的前 \(j\) 个,转移考虑谁填了 \(k=i+j\) 这个数。分为四种情况讨论:

  • 如果要填 \(a_i\) 这个位置,则:
    • 如果 \(i\) 是前缀 \(\min\),则 \(a_i\) 这个位置前面不能填过数,即不能存在 \(k\le j, b_k <a_i\)
    • 如果 \(i\) 是前缀 \(\max\),则 \(a_i\) 位置前不能有空位,即不存在 \(k> j, b_k < a_i\)
  • 如果要填 \(b_j\),则:
    • 如果 \(j\) 是后缀 \(\min\),则不存在 \(k\le i,a_k>b_j\)
    • 如果 \(j\) 是后缀 \(\max\),则不存在 \(k>i,a_k>g_j\)

这个东西你暴力 check 是 \(O(n)\) 的,但是但凡是一个人类都会用前缀和优化这个。

const int mod = 998244353, MAXN = 5e3 + 5;
int n, f[MAXN][MAXN], op[MAXN];
deque<int> a, b;

void add(int &x, int y) {
	x += y;
	if (x >= mod) x -= mod;
}

void work() {
	cin >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> op[i];
	}
	for (int i = 1; i <= n; ++i) {
		if (op[i] == 0) a.push_front(i);
		else if (op[i] == 1) a.push_back(i);
	}
	for (int i = n; i >= 1; --i) {
		if (op[i] == 2) b.push_front(i);
		else if (op[i] == 3) b.push_back(i);
	}
	int na = a.size(), nb = b.size();
	a.push_front(0); b.push_front(0);
	vector<int> amxp(n + 2), amxs(n + 2);
	vector<int> bmnp(n + 2), bmns(n + 2);
	for (int i = 1; i <= na; ++i) 
		amxp[i] = max(amxp[i - 1], a[i]);
	for (int i = na; i >= 1; --i)
		amxs[i] = max(amxs[i + 1], a[i]);
	bmnp[0] = INT_MAX; bmns[nb + 1] = INT_MAX;
	for (int i = 1; i <= nb; ++i)
		bmnp[i] = min(bmnp[i - 1], b[i]);
	for (int i = nb; i >= 1; --i)
		bmns[i] = min(bmns[i + 1], b[i]);
	f[0][0] = 1;
	for (int i = 0; i <= na; ++i) {
		for (int j = 0; j <= nb; ++j) {
			if (i) {
				if (op[a[i]] == 0) {
					if (a[i] < bmnp[j]) add(f[i][j], f[i - 1][j]);
				} else if (op[a[i]] == 1) {
					if (a[i] < bmns[j + 1]) add(f[i][j], f[i - 1][j]);
				}
			}
			if (j) {
				if (op[b[j]] == 2) {
					if (amxp[i] < b[j]) add(f[i][j], f[i][j - 1]);
				} else if (op[b[j]] == 3) {
					if (amxs[i + 1] < b[j]) add(f[i][j], f[i][j - 1]);
				}
			}
		}
	}
	cout << f[na][nb] << endl;
}
posted @ 2025-11-22 20:48  小蛐蛐awa  阅读(0)  评论(0)    收藏  举报