CSP-S-2020

CSP-S-2020

T1 儒略日

向T1出题人致以最高的敬意(磕头)。

T2 动物园

简化题意:给出一些数,保证\(x∈[1,2^k-1]\)且互不相同,给出一些条件,如果存在数\(x\)\(ai\)位为\(1\),那么就必须选物品\(bi\)\(bi\)互不相同。问有多少个数\(y\)满足不存在\(x=y\),并且加入\(y\)后选取的\(b\)不变。

很容易发现如果一个条件不被满足,即不存在数\(x\)\(ai\)位为1,那么选取的数第\(ai\)位也必须为0。对于给出的\(k\)个位置,假设必须为0的位数有\(c\)个,那么剩下的数就有\(2^{k-c}\)个数加入后不会改变\(b\)的选取。再减去给出的\(n\)个数就得到了答案。

\(n=0,k=64\)的情况要特判,记得开\(unsigned\) \(long\) \(long\)

#include<bits/stdc++.h>
using namespace std;
#define ll unsigned long long
const int N=1e6+5;
int n,m,c,k;
ll now,t[70];
int a[N];

int main()
{
	scanf("%d %d %d %d",&n,&m,&c,&k);
	t[0]=1;
	for(int i=1;i<=k;++i) t[i]=t[i-1]*2;
	
	now=0;ll x;
	for(int i=1;i<=n;++i) 
	{
		scanf("%llu",&x);
		now|=x;
	}
	for(int i=1,b;i<=m;++i) scanf("%d %d",&a[i],&b);
	sort(a+1,a+m+1);
	a[0]=unique(a+1,a+m+1)-a-1;
	
	int tmp=k;
	for(int i=1;i<=a[0];++i)
	{
		if(a[i]>=tmp) break;
		if((now&t[a[i]])==0) k--;		
	}
	if(k==64&&n==0) printf("18446744073709551616");
	else printf("%llu",(ll)t[k]-n);
	return 0;
}

T3 函数调用

个人感觉这道题应该压轴啊,怎么会是T3。

30pts

线段树+递归的做法还是很好想到的,线段树只维护乘积,每次1操作的时候就先从线段树把乘积乘下去,再进行1操作,实现是\(O(logn)\),2操作直接在线段树根节点乘,实现是O(1)。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5+5,M=1e6+5;
const ll mod=998244353;

int n,m,Q;
int tot,cz3[M];
struct node{int op,x,y;}cz[N];
ll val[N],mul[N<<2];

void Build(int l,int r,int p)
{
	mul[p]=1;
	if(l==r) return;
	
	int mid=(l+r)>>1;
	Build(l,mid,p<<1);
	Build(mid+1,r,p<<1|1);
	return;
}

void Spread(int p)
{
	if(mul[p]==1) return;
	
	int l=p<<1,r=p<<1|1;
	mul[l]=mul[l]*mul[p]%mod;
	mul[r]=mul[r]*mul[p]%mod;
	mul[p]=1;
	return;
}

void Change(int l,int r,int x,ll y,int p)
{
	if(l==r)
	{
		val[l]=(val[l]*mul[p]+y)%mod;
		mul[p]=1;
		return;
	}
	
	Spread(p);
	int mid=(l+r)>>1;
	if(x<=mid) Change(l,mid,x,y,p<<1);
	else Change(mid+1,r,x,y,p<<1|1);
	
	return;
}

void Update(int l,int r,int p)
{
	if(l==r) 
	{
		val[l]=val[l]*mul[p]%mod;
		return;
	}
	Spread(p);
	int mid=(l+r)>>1;
	Update(l,mid,p<<1);
	Update(mid+1,r,p<<1|1);
	return;
}

void Calc(int pos)
{
	if(cz[pos].op==1) Change(1,n,cz[pos].x,(ll)cz[pos].y,1);
	else if(cz[pos].op==2) mul[1]=mul[1]*cz[pos].x%mod;
	else 
	{
		for(int i=cz[pos].x;i<=cz[pos].y;++i)
		Calc(cz3[i]);
	}
	return;
}

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%lld",&val[i]);
	scanf("%d",&m);
	for(int i=1;i<=m;++i)
	{
		scanf("%d",&cz[i].op);
		if(cz[i].op==1) scanf("%d %d",&cz[i].x,&cz[i].y);
		else if(cz[i].op==2) scanf("%d",&cz[i].x);
		else
		{
			scanf("%d",&Q);
			cz[i].x=tot+1;
			for(int j=1;j<=Q;++j) scanf("%d",&cz3[tot+j]);
			cz[i].y=(tot+=Q);
		}
	}
	Build(1,n,1);
	scanf("%d",&Q);
	for(int i=1,x;i<=Q;++i)
	{
		scanf("%d",&x);
		Calc(x);
	}
	Update(1,n,1);
	for(int i=1;i<=n;++i) printf("%lld ",val[i]);
	return 0;
}

