CSP-序列查询新解(C++简单易懂)

题目背景

上一题“序列查询”中说道:

A=[A0,A1,A2,⋯,An] 是一个由 n+1 个 [0,N) 范围内整数组成的序列,满足 0=A0<A1<A2<⋯<An<N。基于序列 A,对于 [0,N) 范围内任意的整数 x,查询 f(x) 定义为:序列 A 中小于等于 x 的整数里最大的数的下标

对于给定的序列 A 和整数 x,查询 f(x) 是一个很经典的问题,可以使用二分搜索在 O(log⁡n) 的时间复杂度内轻松解决。但在 IT 部门讨论如何实现这一功能时,小 P 同学提出了些新的想法。

题目描述

小 P 同学认为,如果事先知道了序列 A 中整数的分布情况,就能直接估计出其中小于等于 x 的最大整数的大致位置。接着从这一估计位置开始线性查找,锁定 f(x)。如果估计得足够准确,线性查找的时间开销可能比二分查找算法更小。

比如说,如果 A1,A2,⋯,An 均匀分布在 (0,N) 的区间,那么就可以估算出:

f(x)≈(n+1)⋅xN

为了方便计算,小 P 首先定义了比例系数 r=⌊Nn+1⌋,其中 ⌊ ⌋ 表示下取整,即 r 等于 N 除以 n+1 的商。进一步地,小 P 用 g(x)=⌊xr⌋ 表示自己估算出的 f(x) 的大小,这里同样使用了下取整来保证 g(x) 是一个整数。

显然,对于任意的询问 x∈[0,N),g(x) 和 f(x) 越接近则说明小 P 的估计越准确,后续进行线性查找的时间开销也越小。因此,小 P 用两者差的绝对值 |g(x)−f(x)| 来表示处理询问 x 时的误差。

为了整体评估小 P 同学提出的方法在序列 A 上的表现,试计算:

error(A)=∑i=0N−1|g(i)−f(i)|=|g(0)−f(0)|+⋯+|g(N−1)−f(N−1)|

输入格式

从标准输入读入数据。

输入的第一行包含空格分隔的两个正整数 n 和 N。

输入的第二行包含 n 个用空格分隔的整数 A1,A2,⋯,An。

注意 A0 固定为 0,因此输入数据中不包括 A0。

输出格式

输出到标准输出。

仅输出一个整数,表示 error(A) 的值。

解题思路:

这一道题的部分思路承接了上一道题,这里就不再多说了(不知道的可以去回顾上一题)。对于这道题,首先,我们并不能直接定义数组来保存 f(x) 和 g(x) 的值,因为N最大可以取到,超出了我们所能定义的数组大小。那么如何求|f(x)-g(x)|的和呢?我们采取动态处理的方式,每输入一个A[i],就先求出当前的总和。我们用a来保存当前的输入,用b来保存前一次的输入,易知在[b,a)的范围内,f(x)的值是固定的,为 i-1;这时候我们只需求出g(x)的值,两者相减取绝对值再累加即可。最后一个A[i]输入后,我们再对[a,N)范围内的数进行相同的处理,即可得最终结果。

但,是否就只有这些呢?当然不是,提交上去,显示运行超时,我们只得了70分。

70分的代码:

# include <iostream>
# include <math.h>

using namespace std;

int n, N;
long long sum = 0;  //最终值可能会非常大,要定义为long long型
int r;

int main()
{
    cin >> n >> N;
    r = N / (n + 1);
    int a = 0, b; //a:当前值(第一个值为0)  b:前一个值 
    for (int i = 1; i <= n; i++) {
        b = a;
        cin >> a;
        for (int j = b; j < a; j++) {
            //区间[b,a)上f(j)的值固定为 i-1
            sum = sum + abs(j / r - i + 1);
        }
    }
    //再加上区间[a,N)对应的值,方法同上
    for (int i = a; i < N; i++) {
        //区间[a,N)上f(i)的值固定为 n
        sum = sum + abs(i / r - n);
    }
    cout << sum << endl;
    return 0;
}

所以,我们需要进一步改进我们的算法。关键是在什么地方加以改进?外层循环是必要的,已无法再优化,这时候,我们的着眼点就落在了内层循环上。除了f(x)的取值具有一定的规律外,g(x)的取值也有一定的规律。g(x)总是取到一段连续且相同的值,我们只要知道每一段连续的值有多长,就可以将这一段放在一起处理(这一段的g(x)和f(x)值都固定不变)。对此,我们只需要对原来的代码稍做调整,再次提交后,就可以顺利拿到100分了。

100分代码:

# include <iostream>
# include <math.h>

using namespace std;

int n, N;
long long sum = 0;  //最终值可能会非常大,要定义为long long型
int r;

int main()
{
    cin >> n >> N;
    r = N / (n + 1);
    int a = 0, b; //a:当前值(第一个值为0)  b:前一个值 
    int len;      //len:g(x)连续相同值的长度
    for (int i = 1; i <= n; i++) {
        b = a;
        cin >> a;
        for (int j = b; j < a; j = j + len) {
            len = (j / r + 1) * r - j;
            //若计算得到的长度超过区间长度,需进行修正
            if (j<a && j + len>a) {
                len = a - j;
            }
            //区间[b,a)上f(j)的值固定为 i-1,g(j)固定为j/r
            sum = sum + len * abs(j / r - i + 1);
        }
    }
    //再加上区间[a,N)对应的值,方法同上
    for (int i = a; i < N; i = i + len) {
        len = (i / r + 1) * r - i;
        if (i<N && i + len>N) {
            len = N - i;
        }
        //区间[a,N)上f(i)的值固定为 n,g(i)固定为i/r
        sum = sum + len * abs(i / r - n);
    }
    cout << sum << endl;
    return 0;
}

总结:

CSP的第2题,很多都具有这样的特点,第一次的解题方法,一般都容易时间超限,这时候,我们就需要想办法改进我们的算法,这也是难点所在。以上便是我的解题思路,很高兴与大家分享。

猜你喜欢

转载自blog.csdn.net/CXR_XC/article/details/129592623