CSP-S 2021 捡漏记

CSP-S2021 捡漏记

前传: CSP-S2020爆零记

Day 4-4

因为早上 8:008:00 线上模拟赛和升旗冲突和班主任起了矛盾, 结果九牛二虎之力提前来机房后, 尴尬地迎接 7:407:40 升完旗来机房的同学.

模拟赛打得不错, 但是身体有点吃不住了, 前一阵子 SD 来了寒潮, 衣服不够的我显然是受凉了, 气管一直不舒服, 半个多星期不见好, 害怕影响考试, 加上颈椎难受的一批, 所以当天晚上决定回家.

拍了片子发现是气管有问题, 肺没事, 吃点感冒药就可以, 查的血发现一个箭头都没有, 比正常人还健康.

晚上回家颓大废, 颈椎也没有受到晚自习的伤害.

Day 2-2

教练用我去年的亲身经历教导兄弟们要注意文件操作, 防止爆零.

Day 1-1

教练把前一天的注意事项原封不动又发了一遍.

群里有个头铁的老哥发了一个信息学的链接就被踢了, 具体原因懂的都懂.

机房里的哥们被要求想一个口号, 在集合拍照的时候要喊, 不知道是谁为了致敬去年的我创造的经典, 搞出一个充满恶意 (bushi) 的口号:

   人生在勤\ 志达天下\\ 亮剑精神\ 信息之光\\ 脚踏实地\ 勇争第一

最离谱的是, 他们自己也要喊, 我仿佛看见到那一天直接集体社死…

Day 00

学长纷纷用自己的经历现身说法, 更是有扶苏引经据典 (当然又是我), 告诉后人注意文件操作和一些别的事项.

同学都在中午离校了, 有些自驾去日照的哥们已经出发了. 我仍然在

Day 11

哥们通知 8:008:00 火车站集合, 因为当天晚上回来, 如果坐地铁就回不了家了, 火车站又不方便停车, 于是我妈决定骑摩托车送我.

我计算上午 7:307:30 出门完全没有问题, 但是我没有算到从出门到骑上车还有 10min10min 的损耗, 以至于我妈一路危险驾驶, 闯了一万个红灯, 我在后座叫得和傻逼一样, 因为实在是太惊悚了.

不知是路上的汽车都吓坏了, 还是摩托车太牛逼了, 一路上只有别人给我妈让路, 没有我妈给别人让路的.

最后竟然只用了 20min20min 就到了. 事后我妈深刻觉得这种不要本子的开法比较离谱.

到了以后见了同学又是一番你假我, 我假你. 我不胜其烦便去给学弟吹牛逼 (指去年的爆零经验).

真是个 OIer 听了都落泪的好故事, 所以非常令人满意, 我们学校今年没有因为文件操作丢分的人.

检票的时候, 我想到这篇博客(过长慎点)还有一道题没做完, 自己可是立下 Flag 要在 CSP 之前写完这篇博客的啊.

听别人说这题很难, 不建议我做, 还提醒我说需要二项式反演, 但是我觉得在车上没事干, 所以还是开题了.

上车后用热点开题, 因为知道了二项式反演, 所以自以为貌似想出了正解, 看题解果然想的没错, 然后把这个题写了.

交了一发 TLE, 下载数据发现自己的程序可以被一条链卡到 O(n3)O(n^3), 但是我之前这样写的树形背包都能过, 还有人证明了树上背包复杂度是 O(n2)O(n^2) (没错就是我, 在前面链接的那篇超长博客里面有).

但是眼看已经快到了, 没时间纠结了, 一发启发式合并上去就过了, 虽然可能是 O(n2logn)O(n^2\log n), 但是好在没卡 log\log.

事后才发现, 我之前是枚举总共选 ii 个, 特定子树选 jj 个, 这样在子树大小很大的时候, iijj 都是 O(n)O(n), 单次转移 O(n2)O(n^2) 不可均摊. 正宗的树形背包是枚举除特定子树以外选 ii 个, 特定子树选 jj 个, 这样复杂度就对了.

随后在火车上怒发博客.

发完以后没事干, 便去读自己去年的又一经典著作, 虽然字里行间的都是菜, 但是确实有那么一点用.

接下来听说老师家长不让进校园, 大伙欣喜若狂, 这样那个羞耻的口号喊不喊就无所谓了.

在校门口大家气氛愉快地乳了一遍去年我的爆零就进场了.

场上

进场前我寻找二号考场, 准考证上写的是在一楼, 我们从大门进来之后直接就是二楼, 找楼梯发现都是往上的, 还好有个好心的小姐姐告诉我要先上楼再下楼, 路上我在心中默默为设计师的奇妙思路击节叫好.

