【读书笔记】《王道论坛计算机考研机试指南》第四章

第四章 数学问题

%运算符

%运算符的用法非常简单,我们用形如a%b的语句来调用该运算符。其中变量a, b必须为整型变量,例如int、short 等,而不能为浮点数且b变量必须为非零值,若出现模零错误,程序会因为该异常意外终止。在评判系统中表现为评判系统给出了运行时错误,程序未运行完成就异常终止。所以若读者在练习、考试中出现了评判系统返回了运行时错误,可以试着检查是否可能出现模零错误。
以a%b语句为例,首先计算出a的绝对值被b的绝对值除所得的余数,再使该余数的符号与a保持一致。即若a为正数,则该表达式结果必为非负数(可能为0);若a为负数,则表达式结果必为非正数(可能为0)。而表达式结果与b的符号没有直接关系,即a%-b与a%b的结果相同。
我们注意到,通过求模运算符求得的余数存在着负数的可能。而这与数论中关于余数的定义是不相符的。数论指出,余数的取值范围为从0到除数减1,即在a %b表达式中,其符合数论规定的的结果取值范围应是0到b-1。%运算符的运算特性仅保证余数的绝对值在如上所述的范围内,而不保证不会出现负数,出现负余数也为我们下一步操作带来诸多不便。所以必须保证表达式求得的余数在数论定义的区间范围内
我们只需在该负的余数上再加上除数再对除数求一次余

r = a%b;
a = k*b+r;

我们可以统一的对取得的余数加上除数后再对该和求模,即:

r'=(r + b)%b;

这样做,不仅能对可能出现的负余数做适当的修正,同时对出现的零和正余数也不会改变他们的值,在例2.4中我们正是利用该方法,对所有求得的余数都做了修正,保证其将会落在我们需要的区间内。

数位拆解

在这里插入图片描述
在这里插入图片描述
不断地重复对某个数x除以10,对10求模,即可得到数字x各个数位上的数字。

#include <stdio.h>
int main(){
    
    
	int a,b; //保存两个整数的变量
	while (scanf("%d%d",&a,&b)!=EOF) {
    
     //输入两个整数
		int buf1[20],buf2[20],size1=0,size2= 0;/*用buf1, buf2分别保存从两个整数中拆解出来的数位数字,
												其数量由size1, size2表示*/
		while(a!=0) {
    
     //数位拆解,只要当a依然大于零就不断重复拆解过程
			buf1[size1++]=a%10; //取得当前个位上的数字,将其保存
			a/=10;//将所有数位上的数字移动到高一位上
		}
		while(b!=0) {
    
     //拆解第二个数字
			buf2[size2++]=b%10;
			b/=10;
		}
		int ans=0; //计算答案
		for(int i=0;i<size1;i++)
			for (int j=0;j<size2;j++)
				ans+=buf1[i]*buf2[j]; //两两相乘后相加
		printf("%d\n", ans);
	}
return 0;
}

这里有另一种方法,简洁明了。

