Codeforces Round #890 Div.2

link

题号:1856A~E2

A

题面:

给定一个正整数 n 和一个长度为 n 的序列 a,重复执行以下操作直至 a 序列单调不减:

  • 1inai=max(ai1,0)。注意这个算一次操作。

求一共需要执行多少次操作。

多测,共 t 组数据。

对于所有数据,保证 1t5002n501ai109

考虑原操作只会使得 a 单调不减,所以我们只需要考虑将 a 排序后从后往前第一个失配的位置即可。

        read(n);for(int i=1;i<=n;i++)read(a[i]),b[i]=a[i];
		sort(b+1,b+n+1);int tag=0;
		for(int i=n;i;--i){
			if(a[i]!=b[i]){
				tag=1; 
				cout<<b[i]<<"\n";break;
			}
		}
		if(!tag)cout<<"0\n";

B

题面:

给定一个正整数 n 和一个长度为 n 的序列 a,问是否存在正整数序列 b 满足以下条件:

  • 1inaibi

  • i=1nai=i=1nbi

若存在,输出 YES,否则输出 NO

多测,共 t 组数据。

对于所有数据,保证 1t1041n,n1051ai109

对于这种构造与原序列不同,我们显然是选择 调整法,在本题中微调就可以了。

考虑对于 ai=1,我们将其微调为 2,因为它已经不能够再减去了。那么我们统计 k=i=1n[ai=1],则我们再找若干个数减掉 k 即可。那么一个数可以被减掉 ai1,故设 d=i=1n(ai1),只需要判断是否 dk 即可。注意 n=1 需要特判,这显然不合法。

        read(n);for(int i=1;i<=n;i++)read(a[i]);
		int k=0,d=0;
		for(int i=1;i<=n;i++){
			k+=(a[i]==1);d+=a[i]-1;
		}  
		if(d>=k&&n!=1)cout<<"Yes\n";
		else cout<<"No\n";

C

给定两个正整数 nk,以及一个长度为 n 的序列 a,你可以进行不超过 k 次如下操作(以下两个步骤合称一次操作):

  • 选择一个 i 满足 1i<n,且 aiai+1

  • ai 增加 1

求操作完成后,maxi=1nai 的最大可以是多少。

多测,共 t 组数据。

对于所有数据,保证 1t1001n,n10001k,ai108

我们想想一个数是最大值满足什么条件,设最优局面为 b。若最大值为 bi ,则说明什么?

面对此类构造性问题,我们可以从最优局面的角度出发,反推回原本局面寻找性质,类似于双向搜索。运用从后往前推的方法。

说明 bi+1bi1,bi+2bi2,直到第一个不需要从 ai 上增加的数为止。

所以这个最大值必然形成了一段单减序列,且相邻差为一。

那么假设我们已经知道了 bi 的值,怎么计算操作次数?同样的,设 k 为第一个 k>i,akbi(ki),则这个代价显然是:j=ik1(bjaj),而其余数字全部没有动过。

由于最大操作次数是已知的,且答案显然具有单调性,故可对于每一个数来二分答案改为判定。我们只需计算上述代价即可。同时需要注意这个 k 如果不存在也是非法的。

bool check(int id,int x){
	int s=k,tag=0;
	for(int i=1;i<=n;i++)b[i]=a[i];
	s-=x-b[id];b[id]=x;
	for(int i=id+1;i<=n;++i){
		if(b[i]<b[i-1]-1){
			s-=(b[i-1]-1-b[i]);
			b[i]=b[i-1]-1;
		}
		else {
			tag=1;break;
		}
	}
	return s>=0&&tag;
}
signed main(){
	int t;read(t);
	while(t--){
		read(n);read(k);for(int i=1;i<=n;i++)read(a[i]);
		int ans=0;
		for(int i=1;i<=n;i++){
			int l=a[i],r=k+a[i];
			while(l<r){
				int mid=l+r+1>>1;
				if(check(i,mid))l=mid;
				else r=mid-1;
			}
			ans=max(ans,l);
		}
		cout<<ans<<"\n";
	}
}

D

本题交互。

有一个长为 n 的序列 a,最初不知道 a 的任何一个值。

一次询问区间 [l,r] 的逆序对个数,代价为 (rl)2

你需要进行若干次询问,求出 a 的最大值所在的位置,并使得询问总代价 w5n2

多测,1t100,1n2000。所有数据中 n 的总和不超过 2000

对于限定区间代价的问题,我们先判断分开区间判断更优还是合并区间判断更优

看这个代价,对于长为 a+1b+1 的两段区间,将其分开询问的代价为 a2+b2,将其合并询问的代价为 (a+b+1)2>a2+b2

所以显然是询问小段更优,这启发我们自底向上确定最大值,设询问 [l,r] 的答案为 f(l,r)

我们要找最大值位置,怎么判断一个区间的最大值?设这个最大值在位置 x,则显然是 f(x,r)f(x+1,r)=rx

考虑到我们以后必然是要合并若干区间的答案的,则我们对于区间 [l,r],若已知 [l,k] 的最大值位置为 x[k+1,r] 的最大值位置为 y,则若 f(x,y)f(x+1,y)=yx,就有 ax>ay

故可以分治解决该问题,代价?

考虑当 n=2k 的时候,n2000,则 k 取11。n2k 显然优于为2的次幂的情况。

计算代价,对于 len=2a,有 2ka 个区间,最坏情况下代价为 2ka((2a)2+(2a1)2)=2ka(22a+12a+1+1)