在场外放手机, 在窗台上放怕掉了, 发现走廊边上的瓷砖是黑的, 因为手机正反面都黢黑, 所以可能别人一脚就给送走了, 好不容易找到一个哥们带了白手机, 把他的手机扣在我手机上面才放心, 这样干碎我手机少说也要两脚.

从窗外往里看, 大屏幕上写的是这次的文件规范, 发现实例 SD-00001 张三 中间有明显的空格, 但是后面明文说不加空格, 给我们整不会了, 所以我告诉学弟可以把自己的文件夹复制一遍, 一个加空格, 一个不加, 并排放在 D 盘, 让监考员按需取用.

往考场偷瞄发现这次的键盘貌似比上次的强一些, 至少键程不会和上次一样像笔记本键盘那么短了, 而且貌似排列上比上次合理了一些, 没有那么容易误触了, 上了场才发现合理个鸡巴, 原来的绝对值那个符号, 应该在主键盘最右边, Enter 上方, 结果他给整了个 L 形的回车, 然后 | 被挤到了左边, 然后我每次写 \n 都会换个行, 然后打一个 n.

试机

先调编译选项, 首先问候山东组委会, CCF 官方开 C++14, 你本地的破编译器连个 C++14 都开不了, 还在 C 盘放了个更古老的 MinGW, 索性开了 C++11, 凑合用吧, 结果又来个当头一棒没法开栈, 我一度怀疑我 --stack=512000000 是拼错了, 结果出来才发现是都没法开, 只能默默祈祷这次没有大递归.

感觉去年试机的时间极短, 对拍器没写完就发文件了, 所以今天我特意嘱咐学弟要节省时间, 试机时间非常短, 结果今年有哥们试机的时候实在没事干线段树都调完了.

我则是岁月静好, 把所有的缺省源都写了, 甚至像我一直用的缺省源一样, int, unsigned 分别写了快读. 然后用 scanf() 拍我的快读.

据说 (心酸的 “据说”) 去年 NOIp 的时候试机结束要求互删文件, 但是你如果把代码放到缺省源的设置里, 就可以合法保存.

然后发现电脑的表不准, 还好带了手表, 并且, 还好电脑的表快, 如果表慢可能就会出问题.

开题

输了密码后, 打开文件, 沉浸在没法开栈的疑惑中, 正尝试使用 C 盘的离谱编译器, 这时候才发现自己不会使, 对嘲笑在机房卷题用命令行编译的老哥一Iris一道个歉, 是我大意了.

先是看了半天题面前面的东西:

嗯不错 512MB512MB 很慷慨

1s1s 1s1s 1s1s 3s3s 还算正常

果然是 O2-O2 赶紧看看我本题开了没有

草, 我还是不甘心啊, 为什么开不了栈啊啊啊啊

不错, 少爷机也不是太拉

然后怀着去年被 julian 杀的忐忑心情开了 A 题.

Airport

首先思考了一下对于只考虑国内, 是否满足飞机数量随廊桥增加而增加.

然后在求证的过程中发现一个性质, 就是无论廊桥数量如何, 对于有 ii 个廊桥的时候, 被满足的飞机停靠的廊桥编号在有更多廊桥时不会编号.

换个说法, 就是如果在某种情况下, 某飞机停到 ii 号廊桥上, 那么无论廊桥如何增加, 它仍然会停到 ii 号廊桥上.

那么我们就先考虑国内, 假设一开始廊桥无限, 给每个飞机标号, 记录它们停靠的廊桥编号.

很容易得到, 有 ii 个廊桥时, 国内飞机被满足的数量就是标号 i\leq i 的飞机数量.

如果求出国内选 ii 个的满足数量, 还有国外选 ii 个的满足数量, 那么 O(n)O(n) 枚举方案就可以得到答案了.

不是吧, 才开几分钟, 笔都没捂热乎, 就切掉了 T1 吗? 我赶紧读题, 又思考了实现的细节, 发现果然没什么问题, 于是开始打代码.

那么如何求呢? 很显然, 只要模拟题目的贪心过程即可.

我们一开始将国内飞机按到达时间从小到大排序, 把所有廊桥都插入一个小根堆 (只要廊桥数量不小于飞机, 就可以认为廊桥无限). 然后按飞机顺序, 每次取堆顶把飞机停进去, 弹出堆顶即可.

接下来考虑模拟飞机离开, 我们把飞机时间表离散化, 用一万个 vector 维护每个时间离开的飞机, 这样每次枚举时间, 模拟时间推移, 然后将这个时候来的飞机标号赋值, 将这次插入的飞机的结束时间对应的 vecotor 插入这个飞机的编号, 然后把这个时候离开的飞机对应的廊桥重新插入堆里, 相当于释放了这个廊桥的位置.

