CSP-S 2020 题解

T1.儒略日

题目描述

为了简便计算,天文学家们使用儒略日(Julian day)来表达时间。所谓儒略日,其定义为从公元前 4713 年 1 月 1 日正午 12 点到此后某一时刻间所经过的天数,不满一天者用小数表达。若利用这一天文学历法,则每一个时刻都将被均匀的映射到数轴上,从而得以很方便的计算它们的差值。

现在,给定一个不含小数部分的儒略日,请你帮忙计算出该儒略日(一定是某一天的中午 12 点)所对应的公历日期。

我们现行的公历为格里高利历(Gregorian calendar),它是在公元 1582 年由教皇格里高利十三世在原有的儒略历(Julian calendar)的基础上修改得到的(注:儒略历与儒略日并无直接关系)。具体而言,现行的公历日期按照以下规则计算:

  1. 公元 1582 年 10 月 15 日(含)以后:适用格里高利历,每年一月 31 31 31 天、 二月 28 28 28 天或 29 29 29 天、三月 31 31 31 天、四月 30 30 30 天、五月 31 31 31 天、六月 30 30 30 天、七月 31 31 31 天、八月 31 31 31 天、九月 30 30 30 天、十月 31 31 31 天、十一月 30 30 30 天、十二月 31 31 31 天。其中,闰年的二月为 29 29 29 天,平年为 28 28 28 天。当年份是 400 400 400 的倍数,或日期年份是 4 4 4 的倍数但不是 100 100 100 的倍数时,该年为闰年。
  2. 公元 1582 年 10 月 5 日(含)至 10 月 14 日(含):不存在,这些日期被删除,该年 10 月 4 日之后为 10 月 15 日。
  3. 公元 1582 年 10 月 4 日(含)以前:适用儒略历,每月天数与格里高利历相同,但只要年份是 4 4 4 的倍数就是闰年。
  4. 尽管儒略历于公元前 45 年才开始实行,且初期经过若干次调整,但今天人类习惯于按照儒略历最终的规则反推一切 1582 年 10 月 4 日之前的时间。注意,公元零年并不存在,即公元前 1 年的下一年是公元 1 年。因此公元前 1 年、前 5 年、前 9 年、前 13 年……以此类推的年份应视为闰年。

思路

这题模拟就好,其实主要是细节较多,代码很容易打错。
这两种纪年法大体相同,不同的地方只有闰年的判断。我们可以考虑按照闰年来打。注意到儒略历在公元前后闰年的计算方式不同,把公元前后分为不同两端。加上 1582 1582 1582 年之后的那段,总共分成三段。

  1. − 4713 / 1 / 1 -4713/1/1 4713/1/1 − 1 / 12 / 31 -1/12/31 1/12/31 ,此时 r < = 1721423 r<=1721423 r<=1721423 ,判断闰年(y+1)%4==0 y y y 为年份)。注意到四年( 1461 1461 1461 天)一循环,我们直接计算有多少个四年,再进一步求出月份和天数。
  2. 1 / 1 / 1 1/1/1 1/1/1 1582 / 10 / 4 1582/10/4 1582/10/4 ,此时 1721423 < r < = 2299160 1721423<r<=2299160 1721423<r<=2299160 ,判断闰年 y%4==0 ,要找的是 1 / 1 / 1 1/1/1 1/1/1 以后的 ( r − 1721423 − 1 ) (r-1721423-1) (r17214231) 天,后面和之前一样。
  3. 1582 / 10 / 15 1582/10/15 1582/10/15 及以后,此时 r > 2299160 r>2299160 r>2299160 ,判断闰年(y%400==0)||(y%100!=0&&y%4==0)。为了方便处理,我们假设这一段的第一天为 1582 / 10 / 1 1582/10/1 1582/10/1 ,最后加上相应的天数(14)就行,也就是说,要找 1582 / 10 / 1 1582/10/1 1582/10/1 按照格里高利历后面的 ( r − 2299160 + 14 − 1 ) (r-2299160+14-1) (r2299160+141) 天。此时是 400 400 400 年( 146097 146097 146097 天)一循环,因此直接计算有多少个 400 400 400 年,再依次枚举 100 100 100 年、 1 1 1 年,最后求出月份和天数

具体细节请看代码。

