【问题标题】:Segments sum algorithm段求和算法
【发布时间】:2014-11-16 23:12:35
【问题描述】:

我正在尝试解决以下任务:

1) 给定大小为 N 的数组 A。
2) 给定一组 范围更新 查询,即 (L, R, val) 应该为 L A[i] += val。 3) 给定一组 range sum 查询,即 (L, R) 应该返回 sum(A[i]) for L

约束:

1) A、段和查询集的大小 N, N1, N2 2) 0

问题是计算 所有范围求和查询的总和 (S) 以 2^32 为模。

似乎人们可以实现 Segment tree 以在 O(NlogN) 时间内获得所需的总和,但实际上我们 > 需要用到这个数据结构。相反,我们可以用 2 或 3 个数组在 O(N) 时间内以某种方式计算 S。这里的一般想法是什么?

我最近用 C++ 编写了一些算法来解决这个问题,但这并不是最优的。伪代码:

  1. 创建两个数组 Add[0..N-1] 和 Substract[0..N-1]。
  2. 遍历范围更新集并执行 Add[L] += val 和 Substract[R] += val。
  3. 创建数组 Partial_sum[0..N]
  4. Partial_sum[0] = 0,what_to_add = 0。
  5. 对于 [1..N] 中的 i:
    5.1。 Partial_sum[i] = Partial_sum[i - 1] + Add[i - 1] + what_do_add
    5.2. what_do_add = what_to_add + Add[i - 1] - Substract[i - 1]

我们得到 Partial_sum 数组,并且可以像 Partial_sum[R+1] 一样在 O(1) 时间内轻松计算任何段总和 (L, R) - Partial_sum[L].

但是,问题在于第 2 步太慢了。此外,第 5 步中的循环很难理解。那是 O(n) 解决方案,但常数太高了。我知道应该有改进第 5 步的方法,但我不明白如何做到这一点。

有人可以提出一些想法甚至建议他们自己的算法来解决这个问题吗?

谢谢。

我的算法实现:

#include <cstring>
#include <iostream>
#include <stdio.h>

typedef unsigned int UINT;
typedef unsigned long long ULL;


//MOD and size of A
const ULL MOD  = 4294967296LL; // 2^32
const size_t N = 16777216;     // 2^24

//params for next_rand()
UINT seed = 0;
UINT a;
UINT b;


//get random segment
UINT next_rand()
{
    seed = seed * a + b;
    return seed >> 8;
}


int main()
{
    UINT N1, N2;

    std::cin >> N1 >> N2;
    std::cin >> a >> b;

    UINT* add  = new UINT[N];         //Add array
    UINT* subs = new UINT[N];         //Substraction array
    UINT* part_sum = new UINT[N + 1]; //Partial sums array

    memset(add, 0, sizeof(UINT) * N);
    memset(subs, 0, sizeof(UINT) * N);
    memset(part_sum, 0, sizeof(UINT) * (N + 1));  //Initialize arrays

    //step 2 
    for (size_t i = 0; i < N1; ++i)
    {
        UINT val = next_rand();
        UINT l   = next_rand();
        UINT r   = next_rand();

        if (l > r)
        {
            std::swap(l, r);
        }

        add[l]  = (add[l] + val);
        subs[r] = (subs[r] + val);
    }

    part_sum[0]   = 0;
    UINT curr_add = 0;

    //step 5
    for (size_t i = 1; i <= N; ++i)
    {
        part_sum[i] = (part_sum[i - 1] + curr_add + add[i - 1]);

        curr_add = (curr_add + add[i - 1] - subs[i - 1]);
    }

    UINT res_sum = 0;

    //Get any segment sum in O(1)
    for (size_t i = 0; i < N2; ++i)
    {
        UINT l = next_rand();
        UINT r = next_rand();

        if (l > r)
        {
            std::swap(l, r);
        }
        res_sum = (res_sum + part_sum[r + 1] - part_sum[l]);
    }

    std::cout << res_sum;

    delete []add;
    delete []subs;
    delete []part_sum;

    return 0;
}

【问题讨论】:

  • 请展示您目前的代码以进行改进。
  • 如果 next_rand 返回越界索引怎么办?为什么需要随机过程?也许我不明白这个问题。
  • 你可以避免使用 next_rand() 函数,因为它总是从 [0, 2^24 - 1] 返回值。问题出在算法上。如果 N1 = 2^24, N2 = 2^24,它的工作速度太慢了。应该有只使用 2 个数组而不是 3 个数组并更清楚地执行第 5 步的方法,但我不知道如何。
  • 在 next_rand() 中,种子可以超过 2^24,因为它是无符号整数,所以我们有模 2^32 算术。当我们这样做时 (seed >> 8) 我们得到的值不超过 2^32/2^8 = 2^24

标签: c++ arrays algorithm sum time-complexity


【解决方案1】:

我以不同的方式实现了描述的算法。它应该工作得更快。在更新和求和查询大小的最大值下,它应该比以前更快地工作。

