1. 程式人生 > 實用技巧 >CSP-S 2020解題報告(暫T1 和 T4)

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++;