代码

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
const int t1=2299160,t2=1721423;
int dys[2][13]={
	{0,31,28,31,30,31,30,31,31,30,31,30,31},
	{0,31,29,31,30,31,30,31,31,30,31,30,31}
};
int y,m,d;
void calc1(ll r){  //第一种情况
	y=-4713; m=1; d=1;
	y+=(r/1461)*4; r=r%1461;  //4年个数
	while(r>=365+((y+1)%4==0)){  //年
		r-=(((y+1)%4==0)+365);
		y+=1;
	}
	while(r>=dys[((y+1)%4==0)][m]){  //月
		r-=dys[((y+1)%4==0)][m];
		m+=1;
	}
	d+=r;  //日
	return;
}
void calc2(ll r){  //第二种情况
	y=1; m=1; d=1;
	y+=(r/1461)*4; r=r%1461;  //4年个数
	while(r>=365+(y%4==0)){  //年
		r-=((y%4==0)+365);
		y+=1;
	}
	while(r>=dys[(y%4==0)][m]){  //月
		r-=dys[(y%4==0)][m];
		m+=1;
	}
	d+=r;  //日
	return;
}
void clac3(ll r){  //第三种情况
	y=1582; m=10; d=1;
	y+=(r/146097ll)*400; r=r%146097ll;  //400年个数
	while(r>=36524+((y/100+1)%4==0)){  //100年
		r-=(36524+((y/100+1)%4==0));
		y+=100;
	}
	while(r>=365+((y+1)%400==0||((y+1)%100!=0&&(y+1)%4==0))){  //年
		r-=(365+((y+1)%400==0||((y+1)%100!=0&&(y+1)%4==0)));
		y+=1;
	}
	while(r>=dys[(y%400==0||(y%100!=0&&y%4==0))][m]){  //月
		r-=dys[(y%400==0||(y%100!=0&&y%4==0))][m];
		m+=1;
		if(m==13) m=1,y+=1;
	}
	d+=r;  //日
	return;
}
int main(){
//	freopen("julian.in","r",stdin);
//	freopen("julian.out","w",stdout);
	int q;
	ll r;
	scanf("%d",&q);
	while(q--){
		scanf("%lld",&r);
		if(r<=t2) calc1(r);  //分三种情况讨论
		else if(r<=t1) calc2(r-t2-1ll);
		else clac3(r-t1+13ll);
		if(y<0) printf("%d %d %d BC\n",d,m,-y);  //公元前
		else printf("%d %d %d\n",d,m,y);  //公元后
	}
//	fclose(stdin);
//	fclose(stdout);
	return 0;
}

T2.动物园

题目描述

动物园里饲养了很多动物,饲养员小 A 会根据饲养动物的情况,按照《饲养指南》购买不同种类的饲料,并将购买清单发给采购员小 B。

具体而言,动物世界里存在 2 k 2^k 2k 种不同的动物,它们被编号为 0 ∼ 2 k − 1 0 \sim 2^k-1 02k1。动物园里饲养了其中的 n n n 种,其中第 i i i 种动物的编号为 a i a_i ai

《饲养指南》中共有 m m m 条要求,第 j j j 条要求形如“如果动物园中饲养着某种动物,满足其编号的二进制表示的第 p j p_j pj 位为 1 1 1,则必须购买第 q j q_j qj 种饲料”。其中饲料共有 c c c 种,它们从 1 ∼ c 1 \sim c 1c 编号。本题中我们将动物编号的二进制表示视为一个 k k k 位 01 串,第 0 0 0 位是最低位,第 k − 1 k - 1 k1 位是最高位。

根据《饲养指南》,小 A 将会制定饲料清单交给小 B,由小 B 购买饲料。清单形如一个 c c c 01 01 01 串,第 i i i 位为 1 1 1 时,表示需要购买第 i i i 种饲料;第 i i i 位为 0 0 0 时,表示不需要购买第 i i i 种饲料。 实际上根据购买到的饲料,动物园可能可以饲养更多的动物。更具体地,如果将当前未被饲养的编号为 x x x 的动物加入动物园饲养后,饲料清单没有变化,那么我们认为动物园当前还能饲养编号为 x x x 的动物。

现在小 B 想请你帮忙算算,动物园目前还能饲养多少种动物。

思路

