2024年百度之星 初赛 第一场

Preface

周末紧张刺激的两连赛,周六打完篮球杯后周日就要打百度之星的第一场省赛

由于第二场时间和四川省赛冲突了,第三场时间又在考试前一天,比较阴间,想着最好是一次打进决赛节省点钱和时间

虽然学校不报销去北京决赛的钱,但转念一想反正XCPC也都不报销也差不了多少,去北京旅个游也未尝不可

回到这场比赛,虽然总体来看感觉一堆Math Problem恰逢软肋(怎么和蓝桥杯一样),但好在比赛的时候选择开自己擅长的大分类讨论和大模拟,最后40minRush第六题也把思路出的七七八八了

最后写出前5题,总排Rank25,看了眼大学组里好像排Rank3,比较可惜的是和大学组Rank1的川大✌差了半小时的罚时,如果P3或者P5少WA两发就好了(但这种题不WA感觉是不可能的)

由于百度之星在自己电脑上存了代码,因此就和一般的题解一样顺便讲下做法吧


补给

这题没啥好说的,看到数据范围就知道直接O(n2)甚至O(n2logn)模拟一遍即可

当然由于每次只有一个位置会发生变化,理论上可以用平衡树/线段树等东西来做到O(nlogn)

#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=1005;
int n,ans,B,p[N],s[N],w[N];
int main()
{
	RI i,j; for (scanf("%d%d",&n,&B),i=1;i<=n;++i) scanf("%d%d",&p[i],&s[i]);
	for (i=1;i<=n;++i)
	{
		for (j=1;j<=n;++j)
		if (i==j) w[j]=p[j]/2+s[j]; else w[j]=p[j]+s[j];
		sort(w+1,w+n+1); int cnt=0; long long sum=0;
		for (j=1;j<=n;++j)
		if (sum+w[j]<=B) sum+=w[j],++cnt; else break;
		ans=max(ans,cnt);
	}
	return printf("%d",ans),0;
}

跑步

很有CF风格的一个题,难点在于求1nLCM,如果实现不够精细的话可能会在这里T掉

首先不妨设M=LCM(1,2,,n),则第i个人最后会跑Mi

显然对于两个人i,j(i<j),它们相遇的次数就是MiMj

然后这个二维的求和式子很容易化成一维的,直接考虑第i个人的系数就是n2i+1,用这个值乘上1i求和后,最后乘上M即可

现在的问题就是怎么快速计算M,根据经典套路还是想到要把数分解质因数后比较好统计

对于质因数pi,不妨设1n中它出现且指数最大值为ci,则其贡献就是pici

可以先筛出1n中所有的质数,然后暴力计算每个数最大的指数,这部分复杂度是O(π(n)×logn),差不多就是O(n)

最后总复杂度为O(n)

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e7+5,mod=998244353;
int n,M,fact[N],ifac[N],inv[N],ans,p[N],cnt,vis[N];
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
	for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
inline void init(CI n)
{
	RI i; for (fact[0]=i=1;i<=n;++i) fact[i]=1LL*fact[i-1]*i%mod;
	for (ifac[n]=quick_pow(fact[n]),i=n-1;i>=0;--i) ifac[i]=1LL*ifac[i+1]*(i+1)%mod;
	for (i=1;i<=n;++i) inv[i]=1LL*fact[i-1]*ifac[i]%mod;
}
inline void sieve(CI n)
{
	for (RI i=2;i<=n;++i)
	{
		if (!vis[i]) p[++cnt]=i;
		for (RI j=1;j<=cnt&&i*p[j]<=n;++j)
		if (vis[i*p[j]]=1,i%p[j]==0) break;
	}
}
int main()
{
	RI i; for (scanf("%d",&n),init(n),i=1;i<=n;++i)
	(ans+=1LL*inv[i]*(n-2*i+1+mod)%mod)%=mod;
	for (sieve(n),M=1,i=1;i<=cnt;++i)
	{
		int c=0; long long ret=1;
		while (ret*p[i]<=n) ret*=p[i],++c;
		M=1LL*M*quick_pow(p[i],c)%mod;
	}
	return printf("%d",1LL*ans*M%mod),0;
}

联通块

