Codeforces Round 889 Div.2 A-F

前言:wssb

Dalton the Teacher

题意:给定一个排列,每次可以交换两个元素,求使得 i[1,n],aii 的最小操作数。

一次可以操作两个元素,故答案为 i=1n[ai=i]2

Longest Divisors Interval

题意:给定若干个 n,求最长的区间 [l,r],使得 lcm(l,l+1,,r)|n

wssb,这个题猜了结论不敢写,推NM个组合数在乱整

引理:在最优方案中,必定存在一种,使得 l=1

证明:

根据模运算的周期性,在任意区间 [l,r] 的整数中,有且仅有一个数是 rl+1 的倍数。

扩展一下,变为在任意区间 [l,r] 中,最少有一个数是 k(1krl+1) 的倍数。

那么,接着变为 lcm(1,,rl+1)|lcm(l,,r)

由此,若存在 [l,r],使得 lcm(l,,r)|n,而 lcm(1rl+1)|lcm(l,r)

故有 lcm(1rl+1)|n,所以 [1,rl+1] 是一个等长等效的解。

而又有: i[l,r],i|lcm(l,r)

由此,我们从1开始枚举,直到不是 n 的约数为止。最多枚举50个·。

Dual

直接讲C2。

题意:有 n(1n20) 个数,其中 20ai20,每一次可以令 aiai+aj(1i,jn),注意 i=j 也是可行的。求使得原序列单调不减的操作方案。注意你的操作方案不能超过31次。

C1,C2打了两个完全不同的做法,谁知道两个做法粘在一起就过了。。

对于构造题,可以拆分问题:一个简化的形式+将一般问题化为简化的形式。我们一般先考虑前者再考虑后者。这个思想可以在后面的 F 见到

首先来考虑只有正数/负数的情况,显然我们可以做前/后缀和以最多 n1 次的代价完成任务。那么我们还剩下 32n 次操作。

然后考虑将正常情况化为正数/负数。有两个办法:

  1. 找到最大/最小值,将负/正数加上它使得其变为负数
  2. 将一个值自增最多5次再将正负数化掉

注意到操作1的次数是 c,这取决于最大/最小值的最值关系

而操作2的次数是 5+cmin,将正数负数较少的那个化掉。

则当 n 取最大值时,最终还有 12 次操作。此时有两个选择: c12,或者 5+cmin

这是最关键的部分:当 c12 时,我们用操作1解决问题,否则必然有 cmin75+cmin12

所以根据 c 的值选择两种操作即可。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define int long long
#define N 505050
int n,m,a[505];
struct node{
	int x,y;
}ans[505];
int tot=0;
signed main(){
	int t;cin>>t;
	while(t--){
		cin>>n;int c1=0,c2=0;tot=0;for(int i=1;i<=n;i++)cin>>a[i];
		int mn=0x3f3f3f3f,mx=-0x3f3f3f3f;
		for(int i=1;i<=n;i++)mx=max(mx,a[i]),mn=min(mn,a[i]);
		if(mn>=0){
			for(int i=2;i<=n;i++)ans[++tot]={i,i-1};
		}
		else if(mx<=0){
			for(int i=n-1;i;--i)ans[++tot]=(node){i,i+1};
		}
		else {
			for(int i=1;i<=n;i++)c1+=(a[i]<0),c2+=(a[i]>0);
			if(12<c1){
				int id=-1;
				for(int i=1;i<=n;++i)if(a[i]==mn){
					id=i;break;
				} 
				while(mx+mn>0){
					ans[++tot]={id,id};mn+=mn;
				}
				a[id]=mn;
				for(int i=1;i<n;i++)if(a[i]>0)ans[++tot]={i,id},a[i]+=mn;
				for(int i=n-1;i;i--)if(a[i]>a[i+1])a[i]+=a[i+1],ans[++tot]={i,i+1};
				cout<<tot<<" \n";
				for(int i=1;i<=tot;i++)cout<<ans[i].x<<" "<<ans[i].y<<"\n";
				continue; 
			}
			else if(12<c2){
				int id=-1;
				for(int i=n;i;--i)if(a[i]==mx){
					id=i;break;
				} 
				while(mx+mn<0){
					ans[++tot]={id,id};mx+=mx;
				}
				a[id]=mx;
				for(int i=2;i<=n;i++)if(a[i]<0)ans[++tot]={i,id},a[i]+=mx;
				for(int i=2;i<=n;i++)if(a[i]<a[i-1])a[i]+=a[i-1],ans[++tot]={i,i-1}; 
			}
			else {
				int mn=0x3f3f3f3f,mx=-0x3f3f3f3f;
				for(int i=1;i<=n;i++)mx=max(mx,a[i]),mn=min(mn,a[i]);
				if(mx+mn>0){
					int id=-1;
					for(int i=1;i<=n;i++){
						if(a[i]==mx){
							id=i;break;
						}
					}
					for(int i=1;i<=n;i++)if(a[i]<0){
						ans[++tot]=(node){i,id};
					}
					for(int i=2;i<=n;i++)ans[++tot]={i,i-1};
				}
				else{
					int id=-1;
					for(int i=1;i<=n;i++){
						if(a[i]==mn){
							id=i;break;
						}
					}
					for(int i=1;i<=n;i++)if(a[i]>0){
						ans[++tot]=(node){i,id};
					}
					for(int i=n-1;i;--i)ans[++tot]=(node){i,i+1};
				}
			}
		}
		cout<<tot<<" \n";
				for(int i=1;i<=tot;i++)cout<<ans[i].x<<" "<<ans[i].y<<"\n";
	}
}

