「赛后总结」20230722 CSP 模拟赛

K8He / 2023-07-23 / 原文

「赛后总结」20230722 CSP 模拟赛

点击查看目录

目录
  • 「赛后总结」20230722 CSP 模拟赛
    • 反思
    • 题解
      • T1 回文
        • 思路
        • 代码
      • T2 快速排序
        • 思路
        • 代码
      • T3 混乱邪恶
        • 思路
        • 代码
      • T4 校门外歪脖树上的鸽子
        • 思路

吓死我了我还以为 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 里粘过来的,按这位老哥的图画的。