Sequence GCD (February Long 2022 - II, Division 3 (Rated) ) 中文题解

原题链接

题解原文

问题描述:

给你一段长度为N的序列A,和一个整数M,其中M的值大于等于0,小于等于序列的和。

如果一个有N个元素的序列B满足以下条件那么就称这个序列是一个好序列

        ·对于每一个从i到n的元素都有Bi < Ai

        ·序列B的序列和等于M

从所有的好序列B里面找到一组序列使得 gcd(A1-B1,A2-B2,...,An-Bn) 的值最大,并输出这个值。

题目分析:

        首先让我们观察一下问题所描述的情形,不难看出 A1-B1 + A2-B2 + ... +An-Bn == \sum_{i=1}^{N}Ai - \sum_{i=1}^{N}Bi,而我们知道,在下面分别用SA和SB表示序列A的和以及序列B的和。

SB = M ,因为SA - M是一个常数,所以我们不必花费多余的精力去考虑好序列应该是什么样子的。那么什么才是答案呢?答案是一个数 —— 一组序列的最大公因数。假设我们最后得到的答案满足下式:gcd(c1,c2,c3,...,cn) = x (c1 = a1-b1 , 以此类推), 不难发现c1,c2,..,cn都将是x的倍数,而\sum_{i=1}^{N}Ci等于SA - SB,所以我们可以得出结论:答案一定就在\sum_{i=1}^{N}Ci的众多因子之中。

        下面我们要做的事情就是收集\sum_{i=1}^{N}Ci的因子,然后从大到小一个一个地验证看看当前因子是否可以作为答案,如果可以作为答案,我们输出即可。现在的问题显然变成了:如何确认 x == gcd(c1,c2,...,cn)。

        经过我们上面的分析,我们应该要清楚,对于序列C中的每一个元素应该满足以下条件其中之一:

        1.Ci = 0 ,或者;

        2.Ci % x = 0 

        已知Ci = Ai - Bi,我们想要(Ai - Bi)% x = 0,只需要让Bi = Ai % x  就可以了。可以这样来理解:假设你拥有一个容积是Ai的量筒,你想往里面装越多的溶液越好,但你只能在里面装体积是x的倍数的溶液,那么显然,如果你有精确的测量仪器去装溶液,你的量筒剩下的没有装水的体积将会是Ai % x 。按照这样策略去得到每一个Bi,你应该能够发现这样下去\sum_{i=1}^{N}B_i将有可能小于M(比如:考虑每一个Ai%x都得到0的情况)。不要着急,现在你先将当前使用上述策略得到的序列B的和记作T,我们开始考虑以下几种情况:

·T > M : 我们上面的策略显然是一个贪心策略,它意味着,如果你想要让 gcd(c1,c2,...cn)= x 成立,最起码你的每一个Bi加起来之后的和至少等于T。M是Bi之和的最大值,所以在 T > M 的情况下,我们就可以知道 x 是不可能满足条件的。

·T == M :经过我们上面的分析我们不难知道当T==M的时候是满足条件的。

·T < M : 首先我们的先决条件就是(SA - M)%x == 0,然后我们的策略又可以保证(SA - T)%x == 0,假设(SA - M)= K·x,(SA - T)=N·x,于是得到:(SA - M) - ( SA - T)= M - T =(K - N)·x。

我们为什么要在这里证明这个等式呢?

或许你现在已经发现了,我们现在得到的序列B并不是一个好序列(因为它的和是T),所以为了得到好序列,我们需要在原有的序列B的基础上添加整数个x(或者表达成a个x)使得 T + a·x = M。这个操作可行么?当然可行!这个a在这里就是(K-N),这就是为什么我们要证明这个等式。

———————————————————————————————————————————

代码实现1:

/*注意,下面这段代码只能通过一半的样例*/
#include<bits/stdc++.h>
using namespace std;
#define fast ios::sync_with_stdio(false);cin.tie(0)

int main()
{
    fast;
    int t ; cin >> t;
    while(t--){
        int n , m , sa=0;

        cin >> n >> m;
        vector<int> a(n);
        for(auto &i : a){
            cin >> i;
            sa+=i;
        }

        int diff = sa - m,ans = -1;

        for(int i=1;i*i <= diff;i++){
            if(diff % i == 0){

                int x1=i ,x2=diff/i ,temp1=0, temp2=0;

                for(auto i : a) temp1+=(i%x1);
                for(auto i : a) temp2+=(i%x2);

                if(temp1 <= m) ans = max(ans,x1);
                if(temp2 <= m) ans = max(ans,x2);
            }
        }

        cout<<ans<<endl;
    }
    return 0;
}

        上面这段代码只能通过subtask1的测试样例。

———————————————————————————————————————————

      对于每一个Ai,如果我们想让它成为x的倍数的话,我们就必须对它进行一次 Ai - x·(Ai / x)(A % x)  的操作,这样我们的时间复杂度将会是O(n),有没有什么办法可以降低时间复杂度呢?

        我们先考虑Ai属于区间 [0,x-1] 的情况,对于这样的情况,我们经过上述操作之后可以知道:Bi = Ai,所以,在这个情况下\sum B_i = \sum A_i.

        同样的,我们考虑Ai属于区间 [x,2*x-1] 的情况,对于这样的情况,得到:Bi  = Ai - x,所以在这种情况下\sum B_i = \sum (A_i-1*x)=\sum A_i - c*x,其中 c 表示在上述区间范围内,Ai 的个数 。

        我们继续按照每次递进x的范围去枚举我们的区间,接下来你会得到类似于Bi = Ai - 2*x ,Bi = Ai - 3*x... 这样的答案,我们得到对于一个区间[p*x,(p+1)*x-1]内,我们可以得到公式如下:\sum B_i = \sum A_i - c*(p*x),于是我们就可以在O(1)时间内求得对应区间的\sum B_i

        不过为了实现上面所描述的那些操作,我们需要做一些准备工作。比如说,我们可以设计一个数组来帮助我们得到c,不妨就叫这个数组cnt,它可以帮我们统计区间 [p*x,(p+1)*x-1] 内有多少个元素。当然还有一个帮助我们求在一个序列中的范围之和的数组,具体的实现方法看下面的实现代码。

代码实现2:

#include<bits/stdc++.h>
using namespace std;
#define fast ios::sync_with_stdio(false);cin.tie(0)
typedef long long ll;//不用long long会溢出
ll a[100010];
ll cnt[100010];
ll sum[100010];

int main()
{
    fast;
    int t ; cin >> t;
    while(t--){

        memset(a,0,sizeof(a));
        memset(cnt,0,sizeof(cnt));
        memset(sum,0,sizeof(sum));

        ll n , m;
        cin >> n >> m;

        for(ll i=0;i<n;i++){
            cin >> a[i];
            sum[a[i]] += a[i];//记录每一个数的和。
            cnt[a[i]]++; //记录每一个数出现的次数。
        }

        for(ll i=0;i<=100000;i++) sum[i] += sum[i-1], cnt[i]+=cnt[i-1];//从0到i的和是多少,以及从0到i有多少个数。

        ll ans = 1;
        for(ll i=100000;i>1;i--){
            ll t = 0;
            for(ll j = 0; j < 100000; j += i){
                ll l = j, r = min(100000LL,j+i-1);
                t += (sum[r]-sum[l]) - (cnt[r]-cnt[l])*j;
            }
            if(m >= t && (m - t)%i == 0){
                ans = i;
                break;
            }
        }

        cout << ans << endl;
    }
    return 0;
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值