1. 程式人生 > 實用技巧 >「CSP-S2020」貪吃蛇

「CSP-S2020」貪吃蛇

就離譜。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;
}