【问题标题】:What is unoptimized about this code? [closed]这段代码有什么未优化的地方? [关闭]
【发布时间】:2012-11-11 13:52:51
【问题描述】:

我在interviewstreet上写了一个问题的解决方案,问题描述如下:

https://www.interviewstreet.com/challenges/dashboard/#problem/4e91289c38bfd

这是他们给出的解决方案:

https://gist.github.com/1285119

这是我编写的解决方案:

#include<iostream>
#include <string.h>
using namespace std;
#define LOOKUPTABLESIZE 10000000
int popCount[2*LOOKUPTABLESIZE];
int main()
{
int numberOfTests = 0;
cin >> numberOfTests;

for(int test = 0;test<numberOfTests;test++)
{
    int startingNumber = 0;
    int endingNumber = 0;
    cin >> startingNumber >> endingNumber;

    int numberOf1s = 0;


    for(int number=startingNumber;number<=endingNumber;number++)
    {
        if(number >-LOOKUPTABLESIZE && number < LOOKUPTABLESIZE)
        {
            if(popCount[number+LOOKUPTABLESIZE] != 0)
            {
                numberOf1s += popCount[number+LOOKUPTABLESIZE];
            }
            else
            {
                popCount[number+LOOKUPTABLESIZE] =__builtin_popcount (number);
                numberOf1s += popCount[number+LOOKUPTABLESIZE];
            }
        }
        else
        {
        numberOf1s += __builtin_popcount (number);
        }
    }
    cout << numberOf1s << endl;

}

}

您能指出我的代码有什么问题吗?它只能通过 3/10 的测试。时间限制为 3 秒。

【问题讨论】:

  • 1. string.h 而不是 cstring。 2.abusing namespace std。 3.#define 而不是static const。 4. 忽略cin &gt;&gt; numberOfTests 的返回值。现在我很无聊。恐怕没有工作。
  • 什么是__builtin_popcount?
  • @john: gcc.gnu.org/onlinedocs/gcc-4.7.0/gcc/Other-Builtins.html: "返回 x 中 1 位的数量。"
  • 好的,所以基本上代码正在缓存builtin_popcount的返回。这是个好主意吗?
  • 这是一个比这更通用的问题,但在这里这是一个好主意,因为我知道输入类型。不同的输入特性可能并非如此。

标签: c++ optimization


【解决方案1】:

这段代码有什么未优化的地方?

算法。你在循环

for(int number=startingNumber;number<=endingNumber;number++)

计算或查找每个中 1 位的数量。这可能需要一段时间。

一个好的算法使用一点数学计算0 &lt;= k &lt; n 在O(log n) 时间内所有数字中的 1 位数。

Here 是一个在十进制扩展中计数 0 的实现,使其计数为 1 位的修改应该不难。

【讨论】:

  • 谢谢,我认为不存在这样的算法。到时候我会努力去寻找和理解。
  • @EralpB:一般来说,蛮力方法在面试问题中不起作用。它们吸引数学、数据结构和算法知识,以及您将它们串在一起、在混乱中辨别模式以及重用过去经验的能力。
  • 我尝试完全解决问题,但不清楚我采用的方法是否类似于您的计数 0 实现。在我看来是这样。你介意回顾一下吗?
  • 原理相同,@Matthieu。您没有处理的一件事是,在原始问题中,范围的开始和结束可能是负数(因此您需要预先进行一些检查来测试它,并从使用二进制补码表示的假设,[0, N-1] 中的 1 位计数)。您的解决方案中的一个问题是使用 log(double(n - 1))/Log2,这对于接近 2 次方的大 n 来说是错误的,而对于一些较小的 n 已经是错误的。不过,一个不错的,+1。
  • @DanielFischer:哎呀,我必须承认我并没有真正检查过面试问题;)关于log,我想你担心的是精确度。我犹豫是否选择long double,但即便如此我也不确定这是否足够。不过,有一些小技巧可以得到它,这就是我在一个函数中抽象的原因。因此,我并不太担心。感谢您的评论。
【解决方案2】:

当看到这样一个问题时,你需要把它分解成简单的部分。

例如,假设你知道[0, N](我们称之为ones(N))所有数字中有多少个1,那么我们有:

size_t ones(size_t N) { /* magic ! */ }

size_t count(size_t A, size_t B) {
    return ones(B) - (A ? ones(A - 1) : 0);
}

这种方法的优点是one 可能比count 更简单,例如使用递归。因此,第一次天真的尝试是:

// Naive
size_t naive_ones(size_t N) {
    if (N == 0) { return 0; }
    return __builtin_popcount(N) + naive_ones(N-1);
}

但这可能太慢了。即使只是计算count(B, A) 的值,我们也会计算naive_ones(A-1) 两次!

幸运的是,这里总是有 memoization 的辅助,而且转换很简单:

size_t memo_ones(size_t N) {
    static std::deque<size_t> Memo(1, 0);
    for (size_t i = Memo.size(); i <= N; ++i) {
        Memo.push_back(Memo[i-1] + __builtin_popcnt(i));
    }
    return Memo[N];
}

这可能会有所帮助,但是在内存方面的成本可能会......严重。啊。想象一下,为了计算ones(1,000,000),我们将在 64 位计算机上占用 8MB 内存!更稀疏的记忆可能会有所帮助(例如,每 8 次或 16 次才记忆一次):

// count number of ones in (A, B]
static unoptimized_count(size_t A, size_t B) {
    size_t result = 0;
    for (size_t i = A + 1; i <= B; ++i) {
        result += __builtin_popcount(i);
    }
    return result;
}

