1. 程式人生 > 實用技巧 >CSP-S2020 題解

CSP-S2020 題解

儒略日(julian.cpp)

寫了個很醜的程式碼。因為是個大模擬(沒寫二分)講一下具體實現思路。因為是考場程式碼所以醜的 1p,函式也沒裝思路也沒寫,所以程式碼用精神領略即可。

這個東西有點長。考慮一下怎麼把它搞得好看一點。

  • 公元前的閏年與公元不太一樣,拆開。注意沒有公元 \(0\) 年;
  • \(1582.10.5 \sim 1582.10.14\) 不存在,左右拆開;
  • 觀看資料範圍,年份可能很大,需要一個優秀的演算法去算年份。這裡拆成 \(400\) 剩下的可以暴力。比較好處理的是將 \(1582.10.15 \sim 1600.12.31\) 拆開,剩下的處理。

通過計算器可以得出:

  • \(0 \leq r \leq 1721423\)
    ,此時是公元前;
  • \(1721424 \leq r \leq 2299160\),此時是公元,但是是在 \(1582.10.4\) 之前;
  • \(2299161 \leq r \leq 2299238\),此時在 \(1582.10.15 \sim 1600.12.31\) 範圍內;
  • 剩下的暴力算出 \(400\) 年為 \(146097\) 天。隨便算算就能算出來。

注意一下運算子優先順序,推薦將日期的多少天后寫成一個函式,比較好看。不至於每個情況都要寫一遍,雖然也不難打。

幾個坑點:

  • 待會兒附個數據看你的邊界;
  • 負數取模答案是負數,所以 (-1%4==1)==false
  • 閏年判斷注意二路運算子優先順序;
  • 修改月和日的時候不要混淆了;
  • 注意開 long long

貼個程式碼。因為暴力跑了四百年沒有打表優化,所以會很慢,時間複雜度是 \(O(Qc)\),其中 \(c\) 是一個常數,最壞等於 \(400+12+31=443\)

資料點:julian4.in & julian4.ans。這個資料點涵蓋了以下坑點:

  • 公元前閏年;
  • 不存在的公元 \(0\) 年;
  • 適用儒略曆的公元閏年(\(4,100,400\));
  • 不存在的 \(1582.10.5 \sim 1582.10.14\)
  • \(1600,1601\)\(1583\) 的邊界(這份程式碼的需要的除錯點);
  • 使用格里高利曆的公元閏年(\(4,100,400\)
    );
  • 答案比較大的點。

希望能有所幫助。

哇我的資料還沒隨機資料強,權當玩玩就好了。

動物園(zoo.cpp)

這道題怪怪的,開始還能迷惑人。

首先顯然要求出我們已經購買的飼料,然後是我們的飼料能夠供應什麼動物。

供應這個動物的過程可以直接拆位。題目都說成這樣了不會你還不知道吧。

但是對於每一個求一次是不可行的(其實後來是可以的,只不過有點卡),所以我們用位運算把所有的動物編號或起來就完事兒了。

現在我們發現,有 \(x\) 位是任我們支配的,其餘 \(k-x\) 位必須不要。

每一位任我們支配,有 \(0,1\) 兩種選擇,答案就是 \(2^x\);再減去已有的動物 \(n\),答案 \(2^x-n\)

有兩個點:

  • \(x=64,n\neq 0\)。雖然這個時候會直接溢位但是這是 UB。可以直接輸出 -n,你也可以 \(2^{63}-n+2^{63}\),雖然這個輸出法就跟我一樣蠢;
  • \(x=64,n = 0\)。打表輸出 \(2^{64}\)