分别用两个数组存两种飞机在不同数量廊桥的情况下的满足数量, 然后枚举 ii 个廊桥给国内, nin - i 个给国外, 求满足数量之和的最大值即可.

然后测了样例, 从小测到大畅通无阻, 一看表 (我当时已经忘了监考员提醒电脑的表快了) 才用了 45min45min 就过了大样例了 (实际应该是 30min30min, 包括一开始开栈和读表格的时间). 心想, 这个题反正我也没有暴力, 懒得拍了, 我看这个大样例挺强的, 我趁着又劲先开题吧, 于是没有管它, 直接开题去了.

接下来是考场代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include<cstdio>
#include<cstring>
#include<set>
#include<algorithm>
#include<string>
#include<map>
#include<bitset>
#include<vector>
#include<queue>
#include<iostream>
using namespace std;
inline unsigned RD() {
unsigned Rtmp(0);
char Rch(getchar());
while ((Rch < '0') || (Rch > '9')) Rch = getchar();
while ((Rch >= '0') && (Rch <= '9')) Rtmp = (Rtmp << 3) + (Rtmp << 1) + Rch - '0', Rch = getchar();
return Rtmp;
}
inline int RDsg() {
int Rtmp(0), Rsig(1);
char Rch(getchar());
while (((Rch < '0') || (Rch > '9')) && (Rch ^ '-')) Rch = getchar();
if(Rch == '-') Rsig = -1, Rch = getchar();
while ((Rch >= '0') && (Rch <= '9')) Rtmp = (Rtmp << 3) + (Rtmp << 1) + Rch - '0', Rch = getchar();
return Rtmp * Rsig;
}
inline unsigned Rand(unsigned L, unsigned R) {
return L + (rand() * rand() % (R - L + 1));
}
struct Bri {
unsigned Fr, To, Bel;
inline const char operator< (const Bri &x) const {return Fr < x.Fr;}
}P[100005];
priority_queue<unsigned> Br;
vector<unsigned> Dec[200005];
unsigned a[200005], n, m1, m2;
unsigned A, B, C, D;
unsigned Sum1[100005], Sum2[100005];
unsigned Cnt(0), Ans(0), Tmp(0);
inline void Calc(unsigned m, unsigned *Sum) {
Cnt = 0;
for (unsigned i(1); i <= m; ++i) {
P[i].Fr = a[++Cnt] = RD();
P[i].To = a[++Cnt] = RD();
}
sort(a + 1, a + Cnt + 1);
sort(P + 1, P + m + 1);
Cnt = unique(a + 1, a + Cnt + 1) - a;
for (unsigned i(1); i <= m; ++i) {
P[i].Fr = lower_bound(a + 1, a + Cnt, P[i].Fr) - a;
P[i].To = lower_bound(a + 1, a + Cnt, P[i].To) - a;
}
// for (unsigned i(1); i <= m; ++i) printf("%u [%u, %u]\n", i, P[i].Fr, P[i].To);
for (unsigned i(1); i <= m; ++i) Br.push(m + 1 - i);
for (unsigned i(1), j(1); i <= Cnt; ++i) {
while(P[j].Fr == i) {
P[j].Bel = m - Br.top() + 1;
Dec[P[j].To].push_back(Br.top());
Br.pop(), ++j;
}
for (auto k:Dec[i]) {
Br.push(k);
}
}
// for (unsigned i(1); i <= m1; ++i) {
// printf("%u Bel %u\n", i, P[i].Bel);
// }
for (unsigned i(1); i <= m; ++i) ++Sum[P[i].Bel];
for (unsigned i(1); i <= n; ++i) Sum[i] += Sum[i - 1];
memset(P, 0, (m + 2) * sizeof(Bri));
for (unsigned i(1); i <= Cnt; ++i) Dec[i].clear();
while(Br.size()) Br.pop();
}
signed main() {
freopen("airport.in", "r", stdin);
freopen("airport.out", "w", stdout);
n = RD(), m1 = RD(), m2 = RD();
Calc(m1, Sum1);
Calc(m2, Sum2);
for (unsigned i(0); i <= n; ++i) {
Ans = max(Ans, Sum1[i] + Sum2[n - i]);
}
printf("%u\n", Ans);
return 0;
}
/*
3 5 4
1 5
3 8
6 10
9 14
13 18
2 11
4 15
7 17
12 16

7

2 4 6
20 30
40 50
21 22
41 42
1 19
2 18
3 4
5 6
7 8
9 10

4
*/

Bracket

这个时候我洋洋得意, 便来看 T2, 看完小 w 遇到的问题, 大骂出题人这次题怎么这么水, 正要写代码, 发现原来小 w 觉得这个题水了, 心中一个 “危” 字, 然后就看见小 w 迫害小 c 的全过程, 真是小 w 一笑, 生死难料啊.

