Codeforces Round 919 (Div. 2) 题解

本文网址:https://www.cnblogs.com/zsc985246/p/17963255 ,转载请注明出处。

B 题没看数据范围,以为 ai 可以为负数;DE 题都没有清空数组,浪费了很多时间。最后在结束前 10s 调过了 F1,但网页卡死了没交上去(大悲)。最终排名 203。

题目偏向于小清新风格,整体感觉良好,推荐。

2024/1/17update:期末考试期间借用机房电脑捉虫,更新 F2 代码。

传送门

Codeforces Round 919 (Div. 2)

A.Satisfying Constraints

题目大意

给出 n 条对整数 k 的限制,有以下三种格式:

  1. k>=x

  2. k<=x

  3. kx

保证至少有一条限制 1,一条限制 2,且不存在两条限制完全相同

求满足条件的 k 的数量。

多组测试,2n100,1x109,T500

思路

仅考虑限制 1 和限制 2,我们用两个指针 l,r 维护 k 的合法区间。

接下来每出现一个 x[l,r] 内的限制 3,则答案减 1

代码实现

注意答案对 0 取最大值。

#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
const ll N=1e6+10;
using namespace std;

ll n,m;
ll a[N],b[N];

void mian(){
	
	ll ans=0,l=1,r=1e9;
	scanf("%lld",&n);
	For(i,1,n){
		scanf("%lld%lld",&a[i],&b[i]);
		if(a[i]==1)l=max(l,b[i]);
		if(a[i]==2)r=min(r,b[i]);
	}
	
	ans=r-l+1;
	For(i,1,n){
		if(a[i]==3&&l<=b[i]&&b[i]<=r)--ans;
	}
	
	printf("%lld\n",max(0ll,ans));
	
}

int main(){
	int T=1;
	scanf("%d",&T);
	while(T--)mian();
	return 0;
}

B.Summation Game

题目大意

Alice 和 Bob 玩游戏。有一个长度为 n正整数序列 a

首先,Alice 选择至多 k 个数,将它们删除。

然后,Bob 选择至多 x 个数,将它们变为它们的相反数。

接下来游戏结束,分数为最终序列的和。Alice 的目标是使其最大,而 Bob 的目标是使其最小。求最终分数。

多组测试,1k,xn2×105,1ai1000,T104,n2×105

思路

将数组从小到大排序

发现 Bob 一定会选择序列的一段后缀。所以为了“降低损失”,Alice 也会选择删去序列的一段后缀。

考虑 Alice 多删一个数的影响

仔细思考发现当删的位置 i>x 时,影响为 ai2aix;当 ix 时,影响为 ai