#include<cstdio>
#include<algorithm>
#include<queue>
#include<map>
using namespace std;
typedef unsigned long long LL;
LL read()
{
	LL x=0;
	char c=getchar();
	while(c<'0' || c>'9')	c=getchar();
	while(c>='0' && c<='9')	x=(x<<1)+(x<<3)+(c^'0'),c=getchar();
	return x;
}
LL n,m,c,k,a[1000005],p[1000005],q[1000005];
bool ds[65],food[100000005];
int main(){
	n=read(),m=read(),c=read(),k=read();
	LL tester=0;
	for(int i=1;i<=int(n);++i)	a[i]=read(),tester|=a[i];
	for(int i=1;i<=int(m);++i)	p[i]=read(),q[i]=read();
	for(int i=1;i<=int(m);++i)	if(tester&(1llu<<p[i]))	food[q[i]]=true;
	for(int i=1;i<=int(m);++i)	if(!food[q[i]])	ds[p[i]]=true;
	int calc=0;
	for(int i=0;i<=int(k-1);++i)	if(!ds[i])	++calc;
	if(calc==64)
	{
		if(n==0)	puts("18446744073709551616");
		else
		{
			LL ans=~0llu;
			ans-=n;
			++ans;
			printf("%llu\n",ans);
		}
	}
	else	printf("%llu",(1llu<<calc)-n);
	return 0;
}

函式呼叫(call.cpp)

看起來是有點像一個數據結構,但是不是。我相信那個線段樹 2 的部分分肯定把人坑飛了。

注意到題目有這樣一句話:

保證不會出現遞迴。

這句話的意義就在於,如果將函式的呼叫關係當成一個圖,那麼這個圖一定是一個 DAG。

考慮兩個部分分:

  • 只有二類函式:這就是一個拓撲排序,只需要乘了啥東西就沒事兒了。這個過程可以用一個搜尋預處理一發,算一下呼叫一個函式會讓整個序列乘上多少;
  • 只有一類函式:這個東西比較麻煩,但是你還是可以一次搜尋去處理這個問題。假設一個函式的呼叫次數為 \(p\),那麼這個函式的所有呼叫函式也會被多呼叫 \(p\) 遍。所以搜尋往下傳就完事兒了。

發現這個乘法是對所有的進行乘法,考慮 逆序處理函式呼叫。這樣我們的乘法,也能相當於對之前的加法操作乘上一個值。這樣就會變得更好處理。

定義 \(emb_i\) 為,呼叫 \(i\) 函式,其中的單點加法呼叫次數為 \(emb_i\)

這個東西比較好處理。存一下當前整個序列被乘了 \(f\) 倍,分類討論:

  • 一類函式:呼叫的單點次數增加 \(f\)
  • 二類函式:\(f\) 乘上了呼叫它會乘上的倍數;
  • 三類函式:因為我們仍然遞迴算的話還是會超時,考慮先存一個詭異的值。所以呼叫的單點次數先增加 \(f\),然後 \(f\) 乘上了呼叫它會乘上的倍數。

考慮將詭異的值修正,可以用拓撲排序。首先用一個東西 \(pls\) 去存一下每一個值要額外加上的值。又分類討論:

  • 一類函式:直接來,這個函式對應的位置加上 \(emb_x \times val_x\)
  • 二類函式:我們已經做了;
  • 三類函式:注意還是一樣逆序呼叫,因為有順序:根據只有一類函式的做法傳一下標記,然後 \(emb_x\) 要乘上被更新的函式對應的,用只有二類函式的做法的,會使全部增加的倍數。注意用一個值代替原有的 \(emb_x\)

