Codeforces 1806F. GCD Master

发布时间 2023-03-28 18:20:16作者: DeaphetS

题目链接:F1 - GCD MasterF2 - GCD Master

题目大意:给定 \(n,m,k(1\le k\lt n \le 10^6,1\le m\le 9\cdot 10^{18})\) 以及一个长度为 \(n\) 的序列 \({a_i}(1\le a_i\le m)\)。每次操作可以选取两个数 \(x,y\) 从序列中删去,并将 \(\gcd(x,y)\) 加入序列中。求进行恰好 \(k\) 次操作后 \(\sum a_i\) 的最大值。

分析题目性质

显然如果序列中存在相同的两个元素,那么可以通过对这两个相同元素进行操作,把他们合并成一个元素并消耗一次操作次数,于是我们先假设所有元素都互不相同,思考如何解题。

考虑这么一件事情,如果一开始选了两个数 \(x,y\) 并将其合并为 \(\gcd(x,y)\) 加到了序列中,之后我们再把这个 \(\gcd\) 拿出来去和另一个数字 \(z\) 合并。那么这一系列操作就相当于把 \(x,y,z\) 三个数字从序列中删去,并在序列中加入新数字 \(\gcd(x,y,z)\)。于是我们可以把操作分成若干组,每组操作就相当于选取 \(t\) 个数字从序列中删去,然后把这 \(t\) 个数字的 \(\gcd\) 加入到序列中,一组操作所花费的操作次数就是 \(t-1\) 次。

可以很自然地想到,若划分的组数越多,被波及的数字个数就越多,于是就会有一个猜想:是否一次性选取 \(k+1\) 个数字把他们合并就是最优的。我们来证明这个猜想。

猜想 1:一次性选取 \(k+1\) 个元素进行合并是最优策略

考虑反证法,如果操作的组数有多组,那么考虑其中两组操作。不妨设第一组操作涉及到的数字集合为 \(S\),各个数字的 \(\gcd\)\(x\);第二组操作涉及的数字集合为 \(T\)\(\gcd\)\(y\),且有 \(x\ge y\)。那么这两组操作带来的贡献就是 \(x+y-\sum_{i\in S}{i}-\sum_{j\in T}{j}\)

接下来观察把这两组操作进行合并对贡献造成的影响。选取 \(S\) 中最大的数字 \(mx\),由于不可重集 \(S\) 中的 \(\gcd\)\(x\)\(|S|\ge 2\),所以有 \(mx\ge 2x\)。考虑将 \(mx\)\(S\) 中删除(即不对该数字进行操作),并对 \(S,T\) 各自合并后的结果 \(x,y\) 进行一次操作,那么在操作次数不变的情况下,其贡献为 \(\gcd(x,y)-\sum_{i\in S}{i}-\sum_{j\in T}{j}+mx\)

对比两种操作的贡献,去除掉相同的部分有 \(mx+\gcd(x,y)\gt 2x\ge x+y\),得出后者更优。于是最优方案下的操作组数只会有一组,即得证。

题意转换

猜想得到证明后,我们就能将题意转化为:选 \(k+1\) 个数字将他们合并成这些数字的 \(\gcd\),求代价最少的方案。当然,这里有个前提是序列中的元素各不相同。

考虑有相同元素怎么办。实际上如果有相同的元素出现在同一组操作中,可以看做先把相同的元素合并成一个元素,剩下的是互不相同的元素一起操作。那么每次把两个相同元素合并的代价就是对应元素的值。

于是可以把重复的数字拎出来并排序,令 \(b_i\) 表示删去 \(i\) 个重复元素的最小花费。枚举 \(i\) 的值,计算一次性选 \(k-i+1\) 个数字合并的最小花费就能得到最终的答案。

在这一系列转换过后,我们要解决的问题就变成了:对每个 \(i\le k+1\),求选择 \(i\) 个数字合并成他们的 \(\gcd\) 的最小花费。

简单版本:\(m\le 10^6\)

由于值域只有 \(10^6\),那么就有一个经典的做法就是枚举 \(\gcd\) 的值,之后判断 \(\gcd\) 的每个倍数是否存在并计算即可。计算答案部分的时间复杂度为 \(O(m\log m)\)

#include<bits/stdc++.h>
using namespace std;
#define N 1000010
int T,n,m,k,x,cnt,c[N];
long long sum,ans,b[N];
void init()
{
	cnt=sum=ans=0;
	scanf("%d%d%d",&n,&m,&k);
	k++;
	for(int i=1;i<=n;i++){
		scanf("%d",&x);
		c[x]++;
		sum+=x;
		if(c[x]>1)b[++cnt]=x;
	}
	sort(b+1,b+cnt+1);
	for(int i=1;i<=cnt;i++)b[i]+=b[i-1];
	if(cnt>=k-1)ans=max(ans,sum-b[k-1]);
	for(int i=1;i<=m;i++){
		long long res=i;
		for(int j=i,num=0;j<=m && num<k;j+=i)if(c[j]){
			res-=j,num++;
			if(cnt>=k-num)ans=max(ans,sum+res-b[k-num]);
		}
	}
	for(int i=1;i<=m;i++)c[i]=0;
	printf("%lld\n",ans);
}
int main()
{
	scanf("%d",&T);
	while(T--)init();
}

硬版本