先求出饲料清单。根据《饲养指南》,每一种动物需要购买的饲料只与编号中为 1 1 1 的位有关。因此所有要购买的饲料与所有动物为 1 1 1 的为有关。如果某种编号的动物第 i i i 位为 1 1 1 ,并且第 i i i 位有买饲料的要求,则无论其他编号第 i i i 位上是否为 1 1 1 都要买这(几种)饲料。因此只要求出所有可能为 1 1 1 的位就可以了,也就是说,如果令 a = a [ 1 ] ∣ a [ 2 ] ∣ a [ 3 ] ∣ . . . ∣ a [ n ] a=a[1]|a[2]|a[3]|...|a[n] a=a[1]a[2]a[3]...a[n]那么我们只需要对 a a a 进行处理。从这里也可以看出,每一位对应要买的饲料种类对本题没任何影响。
接下来求出能饲养的动物的种数 a n s ans ans ,则初始化 a n s = 1 ans=1 ans=1 。假设已把上面的 a a a 求出来了,那么我们枚举它某一位 i i i,有如下三种情况:

  1. 如果这一位无饲料要求,则能饲养的动物中这一位可能为 0 0 0,也可能为 1 1 1 ,根据乘法原理(下同), a n s ∗ = 2 ans*=2 ans=2
  2. 如果这一位有饲料要求,且数值为 1 1 1 ,也就是说对应的饲料已经购买,取 0 0 0 1 1 1无所谓, a n s ∗ = 2 ans*=2 ans=2
  3. 如果这一位有饲料要求,且数值为 0 0 0 ,也就是说对应的饲料没有购买,这一位上只能取 0 0 0 a n s ∗ = 1 ans*=1 ans=1

因此只需要对位数进行枚举就能求出 a n s ans ans ,最终答案应为 a n s − n ans-n ansn 。值得注意的是,位数最大为 64 64 64 ,刚好爆 u n s i g n e d   l o n g   l o n g unsigned\ long\ long unsigned long long,则 a n s = 0 ans=0 ans=0 ,需要特判。

代码

#include<iostream>
#include<cstdio>
#define ull unsigned long long
using namespace std;
int m,c,k,p,q,vi[64];
ull n,a[1000001],ans=1ull;
int main(){
//	freopen("zoo.in","r",stdin);
//	freopen("zoo.out","w",stdout);
	scanf("%llu%d%d%d",&n,&m,&c,&k);
	for(int i=1;i<=n;i+=1){
		scanf("%llu",&a[i]);
		a[0]|=a[i];  //a[0]即为上文中的a
	}
	while(m--) scanf("%d%d",&p,&q),vi[p]=1;  //记录有饲料要求的位
	for(int i=0;i<k;i+=1){
		if(vi[i]) ans*=(((a[0]>>i)&1)+1);  //有饲料要求
		if(!vi[i]) ans*=2ull;  //无饲料要求
	}
	if(ans==0&&n==0) printf("18446744073709551616\n");  //特判,输出2^64
	else printf("%llu\n",ans-n);  //一般情况
//	fclose(stdin);
//	fclose(stdout);
	return 0;
}

T3.函数调用

题目描述

函数是各种编程语言中一项重要的概念,借助函数,我们总可以将复杂的任务分解成一个个相对简单的子任务,直到细化为十分简单的基础操作,从而使代码的组织更加严密、更加有条理。然而,过多的函数调用也会导致额外的开销,影响程序的运行效率。
某数据库应用程序提供了若干函数用以维护数据。已知这些函数的功能可分为三类:

  1. 将数据中的指定元素加上一个值;
  2. 将数据中的每一个元素乘以一个相同值;
  3. 依次执行若干次函数调用,保证不会出现递归(即不会直接或间接地调用本身)。

在使用该数据库应用时,用户可一次性输入要调用的函数序列(一个函数可能被调用多次),在依次执行完序列中的函数后,系统中的数据被加以更新。某一天,小 A 在应用该数据库程序处理数据时遇到了困难:由于频繁而低效的函数调用,系统在执行操作时进入了无响应的状态,他只好强制结束了数据库程序。为了计算出正确数据,小 A 查阅了软件的文档,了解到每个函数的具体功能信息,现在他想请你根据这些信息帮他计算出更新后的数据应该是多少。答案对 998244353 998244353 998244353 取模

骗分思路&代码

