Codeforces Round 858 (Div. 2)

Preface

这场CF打的好难受啊,A刚开始忘记切换语言了CE了两发,B漏了一种情况挂了一发,C纯靠暴力找性质,D比赛时没人写我题目都没仔细看

然后E本来秒出了解法,结果就是因为unordered_map的大常数一直TLE,而且由于我又菜又懒没去手写Hash

最后综合下来就是大爆炸,苦路西苦路西


A. Walking Master

首先特判掉一些显然无解的情况,然后我们先把\((a,b)\)修到\((c,t)\)

然后若\(t>d\)则显然无解,否则我们可以通过调用两次操作来使得\(t+1\)直到其等于\(d\)

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int t,a,b,c,d;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	{
		scanf("%d%d%d%d",&a,&b,&c,&d); int ans=0;
		if (b>d) { puts("-1"); continue; }
		if (a<c) b+=c-a,ans=c-a,a=c; else ans=a-c;
		if (b>d) puts("-1"); else printf("%d\n",2*(d-b)+ans);
	}
	return 0;
}

B. Mex Master

分类讨论题想清楚了再交啊!!!

由于是要求mex的最小,因此首先考虑统计出\(0\)的个数\(c_0\),显然若\(c_0\le n-c_0+1\),则我们总可以构造一种方案使得相邻两数之和不为\(0\),此时的答案就是\(0\)

否则考虑若数列中存在大于\(1\)的数,则显然我们可以把数列前面一段都置为\(0\),然后在后面放一个大于\(1\)的数,此时任意相邻两数之和要么大于\(1\)要么等于\(0\),此时的答案就是\(1\)

否则则说明数列中只含有\(0\)\(1\),此时不论如何构造答案都只能是\(2\)

但是要注意特判所有数都等于\(0\)的情况

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int t,n,a[N],c0,mx;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	{
		RI i; for (scanf("%d",&n),mx=c0=0,i=1;i<=n;++i)
		scanf("%d",&a[i]),c0+=a[i]==0,mx=max(mx,a[i]);
		if (c0==n) { puts("1"); continue; }
		if (c0<=n-c0+1) { puts("0"); continue; }
		if (mx>1) puts("1"); else puts("2");
	}
	return 0;
}

C. Sequence Master

纯在找规律,不然真的想半天连样例的第三组和第四组是怎么构造出来的都想不通

先扔一个比赛时暴力找规律的代码,虽然跑的有点慢不过基本能通过它找到性质

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005,M=20;
int n=4,a[10];
inline void DFS(CI now=1)
{
	if (now>n)
	{
		bool flag=1;
		for (RI i=0;i<(1<<n);++i)
		{
			RI j; int c=0; for (j=0;j<n;++j) c+=((i>>j)&1);
			if (c!=n/2) continue; int prod=1,sum=0;
			for (j=0;j<n;++j) if ((i>>j)&1) prod*=a[j+1]; else sum+=a[j+1];
			if (prod!=sum) { flag=0; break; }
		}
		if (flag) for (RI i=1;i<=n;++i) printf("%d%c",a[i]," \n"[i==n]);
		return;
	}
	for (RI i=-M;i<=M;++i) a[now]=i,DFS(now+1);
}
int main()
{
	freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	return DFS(),0;
}