仔细想想这个题貌似也不难, 在小 w 遇到的问题上加一维状态就好了, 一看数据, O(n3)O(n^3) 能过. 得! 写代码!

这个可怜的孩子还没发现他读错题了.

设计状态 fi,j,kf_{i, j, k} 表示前 ii 位, 左括号减右括号数量为 jj, 末尾有 kk* 的方案数.

下面的方程中, mm 表示最多连续的 * 数量:

fi,j,k=fi1,j,k1(a[i]=)fi,j,k=0(a[i]=(or)),(k>1)fi,j,0=k=0mfi1,j1,k(a[i]=()fi,j,0=k=0mfi1,j+1,k(a[i]=))fi,j,k=fi1,j,k1(a[i]=?),(k>1)fi,j,0=k=0mfi1,j1,k+fi1,j+1,k(a[i]=?)\begin{aligned} f_{i, j, k} &= f_{i - 1, j, k - 1} (a[i] = '*')\\ f_{i, j, k} &= 0 (a[i] = '(' or ')'), (k > 1)\\ f_{i, j, 0} &= \sum_{k = 0}^{m} f_{i - 1, j - 1, k} (a[i] = '(')\\ f_{i, j, 0} &= \sum_{k = 0}^{m} f_{i - 1, j + 1, k} (a[i] = ')')\\ f_{i, j, k} &= f_{i - 1, j, k - 1} (a[i] = '?'), (k > 1)\\ f_{i, j, 0} &= \sum_{k = 0}^{m} f_{i - 1, j - 1, k} + f_{i - 1, j + 1, k} (a[i] = '?')\\ \end{aligned}

然后大概花了十几分钟把转移方程在纸上写出来, 然后花了十几分钟写出来, 发现死活过不了小样例, 输出了整个 DP 数组都没发现哪里有问题, 然后手玩了样例发现自己的答案是 66, 甚至用组合数学证明了答案就是 66.

看了样例解释后发现手玩的情况有一种不在解释里, 心想, 坏了读错题了, 然后就是当头一棒, 发现我不光读错题了, 而且读对之后的题我不会.

我之前的 DP 会把形如 (*()*) 的序列统计进去, 所以导致答案错误.

这个时候我想了好久, 先是考虑在原先的答案中去除不合法的, 但是显然这更加困难, 然后又想在转移上下功夫, 但是事实证明这也是不可行的.

按考场电脑的时间, 这时我认为已经过去一个半小时了.

于是推倒重来, 因为我发现了合法序列的可合并性, 所以把区间合并起来的做法自然就是区间 DP.

设计状态 fLen,if_{Len, i} 表示区间 [i,i+Len1][i, i + Len - 1] 是合法序列的情况.

发现合法序列左端是 (, 右端是 )

对于 a[i]a[i] 不可能是 (a[i+Len1]a[i + Len - 1] 不可能是 ) 的情况, fLen,i=0f_{Len, i} = 0.

然后转移是

fLen,i=(len=1Len2(flen,i+1(j=0Lenlen2fLenlenj2,i+1+len+j)))+(len=1Len2fLenlen2,i+len+1)+(len=1flen,i(j=0Lenlen2fLenlenj,i+len+j))\begin{aligned} f_{Len, i} &= (\sum_{len = 1}^{Len - 2} (f_{len, i + 1}(\sum_{j = 0}^{Len - len - 2}f_{Len - len - j - 2, i + 1 + len + j})))\\ &+(\sum_{len = 1}^{Len - 2} f_{Len - len - 2, i + len + 1})\\ &+(\sum_{len = 1} f_{len, i}(\sum_{j = 0}^{Len - len - 2} f_{Len - len - j, i + len + j})) \end{aligned}

第一行指开区间 (i,i+Len1)(i, i + Len - 1) 为中间以若干 * 间隔的合法序列, iii+Len1i + Len - 1 是一对括号, 将序列合并成一个长度 LenLen 的合法序列.

第二行指开区间 (i,i+Len1)(i, i + Len - 1) 是合法序列左边加若干个 *, 然后在开区间外面加一对括号变成合法的序列. (右边有 * 和没有 * 的情况在第一行的转移中统计过了)

第三行指直接由两段合法序列合并, 中间若干 * 间隔得到的序列.

三个转移都要满足题目中 kk 的限制和输入数据中每个位是否能是 * 的限制.

问题出在第三个转移上.

因为它会算重, ()(), 会被统计两遍.

当我测试了第二个样例然后发现这个问题的时候, 我心态受到很大的影响, 时间已经两个小时了 (考场机器时间).

