神秘常量!用0x077CB531计算末尾0的个数,32位数首位相连

大家或许还记得 Quake III 里面的一段有如天书般的代码,其中用到的神秘常量 0x5F3759DF 究竟是怎么一回事,着实让不少人伤透了脑筋。今天,我见到了一段同样诡异的代码。
    下面这个位运算小技巧可以迅速给出一个数的二进制表达中末尾有多少个 0 。比如, 123 456 的二进制表达是 1 11100010 01000000 ,因此这个程序给出的结果就是 6 。

unsigned int v;  // find the number of trailing zeros in 32-bit v
int r;           // result goes here
static const int MultiplyDeBruijnBitPosition[32] =
{
  0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
  31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x077CB531U)) >> 27];

熟悉位运算的朋友们可以认出, v & -v 的作用就是取出右起连续的 0 以及首次出现的 1 。当 v = 123 456 时, v & -v 就等于 64 ,即二进制的 1000000 。怪就怪在,这个 0x077CB531 是怎么回事?数组 MultiplyDeBruijnBitPosition 又是什么玩意儿呢?

这还得从 0x077CB531 本身的一个性质开始说起。把这个常数写成 32 位二进制,可以得到

00000111011111001011010100110001

这个 01 串有一个无比牛 B 的地方:如果把它看作是循环的,它正好包含了全部 32 种可能的 5 位 01 串,既无重复,又无遗漏!其实,这样的 01 串并不稀奇,因为构造这样的 01 串完全等价于寻找一个有向图中的 Euler 回路。如下图,构造一个包含 16 个顶点的图,顶点分别命名为 0000, 0001, 0010, …, 1111 。如果某个点的后 3 位,正好等于另一个点的前 3 位,就画一条从前者出发指向后者的箭头。也就是说,只要两个顶点上的数满足 abcd 和 bcde 的关系( a 、 b 、 c 、 d 、 e 可能代表相同的数字),就从 abcd 出发,连一条到 bcde 的路,这条路就记作 abcde 。注意,有些点之间是可以相互到达的(比如 1010 和 0101 ),有些点甚至有一条到达自己的路(比如 0000 )。

构造一个字符串使其包含所有可能的 5 位 01 子串,其实就相当于沿着箭头在上图中游走的过程。不妨假设字符串以 0000 开头。如果下一个数字是 1 ,那么 00001 这个子串就被包含了,同时最新的 4 位数就变成了 0001 ;但若下一个数字还是 0 ,那么 00000 就被包含了进来,最新的 4 个数仍然是 0000 。从图上看,这无非是一个从 0000 点出发走了哪条路的问题:你是选择了沿 00001 这条路走到了 0001 这个点,还是沿着 00000 这条路走回了 0000 这个点。同理,每添加一个数字,就相当于沿着某条路走到了一个新的点,路上所写的 5 位数就是刚被考虑到的 5 位数。我们的目的便是既无重复又无遗漏地遍历所有的路。显然图中的每个顶点入度和出度都是 2 ,因此这个图一定存在 Euler 回路,我们便能轻易构造出一个满足要求的 01 串了。这样的 01 串就叫做 De Bruijn 序列。

De Bruijn 序列在这里究竟有什么用呢?它的用途其实很简单,就是为 32 种不同的情况提供了一个唯一索引。比方说, 1000000 后面有 6 个 0 ,将 1000000 乘以 0x077CB531 ,就得到

   00000111011111001011010100110001
-> 11011111001011010100110001000000

相当于把 De Bruijn 序列左移了 6 位。再把这个数右移 27 位,就相当于提取出了这个数的头 5 位:

   11011111001011010100110001000000
->                            11011

由于 De Bruijn 序列的性质,因此当输入数字的末尾 0 个数不同时,最后得到的这个 5 位数也不同。而数组 MultiplyDeBruijnBitPosition 则相当于一个字典的功能。 11011 转回十进制是 27 ,于是我们查一查 MultiplyDeBruijnBitPosition[27] ,程序即返回 6 。
    注意到当输入数字的末尾 0 个数超过 27 个时,程序也是正确的,因为左移时低位正好是用 0 填充的。