#include <stdio.h>
int main(){
    
    
char a[11],b[11];
while (scanf("%s%s",a,b)!=EOF) {
    
     //利用字符串将两个数字读入,作为字符串保存在内存中
	int ans=0; //累加变量
	for (int i= 0;a[i]!=0;i++) //遍历a中每一个字符,直到a字符串结尾
		for (int j=0;b[j]!=0;j++) //遍历b中每一个字符,直到b字符串结尾
			ans+=(a[i]-'0')*(b[j]-'0'); //计算a, b中每一个字符所代表的数字两两乘积的和
	printf("%d\n", ans); // 出答案
}
return 0;

进制转换

如何完成进制转换呢?我们不妨把从m进制转换到n进制转化为两个进制转换问题: 1.从m进制转换到十进制。2.再从十进制转换到n进制。这样,我们只需要掌握十进制与其它进制的相互转换就可以完成任意进制间的转换了。
我们以十进制转换为二进制为例,首先讨论如何完成十进制转换为其它进制。我们有十进制数x,完成转换其为二进制,即求b0,b1,b2, …bn。
在这里插入图片描述
使其满足如下等式:
我们应从上节数位拆解的方法中得到启发。首先我们对x模2,即:

在这里插入图片描述
这样我们就得到了该数字由二进制表示时最低位数字。接下去读者应该能够反应过来,我们只需对x做整数除法,对其除2,即可同样地将高位数字向低位移动,即
在这里插入图片描述
再通过求模运算依次求得被移动到最低位上的数字,如此往复,直到得到所有数位上的数字。反过来将二进制表示的数字转换为十进制则比较容易,只要依次计算各个数位上的数字与该位权重的乘积再将它们求和,即可得到十进制数字。
总结一下, 当要求十进制数x的k进制表示时,只需不断的重复对x求余(对k),求商(除以k),即可由低到高依次得到各个数位上的数。反过来,要求得由k进制表示的数字的十进制值时,我们需要依次计算各个数位上的数字与该位权重的积(第n位则权重为kn-1),然后将它们依次累加即可得到该十进制值。

Problem 1
在这里插入图片描述

#include <stdio.h>
int main() {
    
    
	long long a,b; //使用数据类型long long确保不会溢出
	int m;
	while (scanf("%d",&m) !=EOF) {
    
    
		if (m==0) break; //当m等于0时退出
		scanf("%lld%lld",&a,&b); //用%lld对long long变量赋值
		a=a+b; //计算a+b
		int ans[50],size=0; //ans用来保存依次转换得到的各个数位数字的值。size表示其个数
		do {
    
     //依次求的各个数位上的数字值
			ans[size++]=a%m; //对m求模
			a/=m;//除以m
		} while (a!=0); //当a不为0时重复该过程
		for(int i=size-1;i>-0;i--){
    
    
			printf("%d",ans[i]);
		} //输出,注意顺序为从高位到低位
		printf("\n"); //输出换行
	}
	return 0;
}

该代码中使用了我们前文未曾使用的数据类型long long (部分平台__int64), 这是一种用64位二进制来表示一个整数的数据类型,它的数字取值范围为-263~263-1。在本例中,虽然题面明确了输入数据将在int范围内(<= 231-1), 但是两个int数字的和可能超过int所能表示的最大值,出现溢出。为了避免这种情况,我们采用long long来表示两个int数的和(代码中两个数字也用long Iong保存)。读者应该对溢出问题保持相当高的警觉,在题面明确的输入范围内,你所写的代码是否会产生溢出?若会,则应该采取相应措施。另外顺便一提的是,如果用scanf和printf来输出long long, 则使用转义字符%lld(__int64对应%I64d)。
另一个与上例不同的地方是进制转换处我们用do while循环来代替上例数位拆解中的while循环,这样做的目的是保证该转换工作至少会被执行一次,那么即使被转换数字是0,程序也能正常工作。当然,我们也可以选择如上一节中所讲的方法,依然采用while循环,但在开始转换前,判断被转换数字是否为0,若是则做相应特殊处理工作。

Problem 2
在这里插入图片描述

#include <stdio.h>
#include <string.h>
int main(){
    
    
	int a,b;
	char str[40];
	while (scanf("%d%s%d",&a,str,&b)!=EOF){
    
    
		int tmp=0,lenth=strlen(str),c=1; /*tmp为我们将要计算的a进制对应的十进制数,lenth为字符串长度方便我们
		从低位到高位遍历每个数位上的数,c为各个数位的权重初始化为1,表示最低位数位权重为1,之后每位权重都是前一位权重的a倍*/
		for(int i=lenth-1;i>=0;i--){
    
    //从低位到高位遍历每个数位上的数
			int x; //计算该位上数字
			if (str[i] >= '0' & str[i] <= '9') {
    
    
				x=str[i] - '0'; //当字符在0到9之间,计算其代表的数字
			}
			else if (str[i] >= 'a' && str[i] <='z') {
    
    
				x=str[i]-'a' + 10; //当字符为小写字母时,计算其代表的数字
			}
			else {
    
    
				x=str[i]-'A'+ 10; //当字符为大写字母时,计算其代表的数字
			}
			tmp+=x*c;//累加该位数字与该数位权重的积
			c*=a; //计算下一-位数位权重
		}
		char ans[40],size=0; //用ans保存转换到b进制的各个数位数字
		do {
    
    
			int x=tmp%b;//计算该位数字
			ans[size++]=(x<10)?x+'0':x-10+'A'; //将数字转换为字符
			tmp /= b;
		} while(tmp);
		for(int i=size-1;i>=0;i--){
    
    
			printf("%c",ans[i]);
		}
		printf("\n");
	}
	return 0;
}

最大公约数(GCD)

可证明a、b的最大公约数同时也是b、amod b的最大公约数。若a、b全为零则它们的最大公约数不存在;若a、b其中之一为零,则它们的最大公约数为a、 b中非零的那个;若a、b都不为零,则使新a=b;新b=a%b然后重复该过程。
在这里插入图片描述在这里插入图片描述

//递归形式
#include <stdio.h>
int gcd(int a,int b) {
    
    
if (b == 0) return a; //若b为零则最大公约数为a
else return gcd(b,a%b); //否则,则改为求b与a%b的最大公约数
}
int main() {
    
    
	int a,b;
	while (scanf("%d%d",&a,&b)!=EOF) {
    
     //输入两个正整数
		printf("%d\n",gcd(a,b)); 
	}
	return 0;
}
//非递归形式
#include <stdio.h>
int gcd(int a,int b) {
    
    
while(b!=0) {
    
     //只要b不为0则一直持续该过程
	int t=a%b;
	a=b; //使a变成b
	b=t;//使b变成a%b
	return a; //当b为0时,a即是所求
}
int main() {
    
    
	int a,b;
	while (scanf("%d%d",&a,&b)!=EOF) {
    
    
		printf("%d\n",gcd(a,b));
	}
	return 0;
}

最小公倍数(LCM)

a、b两数的最小公倍数为两数的乘积除以它们的最大公约数。所欲就把求最小公倍数问题统一到了求最大公约数上来。
在这里插入图片描述
在这里插入图片描述

#include <stdio.h>
int gcd(int a,int b) {
    
     //求最大公约数
	return b!=0?gcd(b,a%b):a;
}
int main() {
    
    
	int a,b;
	while (scanf("%d%d",&a,&b)!=EOF) {
    
    
		printf("%d\n",a*b/gcd(a,b)); //输出两数乘积与最大公约数的商
	}
	return 0;
}

素数筛法

Probelm 1
在这里插入图片描述

#include <stdio.h>
#include <math.h>
bool judge(int x) {
    
     //判断一个数是否为素数
	if (x <= 1) return false; //若其小于等于1,必不是
	int bound =(int)sqrt(x) + 1; /*计算枚举上界,为防止double值带来的精度损失。所以采用根号值取整后再加1,
	即宁愿多枚举一个数也不能少枚举一个数*/
	for (int i=2;i < bound;i++) {
    
    
		if(x%i == 0) return false; //依次枚举这些数能否整除x,若能则必不为素数
	}
	return true; //若均不能则为素数
}
int main() {
    
    
	int x;
	while (scanf("%d",&x) !=EOF) {
    
    
		puts(judge(x) ? "yes" : "no"); //依据函数返回值输出答案
	}
	return 0;
}

先计算出枚举上界,将它赋值给bound,再在for 循环循环条件中与bound作比较的写法,而不采用在for循环循环条件中直接与sqrt(n) + 1进行比较。这是有原因的。我们的写法,将sqrt(n)+1的值赋值给变量bound,然后令i与bound作比较,这样做保证了sqrt运算只进行一次。而假如直接在for循环循环条件中与sqrt(n) + 1作比较, 则比较多少次,sqrt(n)也会运算多少次, 而sqrt是众所周知的几个比较耗时的函数之一,我们采用这样的编码技巧为程序节省了不少时间(该策略同样适用于strlen 函数)。

Problem 2
首先来考虑这样一个命题:若一个数不是素数,则必存在一个小于它的素数为其的因数。这个命题的正确性是显而易见的。那么,假如我们已经获得了小于一个数的所有素数,我们只需确定该数不能被这些素数整除,这个数即为素数。但是这样的做法似乎依然需要大量的枚举测试工作。正因为如此,我们可以换一个角度,在我们获得一个素数时,即将它的所有倍数均标记成非素数,这样当我们遍历到一个数时,它没有被任何小于它的素数标记为非素数,则我们确定其为素数。按照如下步骤完成工作:
从2开始遍历2到100000的所有整数,若当前整数没有因为它是某个小于其的素数的倍数而被标记成非素数,则判定其为素数,并标记它所有的倍数为非素数。然后继续遍历下一个数,直到遍历完2到000000区间内所有的整数。此时,所有没被标记成非素数的数字即为我们要求的素数。这种算法被我们称为素数筛法
在这里插入图片描述

#include <stdio.h>
int prime[10000]; //保存筛得的素数
int primeSize; //保存的素数的个数
bool mark[10001]; //若mark[x] 为true,则表示该数x已被标记成非素数
void init() {
    
     //素数筛法
	for (int i=1;i<10000;i++) {
    
    
		mark[i]=false;
	} //初始化,所有数字均没被标记
	primeSize=0; //得到的素数个数为
	for (int i=2;i <= 10000;i ++) {
    
     //依次遍历2到10000所有数字
		if (mark[i] = true) continue; //若该数字已经被标记,则跳过
		prime[primeSize ++]=i; //否则,又新得到一个新素数
		for(int j=i*i;j<=10000;j+=i){
    
    //并将该数的所有倍数均标记成非素数
			mark[j]=true;
		}
	}
}
int main() {
    
    
	init(); //在程序一开始首先取得2到10000中所有素数
	int n;
	while (scanf("%d",&n) != EOF) {
    
    
		bool isOutput =false; //表示 是否输出了符合条件的数字
		for (int i = 0;i < primeSize;i ++) {
    
     //依次遍历得到的所有素数
			if (prime[i] < n && prime[i] % 10 == 1) {
    
     //测试当前素数是否符合条件
				if (isOutput == false) {
    
     //若当前输出为第一个输出的数字,则标记已经输出了符合条件的数字, 且该数字前不输出空格
					isOutput=true;
					printf("%d",prime[i]);
				}
				else printf(" %d",prime[i]); //否则在输出这个数字前输出一个空格
			}
		}
		if (isOutput == false) {
    
     //若始终不存在符合条件的数字
			printf("-1\n"); //输出-1并换行
		}
		else printf("\n"); //换行
	}
	return 0;
}

读者可能注意到,筛法中我们使用了一个小技巧。当我们判定i为素数,要标记其所有倍数为非素数时,我们并没有从2 * i开始标记,而是直接从i * i开始标记。其原因是显然的,i * k (k<i) 必已经在求得k的某个素因数(必小于i) 时被标记过了,即i * k同时也是k的素因数的倍数。所以这里,我们可以直接从i的平方开始标记起。尽可能的避免重复工作也是程序优化的一大思路。
本例中,我们依旧使用了,一个bool变量表示是否已经输出了符合条件的数字。这样的设置有两个目的,其一保证了除了第一个输出的数字外,其它数字输出时均在其前附加一个空格,以达到题目要求的输出数字之间存在空格而最后一个数字后没有空格的要求。其二,作为判断依据,使在不存在任何符合条件的数可以输出时,按题目要求输出-1。

分解素因数

Problem 1
在这里插入图片描述
在这里插入图片描述
本例题意清晰,即对输入的某个整数分解素因数,并计算出每个素因数所对应的幂指数,即对给定整数x,确定下式中
在这里插入图片描述
p1、p2. …pn 与e1e2…en的取值。这样,我们即能得到最后的结果为e1e2…en的和。
首先我们按照如下思路来为整数分解素因数:我们利用上节内容中所讲的素数筛法预先筛选出所有可能在题面所给定的数据范围内成为素因数的素数。并在程序输入待处理数字n时,依次遍历所有小于n的素数,判断其是否为n的因数。若确定某素数为n的因数,则通过试除确定其对应的幂指数。最后求出各个幂指数的和即为所求。

#include <stdio.h>
bool mark[100001];
int prime[100001];
int primeSize;
void init(){
    
    
	primeSize=0;
	for (int i=2;i <= 100000;i++) {
    
    
		if (mark[i]==true) continue;
		prime[primeSize++]=i;
		if (i >=1000) continue;
		for(int j=i*i;j<=100000;j+=i){
    
    
			mark[j]=true;
		}
	}
} //以上与上例一致,用素数筛法筛选出2到100000内的所有素数
int main(){
    
    
	init(); 
	int n;
	while (scanf("%d",&n)!=EOF){
    
    
		int ansPrime[30]; //按顺序保存分解出的素因数
		int ansSize=0; //分解出素因数的个数
		int ansNum[30]; //保存分解出的素因数对应的幂指数
		for (int i=0;i < primeSize;i++) {
    
     //依次测试每一个素数
			if (n % prime[i]==0) {
    
     //若该素数能整除被分解数
				ansPrime[ansSize]=prime[i]; //则该素数为其素因数
				ansNum[ansSize]= 0; //初始化幂指数为0
				while(n % prime[i]==0) {
    
     //从被测试数中将该素数分解出来,并统计其幂指数
					ansNum[ansSize]++; 
					n/=prime[i];
				}
				ansSize++; //素因数个数增加
				if (n==1) break; //若已被分解成1,则分解提前终止
			}
		}
		if (n!=1) {
    
     //若测试完2到100000内所有素因数,n仍未被分解至1,则剩余的因数定是n一个大于100000的素因数
			ansPrime[ansSize]=n; //记录该大素因数
			ansNum[ansSize++]=1; //其幂指数只能为1
		}
		int ans=0;
		for (int i=0;i < ansSize;i++) {
    
    
			ans+=ansNum[i]; //统计各个素因数的幂指数
		}
		printf("%d\n",ans); 
	}
	return 0;
}

我们首先说明为什么素数筛法只需筛到100000即可,而不是与输入数据同规模的1000000000。这样处理的理论依据是:n至多只存在一个大于sqrt (n)的素因数(否则两个大于sqnt (n)的数相乘即大于n)。这样,我们只需将n所有小于sqrt (n)的素数从n中除去,剩余的部分必为该大素因数。正是由于这样的原因,我们不必依次测试sqrt (n)到n的素数,而是在处理完小于sqrt (n)的素因数时,就能确定是否存在该大素因数,若存在其幂指数也必为1。
在完成查找素因数的工作以后, 我们只需简单的把所有素因数对应的幂指数相加,即可得到该整数素因数的个数。顺便一提的是,当我们完成素因数分解后我们同样可以确定被分解整数因数的个数为(e1+1) * (e2+1)…* (en+1)(由所有的素因数不同组合数得出)。

Problem 2
在这里插入图片描述
要解决该例我们首先应注意这样一个问题,n!和a的k次可能数值非常巨大,而不能被int (甚至long long)保存,也就不能直接用求余数操作判断它们是否存在整除关系。那么,我们不得不从整除的特征入手,转而思考若整数a能整除整数b则它们之间有什么关系?我们不妨对a和b分解素因数:
在这里插入图片描述
则,式b除以a能表示成:
在这里插入图片描述
若a存在素因数px则b也必存在该素因数,且该素因数在b中对应的幂指
数必不小于在a中的幂指数。
现我们设x=n!,y=ak,我们对n!与a分解素因数,令:
在这里插入图片描述
相应的我们也可以得到a的k次的素因数分解情况为:
在这里插入图片描述
即我们要确定最大的非负整数k,使a中任一素因数的幂指数的k倍依旧小于或等于该素因数在x中对应的幂指数。要求得该k,我们只需依次测试a中每一个素因数,确定b中该素因数对应的幂指数是a中幂指数的几倍(利用整数除法),这样所有倍数中最小的那个即为我们要求的k。
由于n!数值非常巨大(当n>30时),类似a一样对其分解质因数几乎是不可能的,那么我们该如何对其分解素因数呢?试着考虑n!中含有素因数p的个数,即确定素因数p对应的幂指数。我们易知,n!中包含了1到n区间内所有整数的乘积,这些乘积中每一个p的倍数(包括其本身)都将对n!贡献至少一个p因子,且我们知道在1到n中p的倍数共有n/p(整数除法)个,则p的因子数至少为n/p个,即有n/p个整数至少贡献了一个p因子。那么有多少个整数将贡献至少两个p因子呢,有了以上的分析读者应该知道所有p*p的倍数将为n!贡献至少2个p因子,且这样的整数有n/(p * p);同理p * p * p的倍数将贡献至少3个,这样的数有n/(p * p * p);且看如下过程:

  1. 计算器清零,该计数器表示n!中将有几个p因子,即n!分解质因数后素因子p对应的幂指数。
  2. 计算n/p,有n/p个整数可以向n!提供一个p因子,则计数器累加n/p。若n/p为0,表示没有一个整数能向n!提供一个或一个以上的p因子,分解结束。
  3. 计算n/ (p * p),有n/ (p * p)个整数可以向n!提供两个p因子,但它们在之前步骤中(p的倍数必包括p * p的倍数)每个数都已经向计数器累加了1个p因子,所以此处他们还能够向计数器贡献n/ (p * p) 个素因子(即每个再贡献一个),累加器累加n/ (p * p)。若n/ (p * p) 为0,表示没有一个整数能向n!提供两个或两个以上的p因子,分解结束。
  4. 计算n/ (p * p * p),有n/(p * p * p)个整数可以向n!提供三个p因子,但它们在之前步骤中(p和p * p的倍数必包括(p * p * p)的倍数)每个数都已经计算向计数器累加了2个p因子,所以此处他们还能够向计数器贡献n/ (p * p * p) 个素因子,累加器累加n/ (p * p * p)。若n/ (p * p * p) 为0,表示没有一个整数能向n!提供三个或三个以上的p因子,分解结束。依次累加p的更高次的倍数能够再提供的素因子数,即每次向计数器累加n/(pk),直到n/(pk)变为0,表示没有整数能提供更多的p因子,关于p的分解结束。
    完成这些步骤后,就能计算出n!中所有p的因子数,即计数器中累加的结果即为素因数p的幂指数。

有了对n!分解素因数的方法,我们只需依次遍历可能成为其素因子(小于等于n的所有素数)的素数,计算它们所对应的幂指数,即可完成对n!的素因数分解。

#include <stdio.h>
#include <string.h>
bool mark[1010];
int prime[1010];
int primeSize;
void init(){
    
    
	primeSize=0;
	for (int i=2;i<=1000;i++) {
    
    
		if (mark[i]) continue;
		mark[i]=true;
		prime[primeSize++]=i;
		for(int j=i*i;j<=1000;j+=i){
    
    
			mark[j]=true;
		}
	}
} //筛选出0到1000范围内的所有素数
int cnt[1010]; //cnt[i]用来表示,prime[i]所保存的素数在n!中的因子数,即n!分解素因数后,素因子prime[i]所对应的幂指数,可能为0
int cnt2[1010]; //cnt2[i]用来表示,prime[i]所保存的素数在a中的因子数
int main(){
    
    
	init();
	while (scanf("%d%d",&n,&a) == 2) {
    
    
		for (int i=0;i < primeSize;i++)
			cnt[i]=cnt2[i]=0; //将两个计数器清零,为新的一次分解做准备
		for (int i=0;i < primeSize;i++) {
    
     //对n!分解素因数,遍历每一个0到1000的素数
			int t=n; //用临时变量t保存n的值
			while(t) {
    
     //确定素数prime[i]在n中的因子数
				cnt[i]+=t/prime[i];
				t=t/prime[i];
			}
		} //依次计算t/prime[i]^k,累加其值,直到t/prime[i]^k变为0
		int ans=123123123; //答案初始值为一个大整数,为取最小值做准备
		for (int i = 0;i < primeSize;i++) {
    
     //对a分解素因数
			while (a % prime[i]==0) {
    
    
				cnt2[i]++;
				a/=prime[i];
			} //计算a中素因数prime[i]对应的幂指数
			if(cnt2[i]==0) continue; //若该素数不能从a中分解到,即其对应幂指数为0,则其不影响整除性,跳过
			if (cnt[i]/cnt2[i] < ans) //计算素数prime[i]在两个数中因子数的商
				ans=cnt[i]/cnt2[i]; //统计这些商的最小值
		}
		printf("%d\n",ans); //该商即为所求
	}
	return 0;
}

二分求幂

再来讨论一个非常实用的小技巧,二分求幂,即怎样快速的求得a的b次方。
在这里插入图片描述
我们的目标即分解a的b次变为若干个a的2k的积,并尽可能减少分解结果的个数。在指数层面即分解b为若干个2k的和,并尽可能减少分解结果的个数。二分求幂对要求的次数并没有特殊的要求,而是对任何要求的次数都可以采用二分求幂来大大减少其乘法运算的次数。
该注意到A^B的后三位数只与A的后三位数和B有关。这样,由于要求的仅是最后结果的后三位数,那么我们在保存为计算该最终值的中间值时也只保存其后三位数即可。即计算过程中的所有中间结果我们仅保存和使用其后三位数。

#include <stdio.h>
int main() {
    
    
	int a,b;
	while (scanf("%d%d",&a,&b) !=EOF) {
    
    
		if(a==0&&b==0) break;
		int ans=1; //保存最终结果变量,初始值为1
		while(b != 0) {
    
     //若b不为0,即对b转换二进制过程未结束
			if(b%2==1) {
    
     //若当前二进制位为1,则需要累乘a的2^k次至变量ans,其中2^k次为当前二进制位的权重
				ans *=a; //最终结果累乘a
				ans %= 1000; //求其后三位数
			}
			b/=2;//b除以2
			a *=a; //求下一位二进制位的权重,a求其平方,即从a的1次开始,依次求的a的2次,a的4次
			a %=1000; //求a的后三位
		} //一边计算b的二进制值,一边计算a的2^k次,并将需要的部分累乘到变量ans上
		printf("%d\n",ans);
	}
	return 0;
}

为了使读者更好的理解该代码的原理,我们模拟该代码计算2的31次的值,并给出相应原理。在这里插入图片描述在这里插入图片描述
即我们从b的最低位开始依次求得b的各二进制位,在当前二进制位为1的条件下将a累乘到变量ans上,在完成本位的操作后对a求其平方计算下一位二进制位的权重,直到完成对b的二进制转换。

高精度整数

对于广大使用C/C++的读者,首先明确高精度整数的保存形式,常用如下结构体来保存一个高精度整数:

struct bigInteger {
    
    
int digit[1000];
int size;
};

Problem 1
在这里插入图片描述

#include <stdio.h>
#include <string.h>
struct bigInteger {
    
     //高精度整数结构体
	int digit[1000]; //按四位数一个单位保存数值
	int size; //下一个我们未使用的数组单元
	void init(){
    
     //对结构体的初始化
		for (int i=0;i<1000;i++) digit[i]=0; //所有 数位清0
		size=0;//下一个未使用数组单元为0,即没有一个单元被使用
	}
	void set(char str[]) {
    
     //从字符串中提取整数
		init(); //对结构体初始化
		int L=strlen(str); //计算字符串长度
		for (int i=L-1,j=0,t=0,c=1;i>=0;i--) {
    
     //从最后一个字符开始倒序遍历字符串,j控制每4个字符转换为一个数字存入数组,t临时保存字符转换为数字的中间值,c表示当前位的权重,按1, 10, 100, 1000顺序变化
			t+=(str[i]-'0')*c; //计算这个四位数中当前字符代表的数字,即数字乘以当前位权重
			j++; //当前转换字符数增加
			c*=10; //计算下一位权重
			if (j==4||i==0) {
    
     //若已经连续转换四个字符,或者已经到达最后一个字符
				digit[size++]=t; //将这四个字符代表的四位数存入数组,si ze移动到下一个数组单位
				j=0;//重新开始计算下4个字符
				t=0;//临时变量清0
				c=1; //权重变为1
			}
		}
	}
	void output() {
    
     //将该高精度整数输出
		for(int i=size-1;i>=0;i--){
    
    
			if (i !=size - 1) printf("%04d",digit[i]); //若当前输出的数字不是最高位数字,用%04的输出前导0,即当前数字不足4位时由0补充,如输出110001的后四位数
			else printf("%d", digit[i]); //若是最高位,则无需输出前导零
		}
		printf("\n"); //换行
	}
	bigInteger operator + (const bigInteger &A) const {
    
     //加法运算符
		bigInteger ret; //返回值, 即两数相加的结果
		ret.init(); //对其 初始化
		int carry=0; //进位,初试值为0
		for(int i=0;i<A.size||i<size;i++){
    
    
			int tmp=A.digit[i] + digit[i] + carry; //计算两个整数当前位以及来自低位的进位和
			carry=tmp/10000; //计算该位 的进位
			tmp %= 10000; //去 除进位部分,取后四位
			ret.digit[ret.size++]=tmp; //保存 该位结果
		}
		if (carry != 0) {
    
     //计算结束后若最高位有进位
			ret.digit[ret.size++] =carry; //保存该进位
		}
		return ret; //返回
	}
}a,b,c;
char str1[1002],str2[1002];
int main() {
    
    
	while (scanf("%s%s",str1,str2)!=EOF) {
    
    
		a.set(str1);b.set(str2); //用两个字符串分别设置两个高精度整数
		c=a+b;//计算它们的和
		c.output();//输出
	}
	return 0;
}

实现高精度加法,即用代码模拟加法的运算法则,按照从低位开始各对应位相加并加上来自低位的进位从而获得本位的数值以及进位的规则进行运算。思路相对简单。读者只需牢记加法运算规则便能写出实现的代码。本例中采用了重载+运算符的方法来实现该加法运算,若读者对其不熟悉也可以使用定义一个参数为两个加数返回值为它们和的求和函数,它们的运算原理完全相同。

Problem 2
在这里插入图片描述
在这里插入图片描述

#include <stdio.h>
#include <string.h>
struct bigInteger {
    
    
	int digit[1000];
	int size;
	void init(){
    
     //初始化
		for (int i=0;i<1000;i ++) digit[i]=0;
		size=0;
	}
	void set (int x) {
    
     //用一个小整数设置高精度整数
		init();
		do {
    
     //对小整数4位为一个单位分解依次存入di gi t当中
			digit[size++]=x%10000; 
			x /= 10000;
		}while (x!=0);
	}
	void output(){
    
     //输出
		for(int i=size-1;i>=0;i--){
    
    
			if (i!=size-1) printf("%04d" ,digit[i]);
			else printf("%d", digit[i]);
		}
		printf("\n");
	}
	bigInteger operator *(int x) const {
    
     //乘法运算符
		bigInteger ret; //将要 返回的高精度整数
		ret.init();
		//初始化
		int carry=0; //进位初始值为0
		for(int i=0;i<size;i++){
    
    
			int tmp=x*digit[i]+carry; //用小整数x乘以当前位数字并加上来自低位的进位
			carry=tmp/10000; //计算进位
			tmp %= 10000; //去除进位部分
			ret.digit[ret.size++]=tmp; //保存该位数字
		}
		if (carry!=0) {
    
     //若最高位有进位
			ret.digit[ret.size ++]=carry; //保存该进位
		}
		return ret; //返回结果
	}
}a;
int main() {
    
    
	int n;
	while (scanf("%d",&n)!=EOF) {
    
    
		a.init(); //初始化a
		a.set(1); //a初始值为1
		for(int i=1;i<=n;i++){
    
    
			a=a*i; //依次乘上每一个整数
		}
		a.output(); //输出a
	}
	return 0;
}

Problem 3
在前文中曾多次出现这样的问题:有一些整数可能数值非常巨大以致于我们不能使用任何内置整数类型来保存它的值,在前面的例子中我们总是利用各种技巧回避了直接对其保存。但在有些问题中,我们不得不保存并处理这些数值巨大的整数。

在这里插入图片描述
我们的高精度整数需要进行以下运算:高精度整数与普通整数的求积,高精度整数之间求和,高精度整数除以普通整数,高精度整数对普通整数求模等。下面给出本例代码,作为本章最后的总结内容。

#include <stdio.h>
#include <string.h>
#define maxDigits 100
struct bigInteger {
    
    
	int digit[1000];
	int size;
	void init(){
    
     //初始化
		for (int i=0;i<1000;i ++) digit[i]=0;
		size=0;
	}
	void set (int x) {
    
     //用一个小整数设置高精度整数
		init();
		do {
    
     //对小整数4位为一个单位分解依次存入digit当中
			digit[size++]=x%10000; 
			x /= 10000;
		}while (x!=0);
	}
	void output(){
    
     //输出
		for(int i=size-1;i>=0;i--){
    
    
			if (i!=size-1) printf("%04d" ,digit[i]);
			else printf("%d", digit[i]);
		}
		printf("\n");
	}
	bigInteger operator *(int x) const {
    
     //乘法运算符
		bigInteger ret; //将要返回的高精度整数
		ret.init();
		//初始化
		int carry=0; //进位初始值为0
		for(int i=0;i<size;i++){
    
    
			int tmp=x*digit[i]+carry; //用小整数x乘以当前位数字并加上来自低位的进位
			carry=tmp/10000; //计算进位
			tmp %= 10000; //去除进位部分
			ret.digit[ret.size++]=tmp; //保存该位数字
		}
		if (carry!=0) {
    
     //若最高位有进位
			ret.digit[ret.size++]=carry; //保存该进位
		}
		return ret; //返回结果
	}
	bigInteger operator + (const bigInteger &A) const {
    
     //加法运算符
		bigInteger ret; //返回值,即两数相加的结果
		ret.init(); //对其初始化
		int carry=0; //进位,初试值为0
		for(int i=0;i<A.size||i<size;i++){
    
    
			int tmp=A.digit[i] + digit[i] + carry; //计算两个整数当前位以及来自低位的进位和
			carry=tmp/10000; //计算该位 的进位
			tmp %= 10000; //去除进位部分,取后四位
			ret.digit[ret.size++]=tmp; //保存 该位结果
		}
		if (carry != 0) {
    
     //计算结束后若最高位有进位
			ret.digit[ret.size++] =carry; //保存该进位
		}
		return ret; //返回
	}
	bigInteger operator / (int x) const {
    
     //高精度整数除以普通整数
		bigInteger ret; //返回的高精度整数
		ret.init(); //返回值初始化
		int remainder=0; //余数
		for(int i=size-1;i>=0;i--){
    
    //从最高位至最低位依次完成计算
			int t=(remainder*10000+digit[i])/x; //计算当前位数值加上高位剩余的余数的和对x求得的商
			int r=(remainder*10000+digit[i])%x; //计算当前位数值加上高位剩余的余数的和对x求模后得的余数
			ret.digit[i]=t; //保存本位的值
			remainder=r; //保存至本位为止的余数
		}
		ret.size=0; //返回高精度整数的size初始值为0,即当所有位数字都为0时,digit[0]代表数字0,作为最高有效位,高精度整数即为数字0
		for (int i=0;i≤maxDigits;i++) {
    
    
			if (digit[i]!=0) ret.size=i;
		} //若存在非0位,确定最高的非0位,作为最高有效位
		ret.size++; //最高有效位的下一位即为下一个我们不曾使用的digit数组单元,确定为size的值
		return ret; //返回
	}
}a,b,c;
char str[10000];
char ans[10000];
int main() {
    
    
	int n,m;
	while (scanf("%d%d",&m, &n)!=EOF) {
    
    
		scanf("%s",str); //输入m进制数
		int L=strlen(str);
		a.set(0); //a初始值为0,用来保存转换成10进制的m进制数
		b.set(1); //b初始值为1,在m进制向10进制转换的过程中,依次代表每一位的权重
		for (int i=L-1;i >= 0;i--) {
    
     //由低位至高位转换m进制数至相应的10进制数
			int t;
			if (str[i]>'0'&&str[i] <= '9') {
    
    
				t=str[i]-'0';
			}
			else t=str[i]-'A'+10; //确定当前位字符代表的数字
			a=a+b*t;//累加当前数字乘当前位权重的积
			b=b*m; //计算下一位权重
		}
		int size=0; //代表转换为n进制后的字符个数
		do {
    
     //对转换后的10进制数求其n进制值
			int t=a%n; //求余数
			if (t>=10) ans[size++]=t-10+'a';
			else ans[size++]=t+'0'; //确定当前位字符
			a=a/n; //求商
		}while(a.digit[0]!=0||a.size!=1); //当a不为0时重复该过程
		for (int i=size-1;i>0;i--) printf("%c",ans[i]);
		printf("\n"); 
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_44029550/article/details/105416655