繁体   English   中英

计算正整数区间内base2位数(位数)总和的有效算法

[英]Efficient algorithm to calculate the sum of number of base2 digits (number of bits) over an interval of positive integers

假设我得到了两个整数ab ,其中a是一个正整数并且小于b 我必须找到一种有效的算法,该算法将给出区间[a, b]内 base2 位数(位数)的总和。 例如,在区间[0, 4]中,数字之和等于 9,因为 0 = 1 位、1 = 1 位、2 = 2 位、3 = 2 位和 4 = 3 位。

我的程序能够通过使用循环来计算这个数字,但我正在寻找对大数字更有效的东西。 以下是我的代码片段,只是为了给您一个想法:

int numberOfBits(int i) {
    if(i == 0) {
        return 1;
    }
    else {
        return (int) log2(i) + 1;
    }
 }

上面的函数用于计算区间内一个数字的位数。

下面的代码向您展示了我如何在主函数中使用它。

for(i = a; i <= b; i++) {
    l = l + numberOfBits(i);
}
printf("Digits: %d\n", l);

理想情况下,我应该能够通过使用我的间隔的两个值并使用一些特殊算法来获得位数。

试试这个代码,我认为它为您提供了计算二进制文件所需的内容:

int bit(int x)
{
  if(!x) return 1;
  else
  {
    int i;
    for(i = 0; x; i++, x >>= 1);
    return i;
  }
}

首先,我们可以提高 log2 的速度,但这只会给我们一个固定因子的加速,而不会改变缩放比例。

更快的 log2 改编自: https : //graphics.stanford.edu/~seander/bithacks.html#IntegerLogLookup

查找表方法只需要大约 7 次操作即可找到 32 位值的日志。 如果扩展到 64 位数量,大约需要 9 次操作。 另一个操作可以通过使用四个表来修剪,每个表都包含可能的添加项。 使用 int 表元素可能会更快,具体取决于您的架构。

其次,我们必须重新思考算法。 如果您知道 N 和 M 之间的数字具有相同的位数,您是将它们一一相加还是宁愿做 (M-N+1)*numDigits?

但是如果我们有一个出现多个数字的范围,我们该怎么办? 让我们找出相同数字的区间,然后将这些区间的和相加。 下面实现。 我认为我的findEndLimit可以通过查找表进一步优化。

代码

#include <stdio.h>
#include <limits.h>
#include <time.h>

unsigned int fastLog2(unsigned int v)
{
    static const char LogTable256[256] = 
    {
    #define LT(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n
        -1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
        LT(4), LT(5), LT(5), LT(6), LT(6), LT(6), LT(6),
        LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7)
    };

    register unsigned int t, tt; // temporaries

    if (tt = v >> 16)
    {
      return (t = tt >> 8) ? 24 + LogTable256[t] : 16 + LogTable256[tt];
    }
    else 
    {
      return (t = v >> 8) ? 8 + LogTable256[t] : LogTable256[v];
    }
}

unsigned int numberOfBits(unsigned int i)
{
    if (i == 0) {
        return 1;
    }
    else {
        return fastLog2(i) + 1;
    }
}

unsigned int findEndLimit(unsigned int sx, unsigned int ex)
{
    unsigned int sy = numberOfBits(sx);
    unsigned int ey = numberOfBits(ex);
    unsigned int mx;
    unsigned int my;

    if (sy == ey) // this also means sx == ex
        return ex;

    // assumes sy < ey
    mx = (ex - sx) / 2 + sx; // will eq. sx for sx + 1 == ex
    my = numberOfBits(mx);
    while (ex - sx != 1) {
        mx = (ex - sx) / 2 + sx; // will eq. sx for sx + 1 == ex
        my = numberOfBits(mx);
        if (my == ey) {
            ex = mx;
            ey = numberOfBits(ex);
        }
        else {
            sx = mx;
            sy = numberOfBits(sx);
        }
    }
    return sx+1;
}

