【CSP2020】贪吃蛇(博弈,结论,双端队列)

Subtask 1: 55 p t s 55pts 55pts

n ≤ 2000 nleq 2000 n2000

这就变成了一道模拟题了呀。

对于每一轮,先假设最大的吃了最小的,然后往下递归每一轮,并设置数组 a l i v e alive alive,维护这之后会有哪些蛇活着。在回溯的时候,如果最大蛇的发现它自己被吃了,就撤销吃这个动作,并重新更新 a l i v e alive alive 数组。

可以用 set维护,时间复杂度 O ( n 2 ) O(n^2) O(n2)

#include<bits/stdc++.h>

#define N 1000010

using namespace std;

struct data
{
	int x,id;
	data(){};
	data(int a,int b){x=a,id=b;}
	bool operator < (const data &a) const
	{
		if(x==a.x) return id<a.id;
		return x<a.x;
	}
};

int t,n,maxn,ans,a[N],num[N],p[N];
bool alive[N],choose[N]; 

set<data>s;
vector<int>v[N];

void update(int k,bool tag)
{
	if(k>maxn) return;
	if(tag) alive[num[k]]=1;
	for(int i=0,size=v[k].size();i<size;i++)
		alive[v[k][i]]=!tag;
	if(tag&&choose[k])
	{
		alive[p[k]]=0;
		update(k+1,1);
	}
	else
	{
		alive[p[k]]=1;
		if(!tag) update(k+1,0);
	}
}

void dfs(int k)
{
	data st=*s.begin(),ed=*(--s.end()),eed=*(--(--s.end()));
	num[k]=ed.id;
	v[k].clear();
	while(s.size()>2&&ed.x-st.x>=eed.x)
	{
		data tmp=data(ed.x-st.x,ed.id);
		s.erase(s.begin());
		alive[st.id]=0;
		v[k].push_back(st.id);
		s.erase(--s.end());
		s.insert(tmp);
		st=*s.begin(),ed=*(--s.end()),eed=*(--(--s.end()));
	}
	if(s.size()==2)
	{
		alive[(*s.begin()).id]=0;
		v[k].push_back(st.id);
		alive[(*(--s.end())).id]=1;
		num[k]=ed.id;
		p[k]=0;
		maxn=k;
		return;
	}
	data tmp=data(ed.x-st.x,ed.id);
	s.erase(s.begin());
	p[k]=st.id;
	s.erase(--s.end());
	s.insert(tmp);
	dfs(k+1);
	choose[k]=alive[ed.id];
	update(k,1);
}

int main()
{
	scanf("%d",&t);
	t--;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		s.insert(data(a[i],i));
	}
	dfs(1);
	for(int i=1;i<=n;i++) ans+=alive[i];
	printf("%d
",ans);
	while(t--)
	{
		memset(alive,0,sizeof(alive));
		int k;
		scanf("%d",&k);
		for(int i=1;i<=k;i++)
		{
			int x,y;
			scanf("%d%d",&x,&y);
			a[x]=y;
		}
		s.clear();
		for(int i=1;i<=n;i++)
			s.insert(data(a[i],i));
		dfs(1);
		ans=0;
		for(int i=1;i<=n;i++) ans+=alive[i];
		printf("%d
",ans);
	}
	return 0;
}

Subtask2: 70 p t s 70pts 70pts

n ≤ 5 × 1 0 4 nleq5 imes 10^4 n5×104

55 p t s sout{55pts} 55pts 还好写。

发现如果进行到这一轮,那么这一轮的操作是固定的。(意思就是第 x x x 轮是谁吃谁是固定的)

那么就把每一轮是谁吃谁维护出来,最后在从后往前用桶扫一遍

set很好维护,时间复杂度 O ( n log ⁡ n ) O(nlog n) O(nlogn)

其实这个和 Subtask1 的做法的思想差不多,只不过这种做法是从首轮往末轮找哪个人最先停止,时间最坏就 O ( n ) O(n) O(n),而 Subtask1 的做法是从末轮往首轮找,时间最坏能 O ( n 2 ) O(n^2) O(n2)

Subtask3: 100 p t s 100pts 100pts

n ≤ 1 0 6 nleq 10^6 n106

我们不用模拟所有轮过程的这个思路,回到原来题目中那个博弈的问题上来。

不妨设 check ⁡ ( i ) operatorname{check}(i) check(i) 表示第 i i i 轮的最强蛇吃了最弱蛇之后会不会被吃,那么我们就是要求出所有的 check ⁡ operatorname{check} check 或者找到第一个 i i i 使得 check ⁡ ( i ) = 0 operatorname{check}(i)=0 check(i)=0,因为在 check ⁡ ( i ) = 0 operatorname{check}(i)=0 check(i)=0 时,最强蛇就会在这一轮停止。

首先是一个比较重要的结论:

如果当前最强的蛇吃了最弱的蛇之后,如果没有变成最弱的蛇,它就可以放心吃,也就是说此时 check ⁡ ( i ) = 1 operatorname{check}(i)=1 check(i)=1

证明:

  1. 如果最强蛇吃完之后仍为最强蛇,那不吃白不吃。

  2. 如果最强蛇吃完之后不是最强蛇,那么设最强蛇为 A A A,次强蛇为 B B B,次弱蛇为 C C C,最弱蛇为 D D D。那么 A ≥ B ≥ C ≥ D Ageq B geq C geq D ABCD

    首先第一轮取出 A A A D D D,扔入 A − D A-D AD,其中满足 B > A − D ≥ C B>A-Dgeq C B>ADC A A A 吃完 D D D 之后不是最弱蛇,也不是最强蛇)。

    那么下一轮扔进去的肯定就是 B − C B-C BC,又由于 B − C ≤ A − D B-Cleq A-D BCAD,所以如果我之后再要吃掉 A − D A-D AD,肯定要先吃掉 B − C B-C BC,那 B − C B-C BC 能不能保证自己不被吃呢?答案是肯定的,因为 B B B 可以选择不吃 C C C 而是停止游戏,这样 B B B 肯定不会被吃,所以 A − D A-D AD 也不会被吃掉。