答案顯然。有點麻煩。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL MOD=998244353;
char buf[1<<18],*p1=buf,*p2=buf;
#define getchar() (p1==p2 && (p2=(p1=buf)+fread(buf,1,1<<18,stdin),p1==p2)?EOF:*p1++)
LL read()
{
	LL x=0,f=1;
	char c=getchar();
	while(c<'0' || c>'9')
	{
		if(c=='-')	f=-1;
		c=getchar();
	}
	while(c>='0' && c<='9')	x=(x<<1)+(x<<3)+(c^'0'),c=getchar();
	return x*f;
}
void write(LL x)
{
	if(x<0)	putchar('-'),x=-x;
	if(x>9)	write(x/10);
	putchar(x%10+'0');
}
struct Funct{
	LL type,p,v,s,mul;
	vector<LL> cal;
	Funct(){type=p=v=mul=0;cal.clear();}
}t[100005];
LL deg[100005],n,m,q,a[100005],calfun[100005],allf,emb[100005],pls[100005];
bool vis[100005];
void dfs(LL now)
{
	if(vis[now])	return ;
	t[now].mul=(t[now].type==2?t[now].v:1);
	vis[now]=true;
	for(unsigned int i=0;i<t[now].cal.size();++i)
	{
		LL to=t[now].cal[i];
		dfs(to);
		t[now].mul*=t[to].mul;
		t[now].mul%=MOD;
	}
}
int main(){
	n=read();
	for(LL i=1;i<=n;++i)	a[i]=read();
	m=read();
	for(LL i=1;i<=m;++i)
	{
		t[i].type=read();
		if(t[i].type==1)	t[i].p=read(),t[i].v=read();
		if(t[i].type==2)	t[i].v=read();
		if(t[i].type==3)
		{
			t[i].s=read();
			for(LL j=1,x;j<=t[i].s;++j)	t[i].cal.push_back(x=read()),++deg[x];
		}
	}
	for(LL i=1;i<=m;++i)	if(!deg[i])	dfs(i);
	q=read();
	for(LL i=1;i<=q;++i)	calfun[i]=read();
	reverse(calfun+1,calfun+1+q);
	allf=1;
	for(LL i=1;i<=q;++i)
	{
		if(t[calfun[i]].type==1)	emb[calfun[i]]+=allf,emb[calfun[i]]%=MOD;
		if(t[calfun[i]].type==2)	allf*=t[calfun[i]].mul,allf%=MOD;
		if(t[calfun[i]].type==3)	emb[calfun[i]]+=allf,emb[calfun[i]]%=MOD,allf*=t[calfun[i]].mul,allf%=MOD;
	}
	queue<LL> Q;
	for(LL i=1;i<=m;++i)	if(!deg[i])	Q.push(i);
	for(LL i=1;i<=m;++i)	reverse(t[i].cal.begin(),t[i].cal.end());
	while(!Q.empty())
	{
		LL now=Q.front();
		Q.pop();
		if(t[now].type==1)	pls[t[now].p]+=t[now].v*emb[now]%MOD,pls[t[now].p]%=MOD;
		LL delta=emb[now];
		for(unsigned int j=0;j<t[now].cal.size();++j)
		{
			LL to=t[now].cal[j];
			--deg[to];
			emb[to]+=delta;
			emb[to]%=MOD;
			delta*=t[to].mul;
			delta%=MOD;
			if(!deg[to])	Q.push(to);
		}
	}
	for(LL i=1;i<=n;++i)	write((a[i]*allf%MOD+pls[i])%MOD),putchar(' ');
	return 0;
}

貪吃蛇(snakes.cpp)

就離譜。70 如此好打考場上面沒敢實現。

首先有一個非常顯然的結論,也就是說如果一條蛇做出了選擇(無論吃與不吃)並且不會成為最小的一條蛇,這條蛇以後就永遠不會被吃了。

考慮到這個問題是具有嚴格偏序性質的。假設當前排出來的蛇的序列為 \(a_1,a_2, \cdots ,a_n\),並且保證 \(a\) 單調不減(因為有編號)。

  • 不吃:結束遊戲,不會被吃;
  • 吃:
    • 如果仍然是最強的蛇,肯定不會被吃;
    • 如果弱了,顯然 \(a_n - a_1 > a_{n-1} - a_2\),所以說如果下一條最大的蛇要吃,肯定也比當前這個最大的蛇吃掉後小,會吃。

所以說我們用一個容器,每次找到最大最小的蛇,相減比較。考慮以下按以下流程進行演算法。

  • 找到當前的最小蛇 \(x\),次小蛇 \(y\),最大蛇 \(z\)
  • 如果 \(z-x > y\),吃掉;
  • 否則,我們的答案就與「下一隻最大的蛇是否會吃」有關。再對這個子問題進行決策判斷即可。

定義「冒險」為當前的最大蛇吃掉了最小蛇變成了最小蛇。

有一個比較顯然的計算答案的方法就是每次遞迴找往上傳答案。但是實際上我們知道有蛇開始「冒險」的時候,還有多少條蛇,設為 \(p\);「冒險」的蛇有多少,設為 \(q\)。答案就是 \(p-(q \bmod 2)\)

對於 \(p\) 是顯然的,關鍵是 \((q \bmod 2)\) 是怎麼來的。

這個可以用感性理解。如果說最後一個「冒險」的蛇「冒險」之後死不掉,那麼倒數第二個「冒險」的蛇就不會選擇去「冒險」。依次往上傳,就有了這樣的答案。但是 \(O(Tn \log n)\) 的程式碼沒那麼寫。