int main(void)
{
    unsigned int a, b, m;
    unsigned long l;
    clock_t start, end;
    l = 0;
    a = 0;
    b = UINT_MAX;

    start = clock();
    unsigned int i;
    for (i = a; i < b; ++i) {
        l += numberOfBits(i);
    }
    if (i == b) {
        l += numberOfBits(i);
    }
    end = clock();

    printf("Naive\n");
    printf("Digits: %ld; Time: %fs\n",l, ((double)(end-start))/CLOCKS_PER_SEC);

    l=0;
    start = clock();
    do {
        m = findEndLimit(a, b);
        l += (b-m + 1) * (unsigned long)numberOfBits(b);
        b = m-1;
    } while (b > a);
    l += (b-a+1) * (unsigned long)numberOfBits(b);
    end = clock();

    printf("Binary search\n");
    printf("Digits: %ld; Time: %fs\n",l, ((double)(end-start))/CLOCKS_PER_SEC);
}

输出

从 0 到 UINT_MAX

$ ./main 
Naive
Digits: 133143986178; Time: 25.722492s
Binary search
Digits: 133143986178; Time: 0.000025s

在某些极端情况下,我的 findEndLimit 可能需要很长时间:

从 UINT_MAX/16+1 到 UINT_MAX/8

$ ./main 
Naive
Digits: 7784628224; Time: 1.651067s
Binary search
Digits: 7784628224; Time: 4.921520s

算法

主要思想是找到向下取整的n2 = log2(x) 那是x的位数。 pow2 = 1 << n2 n2 * (pow2 - x + 1)是值[x...pow2]的位数。 现在找到从 1 到n2-1的 2 次幂数字的太阳

代码

我确信可以进行各种简化。
未经测试的代码 稍后会复查。

// Let us use unsigned for everything.

unsigned ulog2(unsigned value) {
  unsigned result = 0;
  if (0xFFFF0000u & value) {
    value >>= 16; result += 16;
  }
  if (0xFF00u & value) {
    value >>= 8; result += 8;
  }
  if (0xF0u & value) {
    value >>= 4; result += 4;
  }
  if (0xCu & value) {
    value >>= 2; result += 2;
  }
  if (0x2 & value) {
    value >>= 1; result += 1;
  }
  return result;
}

unsigned bit_count_helper(unsigned x) {
  if (x == 0) {
    return 1;
  }
  unsigned n2 = ulog2(x);
  unsigned pow2 = 1u << n;
  unsigned sum = n2 * (pow2 - x + 1u);  // value from pow2 to x
  while (n2 > 0) {
    // ... + 5*16 + 4*8 + 3*4 + 2*2 + 1*1
    pow2 /= 2;
    sum += n2 * pow2;
  }
  return sum;
}

unsigned bit_count(unsigned a, unsigned b) {
  assert(a < b);
  return bit_count_helper(b - 1) - bit_count_helper(a);
}

从概念上讲,您需要将任务拆分为两个子问题 - 1) 从 0..M 和 0..N 中找出数字之和,然后减去。

2)找到地板(log2(x)),因为例如对于数字77 ,数字64,65,...77都有6位数字,接下来的32位有5位数字,接下来的16位有4位数字等等,这使得几何级数。

因此:

 int digits(int a) {
   if (a == 0) return 1;   // should digits(0) be 0 or 1 ?
   int b=(int)floor(log2(a));   // use any all-integer calculation hack
   int sum = 1 + (b+1) * (a- (1<<b) +1);  // added 1, due to digits(0)==1
   while (--b)
     sum += (b + 1) << b;   // shortcut for (b + 1) * (1 << b);
   return sum;
 }
 int digits_range(int a, int b) {
      if (a <= 0 || b <= 0) return -1;   // formulas work for strictly positive numbers
      return digits(b)-digits(a-1);
 }

由于效率取决于可用的工具,一种方法是“模拟”:

#include <stdlib.h>
#include <stdio.h>
#include <math.h> 

unsigned long long pow2sum_min(unsigned long long n, long long unsigned m)
{
  if (m >= n)
  {
    return 1;
  }

  --n;

  return (2ULL << n) + pow2sum_min(n, m);
}

#define LN(x) (log2(x)/log2(M_E))