对于困难版本,由于值域是 \(9\cdot 10^{18}\),我们不能枚举 \(\gcd\) 的值求解,于是需要继续大力分析。

现在不妨设去重后的序列 \({a_i}\) 是有序的,思考我们在简单版本下枚举 \(\gcd\) 的值并求解的过程,显然在 \(\gcd\) 的值确定时,选取最小的几个数字一定是最优的。而在我们不确定 \(\gcd\) 的值的时候,直接选取最小的若干个数字合并可能就会导致被选取数字的 \(\gcd\) 出现断崖式下降,使得最终的答案变劣。但有些时候 \(\gcd\) 下降带来的损失我们又是能接受的,于是考虑对选取方案进行一步步调整,并逐步得到最优解。

调整选取方案

假设一组操作中选取的数字个数为 \(t\),被选中的数字为 \(a_{i_1},a_{i_2},...,a_{i_t}(i_{j}<i_{j+1})\),这些数字的最大公约数为 \(d\),考虑如何调整使代价减小。那么调整的方式就是在这些数字中删掉一个数,再找一个数替换进来,代价的变化就是这两个数字之间的差值减去调整前后 \(\gcd\) 的差值。

基于这种调整方式来考虑,我们选择删掉最大的数字 \(a_{i_t}\),并加入最小的未被选到的 \(a_x\),那么代价的变化量即为 \(a_{i_t}-a_x-d+\gcd(d,a_x)\)。注意到,由于原先这些数字的 \(\gcd\)\(d\),所以一定有 \(a_{i_t}-a_{i_{t-1}}\ge d\)。那么若有 \(x\lt i_{t-1}\),就有 \(a_{i_t}-a_x-d+\gcd(d,a_x)\ge a_{i_t}-a_{i_{t-1}}-d+\gcd(d,a_x)\gt 0\),在这种情况下这样调整就可以使答案更优。

那么什么时候不能这样调整呢?显然这时一定会满足:不存在未被选择的下标 \(x\) 使得 \(x\lt i_{t-1}\)。这个命题其实就等价于对任意 \(j\le t-1\),有 \(i_{j}=j\)。相当于我们先选数组中最小的 \(t-1\) 个数,再选择一个数字使得答案最优,于是我们得到了一个已经被证明了的猜想。

猜想 2:若需要选取一组 \(t\) 个数字进行操作,则最优方案一定包含序列中最小的 \(t-1\) 个数

采用反证法即可证明,若存在 \(i\le t-1\) 使得 \(a_i\) 未被选择,则根据之前的调整方案进行调整可使答案更优,即得证。

算法优化

根据已有结论,令 \(g_i\) 表示序列中前 \(i\) 个数的 \(\gcd\)\(S_i\) 表示序列 \({a_i}\) 的前缀和。那么本题的做法就是枚举需要合并的数字个数 \(i\),得出需要删除的重复数字个数 \(k-i+1\),对应的最优解就是 \(\underset{x\in [i,n]}{\max} \{Sum-S_{i-1}-a_x+\gcd(g_{i-1},a_x)-b_{k-i+1}\}\),其中 \(Sum\) 表示初始序列的元素之和。

那么可以发现,对每个 \(i\),我们要求的就是最大的 \(\gcd(g_{i-1},a_x)-a_x\),直接做肯定会寄。但是由于前缀 \(\gcd\) 有个性质就是值相同的段数是 \(O(\log m)\) 级别的,所以可以对每一段相同的 \(g_i\) 暴力枚举 \(x\) 求解,时间复杂度为 \(O(n\log^2m)\)

#include<bits/stdc++.h>
using namespace std;
#define N 1000010
#define LL long long
int T,n,k,t,cnt,c[N],p[N];
__int128 sum,ans,S[N],b[N];
LL m,a[N],g[N];
set<LL>s;
void out(__int128 x)
{
	if(x>9)out(x/10);
	printf("%c",(char)('0'+x%10));
}
void init()
{
	s.clear();
	t=cnt=sum=ans=0;
	scanf("%d%lld%d",&n,&m,&k);
	k++;
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		sum+=a[i];
		if(s.count(a[i]))b[++cnt]=a[i];
		s.insert(a[i]);
	}
	n=0;
	for(auto i:s)a[++n]=i;
	sort(b+1,b+cnt+1);
	for(int i=1;i<=cnt;i++)b[i]+=b[i-1];
	if(cnt>=k-1)ans=max(ans,sum-b[k-1]);
	for(int i=1;i<=n;i++){
		S[i]=S[i-1]+a[i];
		g[i]=__gcd(a[i],g[i-1]);
		if(g[i]!=g[i-1])p[++t]=i;
		if(cnt>=k-i && k>=i)ans=max(ans,sum+g[i]-S[i]-b[k-i]);
	}
	p[t+1]=n+1;
	for(int i=1;i<=t;i++){
		__int128 mx=-sum;
		for(int j=p[i+1];j<=n;j++)
			mx=max(mx,(__int128)__gcd(g[p[i]],a[j])-a[j]);
		for(int j=p[i];j<min(p[i+1],k);j++)
			if(k-j-1<=cnt)ans=max(ans,sum+mx-S[j]-b[k-j-1]);
	}
	out(ans);
	printf("\n");
}
int main()
{
	scanf("%d",&T);
	while(T--)init();
}