支援動態刪除插入查詢最大最小次小值,可以用 set 去維護。時間複雜度 \(O(Tn \log n)\)

附一個 luogu 民間資料 \(75 \sim 90\) 的程式碼(程式碼很醜,而且愣是沒卡過)。

#include<cstdio>
#include<algorithm>
#include<queue>
#include<map>
#include<set>
using namespace std;
char buf[1<<16],*p1=buf,*p2=buf;
#define getchar() (p1==p2 && (p2=(p1=buf)+fread(buf,1,1<<16,stdin),p1==p2)?EOF:*p1++)
int read()
{
	int x=0,f=1;
	char c=getchar();
	while(c<'0' || c>'9')
	{
		if(c=='-')	f=-1;
		c=getchar();
	}
	while(c>='0' && c<='9')	x=(x<<1)+(x<<3)+(c^'0'),c=getchar();
	return x*f;
}
void write(int x)
{
	if(x<0)	putchar('-'),x=-x;
	if(x>9)	write(x/10);
	putchar(x%10+'0');
}
struct Snake{
	int val,pos;
	Snake(int V=0,int P=0){val=V,pos=P;}
	bool operator < (Snake ano) const {return val<ano.val || (val==ano.val && pos<ano.pos);}
	Snake operator - (Snake ano) const {return Snake(val-ano.val,pos);}
};
set<Snake> S;
int n,a[1000005],ans;
bool dfs(bool flag)
{
	if(int(S.size())==2)	return true;
	int del=0;
	while(int(S.size())>=3)
	{
		set<Snake>::iterator it1=S.begin(),it2=S.begin(),it3=S.end();
		++it2,--it3;
		int tmp=int(S.size());
		Snake x=*it1,y=*it2,z=*it3,dec=z-x;
		S.erase(it3);
		S.erase(it1);
		S.insert(dec);
		if(dec<y)
		{
			if(!dfs(true))
			{
				ans=tmp-1;
				return true;
			}
			else if(del)
			{
				ans=tmp;
				return true;
			}
			else
			{
				ans=tmp;
				return false;
			}
		}
		++del;
		if(del && flag)	return true;
	}
	ans=1;
	return true;
}
int main(){
	int T=read();
	n=read();
	long long sum=0;
	for(int i=1;i<=n;++i)	a[i]=read(),sum+=a[i];
	sum-=a[n];
	if(n==3)
	{
		if(a[1]+a[2]<=a[3])	puts("1");
		else	puts("3");
	}
	else
	{
		if(sum<=a[n])	puts("1");
		else
		{
			for(int i=1;i<=n;++i)	S.insert(Snake(a[i],i));
			ans=n;
			dfs(false);
			write(ans);
			puts("");
		}
	}
	--T;
	while(T-->0)
	{
		int k=read();
		for(int i=1;i<=k;++i)
		{
			int pos=read(),val=read();
			sum-=a[pos];
			a[pos]=val;
			sum+=a[pos];
		}
		if(n==3)
		{
			if(a[1]+a[2]<=a[3])	puts("1");
			else	puts("3");
			continue;
		}
		if(sum<=a[n])
		{
			puts("1");
			continue;
		}
		S.clear();
		for(int i=1;i<=n;++i)	S.insert(Snake(a[i],i));
		ans=n;
		dfs(false);
		write(ans);
		puts("");
	}
	return 0;
}

考慮優化,我們仔細回顧一下之前證明的那個結論。也就是 \(a_n - a_1 > a_{n-1} - a_2\)

這個性質會不會指引我們去用兩個佇列存一下原有蛇和吃掉了小蛇後變成的蛇(分別記成第一個佇列和第二個佇列)呢?

這個方法是有效的,必須滿足一下四種情況:

  • 如果最大最小蛇都是原有的,那麼新蛇小於第二個佇列中的最小蛇;
  • 如果最大最小蛇都不是原有的,那麼新蛇小於第二個佇列中的最小蛇;
  • 如果最大蛇是原有的,最小蛇不是原有的,那麼新蛇小於第二個佇列中的最小蛇;
  • 如果最大蛇不是原有的,最小蛇是原有的,那麼新蛇小於第二個佇列中的最小蛇。

