今日头条 2018 AI Camp 6 月 2 日在线笔试编程题第二道——两数差的和
题目
给 n 个实数 a_1, a_2 … a_n, 要求计算这 n 个数两两之间差的绝对值下取整后的和是多少。
输入描述
第一行为一个正整数 n 和一个整数 m。接下来 n 行,第 i 行代表一个整数 b_i。a_i = b_i / m, i = 1…n。
n <= 1000: 5分
n <= 100000且 a_i 均为整数: 15分
n <= 100000 1 <= m <= 10^9 0 <= b_i <= 10^18: 25分
输出描述
一个整数
示例1
-
输入
3 10
11
22
30 -
输出
2
备注:
请选手在解题时注意精度问题
a_i 取值为 1.1,2.2 和 3.0
|1.1-2.2| 下取整为 1
|1.1-3.0| 下取整为 1
|2.2-3.0| 下取整为 0
思路
1 . 暴力**法。 直接遍历所有的情况,对两个数相减向下取整然后求和,时间复杂度为 O(n^2)。但要注意,浮点数在内存中的特殊表示方式,比如 1.66666666666666667 - 2.66666666666666667 向下取整后可能为 0,会造成计算错误。
2 . 如果所有的数为整数,先对所有的数据按照升序排列。以 1 2 3 4 5 举例说明,|1-3| = |1-2+2-3| = |1-2| + |2-3|,|1-5| = |1-2+2-3+3-4+4-5| = |1-2| + |2-3| + |3-4| + |4-5|。可以看到,任意两个数的差都可以通过相邻两个数的差转换而来,因此,我们只需要统计相邻两个数的差总共被利用了多少次即可 。|1-2| 的差被 (1,2),(1,3),(1,4)(1,5) 共利用了 4 次{=1×4},|2-3| 的差被 (1,3),(1,4)(1,5),(1,3),(1,4)(1,5) 共利用了 6 次{=2×3},即某个区间的差值被利用的次数等于此区间左边数的个数乘以此区间右边数的个数,总的和就等于每段区间的差乘以利用的次数再求和。
3 . 如果不是整数,有小数部分的话,我们再来看看会怎样。如果小数部分也是升序,比如 |1.1 - 2.2| 和 |1 - 2| 、|3.0 - 10.9| 和 |3 - 10| 取整后都是一样的。但如果小数部分,有降序的,比如 |1.2 - 2.1| 和 |1 - 2| 、|3.9 - 10.0| 和 |3 - 10|,前者就比后者取整后少了 1。因此,小数部分每有一个逆序的,也就是前面比后面大的,总的和就减去 1 即可。
4 . 综上所述,我们可以将数字拆分为整数部分和小数部分,依据整数部分对数据升序排列,同时小数部分与整数部分作一致的调整。然后对整数部分相邻区间求差值并乘以对应的系数先求出总和,再求出小数部分的逆序对个数,总和减去逆序对数即为最终所求。其中,排序和求逆序对数的时间复杂度都可以做到 O(nlogn),因此算法总体复杂度也为 O(nlogn)。
代码实现{排序通过快排,求逆序对数采用归并排序}
// int 4 字节,long 8 字节
#include <iostream>
#include <cmath>
#include <stdio.h>
using namespace std;
void Quick_Sort(long data[], double frac[], int left, int right);
void Merge_Array(double data[], int left, int mid, int right, double temp[]);
void Merge_Sort(double data[], int left, int right, double sorted_data[]);
int cnt = 0; // 逆序对数
int main()
{
int n = 0, m = 0;
cin >> n >> m;
long b[n] = {0};
long inte[n] = {0}; // a_i 的整数部分
double frac[n] = {0}; // a_i 的小数部分
int i = 0;
double data[n] = {0};
for (i = 0; i < n; i++)
{
cin >> b[i];
data[i] = double(b[i]) / m;
}
// 暴力求解
long sum1 = 0;
int j = 0;
for (i = 0; i < n-1; i++)
{
for (j = i+1; j < n; j++)
{
sum1 += floor(fabs(data[i] - data[j]));
}
}
cout << sum1 << endl;
// 分别求出整数部分和小数部分
for (i = 0; i < n; i++)
{
inte[i] = b[i] / m;
frac[i] = double(b[i]) / m - inte[i];
}
// 整数部分升序排列,小数部分随整数部分同步调整
Quick_Sort(inte, frac, 0, n-1);
// 相邻区间差乘以系数求总和
int error[n-1] = {0};
long sum = 0;
for (i = 0; i < n-1; i++)
{
error[i] = abs(inte[i] - inte[i+1]);
sum += (i+1) * (n-i-1) * error[i];
}
// 归并排序的同时求得小数部分逆序对数
double sorted_data[n] = {0};
Merge_Sort(frac, 0, n-1, sorted_data);
sum -= cnt;
cout << sum << endl;
return 0;
}
void Quick_Sort(long data[], double frac[], int left, int right)
{
if (left < right)
{
int i = left, j = right;
long choice = data[i];
double temp = frac[i];
while(i < j)
{
while(i < j && data[j] >= choice)
{
j--;
}
if(i < j)
{// 小数部分随整数部分同步调整
data[i] = data[j];
frac[i] = frac[j];
i++;
}
while(i < j && data[i] <= choice)
{
i++;
}
if(i < j)
{// 小数部分随整数部分同步调整
data[j] = data[i];
frac[j] = frac[i];
j--;
}
}
data[i] = choice;//i=j 小数部分随整数部分同步调整
frac[i] = temp;//i=j
Quick_Sort(data, frac, left, i-1);
Quick_Sort(data, frac, i+1, right);
}
}
// O(n(logn))
void Merge_Sort(double data[], int left, int right, double sorted_data[])
{
if(left < right)
{
int mid = (left + right) / 2;
Merge_Sort(data, left, mid, sorted_data);
Merge_Sort(data, mid+1, right, sorted_data);
Merge_Array(data, left, mid, right, sorted_data);
}
}
void Merge_Array(double data[], int left, int mid, int right, double temp[])
{
int i = left, j = mid + 1;
int k = 0;
while(i <= mid && j <= right)
{
if (data[i] < data[j] || fabs(data[i] - data[j]) <= 1e-9)
{
temp[k++] = data[i++];
}
else // 左边序列某个数大于右边某个数,则左边序列此数后面的数均大于右边的数
{
temp[k++] = data[j++];
cnt += mid - i + 1;
}
}
while(i <= mid)
{
temp[k++] = data[i++];
}
while(j <= right)
{
temp[k++] = data[j++];
}
for(i = 0; i < k; i++)
{
data[left+i] = temp[i];
}
}
个人见解,如有错误,欢迎指正与交流!
获取更多精彩,请关注「seniusen」!
上一篇: shell条件语句理解及应用