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$$

我们来具体分析一下:

  1. 首先从两个人的情况考虑。如果只剩下 \(4\)\(5\),那么只要 \(5\) 投反对票,那么 \(4\) 去喂了鲨鱼,\(100\) 块钱就都是 \(5\) 的。
  2. 现在考虑有三个人,既然 \(4\) 很弱势,那么他为了不被喂鲨鱼,无论如何会投赞成票(我们考虑死亡是最坏结果,比拿不到钱还坏)。\(5\) 的票不重要,所以 \(3\)\(100\) 块钱,\(4,5\) 一分钱拿不到。
  3. 考虑 \(4\) 个人。\(5\) 因为在 \(3\) 个人的时候拿不到钱,所以只要 \(2\) 给他 \(1\) 块钱,就会投赞成票。\(3\) 因为在 \(3\) 个人的时候有 \(100\) 块钱,无论如何不会同意 \(2\)。所以 \(2\) 还需要一块钱讨好 \(4\)。这样 \(2\) 最多拿 \(98\) 块钱。
  4. 最后考虑 \(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++;
posted @ 2020-11-27 21:28  huayucaiji  阅读(141)  评论(1编辑  收藏  举报