本篇博文将总结数学相关的内容,涉及概率组合的一些算法,比较简单。
求1的个数
问题描述
给定一个323232 位无符号整数NNN,求整数NNN 的二进制数中111 的个数。
问题分析
方法一
显然:可以通过不断的将整数NNN 右移,判断当前数字的最低位是否为111,直到整数NNN 为000 为止。平均情况下,大约需要161616 次移位和161616 次加法。
int OneNumber(int n)
{
int c = 0;
while (n!=0)
{
c += (n & 1);//n末尾最后一位不断和1作与
n >>= 1;//n=n>>1,n右移一位
}
return c;
}
方法二
每次将nnn 最后一个111 清000,能清多少次就说明有多少个111,只需要n&=(n−1)n\verb'&'=(n-1)n&=(n−1) 即可。例如:n=1010111101n=1010111101n=1010111101,n−1=1010111100n-1=1010111100n−1=1010111100,两数相与,则必可清除nnn 最后一位111。
int OneNumber2(int n)
{
int c = 0;
while (n!=0)
{
n = n&(n - 1);//将n的最后一个“1”清零
c++;//每清除一个1,c就加一
}
return c;
}
方法三:分治
假定能够求出NNN 的高161616 位中111 的个数aaa 和低161616 位中111 的个数bbb,则a+ba+ba+b 即为所求。
为了节省空间,用一个323232 位整数保存aaa 和bbb:
- 高161616 位记录aaa,低161616 位记录bbb;
- (0xFFFF0000&N)(0xFFFF0000\verb'&'N)(0xFFFF0000&N) 筛选得到aaa;
- (0x0000FFFF&N)(0x0000FFFF\verb'&'N)(0x0000FFFF&N) 筛选得到bbb;
- (0xFFFF0000&N)+(0x0000FFFF&N)>>16(0xFFFF0000\verb'&' N) + (0x0000FFFF\verb'&'N)>>16(0xFFFF0000&N)+(0x0000FFFF&N)>>16
如何得到高161616 位和低161616 位中111 的个数aaa,bbb 呢?
- 分治往往伴随着递归
递归过程:
-
如果二进制数NNN 是161616 位,则统计NNN 的***高 888 位*** 和***低 888 位***各自 111 的数目 aaa 和 bbb,而 a、ba、ba、b 用某一个161616 位数 XXX 存储,则使用0xFF00、0x00FF0xFF00、0x00FF0xFF00、0x00FF 分别于XXX 做***与操作***,筛选出 aaa 和 bbb;原问题中的数据是 323232 位,因此分别需要 222 个0xFF00/0x00FF0xFF00/0x00FF0xFF00/0x00FF,即0xFF00FF00/0x00FF00FF0xFF00FF00/0x00FF00FF0xFF00FF00/0x00FF00FF。
-
如果二进制数是 888 位,则统计高 444 位和低 444 位各自111 的数目,使用 0xF0/0x0F0xF0/0x0F0xF0/0x0F 分别做与操作,筛选出高 444 位和低 444 位;原问题中的数据是 323232 位,则分别需要444 个0xF0/0x0F0xF0/0x0F0xF0/0x0F,即0xF0F0F0F0/0x0F0F0F0F0xF0F0F0F0/0x0F0F0F0F0xF0F0F0F0/0x0F0F0F0F。
-
如果是 444 位则统计高 222 位和低 222 位各自 111 的数目,用 0xC/0x30xC/0x30xC/0x3 筛选(高222 位 110011001100 十六进制表示 0xC0xC0xC,低 222 位 001100110011 十六进制表示为 0x30x30x3);原问题中的数据是 323232 位,故各需要需要 888 个0xC/0x30xC/0x30xC/0x3 ,即 0xCCCCCCCC/0x333333330xCCCCCCCC/0x333333330xCCCCCCCC/0x33333333。
-
如果是 222 位则统计高 111 位和低 111 位各自 111 的数目,用 0x2/0x10x2/0x10x2/0x1 筛选;原问题中的数据是323232 位,(因为在十六进制中,以四位为一个单位,则高 111 位为 101010101010 即为 0xA0xA0xA,需要 888 个0xA0xA0xA,同理低 111 位010101010101 即为0x50x50x5,也需要 888 个 )故各需要 888 个0xA/0x30xA/0x30xA/0x3,即为0xAAAAAAAA/0x333333330xAAAAAAAA/0x333333330xAAAAAAAA/0x33333333。
int HammingWeight(unsigned int n)
{
//(n & 0x55555555)每相邻两位忽略高位保留低位为1的二进制序列,
//(n & 0xaaaaaaaa)>>1每相邻两位忽略低位保留高位为1的二进制序列并右移1位,高位补零。
//上面两个子序列相加,则为每相邻两位高位和对应低位为1的相加,往前一位进1。
//也就是检查每对相邻的2位有几个1
n = (n & 0x55555555) + ((n & 0xaaaaaaaa) >> 1);
//每相邻的四位有几个1
n = (n & 0x33333333) + ((n & 0xcccccccc) >> 2);
//每相邻的八位有几个1
n = (n & 0x0f0f0f0f) + ((n & 0xf0f0f0f0) >> 4);
//每相邻的十六位有几个1
n = (n & 0x00ff00ff) + ((n & 0xff00ff00) >> 8);
//32位有几个1
n = (n & 0x0000ffff) + ((n & 0xffff0000) >> 16);
return n;
}
int main()
{
int c = HammingWeight(16);
cout << c << endl;
}
在采用HammingWeightHammingWeightHammingWeight 方法时,对于任何一个323232 位无符号整数NNN 只需要计算555 次运算即可。
总结与应用
- HammingWeightHammingWeightHammingWeight 使用了分治/递归的思想,将问题巧妙解决,降低了运算次数。
- 如果定义两个长度相等的0/10/10/1 串中对应位不相同的个数为海明距离(即码距),则某0/10/10/1 串和全000 串的海明距离即为这个0/10/10/1 串中111 的个数。
- 两个0/10/10/1 串的海明距离,即两个串异或值的111 的数目,因此,该问题在信息编码等诸多领域有广泛应用。
跳跃问题
问题描述
给定非负整数数组,初始时在数组起始位置放置一机器人,数组的每个元素表示在当前位置机器人***最大能够跳跃的数目***。它的目的是用最少的步数到达数组末端。例如:给定数组A=[2,3,1,1,2]A=[2,3,1,1,2]A=[2,3,1,1,2],最少跳步数目是222,对应的跳法是:2−>3−>22->3->22−>3−>2。
如:2,3,1,1,2,4,1,1,6,1,72,3,1,1,2,4,1,1,6,1,72,3,1,1,2,4,1,1,6,1,7,最少需要几步?
问题分析
由上图我们可以看出当前跳的范围为蓝色的 111 范围,下一跳的范围为蓝色的 222 范围。
-
初始步数stepstepstep 赋值为000;
-
记当前步的控制范围是[i,j][i,j][i,j],则用kkk 遍历iii 到jjj;
计算A[k]+kA[k]+kA[k]+k 的最大值,记做j2j2j2;A[k]A[k]A[k] 表示当前位置最远能跳的距离。 -
step++step++step++;继续遍历[j+1,j2][j+1,j2][j+1,j2];
每一个stepstepstep 都有当前可跳到的范围,而当前的范围又确定下一个stepstepstep 的可跳到的范围。每在一个stepstepstep 遍历当前可跳的范围,确定下一跳的范围。这样总可以找到最短的stepstepstep 跳到终点。每一个stepstepstep 可跳的范围内,其stepstepstep 的值都是相同。这个解题过程类似于广度优先搜索。,每一个stepstepstep 可跳的范围为一层。
实现代码
int Jump(int* a,int size)
{
if (size == 1)
return 0;
int i = 0;
int j = 0;//初始可跳的范围即为[i,j]
int k,j2;
int step = 0;
while (j<size)
{
step++;
j2 = j;
for (k = i; k <= j; k++)//遍历当前step可跳的范围来确定下一跳的范围
{
j2 = max(j2, k + a[k]);
if (j2 > size - 1)
return step;
}
i = j + 1;//上一跳的终点的下一个格子为下一跳的起点,注意a[k]为最大可跳的距离,最少可跳一步。
j = j2;//下一跳终点
if (j < i)
return -1;
}
return step;
}
Jump问题总结
虽然从代码上看有两层循环,但是我们分析执行过程可知只是从序列头跳到序列尾,时间复杂度只有O(n)O(n)O(n)。
该算法在每次跳跃中,都是尽量跳的更远,并记录j2j2j2——属于***贪心法***;也可以认为是从区间[i,j][i,j][i,j] (若干结点)扩展下一层区间[j+1,j2][j+1,j2][j+1,j2] (若干子结点)——属于***广度优先搜索***。
错位排列问题
问题描述
111 到nnn 的全排列中,第iii 个数不是iii 的排列共有多少种?
问题分析
- 假定nnn 个数的错位排列数目为dp[n]dp[n]dp[n]
- 先考察数字nnn 的放置方法:显然,nnn 可以放在从111 到n−1n-1n−1 的某个位置,共n−1n-1n−1 种方法;假定放在了第kkk 位。
- 对于数字kkk:
要么放置在第nnn 位
要么不放置在第nnn 位。
数字k放置在第n位
相当于数字kkk 和数字nnn 交互位置后,其他n−2n-2n−2 个数字做错位排列,因此有dp[n−2]dp[n-2]dp[n−2] 种方法。
数字k不放置在第n位
将数字kkk 暂时更名为nnn (这是可以做到的:因为真正的nnn 已经放在第kkk 位上,真正的nnn 不再考虑之列),现在需要将111 到k−1k-1k−1 以及k+1k+1k+1 到nnn 这n−1n-1n−1 个数放置在相应位置上,要求数字和位置不相同!显然是n−1n-1n−1 个数的错位排列,有dp[n−1]dp[n-1]dp[n−1] 种方法。
错位排列递推公式
综上,dp[n]=(n−1)∗(dp[n−1]+dp[n−2])dp[n]=(n-1)*(dp[n-1]+dp[n-2])dp[n]=(n−1)∗(dp[n−1]+dp[n−2]),(n−1)(n-1)(n−1) 表示起始时,数字nnn 有(n−1)(n-1)(n−1) 个可放置的位置。
初值
只有111 个数字,错位排列不存在,dp[1]=0dp[1]=0dp[1]=0;
只有222 个数字,错位排列即交换排列,dp[2]=1dp[2]=1dp[2]=1;
则递推公式为:
实现代码
int dislocationSorting(int n)
{
int* dp = new int[n];
dp--;
dp[1] = 0;
dp[2] = 1;
for (int i = 3; i <= n; i++)
dp[i] = (i - 1)*(dp[i - 1] + dp[i - 2]);
return dp[n];
}
int main()
{
int c = dislocationSorting(2);
cout << c << endl;
}