那么我们定义一个位置的权值 bi={ai2aix,i>xai,ix,并将 bi 的后缀和记作 si

不难想到,si 最大值所在位置即为 Alice 删去的后缀开始位置

直接计算答案即可。

代码实现

Alice 是有删数限制的,所以只能从 n 枚举到 nm+1

注意 Alice 可以不删数。

注意 sn+1 的初始化。

#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
const ll N=1e6+10;
using namespace std;

ll n,m,k;
ll a[N],b[N];

void mian(){
	
	ll ans=0;
	scanf("%lld%lld%lld",&n,&m,&k);
	For(i,1,n){
		scanf("%lld",&a[i]);
	}
	
	sort(a+1,a+n+1);
	b[n+1]=0;
	Rep(i,n,n-m+1){//计算s[i]
		if(i>k)b[i]=b[i+1]+a[i]-2*a[i-k];
		else b[i]=b[i+1]+a[i];
	}
	ll r=n+1;//n+1表示Alice不删数的情况
	For(i,n-m+1,n){//找最大值
		if(b[i]>b[r])r=i;
	}
	--r;//a[r]是被删掉了的
	Rep(i,r,max(1ll,r-k+1))a[i]=-a[i];//Bob操作
	For(i,1,r)ans+=a[i];
	
	printf("%lld\n",ans);
	
}

int main(){
	int T=1;
	scanf("%d",&T);
	while(T--)mian();
	return 0;
}

C.Grouping Increases

题目大意

给定一个长度为 n 的序列 a,你需要找到满足以下条件的 k 的个数:

  • kn 的因数。

  • 能够找到一个大于 1 的整数 m,令 bi=aimodm,满足 {b1,b2,,bk},{bk+1,bk+2,,b2k},,{bnk+1,bnk+2,,bn} 完全相同

两个长度为 k 的序列 a,b 完全相同当且仅当 a1=b1,a2=b2,,ak=bk

多组测试,1n2×105,1ain,T104,n2×105

思路

由于数字 n 的因子个数是 n 级别的,所以我们可以找到所有的 k|n,然后快速判断是否合法。

发现数 a,b,c(a<b<c) 满足取余一个数 m 后值相同,那么 m|gcd(ba,cb)

其实这个结论可以拓展到多个数

所以我们可以对于每个 p[1,k],暴力计算 gp=gcd(|ak+pap|,|a2k+pak+p|,...,|an2k+pank+p|),然后判断是否满足 gcdi=1k(gi) 不为 1 的条件即可。

复杂度 O(nnlogn)

代码实现

注意 gcd=0 时也是合法的,表示任意一个 m 都满足条件。

代码中将计算 gcd 一步写作 gcdi=k+1n(|aiaik|),效果相同。

#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
#define pb push_back
const ll N=1e6+10;
using namespace std;

ll n,m,k;
ll a[N],b[N];

void mian(){
	
	ll ans=0;
	scanf("%lld",&n);
	For(i,1,n){
		scanf("%lld",&a[i]);
	}
	//记录因子
	vector<ll>t;
	for(ll i=1;i*i<=n;++i){
		if(n%i==0){
			t.pb(i);
			if(i!=n/i)t.pb(n/i);
		}
	}
	//判断是否满足条件
	for(ll x:t){
		ll g=0;
		For(i,x+1,n){
			g=__gcd(g,abs(a[i]-a[i-x]));
		}
		if(g!=1)++ans;
	}
	
	printf("%lld\n",ans);
	
}

int main(){
	int T=1;
	scanf("%d",&T);
	while(T--)mian();
	return 0;
}

D.Array Repetition

题目大意

有一个数列 s,初始为空。有两种操作:

  1. 在序列末尾插入一个数 x

  2. 将整个数列复制,并在数列后方粘贴 x

给定 n 个操作。所有操作进行完之后,进行 q 次询问,每次询问序列中下标为 x 的位置上的值。

多组测试,1n,q105,T104,n,q105。操作 1 中 1xn,操作 2 中 1x109,询问中 1xmin(1018,|s|)

思路

发现操作 2 至少会将序列长度增加一倍,而查询范围最大只有 1018,所以我们最多需要处理 60 次操作 2

我们在处理操作时维护一下序列长度,当长度超过 1018 时不再处理操作。

那么,我们采用一个链式结构。每个节点上存储三个信息:上次操作 2 时的序列长度 tmp、上次操作 2 后的序列长度 m、操作 1 插入的数构成的序列 t

每当一个操作 1 出现时,我们向当前节点的 t 中插入数即可。

每当一个操作 2 出现时,我们新建一个节点,记录 tmpm 的值。

对于一个查询,我们从最后一个节点开始。可以根据 m 判断此下标是否在当前节点的 t 上。如果不在,递归向前一个节点查询,否则直接返回。

显然最多只会有 61 个节点,而我们每次查询时最多遍历每个节点各一次。复杂度 O(qlogV)

代码实现

序列长度可能爆 long long,可以用除法防溢出或开 __int128

记得清空。

#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
#define pb push_back
const ll N=1e6+10;
using namespace std;

struct node{
	__int128 m;
	ll tmp;
	vector<ll>t;
}tr[N];
ll n,k,q;
ll a[N],b[N];

void clear(ll x){//清空节点
	tr[x].m=tr[x].tmp=0;
	tr[x].t.clear();
}

void mian(){
	
	k=1;
	clear(k);
	__int128 m=0;//记录当前区间长度
	scanf("%lld%lld",&n,&q);
	For(i,1,n){
		ll x,y;
		scanf("%lld%lld",&x,&y);
		if(m>=1e18)continue;
		if(x==1){
			tr[k].t.pb(y);
			++m;
		}else{
			clear(++k);
			tr[k].tmp=m;
			m*=y+1;
			tr[k].m=m;
		}
	}
	
	For(i,1,q){
		ll x;
		scanf("%lld",&x);
		ll pos=k,ans=-1;
		while(pos){
			if(x>tr[pos].m){
				ans=tr[pos].t[x-tr[pos].m-1];
				break;
			}else{
				x=(x-1)%tr[pos].tmp+1;
				--pos;
			}
		}
		printf("%lld ",ans);
	}
	printf("\n");
	
}

int main(){
	int T=1;
	scanf("%d",&T);
	while(T--)mian();
	return 0;
}

E.Counting Binary Strings

题目大意

一个 01 串是好的,当且仅当其中只有一个 1

一个 01 串的分数定义为它的所有连续子串中,好的 01 串个数。

给定 nk,求出分数为 n,连续子串中好的 01 串最大长度不超过 k 的 01 串个数。

答案对 998244353 取模。

多组测试,1kn2500,1T2500,n2500

思路

思考如何快速计算一个 01 串的分数。

由于一个好的 01 串仅有一个 1,考虑截取 00...0100...0 这样一个结构进行研究。

令左右分别有 l,r0,那么这个结构的分数为 (l+1)(r+1)

由于填入一个 1 的贡献仅与当前结尾的连续的 0 的数量有关,考虑 dp。

dpi,j 表示分数为 i,结尾连续的 0 的个数为 j 的 01 串个数。

不过这样定义不太方便,所以我们令 j 表示结尾连续的 0 的个数 +1

枚举填入 x0,注意 j+xk+1。转移式为 dpi+jk,k=dpi+jk,k+dpi,j

但是乍一看,你这转移是 O(nk2) 的,肯定不能过。

仔细观察我们的转移过程。我们从 dpi,j 转移到 dpi+jk,k,其中 jk 是一个乘积的形式。

我们将 i+jk>n 的情况去掉,发现实际转移次数很少。

转移次数的具体计算比较复杂,所以在正式比赛时建议大家造数据实测来判断。

代码实现

记得清空。

#pragma GCC optimize("Ofast")//codeforces允许使用pragma语句,看上去比较玄学的题目建议开上
#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
const ll N=2500+10;
using namespace std;
const ll p=998244353;

ll n,m,k;
ll a[N],b[N];
ll dp[N][N];

void mian(){
	
	ll ans=0;
	scanf("%lld%lld",&n,&k);
	
	For(i,1,k){
		dp[0][i]=1;
	}
	For(i,0,n-1){
		For(j,1,k){
			For(t,1,k+1-j){
				if(i+j*t>n)break;
				dp[i+j*t][t]=(dp[i+j*t][t]+dp[i][j])%p;
			}
			dp[i][j]=0;//清空
		}
	}
	
	For(i,1,k)ans=(ans+dp[n][i])%p,dp[n][i]=0;
	printf("%lld\n",ans);
	
}

int main(){
	int T=1;
	scanf("%d",&T);
	while(T--)mian();
	return 0;
}

F1.Smooth Sailing (Easy Version)

题目大意

与 F2 不同之处已标红。

有一片 n×m 的网格,上面有三种格子:障碍、陷阱与地面。保证障碍四连通且不与网格边缘接触,至少有一个陷阱格子。

你可以通过四连通方式移动。你不能经过障碍。你需要走一条回路(只要起点和终点相同即可,可以重复经过格子),将障碍包围在回路之内(从障碍的任意一点出发,八连通不经过回路上的格子无法到达网格边缘)。

路线的安全值为你走过的格子中到陷阱的最小曼哈顿距离。计算曼哈顿距离时不受障碍限制

现在给出网格的情况,有 q 次询问,每次给出一个坐标 (x,y),求起点为 (x,y) 的符合条件的回路的最大安全值。保证 (x,y) 是地面。

3n,m105,9nm3×105,1xn,1ym,1q5

思路

询问次数可以看做 1,也就是说我们只需要找到一种方式去计算最大安全值。

最小值最大,考虑二分答案。

首先,通过多源 bfs,预处理出每个点到最近的陷阱的距离 dis,特别的,障碍的 dis 值为 -1,表示不能经过。

然后二分答案,仅能走 dis 不小于当前二分值的格子。可以通过 bfs 将所有与起点四连通且满足条件的格子全部标记出来。

由于障碍是四连通的,所以我们只需要选择一个点去判断八连通能否到达网格边缘。这同样通过 bfs 实现。

复杂度 O(qnmlog(n+m))

代码实现

#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
#define Yes printf("Yes\n")
#define No printf("No\n")
#define pb push_back
const ll N=1e6+10;
using namespace std;
//const ll p=1e9+7;
const ll p=998244353;
ll ksm(ll a,ll b){ll bns=1;while(b){if(b&1)bns=bns*a%p;a=a*a%p;b>>=1;}return bns;}

ll n,m,k,q;
vector<ll>a[N],vis[N],dis[N];
ll nt[4][2]={0,1,1,0,0,-1,-1,0};//四连通
ll nt2[8][2]={0,1,1,0,0,-1,-1,0,1,1,-1,-1,1,-1,-1,1};//八连通
ll tx,ty;

bool check(ll d,ll x,ll y){
	if(d>dis[x][y])return false;
	For(i,1,n)For(j,1,m)vis[i][j]=0;//清空
	//bfs标记所有能走到的满足条件的格子
	queue<ll>qx,qy;
	qx.push(x),qy.push(y);
	vis[x][y]=1;
	while(qx.size()){
		ll x=qx.front(),y=qy.front();
		qx.pop(),qy.pop();
		For(i,0,3){
			ll xx=x+nt[i][0],yy=y+nt[i][1];
			if(xx<1||xx>n||yy<1||yy>m)continue;
			if(vis[xx][yy]||dis[xx][yy]<d)continue;
			vis[xx][yy]=1;
			qx.push(xx),qy.push(yy);
		}
	}
	//从障碍格子八连通出发,尝试不经过已标记格子到达网格边缘
	qx.push(tx),qy.push(ty);
	vis[x][y]=1;
	while(qx.size()){
		ll x=qx.front(),y=qy.front();
		qx.pop(),qy.pop();
		For(i,0,7){
			ll xx=x+nt2[i][0],yy=y+nt2[i][1];
			if(xx<1||xx>n||yy<1||yy>m)return false;//到达网格边缘,说明回路未包含障碍
			if(vis[xx][yy])continue;
			vis[xx][yy]=1;
			qx.push(xx),qy.push(yy);
		}
	}
	return true;
}
void solve(ll x,ll y){
	ll l=0,r=n+m,res=-1;
	while(l<=r){//二分答案
		ll mid=(l+r)>>1;
		if(check(mid,x,y))l=mid+1,res=mid;
		else r=mid-1;
	}
	printf("%lld\n",res);
}

void mian(){
	
	ll ans=0;
	scanf("%lld%lld%lld",&n,&m,&q);
	
	queue<ll>qx,qy,qd;
	For(i,1,n){
		a[i].resize(m+1);
		vis[i].resize(m+1);
		dis[i].resize(m+1);
		while(getchar()!='\n');
		For(j,1,m){
			char ch=getchar();
			if(ch=='#')a[i][j]=1,tx=i,ty=j;//随便记录一个障碍格子
			if(ch=='v')qx.push(i),qy.push(j),qd.push(0),vis[i][j]=1;
		}
	}
	//多源bfs,计算dis
	while(qx.size()){
		ll x=qx.front(),y=qy.front(),d=qd.front();
		qx.pop(),qy.pop(),qd.pop();
		dis[x][y]=d;
		For(i,0,3){
			ll xx=x+nt[i][0],yy=y+nt[i][1];
			if(xx<1||xx>n||yy<1||yy>m)continue;
			if(vis[xx][yy])continue;
			vis[xx][yy]=1;
			qx.push(xx),qy.push(yy),qd.push(d+1);
		}
	}
	For(i,1,n)For(j,1,m)if(a[i][j])dis[i][j]=-1;//将障碍格子的dis标记为-1
	For(i,1,q){
		ll x,y;
		scanf("%lld%lld",&x,&y);
		solve(x,y);
	}
	
}

int main(){
	int T=1;
	while(T--)mian();
	return 0;
}

F2.Smooth Sailing (Hard Version)

题目大意

与 F1 不同之处已标红。

有一片 n×m 的网格,上面有三种格子:障碍、陷阱与地面。保证障碍四连通且不与网格边缘接触,至少有一个陷阱格子。

你可以通过四连通方式移动。你不能经过障碍。你需要走一条回路(只要起点和终点相同即可,可以重复经过格子),将障碍包围在回路之内(从障碍的任意一点出发,八连通不经过回路上的格子无法到达网格边缘)。

路线的安全值为你走过的格子中到陷阱的最小曼哈顿距离。计算曼哈顿距离时不受障碍限制

现在给出网格的情况,有 q 次询问,每次给出一个坐标 (x,y),求起点为 (x,y) 的符合条件的回路的最大安全值。保证 (x,y) 是地面。

3n,m105,9nm3×105,1xn,1ym,1q3×105

思路

询问次数达到了 3×105,显然是想让我们把每个点的答案预处理出来。

那么我们可以省去二分答案,直接按 dis 从大到小枚举每个格子,依次标记为可经过的。

但是我们如何判断回路是否包含障碍呢?

如果有计算几何基础在这一步会容易很多。

我们从障碍处向左画一条水平的射线,如果回路经过了奇数次射线,那么回路包含障碍。

想到这里,自然地考虑使用并查集维护连通性。

我们将一个点 (x,y) 拆成两个,代表经过了奇数或偶数次射线。当两个点在并查集中连通时,说明有一条回路满足条件。由于我们按 dis 从大到小的顺序枚举格子,所以直接记录答案即可。

复杂度 O(nmlognm)

代码实现

注意细节。

#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
#define pb push_back
const ll N=1e6+10;
using namespace std;

ll n,m,k,q;
vector<ll>a[N],vis[N],dis[N],ans[N];
ll nt[4][2]={0,1,1,0,0,-1,-1,0};//四连通
ll nt2[8][2]={0,1,1,0,0,-1,-1,0,1,1,-1,-1,1,-1,-1,1};//八连通
vector<pair<ll,ll>>t[N];//记录每个dis对应的格子
ll tx,ty;

ll fa[N];
vector<ll>s[N];
ll find(ll x){
	return x==fa[x]?x:fa[x]=find(fa[x]);
}
void merge(ll x,ll y,ll res){
	x=find(x),y=find(y);
	if(x==y)return;
	if(s[x].size()<s[y].size())swap(x,y);
	for(ll i:s[y]){
		ll id=(i-1)%(n*m)+1;
		ll j;//i的对应点的编号
		if(i>n*m)j=i-n*m;
		else j=i+n*m;
		if(find(j)==x){
			ans[(id-1)/m+1][(id-1)%m+1]=res;
		}
		s[x].pb(i);
	}
	s[y].clear();
	fa[y]=x;
}

bool check(ll x1,ll y1,ll x2,ll y2){//判断从(x1,x2)到(y1,y2)是否经过了水平射线
	//这里我们把水平射线画在(tx,0)到(tx,ty)这些格子的上边缘,方便判断
	if(x1==x2||y1>=ty)return false;
	if(x1>x2)swap(x1,x2);
	return x1==tx-1&&x2==tx;
}

#define id(x,y) ((x-1)*m+y)
void init(){
	For(i,1,2*n*m)fa[i]=i,s[i].pb(i);
	Rep(i,n+m,0){
		for(auto j:t[i]){
			ll x=j.first,y=j.second;
			For(k,0,3){
				ll xx=x+nt[k][0],yy=y+nt[k][1];
				if(xx<1||xx>n||yy<1||yy>m)continue;
				if(dis[xx][yy]<i)continue;
				if(check(x,y,xx,yy)){
					merge(id(x,y),id(xx,yy)+n*m,i);
					merge(id(x,y)+n*m,id(xx,yy),i);
				}else{
					merge(id(x,y),id(xx,yy),i);
					merge(id(x,y)+n*m,id(xx,yy)+n*m,i);
				}
			}
		}
	}
}

void mian(){
	
	scanf("%lld%lld%lld",&n,&m,&q);
	
	queue<ll>qx,qy,qd;
	For(i,1,n){
		a[i].resize(m+1);
		vis[i].resize(m+1);
		dis[i].resize(m+1);
		ans[i].resize(m+1);
		while(getchar()!='\n');
		For(j,1,m){
			char ch=getchar();
			if(ch=='#')a[i][j]=1,tx=i,ty=j;//随便记录一个障碍格子
			if(ch=='v')qx.push(i),qy.push(j),qd.push(0),vis[i][j]=1;
		}
	}
	//多源bfs,计算dis
	while(qx.size()){
		ll x=qx.front(),y=qy.front(),d=qd.front();
		qx.pop(),qy.pop(),qd.pop();
		dis[x][y]=d;
		For(i,0,3){
			ll xx=x+nt[i][0],yy=y+nt[i][1];
			if(xx<1||xx>n||yy<1||yy>m)continue;
			if(vis[xx][yy])continue;
			vis[xx][yy]=1;
			qx.push(xx),qy.push(yy),qd.push(d+1);
		}
	}
	For(i,1,n){
		For(j,1,m){
			if(a[i][j])dis[i][j]=-1;//将障碍格子的dis标记为-1
			else t[dis[i][j]].pb({i,j});
		}
	}
	init();//预处理
	For(i,1,q){
		ll x,y;
		scanf("%lld%lld",&x,&y);
		printf("%lld\n",ans[x][y]);
	}
	
}

int main(){
	int T=1;
	while(T--)mian();
	return 0;
}

尾声

如果有什么问题,可以直接评论!

熬夜写到 3:40,给个赞不过分吧 QAQ

posted @   zsc985246  阅读(689)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示