吸取去年的教训, 我去看 T3, 发现暴力拿 2828' 貌似非常简单, 努努力甚至能剪枝上 4040', 加上我的 T1 已经过了大样例, 所以迅速稳定了下来, 进行本题的第三次迭代.

我修改了 ff 的转移, 使得 fLen,if_{Len, i} 表示 [i,i+Len1][i, i + Len - 1] 是被一对括号括起来的, 之后的题解里我称之为 “完全合法序列” 的情况数.

然后写了一个关于 gg 的线性 DP 来尝试防止算重.

设计 gi,jg_{i, j} 表示前 ii 位去掉末尾 jj* 是合法序列的方案数.

gi,0=0 (a[i]=(or)gi,j=0 (a[i]=(or))(j>0)gi,j=gi1,j1 (a[i]=or?)(j>0)gi,0=j=0i2len=0j (gj1,lenfij+1,j)(a[i]=)or?)\begin{aligned} g_{i, 0} &= 0~(a[i] = '(' or '*')\\ g_{i, j} &= 0~(a[i] = '(' or ')') (j > 0)\\ g_{i, j} &= g_{i - 1, j - 1}~(a[i] = '*' or '?') (j > 0)\\ g_{i, 0} &= \sum_{j = 0}^{i - 2} \sum_{len = 0}^{j}~(g_{j - 1, len}f_{i - j + 1, j}) (a[i] = ')' or '?')\\ \end{aligned}

最后答案就是 gn,0g_{n, 0}.

但是我发现这样改了我的 ff 就没法转移了, 因为仅凭区间为完全我不能处理题目中 (S), (SA), (AS) 的情况, 我需要区间为合法序列的情况数.

令人崩溃的是, 我将 gg 的 DP 和 ff 的新 DP 写完才发现自己这些状态没算, 值得庆幸的是, 这次迭代的时间消耗不足半小时, 而且因为立马就想到了解决方案, 所以我无比自信至少能写出至少 6565'.

然后接下来的做法和原理参见我写的题解, 在这里就省略了, 着重讲一下心路历程.

虽然没做多少 DP 题, (真的没在假, 机房很多大佬做的区间 DP 可能比我做过的所有 DP 加起来都多), 但是我做 DP 的流氓行为就是要求什么, 就加什么状态, 所以我就把 gg 的 DP, 改成了区间 DP.

gLen,ig_{Len, i} 表示区间 [i,i+Len1][i, i + Len - 1] 是合法区间的方案数.

写方程的时候, 我就发现新 DP 是 O(n4)O(n^4) 的, 但是好在本题给了 6565'O(n4)O(n^4), 所以不亏, 毕竟我也没指望我能得 200200' 甚至以上.

更幸运的是, 根据之前场上多次把暴力优化到正解的经验, DP 的特点就是, 只要设计出答案正确的算法, 对着暴力算法的代码, 把 OO 里面去掉几个 nn 是简单得很的, 况且暴力的代码还能用来拍, 所以我当时的策略就是, 别管时间了, 先保证正确.

又过了不到半个小时, 过了前两个样例, 怀着激动的心测第三个样例, 飞快地过了, 又测第四个, 10s10s 之内过了.

这个时候我脑子里已经浮现出之前我拿不得分的代码和 3030' 代码拍, 拿 3030'4545' 拍, 然后七八次优化迭代到正解的情景.

随后先后想了前缀和优化和数据结构优化, 然后惊喜地发现原来 gg 直接加个 0/1/2 分别表示区间是合法区间, 区间是合法区间左边加 *, 合法区间右边加 * 的方案数, 就可以避免枚举 * 的数量, 优化掉一维, 写代码的时候发现甚至可以舍弃 2 的情况, 只考虑 0/1.

激动的心, 颤抖的手, 十几分钟就过了大样例, 结束了第五个版本的编写. 因为开了加时间戳保存历史版本的自动保存, 文件夹里这个时候已经存了一百多个文件了. 一共有四个大版本:

一开始的线性假 DP bracket_Fake.cpp

接下来的重复统计区间 DP, 被中途改成区间 DP 加线性 DP 胎死腹中的版本 bracket_Range_Fake.cpp

6565'O(n4)O(n^4) 区间 DP 加区间 DP bracket_n4.cpp

O(n3)O(n^3) 最终版区间 DP 加带 0/1 的区间 DP bracket.cpp

一副壮烈的景象.