然后大概总结一下规律就是(默认全为\(0\)总是一种可行解,且以下的数列不考虑顺序):

  • \(n=1\)时,答案为\(|a_1-a_2|\)
  • \(n=2\)时,有以下两种其它的合法序列:
    • \([2,2,2,2]\)
    • \([2,-1,-1-1]\)
  • \(n\)为大于\(3\)的奇数时,此时不存在除了全为\(0\)以外的合法解
  • \(n\)为大于\(2\)的偶数时,此时还存在如下的构造方案:
    • \([n,-1,-1,-1,\cdots,-1]\)(即一个\(n\)\(2n-1\)\(-1\)

然后讨论下原序列到哪个目标状态的变化值最小即可

#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=400005;
int t,n,a[N]; long long ans;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	{
		RI i,j; for (scanf("%d",&n),ans=0,i=1;i<=n*2;++i) scanf("%d",&a[i]),ans+=abs(a[i]);
		if (n==1) { printf("%lld\n",1LL*abs(a[1]-a[2])); continue; }
		if (n==2)
		{
			long long ret=0; for (i=1;i<=4;++i) ret+=abs(a[i]-2); ans=min(ans,ret);
			for (i=1;i<=4;++i)
			{
				for (ret=abs(a[i]-2),j=1;j<=4;++j) if (i!=j) ret+=abs(a[j]+1);
				ans=min(ans,ret);
			}
			printf("%lld\n",ans); continue;
		}
		if (n&1) printf("%lld\n",ans); else
		{
			long long tot=0; for (i=1;i<=n*2;++i) tot+=abs(a[i]+1);
			for (i=1;i<=n*2;++i) ans=min(ans,tot-abs(a[i]+1)+abs(a[i]-n));
			printf("%lld\n",ans);
		}
	}
	return 0;
}

D. DSU Master

比赛时纯题目都没看,今天想了下还是屁都想不出来,只能去看Tutorial,只能说好仙好仙

我们考虑新加入一条边\((i,i+1)\)对答案的影响,不难发现此时\(1\)必须是\(1\sim n\)组成的弱联通分量里出度为\(0\)的点,否则\((i,i+1)\)这条边一定无法产生贡献

除此之外,还必须满足\((i,i+1)\)这条边必须是最后才加入的,并且此时\(a_i=0\)

因此我们可以写出一个关于答案的转移方程,\(ans_i=ans_{i-1}\times i+[a_i=0]\times f_i\)

因为从\(i-1\)转移到\(i\)\((i,i+1)\)这条边不管插入在哪个位置都不会影响\(i-1\)的贡献,因此要乘上\(i\)

\(f_i\)表示\(1\sim n\)组成的弱联通分量的出度为\(0\)的点为\(1\)的方案数,有转移\(f_{i+1}=f_{i}\times (i-a_i)\)

这个转移方程的推导也很trivial,若\(a_i=0\)则这条边\((i,i+1)\)随便怎么连,方案数就是\(f_i\times i\)

否则若\(a_i=1\)则这条边\((i,i+1)\)不能放在最后,不然就会引出一条从\(1\)\(i+1\)的边,因此转移系数要减去\(1\)

综上我们即可\(O(n)\)递推解决该问题

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=500005,mod=998244353;
int t,n,a[N],f[N],ans;
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	{
		RI i; for (scanf("%d",&n),i=1;i<n;++i) scanf("%d",&a[i]);
		for (f[1]=i=1;i<n;++i) f[i+1]=1LL*f[i]*(i-a[i])%mod;
		for	(ans=0,i=1;i<n;++i) printf("%d%c",ans=(1LL*ans*i+f[i]*(1-a[i]))%mod," \n"[i==n-1]);
	}
	return 0;
}

E. Tree Master

这题基本一眼没有什么优秀的做法,暴力跳的部分感觉没有优化空间了,那么不妨大胆以暴力为基础构思

首先对于询问的两个点\(x,y\),我们先找到它们的LCA,记为\(z\), 然后考虑利用类似于根号分治的思想:

  • \(dep_x-dep_z\le \sqrt n\),则我们直接暴力把\(x,y\)跳到\(z\),然后加上提前预处理好的根节点到每个点的答案,这部分复杂度是\(O(n\sqrt n)\)
  • \(dep_x-dep_z>\sqrt n\),不妨设\(dep_x-dep_z=t>\sqrt n\),则这样的点最多有\(\frac{n}{t}\)个,两两配对最多只有\(\frac{n^2}{t^2}\)对,如果我们把每次跳的点对的信息都记忆化一下,总复杂度的上界就是\(\frac{n^2}{t^2}\times t=\frac{n^2}{t}\),当\(t>\sqrt n\)时这部分复杂度也是\(O(n\sqrt n)\)