第一種情況:

因為 \(a_n - a_1 > a_{n-1} - a_2\),歸納可證明;

第二種情況:

乍一看是不符合的,實際上「不是原有的」和「冒險」是等價的。這樣的話最小蛇肯定不會冒險,實際上這種情況是不存在的。

第三種情況:

同第二種情況。

第四種情況:

考慮用第一種情況歸納下來的結論,發現出現這種情況的條件是第二個佇列只有一條蛇而且這條蛇是最大蛇,所以這種情況顯然是成立的。

至此,可以用兩個雙端佇列/連結串列去維護兩個佇列,時間複雜度 \(O(Tn)\),實際操作並沒有什麼兩樣。隨便改一下應該就能過。

#include<cstdio>
#include<algorithm>
#include<queue>
#include<map>
#include<set>
using namespace std;
char buf[1<<16],*p1=buf,*p2=buf;
#define getchar() (p1==p2 && (p2=(p1=buf)+fread(buf,1,1<<16,stdin),p1==p2)?EOF:*p1++)
int read()
{
	int x=0,f=1;
	char c=getchar();
	while(c<'0' || c>'9')
	{
		if(c=='-')	f=-1;
		c=getchar();
	}
	while(c>='0' && c<='9')	x=(x<<1)+(x<<3)+(c^'0'),c=getchar();
	return x*f;
}
void write(int x)
{
	if(x<0)	putchar('-'),x=-x;
	if(x>9)	write(x/10);
	putchar(x%10+'0');
}
struct Snake{
	int val,pos;
	Snake(int V=0,int P=0){val=V,pos=P;}
	bool operator < (Snake ano) const {return val<ano.val || (val==ano.val && pos<ano.pos);}
	Snake operator - (Snake ano) const {return Snake(val-ano.val,pos);}
}q1[1000005],q2[1000005];
//set<Snake> S;
int n,a[1000005],ans,l1,r1,l2,r2;
Snake Max()
{
	if(l1>r1)	return q2[l2++];
	if(l2>r2)	return q1[r1--];
	if(q1[r1]<q2[l2])	return q2[l2++];
	return q1[r1--];
}
Snake Min()
{
	if(l1>r1)	return q2[r2--];
	if(l2>r2)	return q1[l1++];
	if(q1[l1]<q2[r2])	return q1[l1++];
	return q2[r2--];
}
void dfs()
{
	int risk=0,kill=0,dep=0;
	while(1)
	{
		++kill;
		Snake x=Max(),z=Min(),y=min(l1<=r1?q1[l1]:Snake(100860010,n+1),l2<=r2?q2[r2]:Snake(1008610010,n+1)),dec=x-z;
		if(y<dec || kill==n-1)
		{
			if(risk)
			{
				write(n-risk+(dep&1));
				puts("");
				break;
			}
			if(kill==n-1)
			{
				puts("1");
				break;
			}
			q2[++r2]=dec;
		}
		else
		{
			++dep;
			if(!risk)	risk=kill;
			q2[++r2]=dec;
		}
	}
}
int main(){
	int T=read();
	n=read();
	long long sum=0;
	for(int i=1;i<=n;++i)	a[i]=read(),sum+=a[i];
	sum-=a[n];
	if(n==3)
	{
		if(a[1]+a[2]<=a[3])	puts("1");
		else	puts("3");
	}
	else
	{
		if(sum<=a[n])	puts("1");
		else
		{
			l1=1,r1=n,l2=1,r2=0;
			for(int i=1;i<=n;++i)	q1[i]=Snake(a[i],i);
			ans=n;
			dfs();
		}
	}
	--T;
	while(T-->0)
	{
		int k=read();
		for(int i=1;i<=k;++i)
		{
			int pos=read(),val=read();
			sum-=a[pos];
			a[pos]=val;
			sum+=a[pos];
		}
		if(n==3)
		{
			if(a[1]+a[2]<=a[3])	puts("1");
			else	puts("3");
			continue;
		}
		if(sum<=a[n])
		{
			puts("1");
			continue;
		}
		l1=1,r1=n,l2=1,r2=0;
		for(int i=1;i<=n;++i)	q1[i]=Snake(a[i],i);
		ans=n;
		dfs();
	}
	return 0;
}