进位制数的灵活运用
在编写程序解决某些问题时,可以灵活地使用进位制数,例如像二进制枚举就是灵活使用二进制数。下面再讲述一些例题。
1、二进制的应用
【例1】至少一位数字相同
问题描述
给定N个正整数A1,A2,...,AN,求有多少整数对(i,j),满足以下条件:
1≤i<j≤N,Ai和Aj至少有一位数字是相同的(不一定要在相同的数位)。
输入
输入的第一行包含一个正整数N。
接下来N行,每行包含一个正整数Ai。
输出
输出一行一个整数,表示满足条件的整数对的数目。
输入样例
4
32
51
123
282
输出样例
4
(1)编程思路。
以样例为例说明。共有4组整数对满足条件。(32,123)、(32,282)、(51,123)和(123,282)。
显然,若采用二重循环两两组合来判断每组数中是否至少有一位数字是相同的,在数据量较大的情况下,不是一个可取的方法。
实际上要判断两个整数是否至少有一位数字是相同的。我们是不在乎两个数的每一个数位是什么、哪几个位上的数字相同,只用关心0~9这9个数字中是否有某个数字在两个数中都出现过,若都出现过,则两个数至少有一位数字是相同的。
由于十进制中只有0~9共10个数码,因此可以用一个10位的二进制数来表示一个十进制整数X的类型,这个二进制数的第k(0≤k≤9)位为1表示整数X中含有数字k,若数字k在整数X中没出现,则对应二进制数的第k位为0。
用数组a来保存各类型整数的个数,显然任何一个正整数的类型一定在 [0,1023]之间。即数组a最多有1024个元素。初始时,数组a的元素值全部置为0。
还是以样例中的4个数为例。
整数32中含有2、3这两个数字,对应二进制数为0000001100,数的类型号为12,a[12]加1,a[12]=1。
整数51中含有1、5这两个数字,对应二进制数为0000100010,数的类型号为34,a[34]加1,a[34]=1。
整数123中含有1、2、3这3个数字,对应二进制数为0000001110,数的类型号为14,a[14]加1,a[14]=1。
整数282中含有2、8这两个数字,对应二进制数为0100000100,数的类型号为260,a[260]加1,a[260]=1。
这样处理后,再求N个正整数中满足要求的整数对的数量ans(初值为0)就很方便了。分两种情况处理。
1)两个整数的类型相同,则同类型中任意取两个数就满足要求。用循环对a[0]~a[1023]中各a[i]值进行遍历。若 a[i]!=0,则 ans=ans+a[i]*(a[i]-1)/2。
2)两个整数的类型不同。设一个类型为i,一个类型为j (设i<j),若 i & j的值不为0,则i和j对应的二进制数一定在某个位上都是1,也就是存在相同的数字(某个位都为1)。这样,a[i]和a[j]中各取1个数就满足要求。 ans=ans+a[i]*a[j]。
(2)源程序。
#include <stdio.h> int main() { int n; scanf("%d",&n); int i,j; long long a[1024]={0}; int min=1024,max=0; for (i=1;i<=n;i++) { long long x; scanf("%lld",&x); int h[10]; // 记录0~9每个数字在x中是否出现 for (j=0;j<10;j++) h[j]=0; while(x) { h[x%10]=1; // 数字x%10出现了,h[x%10]记为1 x/=10; } int num=0; for (j=0;j<10;j++) { num=num*2+h[j]; // 将h[0]~h[9]中保存数据作为二进制数转换为十进制数num } a[num]++; // num这种数增加1个 if (num>max) max=num; if (num<min) min=num; } long long ans=0; for (i=min;i<=max;i++) // 同一种数内两两组合 ans+=a[i]*(a[i]-1)/2; for (i=min;i<max;i++) // 不同种类的数两两组合 { for (j=i+1;j<=max;j++) if (i & j) ans+=a[i]*a[j]; } printf("%lld\n",ans); return 0; }
将上面的源程序提交给洛谷题库 P7617 [COCI2011-2012#2] KOMPIĆI (https://www.luogu.com.cn/problem/P7617),可以Accepted。
【例2】异或和
问题描述
给定一个长度为N的序列A1,A2,...,AN,求序列元素两两异或的总和。
例如,某序列中有3个数,A1=7,A2=3,A3=5。
则有 A1 ⊕ A2 = 4,A1 ⊕ A3 = 2,A2 ⊕ A3 = 6,
4 + 2 + 6 = 12,因此序列元素两两异或的总和为12。
输入
输入的第一行包含一个正整数N(1≤N≤106)。
接下来N行每行包含一个正整数Ai(1≤Ai≤106)。
输出
输出一行一个整数,表示两两异或后的总和。
输入样例
3
7
3
5
输出样例
12
(1)编程思路。
若通过二重循环对序列中的数据进行两两组合求异或和,在数据量大的情况下肯定是超时的。
首先,我们考虑一个数转成二进制后每个位的操作,每个位上的数据只能是0或1,其异或运算规则是: 0和1 异或得1 , 1和1 或者 0和 0 异或得0 。怎么求多个0 和1 的两两异或和呢?
举个例子: 0,1,0 三个数两两异或和应该是:0 异或 1 加上 0 异或0 再加上 1 异或 0,和值sum=1+0+1=2 。从中可以发现,每个 0 和一个 1 进行异或,和sum 就要加 1 ,也就是说每一个 0 都会使 sum 加上 1 的个数(因为 0 要和 1 的个数个 1 异或)。设在n个0、1序列中有x 个1 ,则有 n-x个0,这样两两异或的和值sum 就等于 0 的个数乘 1 的个数,也就是 sum=(n−x)*x 。
现在要对N个正整数序列求两两异或和。具体做法是:
将原序列中的每个元素都转换为二进制,并用一个数组 a 记录序列中全部元素各二进制数中每一位1 的个数。最后用一个循环,将二进制每一位的两两异或和都算出来,累加到和值sum中。
n 个数中第i 位上每一对 0 和 1 都能造成 2i 的贡献。在n个数中,已求出各二进制数第i位上有a[i]个 1,有 n-a[i]个 0,而每个 0都和a[i]个 1 都能造成2i的贡献,所以
sum=sum+a[i]*(n−a[i])*2i 。
以样例为例说明。7 对应二进制数为 111,因此,a[0]=a[0]+1,a[1]=a[1]+1,a[2]=a[2]+1,由此,a[0]=1,a[1]=1,a[2]=1。
3 对应二进制数为 11,因此,a[0]=a[0]+1,a[1]=a[1]+1,由此,a[0]=2,a[1]=2,a[2]=1。
5 对应二进制数为 101,因此,a[0]=a[0]+1,a[2]=a[2]+1,由此,a[0]=3,a[1]=2,a[2]=2。
为此,在异或和的和值sum中, 第0位贡献 3*0*1=0,第1位贡献 2*(3-2)*2=4,第2位贡献 2*(3-2)*4=8。因此,和值sum=0+4+8=12。
(2)源程序。
#include <stdio.h> int main() { int n; scanf("%d",&n); int i,a[25]={0},len=0; for (i=1;i<=n;i++) { int x; scanf("%d",&x); int k=0; while(x) // 将x转为二进制 { a[k]=a[k]+x%2; // 如果第k位是1,则a[k]加1 x/=2; k++; } if (len<k) len=k; // 各数对应二进制数的最长位数 } long long ans=0; for (i=0;i<len;i++) ans+=1ll*a[i]*(n-a[i])*(1<<i); printf("%lld\n",ans); return 0; }
2、三进制的应用
【例3】天平称重
问题描述
给你一台天平,一件货物重m公斤。然后给你一些重1,3,9,27,…,3^k的砝码。当然,不同权重的砝码数量只有一个。
现在把货物放在天平的左边。然后你应该在天平的两边放一些砝码,使天平平衡。
输入
整数m表示货物的重量(0≤ m≤ 100 000 000)
输出
您应该输出两行。
在第一行中,第一个整数N1是放在天平左边的砝码的数量,然后N1个整数(按升序),表示放在左边的各砝码的重量,每个数用一个空格隔开。
在第二行中,请使用与第一行相同的方式输出N2,即右侧放置的砝码数。然后,按照升序排列的N2个整数表示放在右边的各砝码的重量。
输入样例
42
30
输出样例
3 3 9 27
1 81
0
2 3 27
(1)编程思路。
可以看出砝码重量1、3、9、27、…正好是一个三进制数各位上的权值,因此应考虑三进制数的应用。
称重时砝码可以放在左盘(物体盘),也可以放在右盘(砝码盘)。若砝码只放在右盘,则 物体质量=砝码盘砝码质量;若右盘和左盘中都放置了砝码,则 物体质量=右盘砝码质量-左盘砝码质量。这样,由于可以把砝码加在天平的左盘中,因此,放在左盘中的砝码不是要加在称出的质量上面,而是要从中减去的数。例如,5=9-3-1、6=9-3、7=9+1-3等等。
为了达到这个目的,设所用的三进制数码不是通常的0、1、2,而是-1、0、1。即2可以写成3-1,将其转化成-1这个数字。为了描述简便,把-1写成i,以后只要在三进制中碰到2这个数字,就把它改写成1i(即3-1=2)。例如,三进制中的22102这个数,可以用下面的方法改写成10i11i。
22102 = 20000 + 2000 + 100 + 2 = 1i0000 + 1i000 +100 + 1i
= 1i0000 + 1i000 +11i = 1i0000 + 1i11i = 10i11i
来看几个实际克数的称重情况。
例如,为了称出14克,先将14化成普通三进制112,再进行改写,112=100+10+1i=100+2i=100+20+i = 100 +1i0 +i =100 +1ii = 2ii =1iii。这就是说,把27这块砝码放进右盘,而把9、3、1三块砝码放进左盘中,就可以称出14克出来(27-9-3-1=14)。
再看怎样称出26克来,26化成普通三进制222,进行改写,222=1i00+1i0+1i=1i00+10i=100i。这就是说,把27这块砝码放进右盘,而把1这块砝码放进左盘中,就可以称出26克出来(27-1=26)。
因此,本题的处理办法是:
先将输入的十进制数n转换为3进制数,转换后得到的各位三进制数字保存在数组a中。然后对数组a中的值从低位向高位进行校正。校正方法为:
若 a[i]的值为2,则变为 -1, 同时 a[i+1]加1(相当于向前1位进位);
若 a[i]的值为3,则变为 0, 同时向前1位进位,即 a[i+1] 加1;
若 a[i]的值 为0 或 1 时保持不变。
之后将a[i]值为1所对应的重为3i的砝码放在右盘中,a[i]值为-1 所对应的重为3i的砝码放在左盘中,就是问题的答案。
(2)源程序。
#include <stdio.h> int main() { int table[21]; // table[i]保存3的i次方的值 table[0]=1; int i; for (i = 1; i < 20; i++) // 预先求得3的1次方~3的19次方值保存到数组table中 table[i]=table[i-1]*3; int n; while (scanf("%d", &n)!=EOF) { int len = 0; int a[21]; while (n!=0) // 将n转换为3进制数,并将各位数字保存到数组a中,a[0]保存最低位数字 { a[len++] = n % 3; n = n / 3; } a[len]=0; // 最高位的前面先补个0 int lcnt=0,rcnt=0; // 分别保存放在天平左端和天平右端的砝码个数 for (i=0;i<len;i++) // 从低位到高位对3进制数进行校正 { if (a[i]==1) rcnt++; if (a[i]==2) { a[i]=-1; a[i+1]++; lcnt++; } if (a[i]==3) { a[i]=0; a[i+1]++; } } if (a[len]!=0) { rcnt++; len++; } printf("%d",lcnt); for (i=0;i<len;i++) if (a[i]==-1) printf(" %d",table[i]); printf("\n"); printf("%d",rcnt); for (i=0;i<len;i++) if (a[i]==1) printf(" %d",table[i]); printf("\n"); } return 0; }
将上面的源程序提交给 HDU 3029 Scales (http://acm.hdu.edu.cn/showproblem.php?pid=3029),可以Accepted。