CSP-J/S 2020 题解

考完后花了大概两天时间把除了 tg T4 以外的所有题全部都做出来了,其实老实说,没有什么不会的算法或是数据结构(包括 T4?),但思维难度确实还是有一点大,像我这种平时不怎么练思维,光会写一些数据结构模板题或是刷水题的蒟蒻还是很难拿到高分的,还有就是一些细节很多,没注意到就很容易凉凉,所以总体来说虽然 tg 把两天改成了一天,但确实还是有区分度的把我这种菜鸡区分出来了,pj 还是可以,题目难度可能比去年稍稍难一点,但也不算很难,还是在意料之中。

题面

PJ

pj 主要是 T3 难想一点,然后 T4 是个 DP(居然没有图论!?),其他两个题就比较常规了,T1 还是 sb 签到题,T2 暴力的话很好写,但必须考虑优化,跟去年 T2 一样。

T1 优秀的拆分

签到题,首先看到若干个不同的 22 的正整数次幂之和可以很快想到跟位运算有关,看完题面之后果然没错。

由于必须要是正整数次幂,那就代表 nn 的二进制下的第 00 位不能为 11,也就是说只要是奇数就输出 1-1,否则就一位一位找,如果某一位为 11,就拿个东西记录一下,最后倒序输出即可。

我考试的时候用的是 lowbit,如果不会的话请左转这篇文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<bits/stdc++.h>
#define lowbit(x) (x&(-x))
using namespace std;
int n,k,ans[100];
int main()
{
scanf("%d",&n);
if(n&1)
printf("-1");
else
{
for(;n;n-=lowbit(n))
ans[++k]=lowbit(n);
for(int i=k;i>=1;i--)
printf("%d ",ans[i]);
}
return 0;
}

T2 直播获奖

一个简单的求第 K 大值的问题,方法很多,比较常见的是用个桶来记录每种成绩的数量,然后从后往前找第 K 个就好了,但时间复杂度为 O(600n)O(600n)(好吧这个表述可能有一点点问题,但这个常数是真的不能忽略啊喂),10510^5 的数据有可能会被卡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,w,a[N],num[1010];
int main()
{
scanf("%d%d",&n,&w);
for(int i=1;i<=n;i++)
{
int t=max(1,i*w/100);
scanf("%d",&a[i]);
num[a[i]]++;
for(int j=600,k=0;j>=0;j--)
if(k+num[j]>=t)
{
printf("%d ",j);
break;
}
else
k+=num[j];
}
return 0;
}

我们还有另外的方法来做这道题,考虑一个暴力算法,即每次对序列进行排序,然后输出第 K 项,我们发现插入排序可以很好地对序列进行维护,但美中不足的是它的单次复杂度还是 O(n)O(n),这是个大问题,所以,我们可以尝试用一个 vector 来实现类似的功能,每次用 lower_bound 找到应该插入的位置,然后直接 insert 进去,我在考场上用的就是这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<bits/stdc++.h>
using namespace std;
int n,w;
vector<int> num;
int main()
{
scanf("%d%d",&n,&w);
for(int i=1;i<=n;i++)
{
int t=max(1,i*w/100)-1,a;
scanf("%d",&a);
num.insert(lower_bound(num.begin(),num.end(),a),a);
printf("%d ",num[num.size()-1-t]);
}
return 0;
}

但老实说这种方法的复杂度很玄学,因为据一些大佬所说,insert 的时间复杂度并不是我在考试时以为的 O(1)O(1),而是 O(n)O(n),但由于常数极小,所以可以近似地看成 O(logn)O(\log n)(这里可能有误,欢迎各位大佬指正),另外,由于这道题数据规模还不是很大,所以没有被卡。

事实上,这道题存在一个 O(nlogn)O(n\log n) 的做法不是我在考试时的那种玄学操作,即用一个对顶堆维护第 K 大值。