#include <iostream>
#include <stdio.h>
#include <vector>

typedef unsigned int UINT;
typedef unsigned long long ULL;

const ULL MOD  = 4294967296LL; // 2^32
const size_t N = 16777216;     // 2^24

UINT seed = 0;
UINT a;
UINT b;

UINT next_rand()
{
    seed = seed * a + b;

    return seed >> 8;
}

std::vector <std::pair<UINT, UINT> > add;

int main()
{
    UINT upd_query_count;
    UINT sum_query_count;

    // freopen("fastadd.in",  "r", stdin);
    // freopen("fastadd.out", "w", stdout);

    scanf("%u", &upd_query_count);
    scanf("%u", &sum_query_count);
    scanf("%u", &a);
    scanf("%u", &b);

    add.reserve(N+1);

    for (size_t i = 0; i < upd_query_count; ++i)
    {  
        UINT val = next_rand();
        UINT l   = next_rand();
        UINT r   = next_rand();

        if (l > r)
        {
            add[r].first     += val;
            add[l + 1].first -= val;
        }
        else
        {
            add[l].first     += val;
            add[r + 1].first -= val;
        }
    }

    for (size_t i = 0; i < sum_query_count; ++i)
    {
        UINT l = next_rand();
        UINT r = next_rand();

        if (l > r)
        {
            ++add[r].second;
            --add[l + 1].second;
        }
        else
        {
            ++add[l].second;
            --add[r + 1].second;
        }
    }

    UINT curr_add = 0;
    UINT res_sum  = 0;
    UINT times    = 0;

    for (size_t i = 0; i < N; ++i )
    {
        curr_add += add[i].first;
        times    += add[i].second;

        res_sum += curr_add * times;
    }

    printf("%u\n", res_sum);

    return 0;
}

【讨论】:

    【解决方案2】:

    所以addsubs非常 大的数组。

    您应该在这里寻找加速的第一个地方是内存访问。随着N1 变大,您最终会出现大量缓存未命中。这可能有点超出解释的范围,所以我会链接:http://en.wikipedia.org/wiki/CPU_cache

    您可以加快速度。让我们尝试通过排序访问来改善空间平等。

    std::vector<std::pair<UINT, UINT>> l{N1};
    std::vector<std::pair<UINT, UINT>> r{N1};
    
    for(size_t i = 0; i < N1; ++i){
        const UINT val = next_rand();
        const UINT first = next_rand();
        const UINT second = next_rand();
    
        if(first > second){
            l[i] = std::make_pair(second, val);
            r[i] = std::make_pair(first, val);
        }else{
            l[i] = std::make_pair(first, val);
            r[i] = std::make_pair(second, val);
        }
    }
    std::sort(l.begin(), l.end());
    std::sort(r.begin(), r.end());
    
    for(size_t i = 0; i < N1; ++i){
        add[l[i].first] += l[i].second;
        subs[r[i].first] += r[i].second;
    }
    

    请记住几件事,std::pairoperator&lt; 比较 first 元素,如果相等则比较 second。这就是我无需编写任何代码即可使用std::sort 的方式。但是,如果 first 对两个元素相等,则最高的 val 将始终是添加的第二个元素。在您当前的代码中,这似乎不是问题,但如果它成为问题,您可以通过编写自己的排序循环来解决它,而不是依赖std::sort

    还取决于对每个缓存块的访问稀疏程度,在单独的循环中进行添加可能会更快。

    与往常一样,真正提高性能的唯一方法是使用实​​际数字,因此请务必在比较方法时自己做基准标记。

    【讨论】:

    • 感谢您的解释。两个时刻:1)这是 O(NlogN) 解决方案,所以它比以前花费更多时间
      2)这个解决方案比以前消耗更多内存
      3)我忘了提到我们不需要使用更多大于 256 Mb 有没有什么方法可以只使用 3 个循环而不用排序来解决这个问题?实际上,好的解决方案应该花费少于 256 Mb 并且工作 N = N1 = N2 = 2^24 少于 4 秒。干杯
    • @PeterLeontev 1) Big-O 表示法很有用,如果这里所有操作的成本都相同,它们不会 2) 你总是可以给一个人的电脑增加更多的内存,你永远不能给他们的电脑增加更多的时间生活 3) 我认为从next_rand 读取的订单值很重要,因为您总是将较小的一个放在l 中?如果是这样的话,我不认为你可以通过拆分初始化循环来节省空间。
    • 乔纳森,谢谢你的建议。我已经意识到如何提高这种算法的速度。我们只需要在某个数组中标记段边界(更新查询),然后计算总和查询的边界发生了多少次。此解决方案还需要 3 个循环。
    • @PeterLeontev 太棒了!如果您可以在此处发布您的解决方案作为答案,那么它可能对这个问题的任何未来观众都有帮助。
    猜你喜欢
    • 2018-05-27
    • 1970-01-01
    • 1970-01-01
    • 2021-11-13
    • 2011-02-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多