这题其实没啥思维难度,因为就是纯爆搜,但由于写起来比较繁琐导致最后过的人就不是很多吧,反正很适合我这种写不来思维题的人去大力硬干

首先先大力BFS搜出连通块并判掉连通块数量>1的情况后,并且对于没有黑子的情况要人为地加一个子进去

由于数据范围很小,我们就考虑每次在一个合法的连通块的所有与之连通的格子中挑选一个扩展,因此现在问题集中在如何判断一个连通块是否和另一个同构

考虑用二维坐标来表示一个黑子的位置,并将所有黑子的位置存入vector

vector中的所有坐标排序后,将每个点的坐标统一减去第一个点的坐标,即可消除平移同构的影响

(因为这样相当于定下一个基准点并放到(0,0)处,然后把所有点的坐标都转化为和它的相对位置,显然可以去除平移带来的影响)

接下来考虑旋转同构和对称同构,这个也很简单,直接用对应的变换公式把所有点的坐标变换一下即可

注意这些操作之间是可以组合的,因此我在实现时选择枚举顺时针旋转的次数r[0,3],水平镜像的次数fx[0,1],垂直镜像的次数fy[0,1]

算出对应的点集后用前面的方法去除平移的影响后,扔到set <vector <int>>里就可以判重了,最后输出这个set的大小即可

另外实现的时候还要注意连通块不能超出棋盘大小n×m,具体地只需判断点集横纵坐标的极差是否小于等于n,m即可

具体实现可以看代码,感觉思路还是很自然的,难点在于怎么优美地处理各种同构操作

#include<cstdio>
#include<iostream>
#include<vector>
#include<cstring>
#include<queue>
#include<set>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=15,dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
struct Point
{
	int x,y;
	inline Point(CI X=0,CI Y=0)
	{
		x=X; y=Y;
	}
	friend inline bool operator < (const Point& A,const Point& B)
	{
		return A.x!=B.x?A.x<B.x:A.y<B.y;
	}
	friend inline Point operator - (const Point& A,const Point& B)
	{
		return Point(A.x-B.x,A.y-B.y);
	}
	inline void rotate(void)
	{
		int tx=x,ty=y; x=ty; y=-tx;
	}
	inline void flip_x(void)
	{
		y=-y;
	}
	inline void flip_y(void)
	{
		x=-x;
	}
};
int t,k,n,m,a[N][N],vis[N][N]; set <vector <Point>> rst;
inline void BFS(CI x,CI y)
{
	queue <Point> q; q.push(Point(x,y)); vis[x][y]=1;
	while (!q.empty())
	{
		auto [x,y]=q.front(); q.pop();
		for (RI i=0;i<4;++i)
		{
			int nx=x+dx[i],ny=y+dy[i];
			if (nx<1||nx>n||ny<1||ny>m) continue;
			if (a[nx][ny]==1&&!vis[nx][ny])
			q.push(Point(nx,ny)),vis[nx][ny]=1;
		}
	}
}
inline void normal(vector <Point>& vec)
{
	sort(vec.begin(),vec.end()); Point st=vec[0];
	for (auto &it:vec) it=it-st;
}
inline int calcx(vector <Point>& vec)
{
	int mnx=vec[0].x,mxx=mnx;
	for (auto [x,y]:vec) mnx=min(mnx,x),mxx=max(mxx,x);
	return mxx-mnx+1;
}
inline int calcy(vector <Point>& vec)
{
	int mny=vec[0].y,mxy=mny;
	for (auto [x,y]:vec) mny=min(mny,y),mxy=max(mxy,y);
	return mxy-mny+1;
}
inline bool check(vector <Point>& vec)
{
	int bx=calcx(vec),by=calcy(vec);
	if ((bx<=n&&by<=m)||(by<=n&&bx<=m))
	{
		for (RI r=0;r<4;++r)
		for (RI fx=0;fx<2;++fx)
		for (RI fy=0;fy<2;++fy)
		{
			vector <Point> tmp=vec;
			for (RI i=1;i<=r;++i)
			{
				for (auto &it:tmp) it.rotate();
			}
			if (fx)
			{
				for (auto &it:tmp) it.flip_x();
			}
			if (fy)
			{
				for (auto &it:tmp) it.flip_y();
			}
			normal(tmp);
			if (rst.count(tmp)) return 0;
		}
		return 1;
	} else return 0;
}
inline void DFS(vector <Point> vec,CI k)
{
	rst.insert(vec);
	if (k==0) return;
	set <Point> ext;
	for (auto it:vec) ext.insert(it);
	for (auto [x,y]:vec)
	{
		for (RI i=0;i<4;++i)
		{
			int nx=x+dx[i],ny=y+dy[i];
			if (ext.count(Point(nx,ny))) continue;
			vector <Point> tmp=vec; tmp.push_back(Point(nx,ny));
			normal(tmp); if (check(tmp)) DFS(tmp,k-1);
		}
	}
}
int main()
{
	for (scanf("%d",&t);t;--t)
	{
		RI i,j; for (scanf("%d%d%d",&k,&n,&m),i=1;i<=n;++i)
		for (j=1;j<=m;++j) scanf("%d",&a[i][j]);
		int tot=0; Point st; memset(vis,0,sizeof(vis));
		for (i=1;i<=n;++i) for (j=1;j<=m;++j)
		if (a[i][j]==1&&!vis[i][j]) st=Point(i,j),BFS(i,j),++tot;
		if (tot>=2) { puts("-1"); continue; }
		vector <Point> vec;
		if (tot==0)
		{
			if (k==0) { puts("1"); continue; }
			--k; st=Point(0,0); vec.push_back(st);
		}
		for (i=1;i<=n;++i) for (j=1;j<=m;++j)
		if (a[i][j]==1) vec.push_back(Point(i,j)-st);
		normal(vec); rst.clear(); DFS(vec,k); printf("%d\n",rst.size());
	}
	return 0;
}