int main(int argc, char** argv)
{
  if (2 >= argc)
  {
    fprintf(stderr, "%s a b\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  long a = atol(argv[1]), b = atol(argv[2]);

  if (0L >= a || 0L >= b || b < a)
  {
    puts("Na ...!");
    exit(EXIT_FAILURE);
  }

  /* Expand intevall to cover full dimensions: */
  unsigned long long a_c = pow(2, floor(log2(a)));
  unsigned long long b_c = pow(2, floor(log2(b+1)) + 1);

  double log2_a_c = log2(a_c);
  double log2_b_c = log2(b_c);

  unsigned long p2s = pow2sum_min(log2_b_c, log2_a_c) - 1;

  /* Integral log2(x) between a_c and b_c: */
  double A = ((b_c * (LN(b_c) - 1)) 
            - (a_c * (LN(a_c) - 1)))/LN(2)
            + (b+1 - a);

  /* "Integer"-integral - integral of log2(x)'s inverse function (2**x) between log(a_c) and log(b_c): */
  double D = p2s - (b_c - a_c)/LN(2);

  /* Corrective from a_c/b_c to a/b : */
  double C = (log2_b_c - 1)*(b_c - (b+1)) + log2_a_c*(a - a_c);

  printf("Total used digits: %lld\n", (long long) ((A - D - C) +.5));
}

:-)

这里的主要内容是完成的迭代次数和类型。

号码是

log(floor(b_c)) - log(floor(a_c))

做一个

n - 1 /* Integer decrement  */
2**n + s /* One bit-shift and one integer addition  */

对于每次迭代。

这里要理解的主要事情是,用于表示二进制数字的位数随着 2 的每一次幂增加 1:

 +--------------+---------------+ | number range | binary digits | +==============+===============+ | 0 - 1 | 1 | +--------------+---------------+ | 2 - 3 | 2 | +--------------+---------------+ | 4 - 7 | 3 | +--------------+---------------+ | 8 - 15 | 4 | +--------------+---------------+ | 16 - 31 | 5 | +--------------+---------------+ | 32 - 63 | 6 | +--------------+---------------+ | ... | ... |

对你的蛮力算法的一个微不足道的改进是找出这个数字在传入的两个数字之间增加了多少次(由以二为底的对数给出),并通过乘以数字的数量来加起来可以由给定的位数(由 2 的幂给出)和位数表示。

该算法的一个简单实现是:

int digits_sum_seq(int a, int b)
{
    int sum = 0;
    int i = 0;
    int log2b = b <= 0 ? 1 : floor(log2(b));
    int log2a = a <= 0 ? 1 : floor(log2(a)) + 1;

    sum += (pow(2, log2a) - a) * (log2a);

    for (i = log2b; i > log2a; i--)
        sum += pow(2, i - 1) * i;

    sum += (b - pow(2, log2b) + 1) * (log2b + 1);

    return sum;
}

然后可以通过其他答案中看到的更有效的 log 和 pow 函数版本来改进它。

这是一个完全基于查找的方法。 你甚至不需要log2 :)

算法

首先,我们预先计算位数会发生变化的间隔限制并创建一个查找表。 换句话说,我们创建了一个数组limits[2^n] ,其中limits[i]给出了可以用(i+1)位表示的最大整数。 我们的数组是{1, 3, 7, ..., 2^n-1}

然后,当我们想要确定我们范围的位总和时,我们必须首先将我们的范围限制aba <= limits[i]b <= limits[j]成立的最小索引相匹配,这将然后告诉我们需要(i+1)位来表示a ,需要(j+1)位来表示b

如果索引相同,那么结果就是(b-a+1)*(i+1) ,否则我们必须分别获取从我们的值到相同位数间隔的边缘的位数,并相加之间的每个间隔的总位数也是如此。 无论如何,简单的算术。

代码

#include <stdio.h>
#include <limits.h>
#include <time.h>

unsigned long bitsnumsum(unsigned int a, unsigned int b)
{
    // generate lookup table
    // limits[i] is the max. number we can represent with (i+1) bits
    static const unsigned int limits[32] =
    {
    #define LTN(n) n*2u-1, n*4u-1, n*8u-1, n*16u-1, n*32u-1, n*64u-1, n*128u-1, n*256u-1
        LTN(1),
        LTN(256),
        LTN(256*256),
        LTN(256*256*256)
    };

    // make it work for any order of arguments
    if (b < a) {
        unsigned int c = a;
        a = b;
        b = c;
    }

    // find interval of a
    unsigned int i = 0;
    while (a > limits[i]) {
            ++i;
    }
    // find interval of b
    unsigned int j = i;
    while (b > limits[j]) {
            ++j;
    }

    // add it all up
    unsigned long sum = 0;
    if (i == j) {
        // a and b in the same range
        // conveniently, this also deals with j == 0
        // so no danger to do [j-1] below
        return (i+1) * (unsigned long)(b - a + 1);
    }
    else {
        // add sum of digits in range [a, limits[i]]
        sum += (i+1) * (unsigned long)(limits[i] - a + 1);
        // add sum of digits in range [limits[j], b]
        sum += (j+1) * (unsigned long)(b - limits[j-1]);
        // add sum of digits in range [limits[i], limits[j]]
        for (++i; i<j; ++i) {
            sum += (i+1) * (unsigned long)(limits[i] - limits[i-1]);
        }
        return sum;
    }
}

