CSP2020 Solution

call

有一个非常 \(\text{Naive}\)\(O(nQ)\) 暴力,把类型 \(1,2\) 的函数直接合并到类型 \(3\) 上,对于 \(2\) 函数直接乘,对于 \(1\) 函数计算 \(2\) 函数对 \(1\) 函数的贡献,将单点加的数对 \((x,y)\) 记录在类型 \(3\) 的函数的 \(\text{vector}\) 上。

这个做法无法优化,只能拿到 \(50\sim70\) 分的好成绩。考虑最终将操作序列写成一些单点加和一次全局乘的形式。直接想似乎不是很好想,先看一下 \(\text{Subtask}\):在不存在类型 \(1\) 时,在反图上跑拓扑得到每个函数的乘积,最后按顺序(不按顺序也行)乘到序列上。在不存在类型 \(2\) 时,给出操作序列时,在正图上打 \(\text{tag}\),然后在正图上跑拓扑下发 \(\text{tag}\),最后对于每个类型 \(1\) 的函数 \((p_j,v_j)\),令 \(a_{p_j}\leftarrow a_{p_j}+v_j\times tag_j\) 即可。

我们注意到不存在类型 \(2\) 时的做法已经很接近正解,只需要考虑类型 \(3\) 中,类型 \(2\) 对类型 \(1\) 的贡献即可。还需要考虑操作序列中排在靠后的对排在靠前的贡献。设函数 \(x\) 的乘积为 \(mul_x\),函数 \(y\) 被操作的次数为 \(add_y\),操作序列的后缀积为 \(suf\),则倒序扫描操作序列。当扫描到第 \(i\) 个操作时,之前的函数对当前函数中含有的加法操作的贡献为 \(suf_{i+1}\),则令 \(add_{f_i}\leftarrow suf_{i+1}\)

整个操作序列处理完后,在正图上跑一次拓扑排序把 \(\text{tag}\) 下放即可。其实和第二个 \(\text{Subtask}\) 的做法是差不多的。突破口在于,我们分别考虑类型 \(1\) 的单点加和类型 \(2\) 的全局乘,最后考虑类型 \(2\) 对类型 \(1\) 的贡献。

#include<cstdio>
#include<vector>
#include<queue>
#define int ll
typedef long long ll;
const int mod=998244353;
int m;
std::vector<int> vec[2][100005];
struct operators {
	int op,first,second;
}c[100005];
int du[100005],rd[100005],cd[100005],id[100005];
int mul[100005],add[100005],f[100005],a[100005];
inline int read() {
	register int x=0,f=1;register char s=getchar();
	while(s>'9'||s<'0') {if(s=='-') f=-1;s=getchar();}
	while(s>='0'&&s<='9') {x=x*10+s-'0';s=getchar();}
	return x*f;
}
inline void topo1() {
	std::queue<int> Q;
	for(register int i=1;i<=m;++i) {
		mul[i]=1;
		if(!(du[i]=cd[i])) {
			if(c[i].op==2) mul[i]=c[i].first%mod;
			Q.push(i);
		}
	}
	while(Q.size()) {
		int x=Q.front(); Q.pop();
		for(register int i=0;i<vec[1][x].size();++i) {
			int y=vec[1][x][i];
			mul[y]=mul[y]*mul[x]%mod;
			if(!(--du[y])) Q.push(y);
		}
	}
}
inline void topo2() {
	std::queue<int> Q;
	for(register int i=1;i<=m;++i) {
		if(!(du[i]=rd[i])) Q.push(i);
	}
	while(Q.size()) {
		int x=Q.front(); Q.pop();
		for(register int i=vec[0][x].size()-1;i>=0;--i) {
			int y=vec[0][x][i];
			f[y]=(f[x]+f[y])%mod; f[x]=(f[x]*mul[y])%mod;
			if(!(--du[y])) Q.push(y);
		}
	}
}
signed main() {
//	freopen("call3.in","r",stdin);
//	freopen("call3.out","w",stdout);
	int n=read();
	for(register int i=1;i<=n;++i) a[i]=read(); 
	m=read();
	for(register int i=1;i<=m;++i) {
		c[i].op=read();
		if(c[i].op==1) {c[i].first=read();c[i].second=read();}
		else if(c[i].op==2) {c[i].first=read();}
		else {
			int m=read(); 
			for(register int j=1;j<=m;++j) {
				int x=read(); ++rd[x]; ++cd[i];
				vec[0][i].push_back(x);
				vec[1][x].push_back(i);
			}
		}
	}
	int Q=read(),mul_tag=1;
	topo1(); 
	for(register int i=1;i<=Q;++i) id[i]=read();
	for(register int i=Q;i>=1;--i) {
		f[id[i]]=(f[id[i]]+mul_tag)%mod;
		mul_tag=mul_tag*mul[id[i]]%mod;
	}
	for(register int i=1;i<=n;++i) a[i]=a[i]*mul_tag%mod; topo2();
	for(register int i=1;i<=m;++i) {if(c[i].op==1) a[c[i].first]=(a[c[i].first]+f[i]*c[i].second%mod)%mod;}
	for(register int i=1;i<=n;++i) printf("%lld ",a[i]); printf("\n");
	return 0;
}