看了看机器时间, 更加壮烈, 还有一个多小时, 赶紧 T3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include<cstdio>
#include<cstring>
#include<set>
#include<algorithm>
#include<string>
#include<map>
#include<bitset>
#include<vector>
#include<queue>
#include<iostream>
using namespace std;
inline unsigned RD() {
unsigned Rtmp(0);
char Rch(getchar());
while ((Rch < '0') || (Rch > '9')) Rch = getchar();
while ((Rch >= '0') && (Rch <= '9')) Rtmp = (Rtmp << 3) + (Rtmp << 1) + Rch - '0', Rch = getchar();
return Rtmp;
}
inline int RDsg() {
int Rtmp(0), Rsig(1);
char Rch(getchar());
while (((Rch < '0') || (Rch > '9')) && (Rch ^ '-')) Rch = getchar();
if(Rch == '-') Rsig = -1, Rch = getchar();
while ((Rch >= '0') && (Rch <= '9')) Rtmp = (Rtmp << 3) + (Rtmp << 1) + Rch - '0', Rch = getchar();
return Rtmp * Rsig;
}
unsigned long long Mod(1000000007);
unsigned long long f[505][505], g[505][505][2];
unsigned n, m;
unsigned A, B, C, D;
unsigned Cnt(0), Ans(0), Tmp(0);
char a[505];
signed main() {
freopen("bracket.in", "r", stdin);
freopen("bracket.out", "w", stdout);
n = RD(), m = RD();
scanf("%s", a + 1);
for (unsigned i(n); i; --i) g[0][i][0] = f[0][i] = 1;
for (unsigned Len(2); Len <= n; ++Len) {
for (unsigned i(n - Len + 1); i; --i) {
for (unsigned len(1); len < Len && len <= m; ++len) {
if((a[i + Len - len] == '(') || (a[i + Len - len] == ')')) break;
g[Len][i][1] += g[Len - len][i][0];
if(g[Len][i][1] >= Mod) g[Len][i][1] -= Mod;
}
if((a[i] == '*') || (a[i] == ')') || (a[i + Len - 1] == '*') || (a[i + Len - 1] == '(')) {
continue;
}
unsigned Tol(min(Len - 2, m));
f[Len][i] = g[Len - 2][i + 1][0] + g[Len - 2][i + 1][1];
if(f[Len][i] >= Mod) f[Len][i] -= Mod;
for (unsigned len(1); len <= m && len + 2 <= Len; ++len) {
if((a[i + len] == '(') || (a[i + len] == ')')) break;
f[Len][i] += g[Len - 2 - len][i + len + 1][0];
if(f[Len][i] >= Mod) f[Len][i] -= Mod;
}
g[Len][i][0] = f[Len][i];
for (unsigned len(2); len + 2 <= Len; ++len) {
g[Len][i][0] = (g[Len][i][0] + g[len][i][1] * f[Len - len][i + len]) % Mod;
}
for (unsigned len(2); len + 2 <= Len; ++len) {
g[Len][i][0] = (g[Len][i][0] + g[len][i][0] * f[Len - len][i + len]) % Mod;
}
// printf("[%u, %u] %llu %llu %llu\n", i, i + Len - 1, f[Len][i], g[Len][i][0], g[Len][i][1]);
}
}
printf("%llu\n", g[n][1][0]);
return 0;
}
/*
546949722

4 2
????

7 3
(*??*??

5

10 2
???(*??(?)
*/

Palin

这个题在我 T2 中途情绪波动的时候给了我自信. 所以感谢 T3, 让我能做出 T2.

当时写出 T2 的 6565' 时还考虑 T3 暴力只有 2828', 我优化出一个 O(n3)O(n^3)3535' 不比这个香. 所以过了 T2 才去写 T3.

但是我也是有梦想的, 首先是去想结论, 发现 bb 的前半段的元素是 aa 的前缀和后缀, bb 后半段的元素是 aa 的中间一段连续的区间, 这一下子让我想到一道之前打过的模拟赛的题.

给一个 nn 的排列, 如果其中一个长度为 ii 的子串是 ii 的排列, 那么说这个排列是一个 "ii 排列", 求对于哪些 ii, 给定排列是 ii 排列.

当时我认为是看输入序列是否是 nn 排列, 从而判断可行性的, 但是样例中的无解情况都是 nn 排列, 于是认为正解和这个两端中间如何如何的性质无关, 决策失误, 于是在心中把这个性质扔掉了.

然后看部分分的特殊性质, 发现完全不知所云, 所以放弃拿那 88', 当天晚上在去火车站的大巴上, 学弟告诉我貌似全输出 LL 就能得, 我追悔莫及, 于是一怒之下用 1 1 2 2 把他 Hack 了.

但是离谱的是, 我直到 Day 10Day~10, 也就是 11-01-2021 才把这个性质看懂, 也发现实际上是 1212', 原来它所谓 删除 的意义和题意中 移除 不同, 这个删除就是在序列中找一对相邻的相等元素删除, 然后两边的序列再连接起来, 所以这 1212' 的算法应该是递归构造.

然后我看了看还有 1h1h, 又到了每次模拟赛中最热血的一个小时, “热血暴力时间”.

