Codeforces Round 889 Div.2 A-F

发布时间 2023-08-01 08:12:07作者: weakpyt

前言:wssb

Dalton the Teacher

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

一次可以操作两个元素,故答案为 \(\lceil\frac{\sum_{i=1}^n[a_i=i]}{2}\rceil\)

Longest Divisors Interval

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

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

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

证明:

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

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

那么,接着变为 \(lcm(1,\dots,r-l+1)|lcm(l,\dots ,r)\)

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

故有 \(lcm(1\dots r-l+1)|n\),所以 \([1,r-l+1]\) 是一个等长等效的解。

而又有: \(\forall i\in[l,r],i|lcm(l,\dots r)\)

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

Dual

直接讲C2。

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

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

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

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

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

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

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

而操作2的次数是 \(5+c_{\min}\),将正数负数较少的那个化掉。

则当 \(n\) 取最大值时,最终还有 \(12\) 次操作。此时有两个选择: \(c\le 12\),或者 \(5+c_{\min}\)

这是最关键的部分:当 \(c\le 12\) 时,我们用操作1解决问题,否则必然有 \(c_{\min}\le 7\implies 5+c_{\min}\le 12\)

所以根据 \(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

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

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

求最后所得最大收益。

题解:

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

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

这本质上是求 \(a_1\sim a_n\) 可以凑出哪些数,但有一定限制。

\(f_{i,j}\) 表示前 \(i\) 个数是否可以凑出 \(j\),则有 \(f_{i,j}=f_{i-1,j}|f_{i-1,j-a_i}\)

用二进制数 \(f_i\) 表示的话,则有 \(f_i=f_{i-1}|(f_{i-1}\times 2^{a_i})\)。保留原来的,或者将原来的所有牌数加上 \(a_i\)

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

所以我们要求转移时 \(f_{i-1}\)\(i-1\) 位必须全部为0,可以使用另一个答案数组记录当前位的答案,然后在更新 \(f_i\) 后将 \(f_{i,i}\) 赋值为 0

注意到可能我们会将所有牌拿完而还有剩余次数,所以多扩展一倍,维护到 \(f_{2n}\)。当然 \(a_i=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

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

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

\(f_{i}\) 为将 \(a_i,a_{i+1}\) 合并在一起的期望次数,\(a_{n+1}=m+1\)。则答案为 \(\sum f_i\).。

\(g_{i,j}\) 为将 \(i,j(i<j)\) 合并为一个数的期望操作次数,对于任意一次对这两个数的操作,概率都是 \(\frac{1}{2}\),所以:

\[f_{i}=g_{a_i,a_{i+1}},g_{i,j}=\frac{1+g_{i+1,j}+g_{i,j+1}}{2} \]

注意当 \(i=j\) 时,\(g_{i,j}=0\)。当 \(j=m+1\) 时,\(g_{i,j}=m-i+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,a_i)\),不知道 \(a_i\) 的取值。每一次可以询问 \(u,k,T\),得到点 \(u\) 走完 \(k\) 步后的终点是否在 \(T\) 中,在2000次询问之内,求出与1在同一连通块的集合.\(n\le500\)

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

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

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

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

操作次数为:\(9k+\sum_{i=0}^{2^ik<n}(n-2^ik)+n-2^{i_{\max}}k<2000\)\(k\)\(n\) 除八向上取整的值即可,算下来达不到2000

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

#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<<" ";
}