NOIP2024 题解

考场上一直都不知道在想什么,心态也很不好,结果 B 一直不会,最后会了 C 还没写完。

感觉这个赛季对我来说就已经结束了吧 /hsh /wn

本来是想退役的,但是学文化课对我来说太痛苦了,而且我还是比较热爱 OI 的,所以就再试着走一走吧。

P11361 [NOIP2024] 编辑字符串

发现限制就是将 \(s\)\(t\) 划分成了若干段区间,每段可以任意交换,求 LCS 的最大值。

发现直接贪心匹配肯定是对的,因为你与其让它在后面匹配上不如让它直接在前面匹配上。

模拟这个过程即可,时间复杂度线性。

constexpr int N = 1e5 + 5;
int n;
char s1[N], s2[N], t1[N], t2[N];

void slv() {
	Read(n);
	Read(s1 + 1), Read(s2 + 1);
	Read(t1 + 1), Read(t2 + 1);
	vector<tuple<int, int, int, int>> A, B;
	for (int l = 1, r; l <= n; l = r + 1) {
		r = l;
		if (t1[l] == '0') {
			if (s1[l] == '0') A.emplace_back(l, l, 1, 0);
			else A.emplace_back(l, l, 0, 1);
			continue;
		}
		int c0 = 0, c1 = 0;
		while (r + 1 <= n && t1[r + 1] != '0') r ++;
		for (int i = l; i <= r; i ++) c0 += s1[i] == '0', c1 += s1[i] == '1';
		A.emplace_back(l, r, c0, c1);
	}
	for (int l = 1, r; l <= n; l = r + 1) {
		r = l;
		if (t2[l] == '0') {
			if (s2[l] == '0') B.emplace_back(l, l, 1, 0);
			else B.emplace_back(l, l, 0, 1);
			continue;
		}
		int c0 = 0, c1 = 0;
		while (r + 1 <= n && t2[r + 1] != '0') r ++;
		for (int i = l; i <= r; i ++) c0 += s2[i] == '0', c1 += s2[i] == '1';
		B.emplace_back(l, r, c0, c1);
	}
	int p = 0, q = 0; int ans = 0;
	while (p < A.size() && q < B.size()) {
		int al, ar, ac0, ac1; tie(al, ar, ac0, ac1) = A[p];
		int bl, br, bc0, bc1; tie(bl, br, bc0, bc1) = B[q];
		int c0 = min(ac0, bc0), c1 = min(ac1, bc1);
		ac0 -= c0, bc0 -= c0, ac1 -= c1, bc1 -= c1, ans += c0 + c1;
		int len = min(ar, br) - al + 1;
		len -= c0 + c1;
		if (len) {
			if (ar > br) {
				if (bc0) ac1 -= len;
				if (bc1) ac0 -= len;
			}
			if (ar < br) {
				if (ac0) bc1 -= len;
				if (ac1) bc0 -= len;
			}
		}
		if (ar == br) p ++, q ++;
		else if (ar > br) q ++, A[p] = make_tuple(br + 1, ar, ac0, ac1);
		else p ++, B[q] = make_tuple(ar + 1, br, bc0, bc1);
	}
	Write(ans, '\n');
	return;
}

P11362 [NOIP2024] 遗失的赋值

发现每段是独立的,每段的答案可以用总方案数减掉不合法的得出,最后乘起来即可。

时间复杂度 \(O(m \log n)\),当然也可以做到 \(O(m + \sqrt n)\)

constexpr int N = 1e5 + 5;
int n, m;
mint v;

void slv() {
	Read(n, m), v = Read<int>();
	vector<pair<int, int>> lim;
	for (int i = 1; i <= m; i ++) {
		int c, d; Read(c, d);
		lim.emplace_back(c, d);
	}
	sort(lim.begin(), lim.end());
	lim.erase(unique(lim.begin(), lim.end()), lim.end());
	m = (int)lim.size();
	for (int i = 1; i < m; i ++)
		if (lim[i].fir == lim[i - 1].fir)
			{ Puts("0"); return; }
	mint ans = 1;
	ans *= v.Pow(2 * (lim.front().fir - 1));
	ans *= v.Pow(2 * (n - lim.back().fir));
	for (int i = 1; i < m; i ++) {
		int l = lim[i - 1].fir, r = lim[i].fir;
		ans *= v.Pow(2 * (r - l)) - v.Pow(r - l - 1) * (v - 1);
	}
	Write((int)ans, '\n');
	return;
}

