问题描述:
给定一个长度为n的数组arr和一个正整数k,问从数组中任选两个数使其和是k的倍数有多少种选法。
注: 对于数组a1=1,a2=2,a3=3来说,(a1,a2)和(a2,a1)是同一种选法,(a1,a2)和(a1,a3)是不同的选法。
输入: 第一行有两个正整数n和k,n<=1e6,k<=1000。
第二行有n个正整数,每个数的大小不超过1e9。
输出: 选出一对数使其是k的倍数的选法个数
输入样例:
5 6
1 2 3 4 5
输出样例:
2
思路1: 暴力枚举
for(int i=0;i<n;i++)
for(int j=i+1;j<n;j++)
if( (arr[i]+arr[j])%k==0 )
cnt++;
这个应该很容易想到,枚举所有数据,将每个数都与它后面的数求和再判断是否是k的倍数即可。但是它的时间复杂度是O(n^2)。当n=1e6时,循环验证次数达到了1e12,这显然不是我们想要的。
思路2: 优化枚举
这里需要将判别式进行一个转换,在过程中取余和最后取余是不会改变结果的:
(a[i]+a[j])%k=0 <----> (a[i]%k+a[j]%k)%k=0
根据新的判别式我们可以将arr数组中的数字进行处理,即
for(int i=0;i<n;i++)
b[arr[i]%k]++;
这样就得到了一个处理过后的新数组b[0]~b[k-1],表示的是原来所有数%k 的结果的出现次数。我们可以推导一下b数组的建立。
比如给定一组输入:
5 6
1 4 5 2 7
推导b数组:
b[1%6]=b[1];
b[4%6]=b[4];
b[5%6]=b[5];
b[2%6]=b[2];
b[7%6]=b[1];
即:b[0]=0; b[1]=2(1%6=1,7%6=1); b[2]=1; b[3]=0; b[4]=1; b[5]=1;
我们容易看出 b[i] 和 b[(k-i)%k] 中的数可以求和为k的倍数(0<=i<k)。
比如:上面例子中b[1]=2,b[5]=1,组合个数为b[1] * b[5]=2;即 (1+5)%6=0和(7+5)%6=0。
以此类推······
注
当 i==(k-i)%k时,表示b[i]中的数自我组合,比如5%5=0,10%5=0。b[0]=2.组合个数为b[0]*(b[0]-1)/2=1。即5+10=15。
当 b[i] 的 i>(k-i)%k 时,新的组合是前面的组合交换顺序得到的,比如前面求过(a1,a2)这时是(a2,a1),这是不符合题目要求的,我们就可以终止枚举了。
所以由新的表达式我们可以将枚举对象变化为b[0] ~b[k-1],由原来的单纯枚举数据变为枚举(数据%k)后数出现次数。时间复杂度优化到了O(k)。
for(int i=0;i<k;i++)
{
long long ans=0;
int j=(k-i)%k; //b[0]~b[k-1],b[1]和 b[k-1]可以组合成 k,以此类推
if(i>j) //i>j后的组合都是出现过的,不符合要求
break;
else if(j==i) //自我组合
ans+=1LL*b[i]*(b[i]-1)/2;
else //b[i]和b[j]组合
ans+=1LL*b[i]*b[j];
//1LL表示将1强制转为long long,避免相乘超int数据范围
}
完整代码:
- 暴力枚举:
#include<stdio.h>
const int maxn=1e6+5;
int arr[maxn];
int main()
{
int n,k;
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)
scanf("%d",&arr[i]);
long long cnt=0;
for(int i=0;i<n;i++)//暴力枚举
for(int j=i+1;j<n;j++)
if((arr[i]+arr[j])%k==0)
cnt++;
printf("%lld\n",cnt);
return 0;
}
- 优化枚举
#include<stdio.h>
#include<string.h>
const int maxn=1e6+5;
int arr[maxn];//arr为全局变量,默认已经全部初始化为0
int main()
{
memset(arr,0,sizeof(arr));//批量初始化arr数组为0
int n,k;
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)
{
int temp;
scanf("%d",&temp);
arr[temp%k]++;//计数
}
long long cnt=0;
for(int i=0;i<k;i++)//优化枚举
{
int j=(k-i)%k;
if(i>j)
break;
else if(i==j)
cnt+=1LL*arr[i]*(arr[i]-1)/2;
else
cnt+=1LL*arr[i]*arr[j];
}
printf("%lld\n",cnt);
return 0;
}