这段神一般的代码取自 http://graphics.stanford.edu/~seander/bithacks.html ,欢迎大家前去围观。

出处:http://www.matrix67.com/blog/archives/3985

=============================================================================

同事在研究LZ4 压缩算法时候给我发来了一段代码,看完了顿时表示非常震惊:

static const int[] MultiplyDeBruijnBitPosition = new int[32]
{
    0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
    31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};

/// <summary>
/// Find the number of trailing zeros in 32-bit.
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
static int GetMultiplyDeBruijnBitPosition(uint v)
{
    return MultiplyDeBruijnBitPosition[((uint)((v & -v) * 0x077CB531U)) >> 27];
}

下面依次解释下这段代码的意思:

假设变量v=123456, 那么其二进制表示形式为(...)11110001001000000, -v 在计算机中的二进制表示形式为(...)00001110111000000, 所以(v & -v) == 1000000, 十进制表示形式为64。

(v & -v) * 0x077CB531 的意思是将常量0x077CB531 向左移位6位(左移6位相当于乘64)。

((uint)(v & -v) * 0x077CB5310) >> 27 位的意思是继续将上一步的结果向右移位27位,因为01串总长度是32位,向右移27位以后低位只剩下5个bits。

而0x077CB5310 的二进制表示形式为00000111011111001011010100110001, 所以上面的步骤相当于如下代码:

static int GetMultiplyDeBruijnBitPosition(uint v)
{
    return MultiplyDeBruijnBitPosition[27];
}

根据上面的常量数组,可知当v 等于123456时,其(v & -v) 的二进制表示行为末尾含有6个0。

这个算法的用处目前看主要有两种:

1. 快速计算log2(v & -v);

2. 任意给定两个32-bit 的整型数组,对其中的数据进行异或运算,得到的值v, 采用如上算法判断第几位是不同的,从而用于压缩算法。

以上是关于这个常量的简要介绍,下面重点介绍下这个常量的特点:

1. 32-bit 长度;

2. 上一个5 bits 长度的01串的后四位是下一个01串的前四位,比如10001 的下一位是00010/00011;

3. 首尾是循环的;

根据以上3条规则,设计查找常量值算法代码如下:

using System;
using System.Collections.Generic;

namespace Test
{
    class Program
    {
        static List<string> deBruijnList = new List<string>();
        static List<string> deBruijnReserveList = new List<string>();
        static string[] flagArray = new string[] { "0", "1" };
        static readonly int DeBruijnLength = 5;
        static readonly double MaxDeBruijnListCount = Math.Pow(2, DeBruijnLength) - 4;
        static readonly uint ConstOne = 0x077CB531;
        static readonly uint ConstTwo = 0x0653ADF1;

        static void Init()
        {
            deBruijnReserveList.Add("00010");
            deBruijnReserveList.Add("00100");
            deBruijnReserveList.Add("01000");
            deBruijnReserveList.Add("10000");
        }

        static uint[] GetConstArray(uint constInt)
        {
            //uint constInt = 0x077CB531;
            uint[] constArray = new uint[32];
            uint j = 0;
            for (int i = 0; i < constArray.Length; i++)
            {
                j = (uint)((constInt << i)) >> 27;
                constArray[j] = (uint)i;
            }

            return constArray;
        }

        static const int[] MultiplyDeBruijnBitPosition = new int[32]
        {
            0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
            31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
        };

        /// <summary>
        /// Find the number of trailing zeros in 32-bit.
        /// </summary>
        /// <param name="v"></param>
        /// <returns></returns>
        static int GetMultiplyDeBruijnBitPosition(uint v)
        {
            return MultiplyDeBruijnBitPosition[((uint)((v & -v) * 0x077CB531U)) >> 27];
        }

