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\)
- 最後考慮 \(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++;