// something like this... be wary it's not tested.
size_t memo16_ones(size_t N) {
    static std::vector<size_t> Memo(1, 0);
    size_t const n16 = N  - (N % 16);
    for (size_t i = Memo.size(); i*16 <= n16; ++i) {
        Memo.push_back(Memo[i-1] + unoptimized_count(16*(i-1), 16*i);
    }
    return Memo[n16/16] + unoptimized_count(n16, N);
}

然而,虽然它确实降低了内存成本,但并没有解决主要的速度问题:我们至少必须使用__builtin_popcount B 次!对于较大的 B 值,这是一个杀手。


上述解决方案是机械的,它们不需要一分思考。事实证明,面试与其说是写代码,不如说是思考。

我们能否比愚蠢地枚举所有整数直到B 更有效地解决这个问题?

让我们看看我们的大脑(非常神奇的模式机器)在考虑前几个条目时会选择什么:

N    bin  1s   ones(N)
0    0000 0    0
1    0001 1    1
2    0010 1    2
3    0011 2    4
4    0100 1    5
5    0101 2    7
6    0110 2    9
7    0111 3    12
8    1000 1    13
9    1001 2    15
10   1010 2    17
11   1011 3    20
12   1100 2    22
13   1101 3    25
14   1110 3    28
15   1111 3    32

注意到一个模式?我愿意 ;) 8-15 的范围与 0-7 完全一样,但每行多了一个 1 => 它就像一个 转置。这也很合乎逻辑,不是吗?

因此,ones(15) - ones(7) = 8 + ones(7)、ones(7) - ones(3) = 4 + ones(3) 和 ones(1) - ones(0) = 1 + ones(0)。

好吧,让我们把它变成一个公式:

  • 提醒:ones(N) = popcount(N) + ones(N-1)(几乎)根据定义
  • 我们现在知道ones(2**n - 1) - ones(2**(n-1) - 1) = 2**(n-1) + ones(2**(n-1) - 1)

让我们隔离ones(2**n),它更容易处理,注意popcount(2**n) = 1:

  • 重组:ones(2**n - 1) = 2**(n-1) + 2*ones(2**(n-1) - 1)
  • 使用定义:ones(2**n) - 1 = 2**(n-1) + 2*ones(2**(n-1)) - 2
  • 简化:ones(2**n) = 2**(n-1) - 1 + 2*ones(2**(n-1)),用ones(1) = 1。

快速健全性检查:

1  = 2**0 => 1  (bottom)
2  = 2**1 => 2  = 2**0 - 1 + 2 * ones(1)
4  = 2**2 => 5  = 2**1 - 1 + 2 * ones(2)
8  = 2**3 => 13 = 2**2 - 1 + 2 * ones(4)
16 = 2**4 => 33 = 2**3 - 1 + 2 * ones(8)

看起来很有效!


虽然我们还没有完成。 A 和 B 不一定是 2 的幂,如果我们必须从 2**n 一直数到 2**n + 2**(n-1),那仍然是 O(N)!

另一方面,如果我们设法以 2 为基数表示一个数字,那么我们应该能够利用我们新获得的公式。主要优点是表示中只有 log2(N) 位。

让我们举一个例子来了解它是如何工作的:13 = 8 + 4 + 1

1  -> 0001
4  -> 0100
8  -> 1000
13 -> 1101

...然而,计数不仅仅是总和:

ones(13) != ones(8) + ones(4) + ones(1)

让我们用“换位”策略来表达:

ones(13) - ones(8) = ones(5) + (13 - 8)

ones(5) - ones(4) = ones(1) + (5 - 4)

好的,用一点递归很容易做到。

#include <cmath>
#include <iostream>

static double const Log2 = log(2);

// store ones(2**n) at P2Count[n]
static size_t P2Count[64] = {};

// Unfortunately, the conversion to double might lose some precision
// static size_t log2(size_t n) { return log(double(n - 1))/Log2 + 1; }

// __builtin_clz* returns the number of leading 0s
static size_t log2(size_t n) {
    if (n == 0) { return 0; }
    return sizeof(n) - __builtin_clzl(n) - 1;
}

static size_t ones(size_t n) {
    if (n == 0) { return 0; }
    if (n == 1) { return 1; }

    size_t const lg2 = log2(n);
    size_t const np2 = 1ul << lg2; // "next" power of 2

    if (np2 == n) { return P2Count[lg2]; }

    size_t const pp2 = np2 / 2; // "previous" power of 2

    return ones(pp2) + ones(n - pp2) + (n - pp2);
} // ones

// reminder: ones(2**n) = 2**(n-1) - 1 + 2*ones(2**(n-1))
void initP2Count() {
    P2Count[0] = 1;

    for (size_t i = 1; i != 64; ++i) {
        P2Count[i] = (1ul << (i-1)) - 1 + 2 * P2Count[i-1];
    }
} // initP2Count

size_t count(size_t const A, size_t const B) {
    if (A == 0) { return ones(B); }

    return ones(B) - ones(A - 1);
} // count

还有一个demonstration:

int main() {
    // Init table
    initP2Count();
    std::cout << "0: " << P2Count[0] << ", 1: " << P2Count[1] << ", 2: " << P2Count[2] << ", 3: " << P2Count[3] << "\n";

    for (size_t i = 0; i != 16; ++i) {
        std::cout << i << ": " << ones(i) << "\n";
    }

    std::cout << "count(7, 14): " << count(7, 14) << "\n";
}

胜利!

注意:正如 Daniel Fisher 所指出的,这无法解释负数(但假设两个补码可以从它们的正数中推断出来)。

【讨论】:

    猜你喜欢
    • 2013-02-01
    • 1970-01-01
    • 2014-07-13
    • 2011-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多