        static void GetDeBruijnKeyStr()
        {
            string deBruijnStr = "00000111011111001011010100110001";
            for (int i = 0; i < deBruijnStr.Length - DeBruijnLength; i++)
            {
                Console.WriteLine(deBruijnStr.Substring(i, DeBruijnLength));
            }
        }
        static void GetDeBruijnKey(string currentKey)
        {
            string currentKeysLast4ValueStr = currentKey.Substring(1);
            string nextKeyFormer4ValueStr = currentKeysLast4ValueStr;

            string nextKeyFlagZero = nextKeyFormer4ValueStr + "0";
            string nextKeyFlagOne = nextKeyFormer4ValueStr + "1";

            if (deBruijnList.Count == MaxDeBruijnListCount)
            {
                return;
            }
            else if (deBruijnList.Count > MaxDeBruijnListCount)
            {
                deBruijnList.Remove(currentKey);
                return;
            }

            if ((deBruijnList.Contains(nextKeyFlagZero) || deBruijnReserveList.Contains(nextKeyFlagZero))
                && (deBruijnList.Contains(nextKeyFlagOne) || deBruijnReserveList.Contains(nextKeyFlagOne)))
            {
                deBruijnList.Remove(currentKey);
                return;
            }

            if (!deBruijnList.Contains(nextKeyFlagZero) && !deBruijnReserveList.Contains(nextKeyFlagZero))
            {
                deBruijnList.Add(nextKeyFlagZero);
                GetDeBruijnKey(nextKeyFlagZero);
            }
            if (!deBruijnList.Contains(nextKeyFlagOne) && !deBruijnReserveList.Contains(nextKeyFlagOne))
            {
                deBruijnList.Add(nextKeyFlagOne);
                GetDeBruijnKey(nextKeyFlagOne);
            }

            //No new entry was added, so just remove the parent key.
            int lastIndexOfDeBruijnList = deBruijnList.Count - 1;
            if (deBruijnList[lastIndexOfDeBruijnList] == currentKey)
            {
                deBruijnList.Remove(currentKey);
            }
        }

        static void Main(string[] args)
        {
            Init();
            GetDeBruijnKey("00000");
            foreach (string deBruijnStr in deBruijnList)
            {
                Console.WriteLine(deBruijnStr);
            }
            Console.ReadLine();
        }
    }
}

最后得到的新的“逆天”常量值为0x0653ADF1U, 根据常量可以得到常量数组,算法如下:

//ConstOne = 0x077CB531;
//ConstOne = 0x0653ADF1;
static uint[] GetConstArray(uint constInt)
{
    //uint constInt = 0x077CB531;
    uint[] constArray = new uint[32];
    uint j = 0;
    for (int i = 0; i < constArray.Length; i++)
    {
        j = (uint)((constInt << i)) >> 27;
        constArray[j] = (uint)i;
    }

    return constArray;
}

新的常量数组如下:

static const int[] MultiplyDeBruijnBitPosition2 = new int[32]
{
    0, 1, 28, 2, 29, 7, 3, 12, 30, 10, 8, 17, 4, 19, 13, 22,
    31, 27, 6, 11, 9, 16, 18, 21, 26, 5, 15, 20, 25, 14, 24, 23
};

由此可知,“逆天”常量并不止一个,欢迎大家参与研究、讨论。

参考链接:http://www.matrix67.com/blog/archives/3985

出处:http://www.cnblogs.com/danielWise/p/4378460.html

时间: 2024-08-07 01:55:15

神秘常量!用0x077CB531计算末尾0的个数,32位数首位相连的相关文章

神秘常量复出!用0x077CB531计算末尾0的个数

下面这个位运算小技巧可以迅速给出一个数的二进制表达中末尾有多少个 0 .比如, 123 456 的二进制表达是 1 11100010 01000000 ,因此这个程序给出的结果就是 6 .以下是代码片段:unsigned int v; // find the number of trailing zeros in 32-bit v int r; // result goes here static const int MultiplyDeBruijnBitPosition[32] = { 0,

算法-计算阶乘n!末尾0的个数

