2025.1.14 CW 模拟赛

题面 & 题解

T1

原题链接, 感觉没有紫.

算法

树状数组, 数学.

思路

若对于每一个矩形来考虑, 我们需要 \(\mathcal{O}(n^4)\) 地枚举每一个矩形, 没有太大的优化前途.

换个角度, 我们对于每一个点依次进行考虑. 如果该点是作为一个子集的一个元素, 那么这样的子集一共会有 \(2^{n - 1}\) 种 (因为该点必须选), 也就是每个点都会有 \(2^{n - 1}\) 次贡献. 再来考虑该点不在子集内却被矩形覆盖了的贡献.

 _1_.png

如上图, 我们以红色的点为「原点」, 将坐标系内的点分为左上, 左下, 右上, 右下四个部分.

总共的选择方案有 \(\displaystyle (2^{cnt_l} - 1) \cdot (2^{cnt_r} - 1)\) 种. 考虑非法方案, 显然, 如果我们单单只选上半部分或者下半部分, 那么所形成的矩形一定覆盖不到红点. 减去非法的答案, 剩下的就是合法的方案了.

在实现上, 可以使用树状数组来维护四个部分点的个数, 代码里用的平衡树.

#include "bits/stdc++.h"
#include "bits/extc++.h"

using namespace std;
using namespace __gnu_pbds;

constexpr int N = 1e6 + 10, mod = 998244353;

int n, pw[N];
struct Point {
	int x, y;
	friend bool operator<(Point a, Point b) { return (a.x ^ b.x) ? a.x < b.x : a.y < b.y; }
} p[N];

void init() {
	scanf("%d", &n);
	pw[0] = 1;
	for (int i = 1; i <= 4e5; ++i) pw[i] = pw[i - 1] * 2 % mod;
	for (int i = 1; i <= n; ++i) scanf("%d %d", &p[i].x, &p[i].y);
	sort(p + 1, p + n + 1);
}

tree<int, null_type, less<int>, rb_tree_tag, tree_order_statistics_node_update> l, r;

void calculate() {
	int ans = 1ll * n * pw[n - 1] % mod;
	for (int i = 1; i <= n; ++i) r.insert(p[i].y);
	for (int i = 1; i <= n; ++i) {
		r.erase(p[i].y);
		int ld = l.order_of_key(p[i].y), lu = l.size() - ld;
		int rd = r.order_of_key(p[i].y), ru = r.size() - rd;
		ans = (ans + 1ll * (pw[ld + lu] - 1) * (pw[rd + ru] - 1) - 1ll * (pw[rd] - 1) * (pw[ld] - 1) - 1ll * (pw[lu] - 1) * (pw[ru] - 1) + mod * 2) % mod;
		l.insert(p[i].y);
	}
	printf("%d", ans);
}

void solve() {
	init();
	calculate();
}

signed main() {
	solve();
	return 0;
}

T2

算法

Ad-hoc, 感觉没什么算法.

思路

考虑先将两个询问子串中的 \(\rm{B}\) 全部转化成 \(\rm{AA}\).

注意到 \(\rm{A} \to BB \to AAAA \to A\), 也就是说其实一个长度为 \(\rm{len}\) 的子串所能够凑出的长度为 \(\rm{len} + 3 \times k, k \in \mathbb{Z}\). 将 \(\rm{len}_S, len_T\) (这里指将 \(\rm{B}\) 全部换成 \(\rm{AA}\) 后的长度) 作差, 如果结果能够被 \(3\) 整除, 那么就一定可以凑出, 否则输出 NO.

void init() {
	scanf("%s %s", s + 1, t + 1), read(q);
	lens = strlen(s + 1), lent = strlen(t + 1);
	for (int i = 1; i <= lens; ++i) ss[i] = ss[i - 1] + (s[i] == 'B');
	for (int i = 1; i <= lent; ++i) st[i] = st[i - 1] + (t[i] == 'B');
}

void calculate() {
	while (q--) {
		int a, b, c, d, lt, ls;
		read(a, b, c, d);
		ls = b - a + 1, lt = d - c + 1;
		ls += (ss[b] - ss[a - 1]), lt += (st[d] - st[c - 1]);
		if ((lt - ls) % 3) puts("NO");
		else puts("YES");
	}
}

T3

算法

数学, 动态规划.

思路

搬运自SNCPC2019] Digit Mode - 洛谷专栏

\(a_i\) 表示 \(n\) 在十进制下从高到低的第 \(i\) 位, \(n\) 共有 \(cnt\) 位, \(b_i\) 为最终数在十进制下从高到低的第 \(i\) 位.

类似数位 DP, 我们枚举前 \(x\) 位是「填满」了的 (\(\forall i \in [1, x], b_i = a_i\)), 然后第 \(x + 1\) 位满足 \(b_i < a_i\), 那么第 \(x + 2 \sim cnt\) 位就可以随便填了. 如果有前导 0, 我们枚举前 \(x\) 位是前导 0, 而第 \(x + 1\) 位填入一个正整数, 第 \(x + 2 \sim cnt\) 位也就随便填了. 设当前已经确定下来的有效数位中 \(i\) 的出现次数为 \(add_i\), 随便填的数位个数为 \(len\).