100pts

线段树做法败在了3操作可以调用3操作,会进行很多重复计算,比如5调用2,8调用5(2,5,8为编号),最后给出的操作序列:2 5 8,那么操作顺序就是:2 5 2 8 5 2。重复调用将时间叠上去了。

满分思路是拓补排序,将每次操作都叠加在调用它的操作上,时间复杂度是\(O(n)\),不太好理解。

这种做法是将加法和乘法分开的。

举个例子:假设只有一个数\(2\),先进行\(*3\)操作,再\(+1\),再\(*2\),按照之前的思路,计算过程是\((2*3+1)*2\),加法乘法拆开后计算过程变成\(2*3*2+1*2\)

因为每个乘法都是作用于整个数组的,对于一开始给出的数组,每个数扩大的倍数是一样的,所以把所有的操作跑一遍之后:\(ai=k*ai\)

那么先讲乘法部分,即如何快速求出这个\(k\)

乘法部分

首先要明白,最后给出的Q然后又进行一堆操作,可以看成一个3操作,即调用别的函数,假设编号为\(m+1\)

于是把所有操作都向调用它的操作连边,就可以得到一张一定无环的有向图。(如果存在环,即\(a\)调用\(b\)\(b\)调用\(a\),会陷入死循环)

给出例图(为了方便理解,调用顺序假设都是从左到右调用):

乘法的调用顺序并不影响最终值。

比如\(*3\)操作,如果后面蓝色框框里得到的乘积是\(*5\),那么现在整张图的乘积应该是\(*3*15\)。(对应黄路和紫路)

可以看出来,\(*3\)\(*5\)操作,先算哪一个得到的整张图的乘积都是一样的,所以在乘积上,对于同一个操作,左右顺序不重要,只需要保证这个操作进行的时候子操作都进行完了就可以了。

(比如给出的例子操作里,蓝色框框里要先算最下面的三个点,得到上面的两个点的值,才能用这两个点向上面的一个点贡献\(*5\)

所有可以用拓补排序按入度为0加入队列的规则得到操作顺序。

然后按这个操作顺序每个点向连向的点贡献乘积即可。

对于没有被用到的操作,是不存在路径节点到\(m+1\)的,所有它的贡献是不会被累积到\(k\)里面的。

代码实现(写代码的时候我的边是从父操作到子操作,所有统计的是\(out\)):

inline void add(int u,int v)
{
	out[u]++;G[u].push_back(v);F[v].push_back(u);
}

void Init()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%lld",&a[i]);
	scanf("%d",&m);
	for(int i=1;i<=m;++i)
	{
		scanf("%d %d",&cz[i].op,&cz[i].x);
		if(cz[i].op==1) scanf("%d",&cz[i].y);
		else if(cz[i].op==3) for(int j=1,v;j<=cz[i].x;++j) scanf("%d",&v),add(i,v);
	}
	m++;
	scanf("%d",&Q);
	for(int i=1,v;i<=Q;++i) scanf("%d",&v),add(m,v);
}
void Topo()
{
	int l=1;
	for(int i=1;i<=m;++i)
	if(!out[i]) tq[++tq[0]]=i;
	
	while(l<=tq[0])
	{
		int u=tq[l++];
		
		int S=F[u].size();
		for(int i=0;i<S;++i)
		if(!--out[F[u][i]]) tq[++tq[0]]=F[u][i];
	}
}

void Update()
{
	sum[m]=mul[m]=1;
	for(int i=1;i<m;++i) sum[i]=(cz[i].op==2?cz[i].x:1);
	
	for(int i=1;i<=tq[0];++i)
	for(int j=0,S=F[tq[i]].size();j<S;++j)
	sum[F[tq[i]][j]]=sum[F[tq[i]][j]]*sum[tq[i]]%mod;
    	for(int i=1;i<=n;++i) a[i]=a[i]*sum[m]%mod;
}

加法部分

如图,蓝色框框代表许多许多操作(2节点在1节点之前还有操作,3在2之前也还有操作,画上图太乱了就没画)。

假设2节点累积值是6,3节点累积值是10。

假设这里有个\(+5\)的操作,受到后面操作的影响,最后加上去的数会变成\(5*60+5*30+5*15\)。(蓝,绿,粉)。

可以看出来,1直接连向4节点,这个时候23累积上去的乘积是60,所以1节点累加60。

1连向2节点,但2节点有两种。

2节点直接连向4节点,这时候3累积上的乘积是10,所以2节点累加10。

2节点是连向3节点,3节点连向4节点,无后续影响3节点,此时根节点的累积值是1,所以3累加1。这时候3节点后续有个\(*5\)影响2节点,此时累积到4的积是1,所以在2累加5。

所以2的累加值为\(10+5\)

1在2这里会被\(*3\)影响,所以1的累加值是\(60+30+15\)