110串

这题比赛的时候写完前两题后看了眼榜就先跟了这个题,还是非常典的

看一眼数据范围可以O(n2),那就直接大力设状态fi,j,p=0/1,q=0/1表示处理了前i个位置,一共修改了j次,且当前ai1ai的值分别为p,q时的方案数

转移的话十分trivial就不再赘述,值得一提的是这题空间限制很小,需要把第一维滚存

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=5005,mod=998244353;
int n,m,f[2][N][2][2],a[N]; char s[N];
inline void inc(int& x,CI y)
{
	if ((x+=y)>=mod) x-=mod;
}
int main()
{
	RI i,j,p,q; scanf("%d%d%s",&n,&m,s+1);
	for (i=1;i<=n;++i) a[i]=s[i]-'0';
	if (n==1||n==2) return printf("%d",1<<min(n,m)),0;
	f[0][0][a[1]][a[2]]=1;
	f[0][1][a[1]^1][a[2]]=1;
	f[0][1][a[1]][a[2]^1]=1;
	f[0][2][a[1]^1][a[2]^1]=1;
	for (i=2;i<n;++i)
	{
		int now=i&1,nxt=now^1;
		for (j=0;j<=m;++j) for (p=0;p<2;++p) for (q=0;q<2;++q) f[nxt][j][p][q]=0;
		for (j=0;j<=m;++j) for (p=0;p<2;++p) for (q=0;q<2;++q)
		if (f[now][j][p][q])
		{
			if (!(p==1&&q==1&&a[i+1]==0))
			inc(f[nxt][j][q][a[i+1]],f[now][j][p][q]);
			if (!(p==1&&q==1&&a[i+1]==1))
			inc(f[nxt][j+1][q][a[i+1]^1],f[now][j][p][q]);
		}
	}
	int ans=0; for (i=0;i<=m;++i)
	for (p=0;p<2;++p) for (q=0;q<2;++q) inc(ans,f[n&1][i][p][q]);
	return printf("%d",ans),0;
}

小度的极差

分类讨论你罪大恶极,写这种东西确实挺容易让人红温的,不过好在那天状态挺好WA了两发后就过了

首先一个大致的思路就是要把正数和负数分开,然后两边单独算出一个最大/最小值来算极差

考虑对于若干个符号相同的非负数(非正数的情况同理),如果数的数量2,则最后能得到的最大数就是所有数之和再加上最大数;否则就只能得到那个数本身