证毕。

但如果当前最强的蛇吃了最弱蛇之后变成了最弱的蛇呢?

不妨设当前轮为第 i i i 轮,那么 check ⁡ ( i ) operatorname{check}(i) check(i) 的取值就要取决于:在最强蛇吃了最弱蛇、变成最弱蛇后,下一轮的最强蛇是否选择吃,如果选择吃。

即如果 check ⁡ ( i + 1 ) = 1 operatorname{check}(i+1)=1 check(i+1)=1,那么 check ⁡ ( i ) = 0 operatorname{check}(i)=0 check(i)=0,否则 check ⁡ ( i ) = 1 operatorname{check}(i)=1 check(i)=1

递归的味道就很明显了。

递归边界就是到最后又重新出现了第一种情况或者只剩两条蛇。

有人可能问:那这个和 O ( n 2 log ⁡ n ) O(n^2 log n) O(n2logn) 的做法有什么区别吗?

答案是有的,按照这种方法,每一轮的 check ⁡ ( i ) operatorname{check}(i) check(i) 的取值,只会和下一轮的 check ⁡ ( i + 1 ) operatorname{check}(i+1) check(i+1) 的取值有关,而并不需要维护 a l i v e alive alive 数组,而这一切的原因都是我们发现的那个重要的结论。

同样地,如果我们用 set维护,时间复杂度还是 O ( n log ⁡ n ) O(nlog n) O(nlogn) 的。

考虑 O ( n ) O(n) O(n) 维护。

发现第二种情况(最强蛇吃了最弱蛇之后变成最弱蛇)是可以 O ( n ) O(n) O(n) 维护的,主要是如何维护第一种情况(最强蛇吃了最弱蛇之后不是最弱蛇)。

不妨设当前蛇的能力序列为 a 1 , ⋯   , a n a_1,cdots,a_n a1,,an,那么第一种情况就是 a n − a 1 ≥ a 2 a_n-a_1geq a_2 ana1a2

那么这一轮操作后最小值就变为了 a 2 ≥ a 1 a_2geq a_1 a2a1,最大值变为了 max ⁡ ( a n − 1 , a n − a 1 ) ≤ a 1 max(a_{n-1},a_n-a_1)leq a_1 max(an1,ana1)a1

发现如果一直在第一种情况,这个序列的最小值是单调不下降的,最大值是单调不上升的,那么这个序列最大值与最小值的差也是单调不上升的。

于是启发了我们用两个单调不下降的队列 q 1 q_1 q1 q 2 q_2 q2 来表示这个序列 a a a,并维护这个操作。

也就是说,当前的序列 a a a 可以用 q 1 q_1 q1 q 2 q_2 q2 归并排序后的序列表示。

一开始先设 q 1 q_1 q1 序列就是 a a a 序列, q 2 q_2 q2 序列为空。

然后每次从取出全局最小值 m i n n = min ⁡ ( q 1 , h e a d , q 2 , h e a d ) minn=min(q_{1,head},q_{2,head}) minn=min(q1,head,q2,head) m a x n = max ⁡ ( q 1 , t a i l , q 2 , t a i l ) maxn=max(q_{1,tail},q_{2,tail}) maxn=max(q1,tail,q2,tail),然后将 m a x n − m i n n maxn-minn maxnminn 插入到 q 2 q_2 q2 的头。

