「赛后总结」暑假集训:20230722 CSP 模拟赛

「赛后总结」20230722 CSP 模拟赛

点击查看目录

吓死我了我还以为 K8He 不更博了。

为啥前天模拟赛不写啊?

打过,没参加。

为啥昨天模拟赛不写啊?

一些原因没空打。

image

这个标签补全真的是过于抽象了。

反思

T1 这个 DP 可以看出思考时的死角啊,考场上一直在想从对称线往两角跑,想用哈希,始终没想到 DP。

T2 的过程比较有趣味,大概 10 点多写完暴力后通过观察样例发现了正解用到的性质,但是决定先去看看 T4,然后想了半天 T4 无果,又去看了看 T1,T1 暴力写完又回来看 T4,结束前 10min 才突然想起来我是会 T2 的 😅。赛后写了 1h 一遍过,可见睡觉的重要性。

T3 冲了半天,而且一直是对着 \(\left\lfloor\frac{2m}{3}\right\rfloor\) 冲的,完全偏了。

T4 数据结构根本不会啊!下次遇到这种题看出来自己不可能会就应该赶紧写个暴力扔了。

总结:好好睡觉。

题解

T1 回文

思路

\(f_{i, j, k, l}\) 表示从 \((i, j)\) 走到 \((k, l)\) 的回文串数量,转移感觉比较显然。

不难发现 \(i + j + k + l = n + m + 2\),可以直接去掉最后一维。

刚开始用的记搜但爆栈了,换了递推,常数好像比较大。

模数这么抽象?

代码