算法逻辑转载自计算阶乘n!末尾0的个数: 问题描述    给定参数n(n为正整数),请计算n的阶乘n!末尾所含有"0"的个数.    例如,5!=120,其末尾所含有的"0"的个数为1:10!= 3628800,其末尾所含有的"0"的个数为2:20!= 2432902008176640000,其末尾所含有的"0"的个数为4. 计算公式    这里先给出其计算公式,后面给出推导过程.    令f(x)表示正整数x末尾所含有的&q

Java 计算N阶乘末尾0的个数-LeetCode 172 Factorial Trailing Zeroes

题目 Given an integer n, return the number of trailing zeroes in n!. Note: Your solution should be in logarithmic time complexity. 分析 Note中提示让用对数的时间复杂度求解,那么如果粗暴的算出N的阶乘然后看末尾0的个数是不可能的. 所以仔细分析,N! = 1 * 2 * 3 * ... * N 而末尾0的个数只与这些乘数中5和2的个数有关,因为每出现一对5和2就会产生

N!结果中末尾0的个数 2.2

主要是看N!结果中2的个数和5的个数,多的那个个数即是末尾0的个数 ? ? ? ? 计算Z有两种方法 ? ? 一种是对每个n都去看有多少个5的因子 ? ? 一种是隔5个增加一个5的因子,隔25个再在之前的基础上增加一个5的因子 ? ? 两种方法差不多 ? ? 第二种,循环少 ? ? package numOfZeroFactorialN_2_2; ? ? public class NumOfZeroFactorialN_2_2 { ? ? static int numOfZeros(int n)

【CodeChef】Factorial(n!末尾0的个数)

The most important part of a GSM network is so called Base Transceiver Station (BTS). These transceivers form the areas called cells (this term gave the name to the cellular phone) and every phone connects to the BTS with the strongest signal (in a l

求一个数阶乘末尾0的个数

#include<iostream> using namespace std; //给定一个整数N,那么N的阶乘末尾有几个0?N=10,N!=3628800,末尾有2个0 //1.如果我们从"哪些数相乘能得到 10"这个角度来考虑,问题就变得简单了. //首先考虑,如果 N!= K×10M,且 K 不能被 10 整除,那么 N!末尾有 M 个 0.再考虑 //对 N!进行质因数分解,N!=(2x)×(3y)×(5z)-,由于 10 = 2×5,所以 M 只跟 X 和 Z /

求N的阶乘N!中末尾0的个数

有道问题是这样的:给定一个正整数N,那么N的阶乘N!末尾中有多少个0呢?例如:N=10,N!=3628800,则N!的末尾有两个0: 直接上干货,算法思想如下: 对于任意一个正整数N!,都可以化为N!= (2^X)  * (3^Y)* (5^Z)......的形式,要求得末尾0的个数只需求得min(X, Z)即可, 由于是求N!,则X >= Z; 即公约数5出现的频率小于等于2出现的频率,即Z=min(X, Z),即出现0的个数等于公约数5出现的次数: 源码如下: 方法一: #include <

N的阶乘末尾0的个数和其二进制表示中最后位1的位置

问题一解法: 我们知道求N的阶乘结果末尾0的个数也就是说我们在从1做到N的乘法的时候里面产生了多少个10, 我们可以这样分解,也就是将从0到N的数分解成因式,再将这些因式相乘,那么里面有多少个10呢? 其实我们只要算里面有多少个5就可以了? 因为在这些分解后的因子中,能产生10的可只有5和2相乘了,由于2的个数是大于5的个数的,因此 我们只要算5的个数就可以了.那么这个题目就是算这些从1到N的数字分解成因子后,这些因子里面 5的个数. Python代码 def factorialnumberof

poj1401--Factorial--阶乘末尾0的个数

Description 求出n!的末尾有多少个0(连续的). 每组测试点有t个测试数据,输入格式为第一行一个t,后面2~t+1行每行一个n,输出其结果. Sample Input 6 3 60 100 1024 23456 8735373 Sample Output 0 14 24 253 5861 2183837 题解: 求一个数阶乘的末尾0的个数. 10=2*5,显然2的个数总比5多, 即转化为,求阶乘分解以后有几个5. #include<iostream> #include<cma