我写了写暴力, 发现时间复杂度从 O(T(n4n))O(T(n4^n)) 变成了 O(T(n2n))O(T(n2^n)), 非常开心, 并且我的算法是经过了大量剪枝的, 所以有希望冲一波 4040'. 样例过得很曲折, 因为我需要清空数组, 但是因为数组规模大, 所以必须部分清空, 这就带来很多不清空的问题, 以至于我把样例复制几遍就过不得了. 然后还发现自己 DFS 过程中还有问题, 所以又花心思去改 DFS.

费了点劲过了样例, 去测第二个样例, 发现 4040' 的规模跑得飞快, 内心高兴, 但是一看答案和我的输出, 高兴的心情几秒钟就结束了, 很快啊!

发现不一样后, 我连忙把清空数组改成全部清空, 也没用, 百思不得其解, 又看我的 DFS, 怎么看怎么顺眼. 直到我用我输出的方案手玩第二个样例的第五个数据, 发现自己的方案不光合法, 字典序还比答案小的时候, 我都不知道我把题读错了.

手玩的时候看见机器时间还有 30min30min 结束的我慌得一批.

我一直认为输出使 bb 的字典序最小的方案, 谁成想这个题简单粗暴地要求输出的字典序最小, 我又乐了, 一边乐一边改代码, 然后过了大样例.

接下来是考场代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include<cstdio>
#include<cstring>
#include<set>
#include<algorithm>
#include<string>
#include<map>
#include<bitset>
#include<vector>
#include<queue>
#include<iostream>
using namespace std;
inline unsigned RD() {
unsigned Rtmp(0);
char Rch(getchar());
while ((Rch < '0') || (Rch > '9')) Rch = getchar();
while ((Rch >= '0') && (Rch <= '9')) Rtmp = (Rtmp << 3) + (Rtmp << 1) + Rch - '0', Rch = getchar();
return Rtmp;
}
inline int RDsg() {
int Rtmp(0), Rsig(1);
char Rch(getchar());
while (((Rch < '0') || (Rch > '9')) && (Rch ^ '-')) Rch = getchar();
if(Rch == '-') Rsig = -1, Rch = getchar();
while ((Rch >= '0') && (Rch <= '9')) Rtmp = (Rtmp << 3) + (Rtmp << 1) + Rch - '0', Rch = getchar();
return Rtmp * Rsig;
}
unsigned n, m, a[20005], T;
unsigned Ans[20005], Now[20005];
unsigned Pos[10005];
unsigned L, R;
char Way[20005], AW[20005], Nope(0);
inline void Clr() {
memset(Ans, 0x3f, sizeof(Ans));
memset(Pos, 0,sizeof(Pos));
memset(Now, 0,sizeof(Now));
memset(AW, 0x3f,sizeof(AW));
memset(Way, 0,sizeof(Way));
Nope = 0, L = 1;
R = m = ((n = RD()) << 1);
}
inline void DFS(unsigned Dep) {
// if((Way[1] && (!(Way[2] | Way[3] | Way[4] | Way[5]))) && (Way[6] && (!(Way[7] | Way[8] | Way[9] | Way[10])))) {
// printf("DFS %u\n", Dep);
// for (unsigned i(1); i < Dep; ++i) printf("%u ", Now[i]);
// putchar(0x0A);
// for (unsigned i(1); i < Dep; ++i) printf("%u ", Way[i]);
// putchar(0x0A);
// }
if(Dep > m) {
Nope = 1;
char Flg(0);
for (unsigned i(1); i <= n; ++i) {
if(AW[i] < Way[i]) break;
if(AW[i] > Way[i]) {Flg = 1; break;}
}
if(Flg) {
memcpy(Ans, Now, (m + 1) << 2);
memcpy(AW, Way, m + 1);
}
return;
}
if(Dep > n) {
if(m - Dep == Pos[a[L]] - 1) {
Now[Dep] = a[L], Way[Dep] = 0, ++L;
DFS(Dep + 1);
--L;
}
if(m - Dep == Pos[a[R]] - 1) {
Now[Dep] = a[R], Way[Dep] = 1, --R;
DFS(Dep + 1);
++R;
}
} else {
if(!Pos[a[L]]) {
Now[Dep] = a[L], Way[Dep] = 0, Pos[a[L]] = Dep, ++L;
DFS(Dep + 1);
Pos[a[--L]] = 0;
}
if(!Pos[a[R]]) {
Now[Dep] = a[R], Way[Dep] = 1, Pos[a[R]] = Dep, --R;
DFS(Dep + 1);
Pos[a[++R]] = 0;
}
}
}
signed main() {
freopen("palin.in", "r", stdin);
freopen("palin.out", "w", stdout);
T = RD();
for (unsigned t(1); t <= T; ++t) {
Clr();
for (unsigned i(1); i <= m; ++i) a[i] = RD();
DFS(1);
if(Nope) {
for (unsigned i(1); i <= m; ++i) putchar(AW[i] ? 'R' : 'L');
putchar(0x0A);
} else {
printf("-1\n");
}
}
return 0;
}
/*
RLLLLRLLLLRLLLLLLLLLLRLRLLRRRLLRRLLLLLLL
2
20
10 16 7 13 18 19 14 15 20 9 2 12 17 4 8 1 11 11 8 17 12 5 15 18 3 13 7 16 10 6 19 14 20 9 2 4 1 5 3 6
5
4 1 2 4 5 3 1 2 3 5
3
3 2 1 2 1 3
*/