看到这道题我的第一感觉就是线段树。事实上,第三类函数可以拆分成若干个前两类函数轮流作用的效果。再加上原本就有的前两类函数,就有大量的区间、单点操作。由于是整体乘,我们只需在线段树的根节点打上标记就行,等到单点修改或求值时再下传。这竟然也能骗到 70 70 70 分!

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
int n,m,p[100001],c[100001],t[100001],g[1000001],cnt,q,f;
ll a[400001],mod=998244353,v[100001];
void build(int k,int l,int r){  //建树
	if(l==r){
		scanf("%lld",&a[k]);
		return;
	}
	int mid=(l+r)/2;
	build(k*2,l,mid); build(k*2+1,mid+1,r);
	a[k]=1;
	return;
}
void pushdown(int k){  //标记下传
	if(a[k]==1) return;
	a[k*2]=(a[k*2]*a[k])%mod;
	a[k*2+1]=(a[k*2+1]*a[k])%mod;
	a[k]=1;
	return;
}
void add(int k,int l,int r,int x,ll y){  //单点修改
	if(l==r&&r==x){
		a[k]=(a[k]+y);
		return;
	}
	pushdown(k);
	int mid=(l+r)/2;
	if(mid>=x) add(k*2,l,mid,x,y);
	if(mid+1<=x) add(k*2+1,mid+1,r,x,y);
	return;
}
void print(int k,int l,int r){  //求解,输出
	if(l==r){
		printf("%lld ",a[k]);
		return;
	}
	pushdown(k);
	int mid=(l+r)/2;
	print(k*2,l,mid); print(k*2+1,mid+1,r);
	return;
}
void deal(int x){  //处理单个函数
	if(t[x]==1) add(1,1,n,p[x],v[x]);
	if(t[x]==2) a[1]=(a[1]*v[x])%mod;  //区间修改
	if(t[x]==3){
		for(int i=p[x];i<=p[x]+c[x]-1;i+=1){
			deal(g[i]);
		}
	}
	return;
}
int main(){
//	freopen("call.in","r",stdin);
//	freopen("call.out","w",stdout);
	scanf("%d",&n);
	build(1,1,n);
	scanf("%d",&m);
	for(int j=1;j<=m;j+=1){
		scanf("%d",&t[j]);
		if(t[j]==1) scanf("%d%d",&p[j],&v[j]);
		if(t[j]==2) scanf("%d",&v[j]);
		if(t[j]==3){
			scanf("%d",&c[j]); p[j]=cnt;
			for(int i=cnt;i<=c[j]+cnt-1;i+=1) scanf("%d",&g[i]);
			cnt+=c[j];
		}
	}
	scanf("%d",&q);
	while(q--){
		scanf("%d",&f);
		deal(f);
	}
	print(1,1,n);printf("\n");
//	fclose(stdin);
//	fclose(stdout);
	return 0;
}

满分思路&代码