因此我们首先可以看出0的作用来,我们首先搞出两个集合P,N分别放入正数和负数,同时记z表示0的数量

显然把0归到P,N中都是可以的,但不同的归法对上面式子的计算会有影响,不过好在我们稍微思考后就能发现每个集合中放入一个0和放入多个0是等价的,因此这部分可以枚举讨论一下

然后还有一大类情况就是没有正数/负数的情况,以没有负数为例

显然如果此时有0就直接钦定0为最小值,剩下部分用上述规则构造出最大值是最优的

否则分情况讨论,如果此时只有两个数,则答案为最大的那个正数,比如3,5这种情况,我们可以先操作一次把序列变成8,5

否则钦定最小的那个数为最小值,剩下部分用上述规则构造出最大值,相减得到的结果一定最优

然后写完后感觉自信满满直接提交,直接返回满屏的WA,遂开始反思漏了什么Corner Case

由于出错的情况肯定是数很少的时候,因此索性手动枚举各种情况,然后发现了3,2,1,100这组数据

按照之前正负分开的规则最小能构造出9,最大就是100不变,最后答案就是109

但实际上我们可以先用3,2构造出8,再用1,100构造出199,最后答案就是207

这就提醒我们还需要讨论一种情况,即可以把每个集合中绝对值最小的那个数放到另一个集合中,看看答案是否会变大

然后写完交一发发现又挂了,后面手玩了下发现原来是只有一个正数和一个负数的情况就不能这样移动了,因此需要再特判一下移动后对应的集合不能为空

把上面的Case全部讨论清楚就可以通过此题了,上述做法仅代表我个人比赛时的思路过程,更加简单的分类方法应该也是有的,但只要能过题都是好方法

Upt on 6/14:最近发现这题原来代码现在过不了了,后面@Wfuture给了一组数据发现还有一种Corner Case没判

对于形如3,1,9这样的数据,我们在把1分给9包装成一组之前,可以先把1加到3上,这样可以造成更多的贡献

分析一下会发现会出现这种情况当且仅当某个集合的大小恰好为2,特判一下贡献即可

#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
int t,n,x;
signed main()
{
	for (scanf("%lld",&t);t;--t)
	{
		RI i; vector <int> P,N; int zero=0;
		for (scanf("%lld",&n),i=1;i<=n;++i)
		{
			scanf("%lld",&x);
			if (x==0) ++zero; else
			if (x>0) P.push_back(x); else N.push_back(-x);
		}
		if (n==1) { puts("0"); continue; }
		if (P.empty()&&N.empty()) { puts("0"); continue; }
		sort(P.begin(),P.end(),greater <int>());
		sort(N.begin(),N.end(),greater <int>());
		auto calc=[&](vector <int>& vec,CI z)
		{
			int sum=0; for (auto x:vec) sum+=x;
			if (vec.size()+z>=2&&!vec.empty()) sum+=vec[0];
			return sum;
		};
		if (N.empty())
		{
			if (zero) printf("%lld\n",calc(P,zero-1));
			else
			{
				if (P.size()==2) printf("%lld\n",P[0]); else
				{
					int tmp=P.back(); P.pop_back();
					printf("%lld\n",calc(P,0)-tmp);
				}
			}
			continue;
		}
		if (P.empty())
		{
			if (zero) printf("%lld\n",calc(N,zero-1));
			else
			{
				if (N.size()==2) printf("%lld\n",N[0]); else
				{
					int tmp=N.back(); N.pop_back();
					printf("%lld\n",calc(N,0)-tmp);
				}
			}
			continue;
		}
		auto solve=[&](vector <int>& P,vector <int>& N)
		{
			int ret=0; for (RI i=0;i<=min(1LL,zero);++i)
			ret=max(ret,calc(N,i)+calc(P,zero-i)); return ret;
		};
		int ans=solve(P,N); vector <int> TP=P,TN=N;
		if (N.size()>1)
		{
			int tmp=N.back(); P.push_back(-N.back()); N.pop_back();
			sort(P.begin(),P.end(),greater <int>());
			ans=max(ans,solve(P,N)+(N.size()==1?tmp:0LL));
		}
		P=TP; N=TN;
		if (P.size()>1)
		{
			int tmp=P.back(); N.push_back(-P.back()); P.pop_back();
			sort(N.begin(),N.end(),greater <int>());
			ans=max(ans,solve(P,N)+(P.size()==1?tmp:0LL));
		}
		printf("%lld\n",ans);
	}
	return 0;
}