但是直到我 Day 4Day~4, 也就是 10-26-2021 重新审视我的算法, 我才发现 O(T(n2n))O(T(n2^n)) 也是太愚了, 我完全可以 O(T(2n))O(T(2^n)) 做, 因为每次我搜到一个答案, DFS 本身就能保证它的字典序最小, 所以我就不用每次找到合法方案都判断是否能更新答案了, 所以找到合法方案直接返回就可以.

然后感谢JJK的抛砖引玉 (bushi), 我终于明白一开始被 pass 掉的性质的意义, 可以进一步剪枝 DFS, 人们可以证明它的复杂度是线性的, 这便是正解了.

在重新读题, 找出问题, 修改代码之后, 我心满意足地整理好 T3 的代码和文件操作, 听到监考员说 “离考试结束还有半个小时”, 心里想这监考员怕是表有问题, 我刚刚看的时候就已经五点半了. 最后看了看自己的表才意识到是机器的时间有问题.

Traffic

其实 T2 心态崩的时候就去看 T4 了, 发现题太麻烦懒得读, 而且貌似暴力不好写, 加上还有半小时, 不如去对拍和静态查错, 见好就收, 遂弃之.

对拍

T2 的数据好造, 并且已经有现成的暴力, 写出对拍先拍小的, 可手玩的, 几千组没有问题, 然后拍大的, 6565' 范围内的, 发现跑得飞快, 然后改成 100100' 范围的, 发现随机数据下 O(n4)O(n^4) 仍然飞快.

后来有人指出我最后半小时对拍, 就算拍出错来也调不动了, 这确实很危险, 但是我当时过了大样例春风得意, 选择相信大样例, 加上没想到 T3 会耗这么多时间, 所以有点慌了, 以至于对拍被冷落了.

至于 T1 和 T3, 数据不好造, 暴力不好写, 所以把大样例文件名改成没有数字的版本 (防止去年经典重现), 反复测了几遍, 确认无误后, 放到提交的子文件夹里.

罚坐

最后 15min15min 一遍遍地查代码, 看前面的注意事项, 背单词 (指这次的题目拼写)

无聊到把头文件里的 cstdlib 删了, 然后把代码反复整理. 最后发现 T2 的 100100' 数据拍了几千份, 在监考员的鼓励下, 一度有提前交卷的打算.

最后蜜汁自信没有放空格情况不同的两个文件夹, 最后也没有什么问题.

出场

学弟们大众分是 228228', 非常令人震惊, 我本来觉得 240240' 牛逼炸了, 没想到刚过大众分, 直接心态崩掉.

然后遇到了一些同学, 交流算法发现 T1 大部分人用的三分, 而我全程没有发现三分性. 所以我十分害怕自己的结论假了, 随后给好几个同学胡, 貌似没有被 Hack, 还有一个和我一样的, 心里慢慢有了底.

Day 22

起床看见扶苏的问题还热乎, 准备抢除了 std 的一血, AC 之后打算贡献一篇题解.

当天晚上我从家回学校, 在地铁上得到了山东的程序, 然后远程拜托JJK帮我用洛谷测了, 随着对面测一个题给我发一条消息, “T1A了”, “T2A了”, "T3 4040", “一分没挂”, “恭喜”, 我和我妈都很惊讶.

随后就是回到学校在 HydroOJ 上测得分数在 SD 能混个 RK 1818, 感觉也就这么个情况, 去年这个分都前 1010 了, 可能是今年题比去年水吧.

Day 44

扶苏的问题写了题解, 然后把 T2 题解的题解一并写了.

Day 77

回家打 CF, 发现 E 比 D 简单, 怒切计数.

Day 88

和我妈在街上被通知可以查分了, 一看群里不得了, 三分盆满钵满, 我直接愤怒.

还好一分没挂, 虽然是很正常的事, 因为我的代码在哪个 OJ 上都是一样的分, 不存在什么意外之喜, 但是查官方分数还是很刺激的.

Day 1111

游记写完了, 本贴完结