容易发现 q 2 q_2 q2 是单调不下降的,因为我们刚刚已经的出了结论:序列 a a a 的最大值和最小值的差是单调不上升的。

那么第一种情况就能 O ( n ) O(n) O(n) 维护了。

代码如下:

#include<bits/stdc++.h>

#define N 1000010

using namespace std;

struct data
{
	int x,id;
	data(){};
	data(int a,int b){x=a,id=b;}
};

bool operator < (data a,data b)
{
	if(a.x==b.x) return a.id<b.id;
	return a.x<b.x;
}

bool operator > (data a,data b)
{
	if(a.x==b.x) return a.id>b.id;
	return a.x>b.x;
}

data operator - (data a,data b){return data(a.x-b.x,a.id);}

struct Queue
{
	private:
		int head,tail;
		data q[N<<1];
	public:
		void clear(){tail=1e6,head=tail+1;}
		int size(){return tail-head+1;}
		bool empty(){return !size();}
		data front(){return q[head];}
		data back(){return q[tail];}
		void push_front(data x){q[--head]=x;}
		void push_back(data x){q[++tail]=x;}
		void pop_front(){head++;}
		void pop_back(){tail--;}
}a,b,c;

data getmin(bool flag)//求两个队列的最小值(flag=1就顺便pop掉)
{
	data minn;
	bool tag;
	if(a.empty()) minn=b.front(),tag=1;
	else if(b.empty()) minn=a.front(),tag=0;
	else
	{
		if(a.front()<b.front()) minn=a.front(),tag=0;
		else minn=b.front(),tag=1;
	}
	if(flag)
	{
		if(!tag) a.pop_front();
		else b.pop_front();
	}
	return minn;
}

data getmax(bool flag)//求两个队列的最大值(flag=1就顺便pop掉)
{
	data maxn;
	bool tag;
	if(a.empty()) maxn=b.back(),tag=1;
	else if(b.empty()) maxn=a.back(),tag=0;
	else
	{
		if(a.back()>b.back()) maxn=a.back(),tag=0;
		else maxn=b.back(),tag=1;
	}
	if(flag)
	{
		if(!tag) a.pop_back();
		else b.pop_back();
	}
	return maxn;
}

int T,n,k,val[N];

bool check(int step)
{
	if(c.size()==2) return 1;
	data minn=c.front(),maxn=c.back();
	c.pop_front(),c.pop_back();
	data smin=c.front();
	if(maxn-minn>smin) return 1;
	c.push_front(maxn-minn);
	return !check(step+1);
}

void work()
{
	//处理第一种情况
	int sum=-1;
	for(int i=1;i<n-1;i++)
	{
		data minn=getmin(1),maxn=getmax(1);
		data smin=getmin(0);
		b.push_front(maxn-minn);
		if(maxn-minn<smin)
		{
			sum=i;
			break;
		}
	}
	if(a.size()+b.size()==2&&sum==-1)
	{
		printf("%d
",1);
		return;
	}
	//归并排序还原序列
	c.clear();
	while((!a.empty())&&(!b.empty()))
	{
		if(a.front()<b.front())
		{
			c.push_back(a.front());
			a.pop_front();
		}
		else
		{
			c.push_back(b.front());
			b.pop_front();
		}
	}
	while(!a.empty())
	{
		c.push_back(a.front());
		a.pop_front();
	}
	while(!b.empty())
	{
		c.push_back(b.front());
		b.pop_front();
	}
	//处理第二种情况
	bool flag=check(sum+1);
	if(flag)
	{
		printf("%d
",n-sum+1);
		return;
	}
	else
	{
		printf("%d
",n-sum);
		return;
	}
}

int main()
{
//	freopen("T4.in","r",stdin);
//	freopen("T4.out","w",stdout);
	scanf("%d%d",&T,&n);
	a.clear(),b.clear();
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&val[i]);
		a.push_back(data(val[i],i));
	}
	work();
	T--;
	while(T--)
	{
		scanf("%d",&k);
		for(int i=1;i<=k;i++)
		{
			int x,y;
			scanf("%d%d",&x,&y);
			val[x]=y;
		}
		a.clear(),b.clear();
		for(int i=1;i<=n;i++)
			a.push_back(data(val[i],i));
		work();
	}
	return 0;
}
/*
1 10
60 64 78 111 123 176 210 311 353 354
*/
原文地址:https://www.cnblogs.com/ez-lcw/p/14448639.html