无限房间

感觉很有意思的一个构造题,比赛的时候想到了二分找最大的填数位置然后贪心往下检验,但没想到最后多出的数很少可以直接处理

主要感觉还是时间不足没有去跑一下1011看下剩下来几个数的缘故

首先如果n比较小时有个很自然的做法,就是把数一个个加进去,初始时都默认加在1号位置

显然我们可以发现这样一个Rule,如果当前1号位置有至少2个数,且2i号位置都有至少1个数,同时i+1号位置是空的

那我们显然可以把1ii+1个数全部放到i+1位置上,这个做法的正确性显然,用线段树维护每个位置的值,然后线段树上二分查询第一个0即可

但现在的问题是n=1011没法直接这样处理,那我们不妨转换思路,根据题目的性质,下标最靠后的位置x填的数一定是x

而根据反字典序的比较规则,x的值是区分方案的第一要素,因此我们可以先利用二分找到最大的x,并先构造一组合法的方案

验证的过程也很自然,给位置x填上x后,从高到低枚举每个位置,由于知道后面会有多少个数分下来,因此就能算出当前位置最少要填多少数才能满足要求,直接暴力往下处理即可

由于这样计算最少所需的数时,枚举的次数大致是O(n)级别的,因此复杂度也是正确的

最后按照这样方法显然会多出一部分数,但实际跑一下后会发现n=1011时大概会剩下300000个数,同时放到的最大的位置大概是560000左右

那么我们对多出的这部分直接跑一开始讲的那个做法即可,然后这题就做完了

不过由于题目还没开放同时接下来一堆期末考试,代码就先坑着了等题目放出来/考试考完后再来补吧

Upt on 6/6:今天考完数据库去补了下代码,发现后面那部分直接写暴力就能过了

来个感性的复杂度分析吧,不妨先考虑一种特殊情况,a1=n,ai=0(i>1)

手完一下会发现此时每次找到的最远的那个数大概也就是n级别

然后由于前面用二分填出的方案的性质,i>1的每个位置i都满足ai<i,在这种初值下对上面的过程的复杂度没有影响

因此直接暴力模拟这部分复杂度就是对的,那看来这个题还是十分好写的说

#include<cstdio>
#include<iostream>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5;
int n,all,a[N];
inline bool check(CI x)
{
	int sum=x,dlt=1;
	for (RI i=x-1;i>=1;--i)
	{
		int c=(dlt+i-1)/i,tmp=c*i-dlt;
		dlt+=c; sum+=tmp;
		if (sum>n) return 0;
	}
	return 1;
}
inline int solve(CI x)
{
	a[x]+=x; int sum=x,dlt=1;
	for (RI i=x-1;i>=1;--i)
	{
		int c=(dlt+i-1)/i,tmp=c*i-dlt;
		dlt+=c; sum+=tmp; a[i]+=tmp;
	}
	return sum;
}
signed main()
{
	RI i; scanf("%lld",&n);
	int l=1,r=n,mid,ret=-1;
	while (l<=r) if (check(mid=l+r>>1)) ret=mid,l=mid+1; else r=mid-1;
	n-=solve(ret); a[1]=n;
	while (a[1]>=2)
	{
		for (i=2;;++i) if (!a[i]) break;
		a[i]+=i; --i; while (i>=1) --a[i],--i; --a[1];
	}
	for (i=1;i<N;++i) if (a[i]) printf("%lld %lld\n",i,a[i]);
	return 0;
}

Postscript

剩下的两个题比赛的时候都没怎么看,后面看榜后感觉也不是我这种水平能做的题,暂且就先坑着了

这个学期剩下的比赛应该就只有6.18的省赛了,希望有银牌✌加持,把冠军留在UESTC吧

posted @   空気力学の詩  阅读(1721)  评论(5编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
历史上的今天:
2023-06-04 Codeforces Round 876 (Div. 2)
2018-06-04 Luogu P3398 仓鼠找sugar
点击右上角即可分享
微信分享提示