完美数列(PAT)
题目描述
给定一个正整数数列,和正整数p,设这个数列中的最大值是M,最小值是m,如果M <= m * p,则称这个数列是完美数列。现在给定参数p和一些正整数,请你从中选择尽可能多的数构成一个完美数列。
输入描述:
输入第一行给出两个正整数N和p,其中N(<= 1e5)是输入的正整数的个数,p(<= 1e9)是给定的参数。第二行给出N个正整数,每个数不超过1e9。
输出描述:
在一行中输出最多可以选择多少个数可以用它们组成一个完美数列。
输入例子:
10 8
2 3 20 4 5 1 6 7 8 9
输出例子:
8
解题思路:
本题可采取sort排序+二分的方法进行解答,具体分析如下:
-
题意:获取一个完美数列,该数列中最大值M,最小值m,满足M<=m*p。由此可知,我们的关注点在于获得一个最大值和一个最小值,使得该两最值在满足上述公式的前提下,两者相距尽可能最长(两者中间需包含尽可能多的数)。
-
如何方便地获得任意两对相距最长的最大值和最小值呢?我们采取sort将所有数进行排序,而后选取任一数作为最大值M(不能直接选取整个输入数列的最大值或最小值,因为这样会进入局部最优情况,而非全局最优,因为每一个数均有可能为最长完美数列的最大值),那么我们根据给定的p值,我们可得知相应满足条件的最小m值为ceil(M/p),例如20和8,满足的m值为3。
-
接下来找出满足条件的m值位置pos,而最大值和最小值之间的元素个数(因为有序,所以能通过该方法获取完美数列长度)便是该完美数列的长度。而找出满足条件的m值,由于我们采取了sort对数据进行了排序,所以我们可以使用时间复杂度为log(n)的二分算法查找不小于m的数位置,最后计算:
以 M 为 最 大 值 的 完 美 数 列 长 度 = M 下 标 − m 下 标 + 1 以M为最大值的完美数列长度 = M下标 - m下标 + 1 以M为最大值的完美数列长度=M下标−m下标+1 -
最后,定义遍历Max,为获取的最长完美数列长度,对于每次获取的完美数列长度,取最大值。
源代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 1e5 + 50;
int n, p;
int num[N]; //用于记载以当前数为最大值所拥有的完美数列长度
int Max = -1; //最长长度
vector<int> v; //vector容器存储数组
//从left和right区间,二分算法查找不小于num数的位置
int erf(int num, int left, int right) {
int l = left, r = right;
int mid = (l+r) / 2;
while(l <= r) {
//相等或大于时r = mid-1,说明最后退出循环时,不小于num数位置刚好为r+1
if(v[mid] >= num) {
r = mid - 1;
}
else {
l = mid + 1;
}
mid = (l+r) / 2; //更新mid
}
//返回r+1
return r+1;
}
int main() {
scanf("%d%d", &n, &p);
//输入数据
for(int i = 0; i < n; i++) {
int a;
scanf("%d", &a); //数据较多,采用scanf输入
v.push_back(a);
}
//数据量大小1e5,可采取最简单的sort进行数据排序
sort(v.begin(), v.end());
num[0] = 1; //第一个数一定满足M <= m*p
//从1开始遍历全部数据(下标方便取),获取完美数列长度
for(int i = 1; i < n; i++) {
//其完美数列左端必须不小于Min
int Min;
//如果刚好整除,则Min为v[i]/p,否则进一(满足条件M <= m*p)
if(v[i]%p == 0) Min = v[i] / p;
else Min = v[i] / p + 1;
//调用二分函数,从0~i-1位置获取不小于Min的数的下标位置
int pos = erf(Min, 0, i-1);
//完美数列长度:M所在下标 - m所在下标 + 1
num[i] = i - pos + 1;
//找出最长长度完美数列
Max = max(Max, num[i]);
}
cout << Max; //输出结果
return 0;
}