这时我们枚举出现次数最多的数是 \(m\), 其出现次数为 \(c\), 不难发现, 对于 \(x \ne m\), 在随便填的位置中最多出现 \(lim_x = c - add_x - [x > c]\) 次, 这其实类似于多重背包. 不妨设 \(f_{i, j}\) 表示前 \(i\) 个数 (不含 \(m\)), 一共用了 \(j\) 个数的方案数, 则有转移:

\[f_{i, j} = \sum_{k = 0}^{lim_i} f_{i - 1, j - k} \times \binom{j}{k} \]

则最终对答案的贡献为 \(\displaystyle f_{9, len - (c - add_m)} \times c \times \binom{len}{c - add_m}\). 按照上述方法转移即可.

T4

算法

动态规划, 凸优化.

思路

先写出一个比较显然的 DP. 不妨设 \(len_i = r_i - l_i\), \(f_{i, j}\) 表示枚举到了第 \(i\) 个矩形, 且当前矩形左端点在 \(j\) 的最小答案, 那么有转移方程 \(\displaystyle f_{i, j} = \min_{k = j - len_{i - 1}}^{j + len_i} f_{i - 1, k} + \lvert j - l_i \rvert\).

\(f_{i, j}\) 写成 \(f_i (j)\) (也就是一个函数), 可以证明其为凸函数, 同时 \(\lvert j - l_i \rvert\) 也是一个凸函数, 所以整体也是一个凸函数 (可以用二阶导数证明).

先来看 \(f_i(x)\), 分类讨论.

\(f_{i - 1}(x)\) 这个函数分成三段: 称这个 \(f_i(x)\) 的左边是斜率小于 0 的, 中间是斜率等于 0 的, 右边是斜率大于 0 的.

可以发现若 \(x\) 越靠近中间那一段, \(f_{i - 1}(x)\) 取值越小.

\(L, R\) 分别为 \(f_{i - 1}(x)\) 中间那一段的左右端点, 于是我们可以用上面的性质将 \(f_i(x)\) 的取值写成如下式子:

\[f_i(x) = \lvert x - l_i \rvert + \begin{cases} f_{i - 1}(x + len_i) & x \in (-\infty, L - len_i) \\ f_{i - 1}(L) & x \in [L - len_i, R + len_{i - 1}] \\ f_{i - 1}(x - len_{i - 1}) & x \in (R + len_{i - 1}, +\infty) \end{cases} \]

pEitJLq.png

结合图片进行理解 (想了一个晚自习qwq).

通过取值范围, 可以发现每次相当于将 \(f_{i - 1}(x)\) 向左边平移 \(len_i\), 向右边平移 \(len_{i - 1}\), \(L \gets L - len_i, R \gets R + len_{i - 1}\).

然后我们需要加上 \(\lvert x - l_i \rvert\) 这个绝对值函数, 本质是将 \((-\infty, l_i)\) 这一整段函数斜率整体 \(-1\), \((l_i, +\infty)\) 这一段函数斜率整体 \(+1\). 具体来说, 我们分类讨论一下.

  1. \(L \le l_i \le R\)

    此时最小值只有一个位置 \(l_i\), 将 \(l_i\) 左侧斜率 \(-1\), 右侧斜率 \(+1\).

  2. \(l_i < L\)

    原来在 \(l_i\) 右侧的斜率为 \(-1\) 的这一段斜率会变成 \(0\). 设斜率为 \(-1\) 的这一段函数为 \([s, t]\), 令 \(s' = \max(s, l_i)\), 那么新的 \(L' = s', R' = t\), 原来 \(l_i\) 右侧的斜率全部减一, \([l_i, t]\) 的斜率全部加一, \(R'\) 右侧的斜率整体加一.

  3. \(l_i > R\), 同 2 可得.

考虑使用两个堆 \(q_l, q_r\) 分别维护 \(L\) 左侧, \(R\) 右边的斜率拐点 (斜率 \(+1 / -1\) 的分界点). \(q_l\) 是大根堆, \(q_r\) 是小根堆, 所以 \(L, R\) 分别是 \(q_l,q_r\) 的堆顶.

int n, l[N], r[N], len[N];
priority_queue<int> ql;
priority_queue<int, vector<int>, greater<int>> qr;

void init() {
	read(n);
	for (int i = 1; i <= n; ++i)
		read(l[i], r[i]), len[i] = r[i] - l[i];
	ql.push(l[1]), qr.push(l[1]);
}

void calculate() {
	int ans = 0;
	for (int i = 2, tgl = 0, tgr = 0; i <= n; ++i) {
		tgl -= len[i], tgr += len[i - 1];
		int L = ql.top() + tgl, R = qr.top() + tgr;
		if (l[i] < L) {
			ans += L - l[i];
			ql.pop(), ql.push(l[i] - tgl), ql.push(l[i] - tgl), qr.push(L - tgr);
		} 
		else if (l[i] > R) {
			ans += l[i] - R;
			qr.pop(), qr.push(l[i] - tgr), qr.push(l[i] - tgr), ql.push(R - tgl);
		}
		else 
			ql.push(l[i] - tgl), qr.push(l[i] - tgr);
	}
	print(ans);
}
posted @   Steven1013  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示