问题描述:
给你一段长度为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 == ,而我们知道,在下面分别用SA和SB表示序列A的和以及序列B的和。
SB = M ,因为SA - M是一个常数,所以我们不必花费多余的精力去考虑好序列应该是什么样子的。那么什么才是答案呢?答案是一个数 —— 一组序列的最大公因数。假设我们最后得到的答案满足下式:gcd(c1,c2,c3,...,cn) = x (c1 = a1-b1 , 以此类推), 不难发现c1,c2,..,cn都将是x的倍数,而等于SA - SB,所以我们可以得出结论:答案一定就在
的众多因子之中。
下面我们要做的事情就是收集的因子,然后从大到小一个一个地验证看看当前因子是否可以作为答案,如果可以作为答案,我们输出即可。现在的问题显然变成了:如何确认 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,你应该能够发现这样下去将有可能小于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属于区间 的情况,对于这样的情况,我们经过上述操作之后可以知道:Bi = Ai,所以,在这个情况下
.
同样的,我们考虑Ai属于区间 的情况,对于这样的情况,得到:Bi = Ai - x,所以在这种情况下
,其中 c 表示在上述区间范围内,Ai 的个数 。
我们继续按照每次递进x的范围去枚举我们的区间,接下来你会得到类似于Bi = Ai - 2*x ,Bi = Ai - 3*x... 这样的答案,我们得到对于一个区间内,我们可以得到公式如下:
,于是我们就可以在O(1)时间内求得对应区间的
。
不过为了实现上面所描述的那些操作,我们需要做一些准备工作。比如说,我们可以设计一个数组来帮助我们得到c,不妨就叫这个数组cnt,它可以帮我们统计区间 内有多少个元素。当然还有一个帮助我们求在一个序列中的范围之和的数组,具体的实现方法看下面的实现代码。
代码实现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;
}