对于算法竞赛来说,算法的效率自然是很重要的。有些时候我们可以使用一切巧妙地办法借助数据本身的特点进行处理。比如今天这个题。
二进制数数
问题描述
给定L,R
。统计[L,R]
区间内的所有数在二进制下包含的“1”的个数之和。如5的二进制为101
,包含2个“1”。
输入格式
第一行包含2个数L,R
输出格式
一个数S,表示[L,R]
区间内的所有数在二进制下包含的“1”的个数之和。
样例输入
2 3
样例输出
3
数据规模和约定
L<=R<=100000;
题目解析
进制,即进位计数制,是人为定义的一种带进位的计数方法,便于使用有限的数字字符表示所有的数。对于任何一种进制都表示某一位置上的数字达到某一值后向上进一位。如常用的十进制计算9+1
时,当个位数字9增加1时,应该变为十,但是我们采用十进制计数,所以在这一位上并不会出现表示十的数字,而是向上进位变为10
。同理,对于8进制而言,当计算7+1
时,并不出现8
这个数字,而是变为10
,而这个8进制的10
与十进制的8
是相等的。也可以发现,进制的转换并不会改变数值本身的大小,只是表示方法的改变。
进制的转换通常可以使用连续做除法取余数的方式,如十进制数6
转二进制可以如下方式计算:
6
÷
2
=
3......0
6 ÷2 = 3 ...... 0
6÷2=3......0
3
÷
2
=
1......1
3 ÷2 = 1 ...... 1
3÷2=1......1
1
÷
2
=
0......1
1 ÷2 = 0 ...... 1
1÷2=0......1
将余数从下往上倒过来即是相应进制,即6的二进制表示为110
。
因为在进制转换的过程中数值大小本身并不变化,所以人工计算时对于不方便计算的数值也可以通过转换为其他进制作为媒介来进行。
题解
对于该题目来说,我们可以将过程分为几步:
-
循环
[L,R]
之间所有整数 -
将整数表示为二进制
-
通过变量计数二进制数中1的个数
有以下程序框架
#include <stdio.h>
int main(void)
{
int L, R;
int count = 0;
scanf("%d %d", &L, &R);
//1. 循环`[L,R]`之间所有整数
//2. 将整数表示为二进制
//3. 通过变量计数二进制数中1的个数
printf("%d", count);
return 0;
}
使用for循环[L,R]
之间所有整数
#include <stdio.h>
int main(void)
{
int L, R;
int count = 0;
//加入循环变量
int i = 0;
scanf("%d %d", &L, &R);
//1. 循环`[L,R]`之间所有整数
for(i = L ; i <= R; i++)
{
//2. 将整数表示为二进制
//3. 通过变量计数二进制数中1的个数
}
printf("%d", count);
return 0;
}
将整数表示为二进制的方法在上面已经讲到,在计算机中,我们使用取模运算符%
可以直接得到余数,然后对该整数除以2,再取余数,直到商为0为止。
同时,第3步计数的过程可以直接与取模运算同时进行,这样还可以节省存储二进制数的内存空间。
#include <stdio.h>
int main(void)
{
int L, R;
int count = 0;
//加入循环变量
int i = 0;
//加入计算时的临时变量,以避免改变循环变量导致计算逻辑出错
int t = 0;
scanf("%d %d", &L, &R);
//1. 循环`[L,R]`之间所有整数
for (i = L; i <= R; i++)
{
t = i;
//2. 将整数表示为二进制
do
{
if (t % 2 == 1)
{
//3. 通过变量计数二进制数中1的个数
count++;
}
t = t / 2;
} while (t != 0);
}
printf("%d", count);
return 0;
}
这还不够,这样一个简单的题目我们并不能满足于将其做对,而应该对其进行优化,使其效率更高。这样在复杂问题中才能够尽可能解决更多测试数据。
所以对于本题,我将输入数据扩大到L<=R<=100000000;
来测试算法效率(题目约定范围的1,000倍)
测试,输入数据0 100000000
输出1314447116
用时11.98秒。
优化1
由于我们数的是二进制中1的个数,所以在计数时我们不需要先判断t % 2
是否等于1,可以直接让count
变量加上t % 2
的值,如果t % 2
为1,则count
加1,若为0,则不变。
#include <stdio.h>
int main(void)
{
int L, R;
int count = 0;
//加入循环变量
int i = 0;
//加入计算时的临时变量,以避免改变循环变量导致计算逻辑出错
int t = 0;
scanf("%d %d", &L, &R);
//1. 循环`[L,R]`之间所有整数
for (i = L; i <= R; i++)
{
t = i;
do
{
//2. 将整数表示为二进制
//3. 通过变量计数二进制数中1的个数
//两步合一
count += t % 2;
t = t / 2;
} while (t != 0);
}
printf("%d", count);
return 0;
}
测试输入数据0 100000000
,输出1314447116
,用时6.651秒。
优化2
在计算机中,由于计算机的原理所致,在计算和保存时,实际上都是以二进制方式进行的,每个二进制位称为一个比特(bit),为了便于操作,又有每8个比特为一个字节(byte)。
也就是说实际上我们需要的二进制数据在计算机中已经存在了,不需要做除法就可以获得我们需要的数据。我们只需要将计算机内存中每一位的数据取出计数即可。
对于计算机中“位”的操作和运算,我们称之为“位操作”、“位运算”。而位操作的速度相比数值运算更快,所以我们通过位操作能够提高解题效率。本题中,我们将每一位取出的操作可以使用“移位操作”和“位与运算”。
移位操作即将内存中的二进制数据向指定方向移动指定个位,如有一8位整型变量n = 53
,53
的二进制表示为0011 0101
,对其进行向右移2位的操作语句为n >> 2;
,得到二进制表示为0000 1101
的数13。
位与运算即将两个数按二进制位做与操作,对于8位整型变量n = 53
,将其和22(10)作与操作,53的二进制表示为0011 0101
,22的二进制表示为0001 0110
,将二者按位作与运算,可得二进制表示为0001 0100
的数20。
那么对于二进制数而言,想取出其中某1位数据,只需要对其进行移位操作和位与运算,如对于n = 53
,希望取出其第4位(右数,0开始),即(n >> 4) & 1
。53的二进制表示0011 0101
右移4位后为0000 0011
,位与0000 0001
后为0000 0001
,所以53的第4位为1.
将以上思路带入我们的算法
#include <stdio.h>
int main(void)
{
int L, R;
int count = 0;
int i = 0, t = 0;
scanf("%d %d", &L, &R);
//1. 循环`[L,R]`之间所有整数
for (i = L; i <= R; i++)
{
//2. 将整数表示为二进制
//3. 通过变量计数二进制数中1的个数
//两步合一,改为位运算
for(t = 0; i >> t; t++)
{
count += i >> t & 1;
}
}
printf("%d", count);
return 0;
}
测试输入数据0 100000000
,输出1314447116
,用时4.756秒。