如果将函数的调用关系看成有向边,再将操作序列看成一个第三类函数 m + 1 m+1 m+1 ,并将 m + 1 m+1 m+1 与所有操作序列中的函数连边,再由“保证不会出现递归”可知所有边构成有向无环图 DAG 。暂时地,我们先把它看作一棵树。
在这里插入图片描述
程序调用了函数 m + 1 m+1 m+1 后,序列中的每一个值由 a [ i ] a[i] a[i] 变成了 b ∗ a [ i ] + d [ i ] b*a[i]+d[i] ba[i]+d[i] 。其中 b b b 为所有调用的第二类函数中的 v v v 值的乘积,而 c [ i ] c[i] c[i] 的值与每个作用于i上的第一类函数及这个函数后面所有第二类函数乘积有关,也就是说 d [ i ] = ∑ x ∣ t [ x ] = 1 且 p [ x ] = i k [ x ] ∗ v [ x ] d[i]=\sum_{x|t[x]=1且p[x]=i}{k[x]*v[x]} d[i]=xt[x]=1p[x]=ik[x]v[x]其中的 k [ x ] k[x] k[x] 表示函数x的贡献被“放大”的倍数,它只跟后面调用的有关。比方说现在要求 k [ 6 ] k[6] k[6] 值,则它与函数 9 9 9 8 8 8 3 3 3 有关。若以 f [ i ] [ j ] f[i][j] f[i][j] 表示函数 i i i 调用的第 j j j 个函数给序列乘的值,则 f [ i ] [ j ] f[i][j] f[i][j] 可以通过一遍深搜得到,函数 9 9 9 的影响可以通过 f f f 数组传到函数 7 7 7 ,函数 3 3 3 的影响也传到了 k [ 5 ] k[5] k[5] 。因此, k [ 6 ] = k [ 5 ] ∗ f [ 5 ] [ 2 ] ∗ f [ 5 ] [ 3 ] k[6]=k[5]*f[5][2]*f[5][3] k[6]=k[5]f[5][2]f[5][3] 。若有函数 u u u 调用函数 v v v ,且 v v v 在函数 u u u 中的下标为 j j j ,则有 k [ v ] = k [ u ] ∏ a = j + 1 c [ u ] f [ u ] [ a ] k[v]=k[u]\prod_{a=j+1}^{c[u]}{f[u][a]} k[v]=k[u]a=j+1c[u]f[u][a]时间主要耗费在求乘积上,因此我们可以用后缀积的方式优化。如果 f ′ [ i ] [ j ] f'[i][j] f[i][j] 为旧数组,那么重新定义 f [ i ] [ j ] = ∏ a = j + 1 c [ i ] f ′ [ i ] [ a ] f[i][j]=\prod_{a=j+1}^{c[i]}{f'[i][a]} f[i][j]=a=j+1c[i]f[i][a]注意到那并不是一棵树,而是一张有向无环图,也就是说同一个函数会被调用多次,这需要我们把上式修正一下。假设现在调用关系长这样
在这里插入图片描述
那么 k [ 6 ] k[6] k[6] 就能够被两种途径分别得到。假设某个函数i由两种路径分别得到的是 k k k k ′ k' k ,则有如下三种情况:

  1. t [ i ] = 1 t[i]=1 t[i]=1 ,则它对p[i]的两次贡献分别为 v [ i ] ∗ k v[i]*k v[i]k v [ i ] ∗ k ′ v[i]*k' v[i]k ,总贡献为 v [ i ] ∗ k + v [ i ] ∗ k ′ = v [ i ] ∗ ( k + k ′ ) v[i]*k+v[i]*k'=v[i]*(k+k') v[i]k+v[i]k=v[i](k+k) ,故可以令 k [ i ] = k + k ′ k[i]=k+k' k[i]=k+k
  2. t [ i ] = 2 t[i]=2 t[i]=2 ,则 k [ i ] k[i] k[i] 本身并无多大意义,也同样赋值 k [ i ] = k + k ′ k[i]=k+k' k[i]=k+k
  3. t [ i ] = 3 t[i]=3 t[i]=3 ,则它可以转移到它调用的一个函数 j j j,即 k [ j ] = k ∗ f [ i ] [ j ] + k ′ ∗ f [ i ] [ j ] = ( k + k ′ ) ∗ f [ i ] [ j ] k[j]=k*f[i][j]+k'*f[i][j]=(k+k')*f[i][j] k[j]=kf[i][j]+kf[i][j]=(k+k)f[i][j] ,同样 k [ i ] = k + k ′ k[i]=k+k' k[i]=k+k

对于有两条以上的路径,也做类似讨论。因此最终得出 k [ v ] = ∑ k [ u ] ∗ f [ u ] [ j ] k[v]=\sum{k[u]*f[u][j]} k[v]=k[u]f[u][j]即k数组由父节点转向子节点,结合有向无环图,可以用拓扑排序求解k数组。
深搜和拓扑排序的时间复杂度均小于 O ( m + ∑ c j ) O(m+\sum c_{j}) O(m+cj)

