【问题标题】:Efficient algorithm to calculate the sum of number of base2 digits (number of bits) over an interval of positive integers计算正整数区间内base2位数(位数)之和的高效算法
【发布时间】:2018-11-03 23:06:22
【问题描述】:

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

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

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

上面的函数是计算区间内一个数的位数。

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

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

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

【问题讨论】:

  • 次要:“其中 a 是一个正整数”和“从 0 到 4 的区间”可以被视为矛盾,因为零可以被认为是“非正数”。我建议将代码限制为unsigned
  • (标题使用Effective:给出想要的效果。您似乎对高效:通过合理的努力给出效果。sequence of integers 可能是@ 987654321@ - 你似乎在使用interval。)

标签: c algorithm digits


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    这里要理解的主要是用于表示二进制数的位数以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 函数版本来改进它。

    【讨论】:

    • 请重新测试。传入 255 255 应该给你 8。254 和 255 应该得到 16 ...
    【解决方案3】:

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

    Faster 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
    

    【讨论】:

    • 这不是问题的答案。要求是将整数值 n 到 m 序列中的 二进制 位数相加。问题中的代码已经做到了,问题是如何更有效地做到这一点。
    • @Clifford 修好了,该睡觉了 :)
    • 这行得通,但我仍然需要更快的东西,不需要遍历 中的每个元素。但是谢谢:)
    • 啊,是的,我们可以通过查找具有相同位数的数字间隔然后只计算一次 numBits 并将结果乘以 (intmax-intmin+1) 来避免循环,并避免循环遍历间隔。
    • 已更新。有一个显着的加速:)
    【解决方案4】:

    从概念上讲,您需要将任务拆分为两个子问题 - 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);
     }
    

    【讨论】:

    • 这不起作用,例如,当您输入 10 到 20 的间隔时,代码返回 33,但如果其中一个数字为 0,例如从 0 到 0 的数字,正确的结果也应该是 49 10 效果不好
    【解决方案5】:

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

    #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  */
    

    每次迭代。

    【讨论】:

      【解决方案6】:

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

      算法

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

      然后,当我们要确定范围的位总和时,我们必须首先将范围限制 aba &lt;= limits[i]b &lt;= 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
      

      【讨论】:

        【解决方案7】:

        算法

        主要思想是找到向下舍入的n2 = log2(x)。那是x 中的位数。让pow2 = 1 &lt;&lt; n2n2 * (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);
        }
        

        【讨论】:

        • 你能解释一下吗?例如,n 代表什么?也可以将它与正整数而不是无符号整数一起使用吗?
        • @user7048295, n 应该是 n2。代码可以使用只有正值的int
        【解决方案8】:

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

        朴素算法

        假设 abb 大于 a 的正整数,我们称之为维度/大小区间 [a,b], n = (b-a).

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

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

        直觉

        现在查看一个可能的区间 [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 个操作的 9 个元素的循环减少到只有 2 个。

        所以编写一个使用这种直觉的函数会给我们一个更高效的时间,不一定是内存,算法。使用您的 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 em> 相当于 O(log n)。 为了恢复,我们从线性运算成本 O(n) 传递到最坏情况下的对数运算成本 O(log n)Look on this diagram the diferinces两者之间。

        注意

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

        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
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-07-04
          • 1970-01-01
          • 2021-11-10
          • 1970-01-01
          • 2021-11-27
          相关资源
          最近更新 更多