【问题标题】:Sum (i...j) - is there a more efficient / elegant solution for this?Sum (i...j) - 有没有更有效/优雅的解决方案?
【发布时间】:2012-03-18 05:14:22
【问题描述】:

我们被赋予了一项简单的任务,以最有效的方式分别使用递归和迭代来计算起点和终点(“from”和“to”)之间的所有数字,而无需使用明显的公式这将是 O(1)。

没有应用程序,我只是好奇和挑战,看看我的解决方案是否可以比现有的更多改进/完善:

/* recursion */
unsigned int sum1(unsigned int from, unsigned int to) {
    if (to - from < 2)
        return from + (from == to ? 0 : to);
    else
        return from + to + sum1(from + 1, to - 1);
}

/* iteration */
unsigned int sum2(unsigned int from, unsigned int to) {
    int p = to - from;
    if (p == 0) return from;
    int i, s, n = p / 2;
    if (p % 2 == 0) s = n + from;
    else {
        s = 0;
        n++;
    }
    for (i = 0; i < n; i++) {
        s += from++ + to--;
    }
    return s;
}

【问题讨论】:

  • 这是多么愚蠢的任务!就像“做一个三明治,但你不能用刀,只能用剑,自己种牛,用它的奶在面包上涂黄油”……
  • Carbonic Acid,为什么讨厌试图让一个班级按照派生其他有用算法需要你思考的方式思考?这只是一个简单的练习来实践这个概念。
  • @AlexReynolds,它根本不是重复的,因为另一个问题对欧几里得算法的使用是开放的。
  • 你如何定义效率?
  • @user112358132134 我不认为他在这样做。他做了一个尝试。现在,他只需要有关如何改进他的方法的反馈和建议。我看不出有什么问题。

标签: c algorithm


【解决方案1】:

我尝试改进迭代版本:

unsigned int sum2_improved(unsigned int from, unsigned int to) {
    int p = to - from;
    if (p == 0) return from;
    int x = to + from;
    int s = 0;
    int i;
    for (i = p >> 1; i > 0; i--)
    {
        s += x;
    }
    s += (p % 2 == 0) ? x >> 1 : x;
    return s;
}

我测试了你的版本:

for (i = 0; i < 9999999; i++) sum2(1,999);

这是我看到的:

$ time ./addm
real    0m18.315s
user    0m18.220s
sys     0m0.015s

我用相同数量的循环尝试了我的实现。以下是改进后的功能的执行方式:

$ time ./addm
real    0m14.196s
user    0m14.070s
sys     0m0.015s

更新

在我的实现中,x = to + from 是序列中第一个数字和最后一个数字的总和。如果您考虑任何连续的整数序列,并将第一个和最后一个、第二个和倒数第二个相加,等等......所有这些加起来都是相同的值。例如,(1 ... 6), 1 + 6 = 2 + 5 = 3 + 4 = 7。但是,对于包含奇数个元素的序列,剩下的就是中间的数字,然后您必须将其添加到累积总和中(这就是 for 循环之后的分配所做的事情。

另外,请注意,这仍然是O(n)。在我最初发布我的答案后,我意识到我的方法实际上可以在恒定时间内完成。这是更新的代码:

unsigned int sum0(unsigned int from, unsigned int to) {
    int p = to - from;
    if (p == 0) return from;
    int x = to + from;
    int s = 0;

    s += (x * (p >> 1));

    s += (p % 2 == 0) ? x >> 1 : x;

    return s;
}

我使用与早期测试相同数量的循环来运行它。这是我看到的:

$ time ./addm

real    0m0.158s
user    0m0.093s
sys     0m0.047s

我不确定这是否可以被视为对您而言的公式的变体。无论如何,这对我来说是一个有趣的练习。

【讨论】:

  • 好的,很明显这是一个改进,而且非常有趣。现在要弄清楚这里发生了什么。 :P 谢谢。
  • 您能解释一下您是如何得出这个解决方案的吗?我发现很难找到您使用 x 背后的逻辑。
  • 现在更有意义了,但是如果您仔细观察,您的恒定时间解决方案只是欧几里得算法的一个实现,即使它是正确的,它也绝对更接近于公式而不是迭代。我会处理你的初步答案。再次感谢。 :)
  • @paranoid-android 没问题。我有一种感觉是欧几里得式的。最初的实现是做同样的事情,但循环了大约一半的序列来做这件事,这在更新中显然不是必需的。
【解决方案2】:

将范围(从零到上限n)分成下半部分和上半部分。对于下半部分的每个值,上半部分都有一个大 n/2 的值;有n/2个,所以上半部分的和就是下半部分的和+(n/2)^2。

在 Python 中:

def sum1(lower, upper):
    if upper <= 0: 
        return 0
    m, r = divmod(upper, 2)
    return sum1(0, m) * 2 + m * m + r * upper - sum1(0, lower - 1)

【讨论】:

    【解决方案3】:

    我不打算为它编写代码,但这种事情会直接随着你处理任务的内核数量而增加。

    将范围划分为任务并启动一个线程来对范围的每个子部分求和,这会将您选择的任何实现所需的时间除以核心数量(给予或接受)。

    您还可以使用 SIMD 扩展,通过事先写出内存中的数据来促进加法(向量加法)。把它带到另一个极端,你实际上可以使用 GPU 来计算子范围的加法(但你必须有足够大的范围才能让它值得开销),让它变得愚蠢快;因为这个问题很简单,说明之间没有任何依赖关系。

    【讨论】:

      【解决方案4】:

      您可以使用segment tree 来获取从 i 到 j 的段的总和。该结构的查找时间为 O(log n)。

      【讨论】:

      • 加上构建它所需的时间
      【解决方案5】:

      功能:

      long sum(long lower, long upper) {
          long s = 0;
          s = ((upper * (upper + 1)) - (lower - 1) * (lower))/2;
          return s;
      }
      

      使用参数调用:(1,9999999) 返回 49999995000000,它与求和公式 n(n+1)/2 一致,并在核心二重奏上使用以下配置文件运行:

      real    0m0.005s
      user    0m0.002s
      sys     0m0.003s
      

      可能值得检查您的函数,我看不到它们返回此结果 - 数学选项是一个更优越的解决方案;)

      【讨论】:

      • 那是因为整数溢出,我相信。请注意,您使用的是长值。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-05
      • 1970-01-01
      • 1970-01-01
      • 2015-07-03
      • 2018-04-28
      • 1970-01-01
      相关资源
      最近更新 更多