#include<iostream>
#include<cstdio>
#include<vector>
#define ll long long
using namespace std;int xhx;
const ll mod=998244353;
const int N=100005;
ll a[N],v[N],k[N];
int n,m,q,g;
int t[N],p[N],c;
int deg[N],vi[N];
int top,stk[N];
vector<int>to[N];
vector<ll>f[N];
void add(int x,int y){
	to[x].push_back(y);
	return;
}
void dfs(int x){  //深搜求解f[i][j]
	vi[x]=1;
	if(t[x]==1){
		f[x].push_back(1ll);
		return;
	}
	if(t[x]==2){
		f[x].push_back(v[x]);
		return;
	}
	int y,s=to[x].size();
	if(!s){
		f[x].push_back(1ll);
		return;
	}
	f[x].resize(to[x].size());
	for(int i=s-1;i>=0;i-=1){
		y=to[x][i];
		deg[y]+=1;  //入度在这里处理
		if(!vi[y]) dfs(y);
		f[x][i]=(f[y][0]*(i==s-1?1ll:f[x][i+1])%mod)%mod;
	}
	return;
}
void deal(int x){
	top-=1;
	int y,s=to[x].size();
	for(int i=0;i<s;i+=1){
		y=to[x][i];
		deg[y]-=1;
		if(deg[y]==0) stk[++top]=y;
		k[y]=(k[y]+k[x]*(i==s-1?1ll:f[x][i+1])%mod)%mod;  //求解k[i]
	}
	return;
}
int main(){
//	freopen("call.in","r",stdin);
//	freopen("call.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i+=1) scanf("%lld",&a[i]);
	scanf("%d",&m);
	for(int i=1;i<=m;i+=1){
		scanf("%d",&t[i]);
		if(t[i]==1) scanf("%d%lld",&p[i],&v[i]);
		if(t[i]==2) scanf("%lld",&v[i]);
		if(t[i]==3){
			scanf("%d",&c);
			while(c--){
				scanf("%d",&g);
				add(i,g);
			}
		}
	}
	scanf("%d",&q);
	for(int i=1;i<=q;i+=1){
		scanf("%d",&g);
		add(m+1,g);
	}
	dfs(m+1);
	stk[++top]=m+1;  //拓扑排序
	k[m+1]=1;
	while(top){
		deal(stk[top]);
	}
	for(int i=1;i<=n;i+=1) a[i]=(a[i]*f[m+1][0])%mod;  //b=f[m+1][0]
	for(int i=1;i<=m;i+=1){
		if(t[i]==1){
			a[p[i]]=(a[p[i]]+k[i]*v[i]%mod)%mod;  //+d[i]
		}
	}
	for(int i=1;i<=n;i+=1) printf("%lld ",a[i]);
	printf("\n");
//	fclose(stdin);
//	fclose(stdout);
	return 0;
}

T4.贪吃蛇

题目描述

草原上有 n n n 条蛇,编号分别为 1 , 2 , … , n 1, 2, \ldots , n 1,2,,n 。初始时每条蛇有一个体力值 a i a_i ai ,我们称编号为 x x x 的蛇实力比编号为 y y y 的蛇强当且仅当它们当前的体力值满足 a x > a y a_x > a_y ax>ay ,或者 a x = a y a_x = a_y ax=ay x > y x > y x>y

接下来这些蛇将进行决斗,决斗将持续若干轮,每一轮实力最强的蛇拥有选择权,可以选择吃或者不吃掉实力最弱的蛇:

  1. 如果选择吃,那么实力最强的蛇的体力值将减去实力最弱的蛇的体力值,实力最弱的蛇被吃掉,退出接下来的决斗。之后开始下一轮决斗。
  2. 如果选择不吃,决斗立刻结束。

每条蛇希望在自己不被吃的前提下在决斗中尽可能多吃别的蛇(显然,蛇不会选择吃自己)。

现在假设每条蛇都足够聪明,请你求出决斗结束后会剩几条蛇。

本题有多组数据,对于第一组数据,每条蛇体力会全部由输入给出,之后的每一组数据,会相对于上一组的数据,修改一部分蛇的体力作为新的输入。

思路

想要做出这道题的人必须变得和蛇一样聪明!
对于目前最长的蛇(假设为 A A A ),和它要吃的蛇(也是本轮最短的蛇,假设为 B B B ),有如下 3 3 3 种情况:

  1. A A A 吃完 B B B 后,仍是最长的蛇,此时一定选择吃;
  2. A A A 吃完 B B B 后,不再是最短的蛇,也不是最长的蛇。如果在它后面的蛇选择吃,由于其本身不比 A A A 长,吃的蛇也不比 B B B,故吃完后的长度不比 A A A 吃完后的长度长,故 A A A 还有一次选择吃或不吃的机会,因此在本轮选择吃;
  3. A A A 吃完 B B B 后,变成最短的蛇,那么 A A A 的生死将掌握在下一条蛇那里。如果下一条蛇选择吃,那么 A A A 选择不吃,否则 A A A 选择把 B B B 吃掉。现在问题变为考虑下一条吃不吃的问题。

