CSP-S 2020解题报告(暂T1 和 T4)
CSP-S 2020解题报告
考场上 150 分真的难受死了
T1 julian
这个题没什么难度,大模拟。
- 首先可以想到分段处理
- 其次我们可以想到每 \(400\) 年作为一个周期来处理加快速度,
- \(1582\) 年的情况单独分段处理。
很简单,但是细节很多,考场上45min 过了大样例,最后只有 80。原因是在跳 \(400\) 年的时候,可能调到了 \(12.31\),针不戳,直接给你搞成下一年的 \(1.0\)。特判一下即可。
#include<bits/stdc++.h>
using namespace std;
int mth[]={0,31,28,31,30,31,30,31,31,30,31,30,31,29};
long long n;
bool run_b(int x) {
if(x<0&&(-x)%4==1) {
return 1;
}
if(x>0&&x%4==0) {
return 1;
}
return 0;
}
bool run(int x) {
if(x%400==0) {
return 1;
}
if(x%4==0&&x%100!=0) {
return 1;
}
return 0;
}
int main(){
freopen("julian.in","r",stdin);
freopen("julian.out","w",stdout);
int t;
cin>>t;
//146100
while(t--) {
scanf("%lld",&n);
int yyyy=n;
n++;
int y=-4713,m=1,d=1;
if(n<=2299161) {
int t=n/146100;
int tt=n-t*146100;
y+=t*400;
if(y>=0) y++;
n=tt;
while((!run_b(y)&&n>365)||(run_b(y)&&n>366)) {
if(run_b(y)) {
n--;
}
n-=365;
y++;
if(y==0)
y++;
}
if(run_b(y)) {
mth[2]++;
}
for(m=1;m<=12;m++) {
if(n>mth[m]) {
n-=mth[m];
}
else {
break;
}
}
d=n;
if(run_b(y)) {
mth[2]--;
}
if(d==0) {
d=31;m=12;y--;
if(y==0) {
y--;
}
}
if(y<0)
printf("%d %d %d BC\n",d,m,-y);
else
printf("%d %d %d\n",d,m,y);
continue;
}
n-=2299161;
y=1582;
if(n<=17) {
m=10;d=15+n-1;
printf("%d %d %d\n",d,m,y);
continue;
}
n-=17;
if(n<=30) {
m=11;d=n;
printf("%d %d %d\n",d,m,y);
continue;
}
n-=30;
if(n<=31) {
m=12;d=n;
printf("%d %d %d\n",d,m,y);
continue;
}
n-=31;
y=1583;
//146097
long long t=n/146097;
long long tt=n-t*146097;
y+=t*400;
n=tt;
while((!run(y)&&n>365)||(run(y)&&n>366)) {
if(run(y)) {
n--;
}
n-=365;
y++;
}
if(run(y)) {
mth[2]++;
}
for(m=1;m<=12;m++) {
if(n>mth[m]) {
n-=mth[m];
}
else {
break;
}
}
d=n;
if(run(y)) {
mth[2]--;
}
if(d==0) {
d=31;m=12;y--;
}
printf("%d %d %d\n",d,m,y);
}
fclose(stdin);
fclose(stdout);
return 0;
}
好长
T4 snakes
考场差点忘记加s
估计我是第一个本题考场抱灵来写题解的。
话说考场上想到了70分解法,居然调了两个小时没跳出来我也是醉了。然而我又觉得T4比T3简单(什么神奇想法),所以T3只有暴力的分数qwq。
既然在这道题上反例大错,那么赛后就该好好反思,以后不要再犯这样的聪明。OI,要以核为贵,我劝出题人耗子尾汁,不要搞窝里斗。
咳咳,我们正式来讲解法。
引入
首先我们以一道经典的海盗分金问题来做引入,这对这道题有很大启发。
我们有 \(5\) 个海海盗,他们要分 \(100\) 个金币,\(5\) 个海岛从一号开始一次提出自己的分金方案,然后投票表决。如果第一个人的方案得到了半数以上的人的同意,那么按照一号的方案分钱。否则一号会被扔到海里喂鲨鱼。然后再表决二号的方案,以此类推。假设所有海盗足够聪明,请问一号最多获得多少钱?
先说答案,如果没有做过这道题,你肯定会大吃一惊:
$$97$$
我们来具体分析一下:
- 首先从两个人的情况考虑。如果只剩下 \(4\) 和 \(5\),那么只要 \(5\) 投反对票,那么 \(4\) 去喂了鲨鱼,\(100\) 块钱就都是 \(5\) 的。
- 现在考虑有三个人,既然 \(4\) 很弱势,那么他为了不被喂鲨鱼,无论如何会投赞成票(我们考虑死亡是最坏结果,比拿不到钱还坏)。\(5\) 的票不重要,所以 \(3\) 拿 \(100\) 块钱,\(4,5\) 一分钱拿不到。
- 考虑 \(4\) 个人。\(5\) 因为在 \(3\) 个人的时候拿不到钱,所以只要 \(2\) 给他 \(1\) 块钱,就会投赞成票。\(3\) 因为在 \(3\) 个人的时候有 \(100\) 块钱,无论如何不会同意 \(2\)。所以 \(2\) 还需要一块钱讨好 \(4\)。这样 \(2\) 最多拿 \(98\) 块钱。
- 最后考虑 \(5\) 个人。现在 \(3\) 只要有 \(1\) 块钱就会支持 \(1\),所以给他 \(1\) 块钱。\(2\) 肯定反对(理由同上一种情况 \(3\) 的反对理由)。\(1\) 只需要讨好 \(4,5\) 中的一个即可,给 \(4\) 或 \(5\) 其中一个多一块钱,即 \(2\) 块钱即可。因此 \(1\) 最多有 \(97\) 块钱。
这个题的思路就和贪吃蛇的思路有点类似了,我们现在再来理解 snakes 这道题会好很多。
解题
我们现在考虑解题方法。
现在我们的蛇长度是 \(a_1,a_2\dots a_n\)。吃一次会得到一条新的蛇,长度为 \(a_n-a_1\)。因为蛇足够聪明,只要能吃,最长的蛇就一直吃,直到最长的蛇吃了最短的蛇后变成了最短的蛇。这符合贪心的策略。这个贪心思路很简单,一开始我就想到这里,开心打了代码。结果大样例和答案老是差 \(1\)。大概最后还剩半个小时的时候想到了这种情况:
1
3
3 4 5 6
这种情况下,\(6\) 吃了 \(3\) 后会变成 \(3\),但是 \(5\) 不敢动它,因为动了自己会被吃掉,因此 \(6\) 可以吃 \(3\) 最后答案为 \(3\)。因此在最后我们还需要判断一下是否还可以再吃一口。我们的判断过程类似于一个递归,即假设能吃,看看接下来是否能推出不能吃,导致矛盾。举个例子:
我们现在假设 \(a_n-a_1<a_2\),开始判断能否继续吃,假设我们吃了,现在有 \(a_n-a_1,a_2,a_3\dots a_{n-1}\)。如果 \(a_{n-1}-(a_n-a_1)\geq a_2\),那么原来就不能吃,否则继续判断。现在有 \(a_{n-1}-(a_n-a_1),a_2,a_3\dots a_{n-3}\)。如果这一口发力吃了,没有把最短的蛇打骨折,无法吃掉最短的蛇,仍然要继续判断;否则说明 \(a_{n-1}\) 吃是不安全的,那么 \(a_n\) 就该吃,以此类推直到只剩 \(2\) 条蛇,除非中间已经判断出了结果,即某一时刻最长的蛇吃了最短的蛇后不是最短的蛇。
用递归求解即可。
我们采用 set
来维护当前最大值和最小值。时间复杂度 \(O(n\log n)\)。非考场代码(我考场上都抱灵了qwq)
//Don't act like a loser.
//This code is written by huayucaiji
//You can only use the code for studying or finding mistakes
//Or,you'll be punished by Sakyamuni!!!
#include<bits/stdc++.h>
using namespace std;
int read() {
char ch=getchar();
int f=1,x=0;
while(ch<'0'||ch>'9') {
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9') {
x=x*10+ch-'0';
ch=getchar();
}
return f*x;
}
const int maxn=1e6+10;
int n;
struct snakes {
int id,len;
bool operator <(snakes b) const {
if(len!=b.len) {
return len<b.len;
}
return id<b.id;
}
snakes operator -(snakes b) {
snakes ret;
ret.len=len-b.len;
ret.id=id;
return ret;
}
}a[maxn];//结构体针不戳
typedef set<snakes>::iterator its;
set<snakes> s;
bool eatornot() {//判断是否还能再吃
if(s.size()==2) {
return 1;//还剩下两条蛇淡然随便吃,返回1
}
its ib,ie,ib2;
ib=s.begin();
ie=s.end();
ie--;
ib2=ib;
ib2++;
snakes now=(*ie);
now.len=(*ie).len-(*ib).len;
if(!(now<(*ib2))) {
return 1;//如果吃了不是最短的,说明可以吃
}
s.erase(ib);
s.erase(ie);
s.insert(now);
return !eatornot();
//注意取反操作
//如果现在能吃,说明上一条蛇不该吃
//反之亦然
}
signed main() {
freopen("snakes.in","r",stdin);
freopen("snakes.out","w",stdout);
int t;
t=read();
for(int Q=1;Q<=t;Q++) {
if(Q==1) {
n=read();
for (int i = 1; i <= n; ++i) {
a[i].len=read();
a[i].id=i;
s.insert(a[i]);
}
}
else {
int k=read();
for(int i=1;i<=k;i++) {
int x=read();
int y=read();
a[x].len=y;
}
s.clear();
for (int i=1;i<=n;++i) {
s.insert(a[i]);
}
}//两种输入方式
while(1) {
its ib,ie,ib2;
ib=s.begin();
ie=s.end();
ie--;
ib2=ib;
ib2++;
snakes now=(*ie);
now.len=(*ie).len-(*ib).len;
if(now<(*ib2)) {//模拟
break;
}
s.erase(ib);
s.erase(ie);
s.insert(now);
}
int ans=s.size();
if(eatornot()) {
ans--; //如果能吃,就在咬一口咯~
}
printf("%d\n",ans);
}
fclose(stdin);
fclose(stdout);
return 0;
}
以上是 \(70pts\) 做法,现在来考虑 \(100pts\) 做法。算法肯定没问题,主要是要把时间复杂度降到 \(O(n)\)。
我们其实可以联想到“蚯蚓”这道题,开两个队列来维护最大最小,吃完后留下的蛇的长度肯定是单调递减的,很好证明,这里不再赘述。我们现在维护两个双端队列,由于有单调性,每个队列中蛇的长度单调递增,这样我们就省去了那个求最大最小值的 \(O(\log n)\)。具体看代码。
//Don't act like a loser.
//This code is written by huayucaiji
//You can only use the code for studying or finding mistakes
//Or,you'll be punished by Sakyamuni!!!
//#pragma GCC optimize("Ofast","-funroll-loops","-fdelete-null-pointer-checks")
//#pragma GCC target("ssse3","sse3","sse2","sse","avx2","avx")
#include<bits/stdc++.h>
using namespace std;
int read() {
char ch=getchar();
int f=1,x=0;
while(ch<'0'||ch>'9') {
if(ch=='-')
f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9') {
x=x*10+ch-'0';
ch=getchar();
}
return f*x;
}
const int maxn=1e6+10;
int n;
struct snakes {
int id,len;
bool operator <(snakes b) const {
if(len!=b.len) {
return len<b.len;
}
return id<b.id;
}
snakes operator -(snakes b) {
snakes ret;
ret.len=len-b.len;
ret.id=id;
return ret;
}
}a[maxn];
deque<snakes> q1,q2,q;
void work() {
q1.clear();
q2.clear();//记得多组数据初始化
q.clear();
for (int i = 1; i <= n; ++i) {
q1.push_back(a[i]);
}
while(1) {
if(q1.size()+q2.size()==2) {
printf("1\n"); //还剩下2条蛇直接输出
return ;
}
snakes st=q1.front();
q1.pop_front();
snakes ed=q1.back();
if(!q2.empty()&&ed<q2.back()) {
ed=q2.back();
q2.pop_back();//如果q2中的蛇较长,去q2中的蛇
}
else {
q1.pop_back();
}
snakes tmp;
tmp.len=ed.len-st.len;
tmp.id=ed.id;
if(q1.front()<tmp) {
q2.push_front(tmp);
}//将新蛇根据单调性放入队列中
else {
q1.push_front(tmp);
break;//现在这条蛇吃了一口,发现变成最短的了
//那么进入第二阶段
//注意此时放到q1还是q2中没有任何区别了
}
}
while(!q1.empty()&&!q2.empty()) {
if(q1.front()<q2.front()) {
q.push_back(q1.front());
q1.pop_front();
}
else {
q.push_back(q2.front());
q2.pop_front();
}
}
while(!q1.empty()) {
q.push_back(q1.front());
q1.pop_front();
}
while(!q2.empty()) {
q.push_back(q2.front());
q2.pop_front();
}
//为了操作方便把两个队列合并
//和学归并时O(n)合并有序数组的做法一致
int ans=q.size();
int eat=0;
while(q.size()>1) {
snakes st=q.front();
q.pop_front();
snakes ed=q.back();
q.pop_back();
snakes tmp;
tmp.len=ed.len-st.len;
tmp.id=ed.id;
eat++;
if(q.size()==0||q.front()<tmp) {
break;
}
else {
q.push_front(tmp);//还是珂爱的单调性
}
}//用while模拟递归判断,本质上无差别
printf("%d\n",ans+(eat&1? 1:0));
//注意我们这份代码中和上一份有所不同
//上面是假设还没吃,判断是否要吃 -1
//这里是已经吃了,判断是否要吐出来 +1
}
signed main() {
freopen("snakes.in","r",stdin);
freopen("snakes.out","w",stdout);
int t;
t=read();
for(int Q=1;Q<=t;Q++) {
if(Q==1) {
n=read();
for (int i = 1; i <= n; ++i) {
a[i].len=read();
a[i].id=i;
}
}
else {
int k=read();
for(int i=1;i<=k;i++) {
int x=read();
int y=read();
a[x].len=y;
}
}//似曾相识的读入
work();//求解
}
fclose(stdin);
fclose(stdout);
return 0;
}
这道题就分析完了,总体来说难度不算大,不至于到黑题(但是在账户里多一道黑题收入还是不错的)。主要是一个思维题,偏博弈类型。说句实话这题于我来说不只是搞懂了一道题,还让我好好反思了我的做题策略,可以说,在我人生道路上可能是一个重要的节点吧。特此写了一篇题解来谢谢自己的感悟,也把知识分享给大家。
最后的最后:
NOIP2020_rp++;