这样会发现好像是一个递归的操作,但其实倒着想可以看成一个从上到下的下放操作,实现极其简单,代码非常短。

	for(int i=tq[0];i;--i)
	for(int j=G[tq[i]].size()-1,ml=1;j>=0;--j)
	{
		mul[G[tq[i]][j]]=((ll)ml*mul[tq[i]]+mul[G[tq[i]][j]])%mod;
		ml=(ll)ml*sum[G[tq[i]][j]]%mod;
	}
		for(int i=1;i<=n;++i) a[i]=a[i]*sum[m]%mod;
	for(int i=1;i<m;++i)
	if(cz[i].op==1) a[cz[i].x]=(a[cz[i].x]+cz[i].y*mul[i])%mod;

放上完整代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5+5;
const ll mod=998244353;
int n,m,Q,out[N],tq[N];
ll a[N],mul[N],sum[N];
struct node{int op,x,y;}cz[N];
vector<int>G[N],F[N];

inline void add(int u,int v)
{
	out[u]++;G[u].push_back(v);F[v].push_back(u);
}

void Init()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%lld",&a[i]);
	scanf("%d",&m);
	for(int i=1;i<=m;++i)
	{
		scanf("%d %d",&cz[i].op,&cz[i].x);
		if(cz[i].op==1) scanf("%d",&cz[i].y);
		else if(cz[i].op==3) for(int j=1,v;j<=cz[i].x;++j) scanf("%d",&v),add(i,v);
	}
	m++;
	scanf("%d",&Q);
	for(int i=1,v;i<=Q;++i) scanf("%d",&v),add(m,v);
}
void Topo()
{
	int l=1;
	for(int i=1;i<=m;++i)
	if(!out[i]) tq[++tq[0]]=i;
	
	while(l<=tq[0])
	{
		int u=tq[l++];
		
		int S=F[u].size();
		for(int i=0;i<S;++i)
		if(!--out[F[u][i]]) tq[++tq[0]]=F[u][i];
	}
}

void Update()
{
	sum[m]=mul[m]=1;
	for(int i=1;i<m;++i) sum[i]=(cz[i].op==2?cz[i].x:1);
	
	for(int i=1;i<=tq[0];++i)
	for(int j=0,S=F[tq[i]].size();j<S;++j)
	sum[F[tq[i]][j]]=sum[F[tq[i]][j]]*sum[tq[i]]%mod;
	
	for(int i=tq[0];i;--i)
	for(int j=G[tq[i]].size()-1,ml=1;j>=0;--j)
	{
		mul[G[tq[i]][j]]=((ll)ml*mul[tq[i]]+mul[G[tq[i]][j]])%mod;
		ml=(ll)ml*sum[G[tq[i]][j]]%mod;
	}
	
	for(int i=1;i<=n;++i) a[i]=a[i]*sum[m]%mod;
	for(int i=1;i<m;++i)
	if(cz[i].op==1) a[cz[i].x]=(a[cz[i].x]+cz[i].y*mul[i])%mod;
}
int main()
{	
	Init();
	Topo();
	Update();
	
	for(int i=1;i<=n;++i) printf("%lld ",a[i]);
}

[T4 贪吃蛇22)

如果蛇A吃了蛇B后,会被别的蛇吃掉,那他就不会吃B。那么假设所有蛇都不聪明,那么当被吃掉的蛇,假设为A,A吃过别的蛇,假设被A吃的是B,那么A当初就不会吃B,因为它吃了B后会被别的蛇吃掉。所以每个蛇吃别的蛇的时候,都储存一下如果这个蛇没有吃的话现在还剩了几条蛇,吃到第一条吃过别的蛇的蛇就返回这个的储存值。

可以用\(set\),平衡树来快速插入删除,但时间复杂度是A不了的,可以拿\(70pts\)左右。
正解思路与蚯蚓雷同。

每次被吃掉的蛇一定是没有吃过别的蛇的,就是在初始数组的蛇,所以被吃的蛇一定是递增,但不一定是严格递增。
另开一个数组q来储存A吃B之后的值。
假设上一次拿出来的蛇体力值为A,这次拿出来的是B,B一定小于等于A。
因为如果B不是由A-C得到的,且B比A大,那么上一次就该拿出B而不是A。如果B是由A-C得到的,由于\(C>0\),那么就一定有\(A>B\)
所以每次拿出来吃别的蛇的蛇一定递减,但不一定严格递减。
假设上次是A吃B,这次是C吃D,按上述证明,就有\(A>=C,B<=D\),所以就可以得到\(A-B>=C-D\),即每次吃之后产生的值是具有单调性的(递减)。

那么计算的时候,每次最大的要么是q的要么是原始数组里的,每次最小的只能是原始数组的,如果q中的最小值比原始数组小,本次计算就结束了,返回储存值。

posted @ 2020-11-09 20:12  林生。  阅读(298)  评论(0)    收藏  举报