然后我写的时候在记忆化的地方直接开个unordered_map,然后常数有点大直接爆炸了

后来发现由于\(n\)\(10^5\),可以调整下阈值来减少对记忆化部分的调用,遂把阈值改到\(500\)即可通过

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<unordered_map>
#include<utility>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005,S=500;
int t,n,q,a[N],x,y,anc[N][20],dep[N]; long long pfx[N];
vector <int> v[N]; unordered_map <int,long long> hsh[N];
inline void DFS(CI now=1,CI fa=0)
{
	RI i; pfx[now]=pfx[fa]+1LL*a[now]*a[now]; dep[now]=dep[anc[now][0]=fa]+1;
	for (i=0;i<19;++i) if (anc[now][i]) anc[now][i+1]=anc[anc[now][i]][i]; else break;
	for (int to:v[now]) if (to!=fa) DFS(to,now);
}
inline int getLCA(int x,int y)
{
	if (dep[x]<dep[y]) swap(x,y);
	for (RI i=19;~i;--i) if (dep[anc[x][i]]>=dep[y]) x=anc[x][i];
	if (x==y) return x;
	for (RI i=19;~i;--i) if (anc[x][i]!=anc[y][i])
	x=anc[x][i],y=anc[y][i]; return anc[x][0];
}
inline long long jump(int x,int y,CI fa,long long ret=0)
{
	if (x==fa) return pfx[fa]; if (x>y) swap(x,y);
	if (dep[x]-dep[fa]<=S) return jump(anc[x][0],anc[y][0],fa)+1LL*a[x]*a[y];
	if (hsh[x].count(y)) return hsh[x][y];
	return hsh[x][y]=jump(anc[x][0],anc[y][0],fa)+1LL*a[x]*a[y];
}
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i; for (scanf("%d%d",&n,&q),i=1;i<=n;++i) scanf("%d",&a[i]);
	for (i=2;i<=n;++i) scanf("%d",&x),v[x].push_back(i);
	for (DFS(),i=1;i<=q;++i) scanf("%d%d",&x,&y),printf("%lld\n",jump(x,y,getLCA(x,y)));
	return 0;
}

然后看了Tutorial后发现其实可以只对每种深度的节点数小于\(\sqrt n\)的某些点做记忆化

这样只要开个数组就好了,而且还不用写LCA了,最后跑起来也是飞快

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<unordered_map>
#include<utility>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005,S=340;
int t,n,q,a[N],x,y,dep[N],id[N],c[N],fa[N]; long long hsh[N][S+5];
inline long long solve(int x,int y)
{
	if (!x) return 0; if (x>y) swap(x,y);
	if (id[y]<=S&&hsh[x][id[y]]) return hsh[x][id[y]];
	long long tmp=solve(fa[x],fa[y])+1LL*a[x]*a[y];
	if (id[y]<=S) hsh[x][id[y]]=tmp; return tmp;
}
int main()
{
	//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
	RI i; for (scanf("%d%d",&n,&q),i=1;i<=n;++i) scanf("%d",&a[i]);
	for (i=2;i<=n;++i) scanf("%d",&fa[i]),dep[i]=dep[fa[i]]+1;
	for (i=1;i<=n;++i) id[i]=++c[dep[i]];
	for (i=1;i<=q;++i) scanf("%d%d",&x,&y),printf("%lld\n",solve(x,y));
	return 0;
}

Postscript

最后的F1/F2感觉有点仙啊,周末过完了就先放一放了

希望下场别遇到这种纯在坐牢的场次了,感觉比赛的时候都没有太多好的idea,只是在找规律和卡时间的说

posted @ 2023-03-19 19:13  空気力学の詩  阅读(119)  评论(0编辑  收藏  举报