「赛后总结」20230722 CSP 模拟赛
「赛后总结」20230722 CSP 模拟赛
点击查看目录
目录
- 「赛后总结」20230722 CSP 模拟赛
- 反思
- 题解
- T1 回文
- 思路
- 代码
- T2 快速排序
- 思路
- 代码
- T3 混乱邪恶
- 思路
- 代码
- T4 校门外歪脖树上的鸽子
- 思路
吓死我了我还以为 K8He 不更博了。
为啥前天模拟赛不写啊?
打过,没参加。
为啥昨天模拟赛不写啊?
一些原因没空打。

这个标签补全真的是过于抽象了。
反思
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 校门外歪脖树上的鸽子
思路
看了题解好像会了,但是树剖写不动了。
好像很多学长也没有补这道题,我至少还口胡了一下,这方面还是强于他们的。
观察这个图:
假设我们对区间 \([3, 10]\) 操作,首先求出其 lca 然后割成左右链。
对于左链,我们从 \(2\) 节点开始不断向上爬,当前节点为左儿子时操作其兄弟节点,右链同理。手模一下,正确性显然。
这个可以使用树剖维护,链上要维护两个线段树。
另外上面这个图是直接从 graph editor 的 html 里粘过来的,按这位老哥的图画的。