点击查看代码
const ll N = 510, P = 993244853;
namespace SOLVE {
	ll n, m, len, f[N][N][N], ans;
	bool vis[N][N][N];
	char s[N][N];
	inline ll rnt () {
		ll x = 0, w = 1; char c = getchar ();
		while (!isdigit (c)) { if (c == '-') w = -1; c = getchar (); }
		while (isdigit (c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar ();
		return x * w;
	}
	/* MLE 了
	inline ll Dfs (ll i, ll j, ll k) {
		ll l = n + m + 2 - i - j - k;
		if (l < 1 || l > m || i > k || j > l || s[i][j] != s[k][l]) return 0;
		if (vis[i][j][k]) return f[i][j][k];
		vis[i][j][k] = true;
		if (i == k && j == l) f[i][j][k] = 1;
		else if (i + 1 == k && j == l && s[i + 1][j] == s[k][l]) f[i][j][k] = 1;
		else if (i == k && j + 1 == l && s[i][j + 1] == s[k][l]) f[i][j][k] = 1;
		else f[i][j][k] = (Dfs (i + 1, j, k - 1) + Dfs (i + 1, j, k) + Dfs (i, j + 1, k - 1) + Dfs (i, j + 1, k)) % P;
		return f[i][j][k];
	}
	*/
	inline void In () {
		n = rnt (), m = rnt ();
		_for (i, 1, n) scanf ("%s", s[i] + 1);
		return;
	}
	inline void Solve () {
		for_ (i, n, 1) {
			for_ (j, m, 1) {
				_for (k, 1, n) {
					ll l = n + m + 2 - i - j - k;
					if (l < 1 || l > m || i > k || j > l || s[i][j] != s[k][l]) continue;
					if (i == k && j == l) f[i][j][k] = 1;
					else if (i + 1 == k && j == l && s[i + 1][j] == s[k][l]) f[i][j][k] = 1;
					else if (i == k && j + 1 == l && s[i][j + 1] == s[k][l]) f[i][j][k] = 1;
					else f[i][j][k] = (f[i + 1][j][k - 1] + f[i + 1][j][k] + f[i][j + 1][k - 1] + f[i][j + 1][k]) % P;
				}
			}
		}
		return;
	}
	inline void Out () {
		printf ("%lld\n", f[1][1][n]);
		return;
	}
}

T2 快速排序

思路

可以尝试一下手模排序过程,不难发现最终序列相当于把所有非 nan 数字往前移直到序列去掉 nan 之后升序。

暴力模拟这个过程即可。

复杂度瓶颈在排序。

代码

点击查看代码
const ll N = 5e5 + 10;
namespace SOLVE {
	ll n, m; char s[10];
	class Number {
	public:
		bool isnan; ll val;
	} a[N];
	class Tmp {
	public:
		ll val, i, cnt; bool isswapped, ed;
		inline bool operator < (const Tmp& another) const {
			return val == another.val ? i < another.i : val < another.val;
		}
	} b[N];
	inline ll rnt () {
		ll x = 0, w = 1; char c = getchar ();
		while (!isdigit (c)) { if (c == '-') w = -1; c = getchar (); }
		while (isdigit (c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar ();
		return x * w;
	}
	inline ll StringToNumber (char *s) {
		ll len = strlen (s + 1), x = 0;
		_for (i, 1, len) x = (x << 3) + (x << 1) + (s[i] ^ 48);
		return x;
	}
	inline void In () {
		ll mx = 0;
		n = rnt (), m = 0;
		b[0].cnt = 0;
		_for (i, 1, n) {
			scanf ("%s", s + 1);
			if (s[1] == 'n') a[i].isnan = true, a[i].val = 0;
			else {
				a[i].isnan = false, a[i].val = StringToNumber (s);
				++m, b[m].val = a[i].val, b[m].i = i, b[m].cnt = 0;
				b[m].isswapped = b[m].val < b[mx].val;
				if (b[mx].val <= b[m].val) mx = m;
			}
		}
		ll k = mx;
		for_ (i, n, 1) {
			if (a[i].isnan) ++b[k].cnt, b[k].ed = true;
			else if (b[k].i == i) do { --k; } while (k && b[k].isswapped);
		}
		return;
	}
	inline void Solve () {
		std::sort (b + 1, b + m + 1);
		return;
	}
	inline void Out () {
		_for (i, 1, b[0].cnt) printf ("nan ");
		_for (i, 1, m) {
			printf ("%lld ", b[i].val);
			_for (j, 1, b[i].cnt) printf ("nan ");
		}
		puts ("");
		return;
	}
}

T3 混乱邪恶

思路

Chaotic evil。这个词应该是用来形容出题人的。

我们设 \(n\) 为偶数,当 \(n\) 为奇数时向序列里加一个 \(0\),不影响计算。

\(a\) 排序定义一个差分数组 \(d_i = a_{2i} - a_{2i - 1}\),考虑构造一个 \(e\) 数组(\(e\in \{1, -1\}\)),使得 \(\sum d_ic_i = 0\),同时可以发现 \(\sum d_i\le m - \frac{n}{2} < n\)

\(e\) 数组可以轻松构造出答案,所以先只考虑构造它。

首先考虑这个 \(e\) 何时有解。设 \(d\) 数组长度为 \(l\),显然当 \(l = 1\)\(d_1 = 0\) 有解。

根据直觉考虑使用数学归纳法。\(l > 1\) 时可以把最大值减最小值放进序列,然后把它俩扔了,此时 \(\sum d_i\) 减少 \(2\min d_i\ge2\),所以新序列完全合法,可以归结到序列长度为 \(l - 1\) 的情况,也就说明永远有解,没有 Chaotic evil。

然后构造方法很神奇,你建一棵树就行,两点点权需要相减就建一个点权为其差的点当爹。根节点点权为零,叶子结点是原序列。

维护最大值和最小值用 multiset

我手上没有画这种东西的画板,你们意会一下。

代码

点击查看代码
const ll N = 2e6;
namespace SOLVE {
	ll n, m, l, b[N], son[N][2], tot, ans[N];
	class TMP{
	public:
		ll val, id;
		inline bool operator < (const TMP& another) const {
			return val < another.val;
		}
	} a[N];
	std::multiset <TMP> st;
	inline ll rnt () {
		ll x = 0, w = 1; char c = getchar ();
		while (!isdigit (c)) { if (c == '-') w = -1; c = getchar (); }
		while (isdigit (c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar ();
		return x * w;
	}
	inline void Dfs (ll p, ll w) {
		if (p <= l / 2) ans[a[p * 2 - 1].id] = -w, ans[a[p * 2].id] = w;
		else {
			Dfs (son[p][0], -w);
			Dfs (son[p][1], w);
		}
		return;
	}
	inline void In () {
		n = rnt (), m = rnt ();
		_for (i, 1, n) a[i].id = i, a[i].val = rnt ();
		return;
	}
	inline void Solve () {
		l = n + (n & 1), tot = l / 2;
		std::sort (a + 1, a + l + 1);
		_for (i, 1, l / 2) st.insert ((TMP) { b[i] = a[i * 2].val - a[i * 2 - 1].val, i });
		while (st.size () > 1) {
			TMP mn = *st.begin (), mx = *--st.end ();
			st.erase (st.begin ()), st.erase (--st.end ());
			++tot, b[tot] = mx.val - mn.val, st.insert ((TMP) { b[tot], tot });
			son[tot][0] = mn.id, son[tot][1] = mx.id;
		}
		Dfs (tot, 1);
		return;
	}
	inline void Out () {
		puts ("NP-Hard solved");
		_for (i, 1, n) printf ("%lld ", ans[i]);
		puts ("");
		return;
	}
}

T4 校门外歪脖树上的鸽子

思路

看了题解好像会了,但是树剖写不动了。

好像很多学长也没有补这道题,我至少还口胡了一下,这方面还是强于他们的。

观察这个图:

211615121132021434567198181791011

假设我们对区间 \([3, 10]\) 操作,首先求出其 lca 然后割成左右链。

对于左链,我们从 \(2\) 节点开始不断向上爬,当前节点为左儿子时操作其兄弟节点,右链同理。手模一下,正确性显然。

这个可以使用树剖维护,链上要维护两个线段树。

另外上面这个图是直接从 graph editor 的 html 里粘过来的,按这位老哥的图画的。

posted @ 2023-07-23 10:08  K8He  阅读(86)  评论(0编辑  收藏  举报