int main(void)
{
    clock_t start, end;
    unsigned int a=0, b=UINT_MAX;

    start = clock();
    printf("Sum of binary digits for numbers in range "
    "[%u, %u]: %lu\n", a, b, bitsnumsum(a, b));
    end = clock();
    printf("Time: %fs\n", ((double)(end-start))/CLOCKS_PER_SEC);
}

输出

$ ./lookup 
Sum of binary digits for numbers in range [0, 4294967295]: 133143986178
Time: 0.000282s

对于这个问题,您的解决方案是最简单的,称为“天真”的解决方案,您可以在其中查找序列或案例间隔中的每个元素以检查某些内容或执行操作。

朴素算法

假设ab是正整数,其中b大于a让我们将区间的维度/大小称为[a,b] , n = (ba)

拥有我们的元素数量 n 并使用一些算法符号(如大 O 符号链接),最坏情况的成本是O(n*(numberOfBits_cost))

从中我们可以看到,我们可以通过使用更快的算法来计算numberOfBits()来加速我们的算法,或者我们需要找到一种方法来不查看间隔中花费我们 n 次操作的每个元素。

直觉

现在看一个可能的区间[6,14]你可以看到,67,我们需要3,4需要8,9,10,11,12,13,14。 这导致为每个使用相同位数表示的数字调用numberOfBits( ),而以下乘法运算会更快:

(number_in_subinterval)*digitsForThisInterval
((14-8)+1)*4 = 28
((7-6)+1)*3 = 6

所以我们将 9 个元素的循环减少到只有 2 个操作,9 个操作。

因此,编写一个使用这种直觉的函数将使我们在时间上(不一定在内存中)算法更有效率。 使用您的numberOfBits()函数,我创建了这个解决方案:

   int intuitionSol(int a, int b){
    int digitsForA = numberOfBits(a);
    int digitsForB = numberOfBits(b);
    
    if(digitsForA != digitsForB){
        //because a or b can be that isn't the first or last element of the
        // interval that a specific number of digit can rappresent there is a need
        // to execute some correction operation before on a and b
        int tmp = pow(2,digitsForA)  - a;
        int result = tmp*digitsForA; //will containt the final result that will be returned
        
        int i;
        for(i = digitsForA + 1; i < digitsForB; i++){
            int interval_elements = pow(2,i) - pow(2,i-1);
            result = result + ((interval_elements) * i);
            //printf("NumOfElem: %i for %i digits; sum:= %i\n", interval_elements, i, result);
        }
        
        int tmp1 = ((b + 1) - pow(2,digitsForB-1));
        result = result + tmp1*digitsForB;
        return result;
    }
    else {
        int elements = (b - a) + 1;
        return elements * digitsForA; // or digitsForB
    }
}

我们来看看成本,这个算法的成本是对ab做修正操作的成本加上最昂贵的for循环的成本。 然而,在我的解决方案中,我并没有遍历所有元素,而是只在numberOfBits(b)-numberOfBits(a)上循环,在最坏的情况下,当[0,n]变为log(n)-1 时,相当于O(log n) 为了恢复,在最坏的情况下,我们从线性操作成本O(n)传递到对数O(log n) 在这张图上看两者之间的区别

笔记

当我谈论区间或子区间时,我指的是使用相同位数来表示二进制数的元素的区间。 下面是我测试的一些输出,最后一个显示了差异:

Considered interval is [0,4]
YourSol: 9 in time: 0.000015s
IntuitionSol: 9 in time: 0.000007s

Considered interval is [0,0]
YourSol: 1 in time: 0.000005s
IntuitionSol: 1 in time: 0.000005s

Considered interval is [4,7]
YourSol: 12 in time: 0.000016s
IntuitionSol: 12 in time: 0.000005s

Considered interval is [2,123456]
YourSol: 1967697 in time: 0.005010s
IntuitionSol: 1967697 in time: 0.000015s

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM