CSP-S 2019 题解(部分)& 游记(伪)
day 0 - 8:00 a.m. \text{day 0 - 8:00 a.m.} day 0 - 8:00 a.m.
GM: 欸童鞋们我们今天不学新知识点哦!
We: ヾ(✿゚▽゚)ノ好吔!
GM: ⋯ \cdots ⋯ 我们要考 CSP-S 2019 \text{CSP-S 2019} CSP-S 2019。
We: 鄵儞亇!
day 1 - 8:10 a.m. \text{day 1 - 8:10 a.m.} day 1 - 8:10 a.m.
一眼望去,啊,这就是传说中的 S \text S S 组难度吗?
T1 格雷码
心路历程
看题比做题久系列
好险,还好敲完了闲着没事自己乱造数据玩,玩出来一组hack
估分 100 pts \text{100 pts} 100 pts 吧。
Solution
题目大意:
给定 n n n 和 k k k ,求按指定规则生成的 n n n 位 0 ∼ 2 n − 1 0\sim 2^n-1 0∼2n−1 的二进制码中的第 k k k 个元素。规则如下:
对于长度为 2 n 2^n 2n 的 n n n 位二进制码序列, 0 ∼ 2 n − 1 − 1 0\sim 2^{n-1}-1 0∼2n−1−1 个元素为长度为 2 n − 1 2^{n-1} 2n−1 的序列做高位加上一个 0 0 0 的结果;
后面的元素为长度为 2 n − 1 2^{n-1} 2n−1 的二进制序列倒置后最高位加上一个 1 1 1 的结果。
1 1 1 位二进制码序列为 0 , 1 0,1 0,1。
数据范围: 1 ⩽ n ⩽ 64 , 0 ⩽ k < 2 n 1\leqslant n\leqslant 64,0\leqslant k<2^n 1⩽n⩽64,0⩽k<2n
不可能模拟啦~
众所周知,数据范围在 long long
规模的,基本上就是 log \log log 级的做法了。
这里提供一个码量较少的做法。(不要跟我扯那些大佬们的 Θ ( 1 ) \Theta(1) Θ(1) 做法)
对于 n n n 位二进制序列,我们简称 0 ∼ 2 n − 1 − 1 0\sim 2^{n-1}-1 0∼2n−1−1 这一段为前半段,剩余部分为后半段。
如果要求的 k k k 在前半段,那说明不是由 n − 1 n-1 n−1 位序列翻转最高位置 1 1 1 得到的,那么最高位就会被置 0 0 0,也就是说第 n n n 位为 0 0 0。
否则,最高位为 1 1 1, k k k 会变成 2 n − 1 − [ k − ( 2 n − 1 − 1 ) ] = 2 n − 1 − k 2^{n-1}-[k-(2^{n-1}-1)]=2^n-1-k 2n−1−[k−(2n−1−1)]=2n−1−k 。
然后就没了。
假代码
#include<cstdio>
#define int long long
int n,k;
signed main(){
scanf("%lld%lld",&n,&k);
while(n){
if(k>=(1<<n-1)){
putchar('1');
k=(1<<n)-k-1;
}
else putchar('0');
n--;
}
return 0;
}
你 以 为 这 题 就 这?
有两个问题。
-
题目中的 k < 2 n k<2^n k<2n,而 n n n 最多可以取到 64 64 64。
long long
的范围是 − 2 63 ∼ 2 63 − 1 -2^{63}\sim 2^{63}-1 −263∼263−1,而unsigned long long
才能达到 0 ∼ 2 64 − 1 0\sim 2^{64}-1 0∼264−1。所以 k k k 要用
unsigned long long
存。这样想,代码中的
1<<n-1
范围int
类型的结果,会直接溢出然后 UB \text{UB} UB。所以保险和节省码量起见,使用
const unsigned long long x
存储(unsigned long long)1
的值,然后将1<<n-1
替换为x<<n-1
。 -
注意到这一行语句:
k=(x<<n)-k-1;
数据出的好,可以卡住
x<<n
。(事实上第 20 20 20 个点确实卡了)x<<64
,ull
刚好溢出。直接自然溢出啥事没有所以只好把
x<<64
拆成(x<<63)+(x<<63)
。但是这并没有实质性地解决问题,虽然两个加数都没有爆,但它们之间的和却挂了。
我们开心地发现后面做了一个减法,于是我们完全可以先 − - − 后 + + +。
真代码
#include<cstdio>
#define int unsigned long long
const int x=1;
int n,k;
signed main(){
scanf("%llu%llu",&n,&k);
while(n){
if(k>=(x<<n-1)){
putchar('1');
k=(x<<n-1)+((x<<n-1)-1)-k;
}
else putchar('0');
n--;
}
return 0;
}
关于 hack:
问题一可以用 64 999999999
和 64 10000000000
hack,你会发现它们的值差的不是一点两点。。。
(题目中提到格雷码的一个性质:相邻两个数码恰好一位的值不同)
问题二,可以用 64 18446744073709551615
hack,标答是 1 1 1 后面全是 0 0 0 。
T2 括号树
心路历程
我鄵,T2难度骤增。
先打了个链的部分分,推广到树上就是正解了。
但是我的树形DP(or 树上DP?反正都差不多)传参传的是个长度为 n n n 的 stack
,应该会T。
(然后下来我就去跟 @Nefelibata 吹自己常数5e5)
链应该是 Θ ( n ) \Theta(n) Θ(n) 的,没问题,树有点险。
估分 55+rp pts \text{55+rp pts} 55+rp pts
Solution
先把一条链的情况打出来再说。
因为已确定根节点为 1 1 1 且 f i = i − 1 f_i=i-1 fi=i−1,所以 s i = S 1 ∼ S i s_i=S_1\sim S_i si=S1∼Si,其中 s s s 的含义题目中有给出, S S S 为给定括号串。
k i k_i ki 的含义就可简化为 S 1 ∼ S i S_1\sim S_i S1∼Si 中的合法子串个数。
相信各位都做过括号匹配之类的题目,这里我们也用一个栈处理。
每遇到一个 ( \texttt ( ( 就 push
进去,每遇到一个 ) \texttt ) ) 就判断栈内是否有 ( \texttt ( ( ,有就计算然后 pop
。
因为要统计从 S 1 ∼ S i S_1\sim S_i S1∼Si 所有合法括号串,所以定义一个 c n t cnt cnt 表示从 S 1 ∼ S i S_1\sim S_i S1∼Si 的合法括号串个数,若当前字符可以完成一个匹配,则 cnt+=以第i个元素结尾的合法子串个数
。
那么,这个“合法子串个数”应该怎样计算呢?
定义 l s t lst lst,表示以当前元素的上一个元素结尾的合法子串个数。
若当前元素是 ( \texttt ( ( ,把 l s t lst lst 丢进栈里,记录如果当前 ( \texttt ( ( 能被匹配,新增的合法括号串个数;
否则当前元素就和之前的合法子串挨不到一起了, l s t lst lst 清零。(突然飚方言)
如果当前元素可以完成一个匹配,则栈顶记录的数就是和当前匹配上的括号紧挨着的之前的合法子串个数,由于当前又新增了一个匹配,记录最新的 l s t lst lst 为栈顶元素 + 1 +1 +1,同时这也是当前的 k i k_i ki。此时 cnt+=lst
。
可得出以下代码:
//55pts 链
namespace Task1{
int lst,cnt;
inline void solve(void){
for(int i=1;i<=n;++i){
if(s[i]=='('){
stk.push(lst);
lst=0;
}
else if(stk.size()){
lst=stk.top()+1;
cnt+=lst;
stk.pop();
}
else lst=0;
ans^=cnt*i;
}
printf("%lld",ans);
return;
}
}
然后直接把这些搬到树上即可。
其中, c n t cnt cnt 不能混合起来记了,必须每个点继承一下自己父亲结点的 c n t cnt cnt,然后自个儿记自个儿的。
l s t lst lst 同理。
但是,由于作者清奇的思考方式与码风,她选择了把 c n t cnt cnt 改成数组, l s t lst lst 改成 dfs 时传参。。。
总之,因为dfs在匹配过程中可能出现新增的 ( \texttt ( ( 还没匹配完,留在栈里,或是后面一大堆 ) \texttt ) ),把之前的给匹配掉了,所以必须即时传参。
然后最后统一计算 cnt
值即可。
Code
差点被常数从 Θ ( n ) \Theta(n) Θ(n) 卡成 Θ ( n 2 ) \Theta(n^2) Θ(n