对顶堆的原理比较有趣,我们用两个堆,一个是小根堆,一个是大根堆,这道题要求维护的是第 K 大值,对于每一个新插入的数 xx,我们进行如下操作:

  1. xx 与小根堆堆顶元素 yy 比较,如果 xx 大于 yy,则把 xx 塞进小根堆里,反之,则把 xx 塞进大根堆里。
  2. 若小根堆里的数的数量小于 K,则把大根堆堆顶元素塞进小根堆里,并弹出,如此反复,直到小根堆里的数的数量等于 K。
  3. 若小根堆里的数的数量大于 K,则把小根堆堆顶元素塞进大根堆里,并弹出,如此反复,直到小根堆里的数的数量等于 K。
  4. 此时小根堆堆顶元素即为答案。

这个过程还是比较好想的,就像是把这个数列分成了两段,其中一段包含了最大的 K 个数,另外一段包含了其余的,当发现前一段的数的数量小于 K 时,就把后一段里最大的数划给前一段,反之则把前一段里最小的数划给后一段。

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
#include<bits/stdc++.h>
using namespace std;
int n,w,a;
priority_queue<int> p;
priority_queue< int,vector<int>,greater<int> > q;
int main()
{
scanf("%d%d",&n,&w);
for(int i=1;i<=n;i++)
{
int t=max(1,i*w/100);
scanf("%d",&a);
if(q.empty()||a>q.top())
q.push(a);
else
p.push(a);
while(q.size()<t)
{
q.push(p.top());
p.pop();
}
while(q.size()>t)
{
p.push(q.top());
q.pop();
}
printf("%d ",q.top());
}
return 0;
}

当然你要是实在闲着没事干的话平衡树也能很好地解决这道题,就是码量较上面这三种写法可能大了不少,不过时间复杂度也是比较优秀的 O(nlogn)O(n\log n)

T3 表达式

这道题是一道字符串的模拟题,要求求一个后缀逻辑表达式的值,后缀表达式的求解就不赘述了,可以参考 OI Wiki,这里主要讲一下如何快速求出反转变量的值后表达式的值。

首先,由于这是一个逻辑表达式,所以其值只有可能为 0011,换句话说,如果我们知道它的值不为 00,那它就一定为 11,这对里面的每一个变量都同样适用。

其次,每个变量都只出现一次,所以即使反转某个变量的值,也不可能出现某个运算符两边的值同时反转的情况。

最后,询问之间相互独立,也就是说我们每一次只用考虑一个变量的值的改变即可。

有这几点之后,结论就比较清晰了,我们可以预处理出哪些变量的值反转后会影响到整个表达式的值(这里我将其称为“关键的变量”),如果某次询问反转的是一个关键的变量的值,那么整个表达式的值一定会被反转。

