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 eq 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
",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 imes 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 mod 2))

对于 (p) 是显然的,关键是 ((q mod 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;
}
原文地址:https://www.cnblogs.com/amagaisite/p/13974828.html