Earn or Unlock

题意:最初有一堆牌,自顶向下编号 1n,最初只有第一张牌是解锁了的,每一次操作选择最顶上的牌 i(如果没有解锁亦或者牌拿完了就结束游戏),选择以下两种操作:

  1. 按顺序解锁 ai 张牌(已解锁的牌跳过,总解锁 ai 张)。
  2. 获得收益 ai

求最后所得最大收益。

题解:

已经解锁的牌肯定都要用完。
考虑一种均摊的思想,其实选择操作一,本质上是选择将解锁的和自己总共 i+1 张牌的收益减一。显然代价是一样的。由此可以推出:

若解锁了 k 张牌,则答案为 1+i=1k(ai1)。这是因第一张牌本身已经解锁。所以,我们只需要求出哪些 k 可以被解锁即可。

这本质上是求 a1an 可以凑出哪些数,但有一定限制。

fi,j 表示前 i 个数是否可以凑出 j,则有 fi,j=fi1,j|fi1,jai

用二进制数 fi 表示的话,则有 fi=fi1|(fi1×2ai)。保留原来的,或者将原来的所有牌数加上 ai

但考虑是不是真的可以这样转移呢?注意到能用 ai 转移,则代表其原本可以凑出的数 i

所以我们要求转移时 fi1i1 位必须全部为0,可以使用另一个答案数组记录当前位的答案,然后在更新 fi 后将 fi,i 赋值为 0

注意到可能我们会将所有牌拿完而还有剩余次数,所以多扩展一倍,维护到 f2n。当然 ai=0(i>n)

#include<bitset>
#include<iostream>
#define N 205050
#define int long long
using namespace std;
bitset<N>f;
int a[N],n,s[N],ans[N]; 
signed main(){
	ios::sync_with_stdio(false);
	cin>>n;for(int i=1;i<=n;i++)cin>>a[i];
	s[0]=1;
	for(int i=1;i<=n;i++)s[i]=s[i-1]+a[i]-1;
	f[1]=1;
	for(int i=1;i<=n;i++){
		f|=(f<<a[i]);
		ans[i]=f[i];f[i]=0;
	}
	for(int i=n+1;i<=n<<1;i++){
		ans[i]=f[i];s[i]=s[i-1]-1;
	}
	int res=0;
	for(int i=1;i<=n<<1;i++){
		if(ans[i])res=max(res,s[i]);
	}
	cout<<res<<"\n";
}

Expected Destruction

题意:有 n1m 的数,这些数字严格单调递增,在每一时刻,会随机选择某一个数 ai,将其删去。若剩下的数中没有数与 ai+1 相等,且 ai+1m,则插入 ai+1,否则不插入。求使得最后没有数剩下的最小方案数。

题解;注意在本题中,一个数没被删之前,排名比它小(自大到小)的那个数也不会被删,所以本质上问题是独立的。我们需要将 ai 合并到 ai+1 上,此时 ai 就会噶掉。如果我们将两个数相等不重复插入理解为一种合并的话,那么我们就要合并 ai,ai+1,最终 anm+1 合并。所以, ai,ai+1 的合并其实是一个独立于其他数字之外的问题,发掘相对关系,划分同类子问题

fi 为将 ai,ai+1 合并在一起的期望次数,an+1=m+1。则答案为 fi.。

gi,j 为将 i,j(i<j) 合并为一个数的期望操作次数,对于任意一次对这两个数的操作,概率都是 12,所以:

fi=gai,ai+1,gi,j=1+gi+1,j+gi,j+12

注意当 i=j 时,gi,j=0。当 j=m+1 时,gi,j=mi+1;

用记忆化搜索即可。

#define int long long
int f[N][N],n,m,a[N],ans;
const int p=1e9+7;
const int inv=p+1>>1;
int dfs(int x,int y){
	if(x==y)return 0;
	if(f[x][y]!=-1)return f[x][y];
	if(y==m+1)return m+1-x;
	return f[x][y]=(1+dfs(x+1,y)+dfs(x,y+1))*inv%p; 
}
signed main(){
	cin>>n>>m;
	memset(f,-1,sizeof f);
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=m;i++)for(int j=i+1;j<=m;j++)f[i][j]=dfs(i,j);
	ans+=m+1-a[n]; 
	for(int i=2;i<=n;i++)ans=(ans+f[a[i-1]][a[i]])%p;
	cout<<ans<<"\n";
}

Michael and Hotel

有趣的问题。

题意:交互题,给一张不知道边指向的图,有有向边 (i,ai),不知道 ai 的取值。每一次可以询问 u,k,T,得到点 u 走完 k 步后的终点是否在 T 中,在2000次询问之内,求出与1在同一连通块的集合.n500

这是一颗内向基环树,如果可以知道1所在连通块的环,则在 n 次内可以跳到环上的点,都与1在同一连通块。我们的问题化为找环。
首先,我们可以在9次操作内找到点 ik 步后的落脚点,这可以通过 logn 的类似快速排序的算法实现

其次,我们找到点 1n 步后的点,这个点在环上。我们先考虑顺着这个点找到 k 个在环上的点,通过前一个点进行九次操作,代价 9k

(如果这一步找到重复点,说明环已经找到,直接跳到最后一步)
然后,我们不断倍增这个步长,最初为 k。然后新的能够跳在上面来的数,则说明其在该连通块内,且至少能够搜集到 min(lenk,k) 个环上的点。当搜集到的数 lenk,说明环早已收集完毕,退出即可。

我们不断倍增,直到 2k>n,此时可以确定整个环绝对已经囊括其中,再最后扫一遍所有未确定的点,走 n 次是否到环上即可

操作次数为:9k+i=02ik<n(n2ik)+n2imaxk<2000kn 除八向上取整的值即可,算下来达不到2000

本质来讲,这是通过用 9 使得环长扩大1,还是用 nc,使得环长扩大两倍这两种方案的平衡解决了问题。

#include<bits/stdc++.h>
using namespace std;
int n,vis[5050];
vector<int>g,f;
int get(int l,int r,int k,int id){
	if(l==r)return l;
	int mid=l+r>>1;
	cout<<"? "<<id<<" "<<k<<" "<<mid-l+1<<" ";for(int i=l;i<=mid;i++)cout<<i<<" ";
	cout<<"\n";cout.flush();
	int x;cin>>x;
	if(x)return get(l,mid,k,id);
	return get(mid+1,r,k,id);
}
bool ask(int x,int k){
	cout<<"? "<<x<<" "<<k<<" "<<g.size()<<" ";
	for(auto x:g)cout<<x<<" ";cout<<"\n";cout.flush();
	int m;cin>>m;return m; 
} 
int c=63;
int main(){
	cin>>n;c=n>8?(n+7)/8:1; 
//	g.push_back(1);
	int k=get(1,n,n,1);
	g.push_back(k);vis[k]=1;
	for(int i=1;i<c;i++){
		int x=get(1,n,1,g.back());if(vis[x]==1){
			c=g.size();
			for(int i=1;i<=n;i++)if(vis[i]==0&&ask(i,n-c))g.push_back(i);
			sort(g.begin(),g.end());
			cout<<"! "<<g.size()<<" ";for(auto x:g)cout<<x<<" ";return 0;
		}
		g.push_back(x);vis[x]=1;
	}c=g.size();
	for(auto x:g)vis[x]=1;
	while(c<n){
		int cnt=0;
		for(int i=1;i<=n;i++)if(!vis[i]){
			if(ask(i,c))g.push_back(i),vis[i]=1,++cnt;
		}
		if(cnt<c)break;
		c+=c;
	}
//	if(c>n)c/=2;
	for(int i=1;i<=n;i++)if(vis[i]==0&&ask(i,n))vis[i]=1,g.push_back(i);
	sort(g.begin(),g.end());
	cout<<"! "<<g.size()<<" ";for(auto x:g)cout<<x<<" ";
}
posted @   spdarkle  阅读(84)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示