接下来我们考虑如何求出这些变量,可以分两种情况讨论。

  1. 与运算符(x1x2x_1\land x2
    • 如果 x1x_1x2x_2 都为 11,则这个表达式的值为 11,此时不管反转哪个变量的值,表达式的值都会变成 00,所以 x1x_1x2x_2 都是关键的。
    • 如果 x1x_1x2x_2 只有一个为 11,则这个表达式的值为 00,此时如果把为 00 的那个变量的值反转,表达式的值就会变成 11,所以为 00 的那个变量是关键的,而如果为 11 的那个变量反转,表达式的值还是 00,所以为 11 的那个变量不是关键的。
    • 如果 x1x_1x2x_2 都为 00,则不管反转哪个,表达式的值都为 00,所以它们都不是关键的。
  2. 或运算符(x1x2x_1\lor x_2
    • 如果 x1x_1x2x_2 都为 00,则这个表达式的值为 00,此时不管反转哪个变量的值,表达式的值都会变成 11,所以 x1x_1x2x_2 都是关键的。
    • 如果 x1x_1x2x_2 只有一个为 00,则这个表达式的值为 11,此时如果把为 11 的那个变量的值反转,表达式的值就会变成 00,所以为 11 的那个变量是关键的,而如果为 00 的那个变量反转,表达式的值还是 11,所以为 00 的那个变量不是关键的。
    • 如果 x1x_1x2x_2 都为 11,则不管反转哪个,表达式的值都为 11,所以它们都不是关键的。

(至于取反运算符(¬x\lnot x)可以忽略,因为它是单目运算符,我们甚至可以直接把它取反的那个变量的值取反,然后删掉它,当然我个人不推荐这样,因为会多出许多代码)

容易发现,上述规则不仅适用于单个变量,也同样适用于表达式,即 x1x_1x2x_2 可以是表达式,我们对这两个表达式分别求出它的关键的变量之后,在运算时按照上述规则进行判断,如果某个表达式是关键的,则它的所有关键的变量都是这个新表达式的关键的变量,反之则都不是,这样不停地对表达式进行合并,最后就能求出整个表达式的关键的变量和表达式的值,将其存在数组里排个序,在询问时 lower_bound 一下,判断反转的变量是否是关键的,如果是,就输出表达式值反转后的值,如果不是,直接输出原值即可,时间复杂度 O(s+qlogn)O(|s|+q\log n)

(看到很多大佬是对表达式建了棵树,然后打标记,可以做到 O(s)O(|s|),然而我太弱了,考场上只想出来用栈来做,所以只好借助了 STL 的力量,而且时间复杂度比较**,常数也是能上天的那种,在 lg 上跑了整整 1.77s,所以强烈建议看完本篇题解后再看一下 lg 上的题解)

STL 操作较多,主要是用了很多 assign 和 insert,如果有疑问,请自行百度。

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
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
string s;
int n,q,len,tid,ans,x[N],p[N];
stack<int> num;
stack< vector<int> > val;
vector<int> k;
void solve()
{
for(int i=0;i<len;i++)
{
if(s[i]==' ')
{
if(tid)
{
num.push(x[tid]);
vector<int> tmp;
tmp.push_back(tid);
val.push(tmp);
}
tid=0;
}
else
{
if(s[i]=='x')
tid=0;
if(s[i]>='0'&&s[i]<='9')
tid=tid*10+s[i]-'0';
if(s[i]=='|')
{
int x1=num.top();
num.pop();
int x2=num.top();
num.pop();
num.push(x1|x2);
vector<int> t1,t2,empty;
t1.assign(val.top().begin(),val.top().end());
val.pop();
t2.assign(val.top().begin(),val.top().end());
val.pop();
if(!x1&&!x2)
t1.insert(t1.end(),t2.begin(),t2.end());
if(!x1&&x2)
swap(t1,t2);
if(!x1||!x2)
val.push(t1);
else
val.push(empty);
}
if(s[i]=='&')
{
int x1=num.top();
num.pop();
int x2=num.top();
num.pop();
num.push(x1&x2);
vector<int> t1,t2,empty;
t1.assign(val.top().begin(),val.top().end());
val.pop();
t2.assign(val.top().begin(),val.top().end());
val.pop();
if(x1&&x2)
t1.insert(t1.end(),t2.begin(),t2.end());
if(x1&&!x2)
swap(t1,t2);
if(x1||x2)
val.push(t1);
else
val.push(empty);
}
if(s[i]=='!')
{
int x1=num.top();
num.pop();
num.push(!x1);
}
}
}
ans=num.top();
k.assign(val.top().begin(),val.top().end());
}
int main()
{
getline(cin,s);
len=s.size();
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&x[i]);
solve();
sort(k.begin(),k.end());
scanf("%d",&q);
for(int i=1;i<=q;i++)
{
int j;
scanf("%d",&j);
int t=lower_bound(k.begin(),k.end(),j)-k.begin();
if(t<k.size()&&k[t]==j)
printf("%d\n",ans^1);
else
printf("%d\n",ans);
}
return 0;
}

能想到用栈来存 vector 的恐怕也只有我了

T4 方格取数

从数据规模和题目名称可以看出来应该是一道 DP,可以用 dpi,jdp_{i,j} 表示走到第 iijj 列所能取到的最大的数字和,但由于这道题有可能从下面走到上面来,所以不能用传统的转移方式。

注意到这道题不能从右边走到左边,这启发我们一列一列地进行递推,具体来说,我们可以发现,从 (x1,y1)(x-1,y_1) 走到 (x,y2)(x,y_2),一定是先走到 (x,y1)(x,y_1),再一路向上或向下走到 (x,y2)(x,y_2),并收集从 (x,y1)(x,y_1)(x,y2)(x,y_2) 的所有数字,因此,应该可以想到如下递推式

dpi,j=max1kndpk,j1+{x=ikax,j1(ik)x=kiax,j1(i>k)dp_{i,j}=\max_{1\le k\le n} dp_{k,j-1}+\begin{cases}\sum_{x=i}^k a_{x,j-1}(i\le k)\\\sum_{x=k}^i a_{x,j-1}(i>k)\end{cases}

但如果对于每一个 dpi,jdp_{i,j} 都枚举一遍这个最大值的话,那么时间复杂度为 O(n2m)O(n^2m),很明显不行,考虑优化。

可以发现,对于同一列的 dpi,jdp_{i,j},这个枚举的过程会有很多重复的运算,我们可以用一个 mxmx 变量记录 dpi1,jdp_{i-1,j} 所取到的最大的值,在此基础上得出 dpi,jdp_{i,j} 应取的最大值,先考虑第二种,即 i>ki>k 的情况,为了便于理解,这里放一张图片,阐述 dpi,jdp_{i,j} 的得出过程。

如图,这是已经求出的一个 dpi1,jdp_{i-1,j},而 dpi,jdp_{i,j} 的来源有两种。

可以看出,如果我们从 (k,j1)(k,j-1) 走到 (k,j)(k,j) 是走到 (k,j)(k,j) 的最优方案,那么可以一直往下,中间不停地把方格里的值累加到 mxmx 里,直到遇到一个地方 (i,j)(i,j),使得 dpk,j1+x=kiax,j<dpi,j1+ai,jdp_{k,j-1}+\sum_{x=k}^i a_{x,j}<dp_{i,j-1}+a_{i,j},就说明这个地方从上一列走过来会更优,那我们就可以更新 mxmx 变量的值为 dpi,j1+ai,jdp_{i,j-1}+a_{i,j},并重复上述过程,而对于 iki\le k 的情况也是一样的思路。

还有一点需要注意的地方,因为第 11 列不存在从上一列走过来的情况,我们需要进行预处理。

还有什么不明白的就看代码吧,个人感觉码风还是不错的。

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
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e3+10;
ll n,m,mx,a[N][N],dp[N][N];
int main()
{
memset(dp,-0x3f,sizeof(dp));//初始化最小值
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%lld",&a[i][j]);
dp[0][1]=0;
for(int i=1;i<=n;i++)
dp[i][1]=a[i][1]+dp[i-1][1];//预处理第一列
for(int j=2;j<=m;j++)
{
mx=dp[1][j-1];
for(int i=1;i<=n;i++)//先统计 i>=k 的情况
{
mx=max(dp[i][j-1],mx)+a[i][j];//用一个变量不断累加,遇到更优的情况就更新之
dp[i][j]=max(dp[i][j],mx);
}
mx=dp[n][j-1];
for(int i=n;i>=1;i--)//再统计 i<=k 的情况
{
mx=max(dp[i][j-1],mx)+a[i][j];//同上
dp[i][j]=max(dp[i][j],mx);
}
}
printf("%lld",dp[n][m]);
return 0;
}

DP 不是我最擅长的东西准确来说是我最不擅长的东西,所以考试时并没有做对这道题,只得了 30pts,不过还好前三题都对了,应该是有 1= 了。

TG

这次的提高真的是让我感觉心态爆炸,尤其是 T1T2,T1 就不用说了,恶心大模拟,数据也难算,调试也很**,整体来说很没有体验,T2 犯了一个超级 sb 的错误,导致挂了 35pts,T3T4 还算正常发挥,主要是前两题出错太多,最后只有 150pts,1= 很悬。

T1 儒略日

在开始这道题的题解之前请允许我问候出题人全家。

简单来说,这道题就是让你求从公元前 4713 年 1 月 1 日往后数 rr 天是哪一天,但有几个细节。

  • 从公元前 4713 年 1 月 1 日到公元 1582 年 10 月 4 日这段时间里没有现在百年不闰的说法,只要年份是 44 的倍数就是闰年。
  • 公元 1582 年 10 月 4 日的下一天是公元 1582 年 10 月 15 日。
  • 公元 0 年不存在,即公元前 1 年的下一年是公元 1 年,所以对于公元前年份来说,年份模 4411 的才是闰年。
  • 其他的和现在的历法一样。

一个很简单的方法是用这个天数不停地减去这一年的天数,然后把年份数加一,如果天数小于等于这一年的天数了,就再让它不停的减去这一年的每个月份的天数,直到它小于等于这一月的天数。

然而,CCF 给我们的数据范围是

年份答案不超过 10910^9

况且还有多组数据,所以很明显这个方法行不通,考虑优化。

相信大家都做过这道题,它的思路是用取模来代替周期性的模拟,我们也可以依葫芦画瓢。

具体来说,我们分三段来考虑。

  • 公元前 4713 年 1 月 1 日到公元 1582 年 10 月 4 日,这一段四年一闰,即四年一个周期。
  • 公元 1582 年 10 月 15 日到公元 1582 年 12 月 31 日,这一段由于不是整年份,又有间隔的天数,所以我们分开特判。
  • 公元 1583 年 1 月 1 日到公元 inf 年 12 月 31 日以后,这一段四年一闰,百年不闰,四百年再闰,即每四百年为一个周期。

具体的就不详细说了,也没什么好说的了,还是看代码吧。

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
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll q,n,mon[20]={0,31,28,31,30,31,30,31,31,30,31,30,31};
ll check_year_special(ll x)//带 _special 的就是用来判断 1582 年以前的年份或月份的
{
if(x<0)
if((-x)%4==1)
return 366;
else
return 365;
else
if(x%4==0)
return 366;
else
return 365;
}
ll check_month_special(ll x,ll y)
{
if(check_year_special(x)==366&&y==2)
return 29;
else
return mon[y];
}
ll check_year(ll x)
{
if(x%400==0||(x%4==0&&x%100!=0))
return 366;
else
return 365;
}
ll check_month(ll x,ll y)
{
if(check_year(x)==366&&y==2)
return 29;
else
return mon[y];
}
int main()
{
scanf("%lld",&q);
while(q--)
{
scanf("%lld",&n);
if(n<=2299160)//从公元前 4713 年 1 月 1 日到公元 1582 年 10 月 4 日的天数
{
ll y=n/1461*4-4713,m=1,flag=0;//1461 为四年的天数
n%=1461;
if(y>=0)
{
flag=1;//特判是否过了公元前 1 年
y++;
}
while(n>=check_year_special(y))//一年一年地减
{
n-=check_year_special(y++);
if(y==0)
{
flag=1;
y++;
}
}
while(n>=check_month_special(y,m))//一月一月地减
n-=check_month_special(y,m++);
if(flag)
printf("%lld %lld %lld\n",n+1,m,y);
else
printf("%lld %lld %lld BC\n",n+1,m,-y);
}
if(2299160<n)
{
n-=2299161;//减去之前的天数,重新开始计算
if(n<=16)//特判 1582 年 10 月 15 日到 1582 年 12 月 31 日
printf("%lld 10 1582\n",n+15);
if(16<n&&n<=46)
printf("%lld 11 1582\n",n-16);
if(46<n&&n<=77)
printf("%lld 12 1582\n",n-46);
if(77<n)
{
n-=78;
ll y=1583+n/146097*400,m=1;//146097 为每四百年的天数
n%=146097;
while(n>=check_year(y))
n-=check_year(y++);
while(n>=check_month(y,m))
n-=check_month(y,m++);
printf("%lld %lld %lld\n",n+1,m,y);
}
}
}
return 0;
}

没什么复杂的算法,但需要非常注意细节,同时数学也要好计算器也要用的好,否则是真的难调,我在考场上调了一个半小时都没整对,最后还是爆零。。。

T2 动物园

这道题个人感觉比 T1 简单多了,不过位运算的特性是真的难受,两三处小小的错误直接就丢了 35pts,真的难受。。。

对于这道题,我们可以先根据饲养的动物和要求算出清单,再根据清单反过来算一遍这些动物编号的哪些二进制位可以为 11,假设有 xx 位可以为 11,那就一共可以饲养 2x2^x 种动物,因为我们可以把中间不能为 11 的那些位扣掉,剩下来的就是一个每一位都为 11 的共有 xx 位的二进制数,其中每一位都可以为 00 或者为 11,所以总的方案数就是 2x2^x,再从其中扣掉已经饲养的 nn 种动物,就是还能饲养的动物数。

不过这道题有很多注意点,下面列举一下(可能不全,欢迎补充)。

  • 这道题 k64k\le 64,超出了 long long 的范围,必须要用 unsigned long long,如果你不会拼 unsigned,请找你的英语老师。(40pts)

  • 由于位运算的神奇特性,1<<x会被默认为 int 类型,即如果 xx 大于等于 3131,即使你是将这个值赋给一个 unsigned long long 类型的值,它还是会在运算时自然溢出,解决方法是使用1ull<<x。(40pts)(这就是我在考试时犯的傻逼错误,少打了两个 ull

  • 根据数据范围,xx 有可能为 6464,如果 nn 此时为 00,那么答案就是 2642^{64},超出了 unsigned long long 的范围,需要特判,另外,虽然 CCF 的数据没有卡,但个人认为最好是特判一下 x=64x=64 的情况,毕竟这样虽然最后会减一个 nn,但在运算过程中也会溢出。(5pts)

  • 如果你用的是 scanf(用 cin 貌似会超时?),记住 unsigned long long 的格式控制符是 %ull,不是 %llu(没有这种格式控制符),也不是 %uld(也没有这种格式控制符)。(100pts)

我很好奇有多少人挂在了第 44 点。

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
#include<bits/stdc++.h>
#define lowbit(x) (x&(-x))
using namespace std;
typedef unsigned long long ll;//unsigned!!!
const ll N=1e6+10;
ll cnt(ll x)
{
ll sum=0;
for(;x;x-=lowbit(x))
sum++;
return sum;
}
ll n,m,c,k,mk,ans,a[N],p[N],q[N];
bitset<100000010> lt;//我用的 bitset 存清单QwQ
int main()
{
scanf("%llu%llu%llu%llu",&n,&m,&c,&k);
for(ll i=1;i<=n;i++)
{
scanf("%llu",&a[i]);
mk|=a[i];
}
for(ll i=1;i<=m;i++)
{
scanf("%llu%llu",&p[i],&q[i]);
if(mk&(1ull<<p[i]))
lt[q[i]]=1;
}
for(ll i=1;i<=m;i++)
if(!lt[q[i]]&&!(ans&(1ull<<p[i])))
ans^=(1ull<<p[i]);
ll tot=k-cnt(ans);
if(tot==64)
if(n==0)
printf("18446744073709551616");//特判
else
printf("%llu",18446744073709551616-n);//特判*2
else
printf("%llu",(1ull<<tot)-n);
return 0;
}

T3 函数调用

作者

AzuSemisa

发布于

2020-11-15

更新于

2020-11-22

许可协议

评论