也即代价为 2a+12k2·2k+2kak 取11,分开求和,原代价为:

a=1k(2k2a+12·2k+2ka)=2k(2k+24)2k+1k+2k1=22k+22k+22k+1k+2k1<5×22k

所以是一个可行的方案。

int ask(int l,int r){
	if(l==r)return 0;
	cout<<"? "<<l<<" "<<r<<"\n";cout.flush();
	int x;cin>>x;return x;
} 
int solve(int l,int r){
	if(l==r)return l;
	int mid=l+r>>1;
	int x=solve(l,mid),y=solve(mid+1,r);
	int a=ask(x,y),b=ask(x+1,y);
	if(a-b==y-x)return x;
	return y;
}
int main(){
	ios::sync_with_stdio(false);
	int t;cin>>t;
	while(t--){
		int n;cin>>n;int ans=solve(1,n);
		cout<<"! "<<ans<<"\n";
	}
}

事实上分治法在确定位置的问题上是常用的。但为什么我没想到呢?

确定一个最大值必然需要确定两段区间的最大值再比较谁更大啊。

E2

这是问题的困难版本,两个版本之间的唯一区别是 n 的范围和时间限制的不同。

给定一棵以 1 为根的有根树,你需要给出一个 1n 的排列 a,最大化二元组 (u,v) 的数量,满足 au<alca(au,av)<av,输出这个最大值。

2n106

考虑一个简化版问题,这很像二叉树的中序遍历。亦或者说是中序遍历版dfs序。

扩展一下,原问题就变为确定一个中序遍历的顺序,使满足要求的点对尽量多。

那么,假设进入子树 v1vx 后,确定 u 的值,然后进入子树 vx+1vm,则带来的收益为:(sizv1++sizvx)(sizvx+1++sizvm)

由于 sizv=sizu1 为定值,由均值不等式,当 |(sizv1++sizvx)(sizvx+1++sizvm)| 取得最小值时,收益最大。

则问题化为:给定 sizv1sizvm,求从中选出若干个数字使得凑出的和最接近 sizu2

这是一个经典的动态规划求解可行性的问题,可以使用01背包在 O(n) 中求解。

对于每一颗子树做这样的动态规划,其复杂度是 O(n2)。因为每一个节点只会被当作子节点更新一次。

这时候我们就可以通过E1的 n5000,但怎么扩展到 E2 的 n106 呢?

先用一个启发式,若 maxsizvsizu2,直接将 maxsizv(sizumaxsizv1) 计入答案,不进行DP。

在不久之前的一把CF里有一个去重后使用倍数法保障复杂度 O(nlogn) 的题目,在这里,我们 在算法上优化似乎无路可走,也可以从值域的角度上考虑问题,而从值域上考虑问题的经典套路是看去重后不同数字的个数。类比mex的计算,注意到 sizv<n,所以这些数去重后最多只有 2n 个相同。

重要性质:

a=n a 的不同值最多只有 2n 个相同。因为 1++n=n(n+1)2

推论:

  1. a2=n a 的不同值最多只有 t 个,其中tt(t+1)(2t+1)6n 中最小的 t,显然正实数根只有这一个,可以提前预处理亦或者二分求解。

  2. a3=n a 的不同值最多只有 t 个,其中 t 为满足 t2(t+1)24n 中最小的 t,这可以直接定为 n4+dd 为一个自己设置的参数,一般二三四吧。

那么我们可以通过统计个数,去重做多重背包,用余数分组可以做到 O(nn),用二进制拆分可以做到 O(nnlogn),听说还有多项式黑科技可以做到 O(nlog3n)。第三种做法理论复杂度最优,但常数过大。

二进制拆分的做法可以使用 bitset 优化,复杂度为 O(nnlognw)。当 n=106 时,已经接近 O(nlog2n),是最优的做法,且远远跑不满,常数极小。

但问题是,即使不加bitset也可以在1902ms内跑过去。。。。。。

加bitset是一个实现难点,因为我们无法使用不定长bitset,此时就自己手写,亦或者把2的幂次的bitset全部定义一遍,一共20个,再写20个if判断即可。

void dfs(int u,int fa){
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(v==fa)continue;
		dfs(v,u);siz[u]+=siz[v]; 
	}
	siz[u]++;int mx=0;cnt=num=0;
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(v==fa)continue;
		mx=max(mx,siz[v]);a[++cnt]=siz[v]; 
	}
	if(mx>=siz[u]/2){
		ans+=(siz[u]-mx-1)*mx;return;
	}
	sort(a+1,a+cnt+1);
	for(int i=1;i<=cnt;i++){
		if(a[i]!=a[i-1])w[++num]=a[i],c[num]=0;
		++c[num];
	}
	f[0]=1;
	cnt=0;
	for(int i=1;i<=num;i++){ 
		int x=c[i];
		for(int j=0;j>=0;++j){
			if(x<(1<<j)){
				a[++cnt]=x*w[i];break;
			}
			x-=(1<<j);
			a[++cnt]=(1<<j)*w[i];
			if(!x)break;
		}
	}
	for(int i=1;i<=cnt;i++){
		for(int j=siz[u]/2;j>=a[i];j--)f[j]|=f[j-a[i]];
	}
	int s=0;
	for(int i=1;i<=siz[u]/2;++i)if(f[i]){
		s=max(s,i*(siz[u]-i-1));f[i]=0;
	}ans+=s;
}
posted @   spdarkle  阅读(57)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示