还有另一种情况:剩下两条蛇。这时最长的蛇一定选择吃。
对于前两种情况,我们只要判断并模拟就行,每轮将蛇的条数减一。
不难发现,对于第三种情况,如果 A A A 选择不吃,那么游戏结束;否则下一条蛇也必定会选择不吃。因此继续模拟,每轮将蛇的条数减去或加上一(对应着最开始出现第三种情况的蛇吃或不吃),直到找到一条必吃的蛇为止。我们用一个 f l a g flag flag 记录,最开始出现第三种情况时,令 f l a g = 1 flag=1 flag=1 ,每轮只要将剩下蛇的条数减去 f l a g flag flag ,再令 f l a g = − f l a g flag=-flag flag=flag 就行。
对于找到当前最长或最短的蛇,我们只需要用两个双端队列维护就行。在上面的讨论中,不难发现产生新蛇的长度是单调不递增的,用两个双端队列 q 1 q1 q1 q 2 q2 q2 ,就能解决这类问题。 q 1 q1 q1 存储当前还是原长的蛇, q 2 q2 q2 存储产生的新蛇。如果两个队列都从队头到队尾单调不递减,那么每次都从 q 2 q2 q2 队头加入新蛇,从两个队列的队尾或队头查找最长蛇或最短蛇就行了。

代码

#include<iostream>
#include<cstdio>
#include<deque>
using namespace std;
int t,n,k,a[1000001],ans,flag;
deque<pair<int,int> >q1,q2;
pair<int,int> take_max(){  //取出并弹出最长蛇
	pair<int,int>x;
	if(!q1.empty()){
		if(q2.empty()||q1.back()>q2.back()) x=q1.back(),q1.pop_back();
		else x=q2.back(),q2.pop_back();
	}
	else x=q2.back(),q2.pop_back();
	return x;
}
pair<int,int> take_min(){  //取出并弹出最短蛇
	pair<int,int>x;
	if(!q1.empty()){
		if(q2.empty()||q1.front()<=q2.front()) x=q1.front(),q1.pop_front();
		else x=q2.front(),q2.pop_front();
	}
	else x=q2.front(),q2.pop_front();
	return x;
}
pair<int,int> find_min(){  //查询但不弹出最短蛇
	pair<int,int>x;
	if(!q1.empty()){
		if(q2.empty()||q1.front()<=q2.front()) x=q1.front();
		else x=q2.front();
	}
	else x=q2.front();
	return x;
}
void solve(){
	ans=n; flag=0;  //ans表示剩下蛇的条数
	q1.clear(); q2.clear();  //记得要清空
	pair<int,int>x,y,z;
	for(int i=1;i<=n;i+=1){
		q1.push_back(make_pair(a[i],i));  //数据保证每条蛇的长度单调不递减
	}
	while(q1.size()+q2.size()>=2){
		x=take_max(); y=take_min();
		if(q1.size()+q2.size()==2&&x.first>y.first){  //特殊情况
			if(flag==0) ans-=1;
			else ans+=flag;
			break;
		}
		z=find_min();
		if(x.first-y.first>z.first||(x.first-y.first==z.first&&x.second>z.second)){  //第1、2种情况
			if(flag==0) ans-=1;
			else{
				ans+=flag;
				break;
			}
		}
		else{   //第三种情况
			if(flag==0) flag=1,ans-=flag;  //按照上述讨论处理剩下蛇的条数
			else flag=-flag,ans-=flag;
		}
		q2.push_front(make_pair(x.first-y.first,x.second));  //加入新蛇
	}
	return;
}
int main(){
//	freopen("snakes.in","r",stdin);
//	freopen("snakes.out","w",stdout);
	int x,y;
	scanf("%d",&t);
	for(int i=1;i<=t;i+=1){
		if(i==1){
			scanf("%d",&n);
			for(int j=1;j<=n;j+=1) scanf("%d",&a[j]);
		}
		else{
			scanf("%d",&k);
			while(k--){
				scanf("%d%d",&x,&y);
				a[x]=y;
			}
		}
		solve();
		printf("%d\n",ans);
	}
//	fclose(stdin);
//	fclose(stdout);
	return 0;
}

预祝大家来今后的比赛中取得优异的成绩!

谢谢观看!

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值