snake

直接按照题意模拟,判断在哪一轮当前最大的蛇会选择结束游戏即可。

现在问题就变为:判断何时游戏会结束。显然有一个贪心策略:如果当前这条蛇吃了以后,不会被之后的蛇吃掉那么它一定会吃。考虑判断一条蛇什么时候吃了最小的蛇,会被之后的蛇吃了。

这里有一个结论:如果当前蛇 \(x\) 吃了没有变成最小的蛇,那么它一定不会被后面的蛇吃掉,这一点可以使用简单的单调性证明。现在只需要考虑如果蛇 \(x\) 吃了以后变成最小的蛇的情况。如果蛇 \(x\) 吃了以后变成最小的蛇,则此后的第 \(1\) 回合,若此时最大的蛇 \(y\) 吃了不会变成最小的蛇,根据上述结论,它没有被吃的风险,自然会选择吃。所以蛇 \(x\) 就不会选择吃,而会在那一轮选择结束。就这样套娃证明,最后会发现,如果在蛇 \(x\) 吃了之后的第 \(i\) 个回合,此时最大的蛇选择吃了(说明它不会被吃,因为假设蛇都是聪明的),则第 \(i-1\) 个回合蛇不会选择吃,第 \(i-2\) 个回合蛇会选择吃...得到结论:若 \(i\) 为偶数,蛇 \(x\) 会选择吃,因为此后的第 \(1\) 回合蛇 \(x\) 不会被吃。

我们发现每一轮蛇吃了都不会变成最小的这些回合是连续的,我们称它为第 \(1\) 阶段。蛇吃了变成最小的这些回合,也是连续的,我们称它为第 \(2\) 阶段。事实上第 \(1\) 阶段和第 \(2\) 阶段是交替出现的,但由于第 \(2\) 阶段开始的那个回合和结束的那个回合,蛇要决定是否结束游戏,所以事实上整个过程只会由 \(1\) 个第 \(1\) 阶段和 \(1\) 个第 \(2\) 阶段,或者只有一个阶段构成。分阶段按上述结论模拟并判断即可,现在就得到了一个 \(O(Tn \log n)\) 的做法。

如果写过切蚯蚓应该都知道,像这样的一个东西是可能存在单调性的。在第 \(1\) 阶段中,最大的蛇吃了以后的长度是非严格单调递减的。而在第 \(2\) 阶段中,虽然不存在单调性,但最大的蛇吃了以后的长度一直都会是最小值。所以可以用一个队列维护第 \(1\) 阶段一条蛇吃了以后的长度。时间复杂度为 \(O(Tn)\)

#include<cstdio>
#include<queue>
#define val(p) a[p].val
#define id(p) a[p].id
int hd,tl,n;
struct node {
	node(int x=0,int y=0) {val=x;id=y;}
	int val,id;
} a[1000005];
std::queue<node> Q;
inline int read() {
	register int x=0,f=1;register char s=getchar();
	while(s>'9'||s<'0') {if(s=='-') f=-1;s=getchar();}
	while(s>='0'&&s<='9') {x=x*10+s-'0';s=getchar();}
	return x*f;
}
inline bool operator==(const node &x,const node &y) {return x.val==y.val&&x.id==y.id;}
inline bool operator<(const node &x,const node &y) {return x.val!=y.val? x.val<y.val:x.id<y.id;}
inline node max(const node &x,const node &y) {return x<y? y:x;}
inline node min(const node &x,const node &y) {return x<y? x:y;}
inline int ask(node cur) {
	node mn2=min(hd<tl? a[hd+1]:node(1e9,1e9),Q.size()? Q.back():node(1e9,1e9)); int res=0;
	while(1) {
		node mx=max(hd<tl? a[tl]:node(),Q.size()? Q.front():node());
		if(mx==a[tl]) {--tl;} else {Q.pop();}
		if(mn2==mx) break;
		mx.val-=cur.val;
		if(mn2<mx) break;
		cur=mx; res^=1;
	}
	return res;
}
inline void solve() {
	hd=1; tl=n; int mn=0;
	while(Q.size()) Q.pop();
	while(hd<=tl) {
		node mx=max(Q.size()? Q.front():node(),hd<tl? a[tl]:node());
		if(mx==a[tl]) {--tl;} else {Q.pop();}
		mx.val-=val(hd); 
		if(n-hd>1&&mx<a[hd+1]) {
			hd+=ask(mx);
			break;
		}
		Q.push(mx); ++hd;
	}
	printf("%d\n",n-hd+1);
}
int main() {
	int T=read()-1; n=read();
	for(register int i=1;i<=n;++i) {val(i)=read(); id(i)=i;}
	solve();
	while(T--) {
		int k=read();
		for(register int i=1;i<=k;++i) {int x=read(),y=read(); val(x)=y;}
		solve();
	}
	return 0;
}
posted @ 2020-11-26 18:51  tommymio  阅读(91)  评论(0编辑  收藏  举报