P11363 [NOIP2024] 树的遍历

先考虑 \(k = 1\) 怎么做,发现每个点的连着的边一定是连成一个链,然后你钦定根就是给每条链钦定了一个入边,所以答案是 \(\prod (deg_u - 1)!\)

不难想到容斥,现在要计算的是钦定一个集合的边为根时的方案数。

根据 \(k = 1\) 的结论,发现只有在钦定的边在一条链上的时候方案数不为 0,否则一条链会钦定三个入边。

发现如果钦定在一条链上,只有钦定了 \(\le 2\) 条边的时候有贡献,因为:

\[\sum_{T \in S}(-1)^{\lvert T \rvert} = [S = \varnothing] \]

钦定一条边是的方案数是 \(\prod(deg_u - 1)!\);钦定两条边的时候对于这两条边之间的点,它们相连的边最后一定是钦定了一条入边一条出边,所以它们的贡献是 \((deg_u - 2)!\),奇遇点的贡献还是 \((deg_u - 1)!\),所以直接 DP 即可。

时间复杂度 \(O(n)\)

constexpr int N = 1e5 + 5;
int n, k, deg[N];
vector<pair<int, int>> G[N];
mint val[N], f[N], g[N];
bool mrk[N];

void DP(int u, int fa) {
	f[u] = g[u] = 0;
	for (auto [v, id] : G[u]) {
		if (v == fa) continue; DP(v, u);
		if (mrk[id]) g[u] += f[v], f[v] = 1;
		g[u] += g[v] + f[u] * f[v] * val[u], f[u] += f[v];
	}
	f[u] *= val[u]; return;
}

void slv() {
	Read(n, k);
	for (int i = 1; i < n; i ++) {
		int u, v; Read(u, v);
		G[u].emplace_back(v, i);
		G[v].emplace_back(u, i);
		++ deg[u], ++ deg[v], mrk[i] = false;
	}
	for (int i = 1; i <= k; i ++)
		mrk[Read<int>()] = true;
	mint ans = 1;
	for (int u = 1; u <= n; u ++)
		ans *= comb.fac(deg[u] - 1), val[u] = comb.inv(deg[u] - 1);
	DP(1, 0);
	Write((int)(ans * (k - g[1])), '\n');
	return;
}
void clr() {
	for (int u = 1; u <= n; u ++) G[u].clear(), deg[u] = 0;
	return;
}

P11364 [NOIP2024] 树上查询

首先有个结论:

\[dep_{\operatorname{LCA*}(l, r)} = \min_{l \le i < r} dep_{\operatorname{LCA}(i, i +1)} \]

证明就是考虑一定有相邻的两个位置在 \(\operatorname{LCA*}(l, r)\) 的不同子树中,否则 \(\operatorname{LCA*}(l, r)\) 就是错的。

所以我们把 \(k = 1\) 干掉之后就是查询序列上所有长度 \(\ge k - 1\) 的子区间最小值最大是多少。

暴力就是枚举每个长度恰好为 \(k - 1\) 的子区间,查询是所有包含它的区间的区间最小值的最大值。

枚举区间最小值,求出极长区间,不难发现只有 \(O(n)\) 个。

放到二维平面上,就是 \(O(n)\) 个点,\(O(n^2)\) 次左上角矩形最大值查询。

然后发现对于一组 \((l, r, k)\),查询的就是 \((l, l + k - 2) \sim (r - k + 1, r - 1)\) 这条直线的左上角的最大值。

可以这样拆一下:

![[Pasted image 20241216175033.png]]

原本的查询是灰线内的部分,现在变成了三个矩形,两边扫描线即可。

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

constexpr int N = 5e5 + 5, LG = 19;
int n, q, dep[N], dfn[N], val[N], ans[N];
pair<int, int> st[LG][N], rng[N];
vector<int> G[N];

struct Segment_Tree {
	int mx[N << 1], pos[N], fa[N << 1];
	
	#define ls(p) (L + R)
	#define rs(p) (ls(p) ^ 1)
	
	void Build(int p = 1, int L = 1, int R = n) {
		mx[p] = 0;
		if (L == R) { pos[L] = p; return; }
		int Mid = L + R >> 1;
		Build(ls(p), L, Mid);
		Build(rs(p), Mid + 1, R);
		fa[ls(p)] = p, fa[rs(p)] = p;
		return;
	}
	void Update(int p, int k) {
		p = pos[p];
		while (p) cmax(mx[p], k), p = fa[p];
		return;
	}
	int Query(int l, int r, int p = 1, int L = 1, int R = n) {
		if (l <= L && R <= r) return mx[p];
		int Mid = L + R >> 1;
		if (r <= Mid) return Query(l, r, ls(p), L, Mid);
		if (Mid < l) return Query(l, r, rs(p), Mid + 1, R);
		return max(Query(l, r, ls(p), L, Mid), Query(l, r, rs(p), Mid + 1, R));
	}
	
	#undef ls
	#undef rs
} tr;
struct Counter {
	vector<tuple<int, int, int>> upd;
	vector<tuple<int, int, int, int>> qry;
	
	void Add_Point(int x, int y, int val)
		{ upd.emplace_back(x, y, val); return; }
	void Add_Query(int x, int l, int r, int id)
		{ qry.emplace_back(x, l, r, id); return; }
	vector<int> Solve() {
		vector<int> ans(q, 0); tr.Build();
		sort(upd.begin(), upd.end());
		sort(qry.begin(), qry.end());
		for (int i = 0, j = 0; i < (int)qry.size(); i ++) {
			auto [x, l, r, id] = qry[i];
			while (j < (int)upd.size() && get<0>(upd[j]) <= x)
				tr.Update(get<1>(upd[j]), get<2>(upd[j])), ++ j;
			cmax(ans[id], tr.Query(l, r));
		}
		return ans;
	}
};

void DFS(int u, int fa) {
	static int dfc = 0; dfn[u] = ++ dfc;
	st[0][dfn[u]] = {dep[u] = dep[fa] + 1, fa};
	for (auto v : G[u]) if (v ^ fa) DFS(v, u);
	return;
}
pii Query(int l, int r) {
	int k = __lg(r - l + 1);
	return min(st[k][l], st[k][r - (1 << k) + 1]);
}
int Get_Lca(int u, int v) {
	if (u == v) return u;
	if ((u = dfn[u]) > (v = dfn[v])) swap(u, v);
	return Query(u + 1, v).sec;
}

void slv() {
	Read(n);
	for (int i = 1; i < n; i ++) {
		int u, v; Read(u, v);
		G[u].emplace_back(v), G[v].emplace_back(u);
	}
	DFS(1, 0), Read(q);
	for (int i = 1; i <= __lg(n); i ++)
		for (int j = 1; j + (1 << i) - 1 <= n; j ++)
			st[i][j] = min(st[i - 1][j], st[i - 1][j + (1 << i - 1)]);
	for (int u = 1; u < n; u ++) val[u] = dep[Get_Lca(u, u + 1)];
	val[n] = 0; vector<int> stk; stk.emplace_back(0);
	for (int u = 1; u <= n; u ++) {
		while (stk.size() > 1 && val[u] < val[stk.back()])
			rng[stk.back()].sec = u - 1, stk.pop_back();
		rng[u].fir = stk.back() + 1, stk.emplace_back(u);
	}
	for (int u = 1; u <= n; u ++) st[0][u] = {dep[u], u};
	for (int i = 1; i <= __lg(n); i ++)
		for (int j = 1; j + (1 << i) - 1 <= n; j ++)
			st[i][j] = max(st[i - 1][j], st[i - 1][j + (1 << i - 1)]);
	Counter c1, c2;
	for (int u = 1; u < n; u ++)
		c1.Add_Point(rng[u].fir, rng[u].sec, val[u]),
		c2.Add_Point(rng[u].fir - rng[u].sec, rng[u].fir, val[u]);
	for (int i = 0; i < q; i ++) {
		int l, r, k; Read(l, r, k);
		if (k == 1) {
			k = __lg(r - l + 1);
			ans[i] = max(st[k][l], st[k][r - (1 << k) + 1]).fir;
		} else {
			c1.Add_Query(l, l + k - 2, n, i);
			c1.Add_Query(r - k + 1, r - 1, n, i);
			c2.Add_Query(2 - k, l, r + 1 - k, i);
		}
	}
	auto res1 = c1.Solve(), res2 = c2.Solve();
	for (int i = 0; i < q; i ++)
		Write(max({ans[i], res1[i], res2[i]}), '\n');
	return;
}
posted @ 2024-12-16 18:00  definieren  阅读(377)  评论(0)    收藏  举报