【算法与数据结构】—— 大数运算

大数运算

定义
由于编程语言提供的基本数值数据类型表示的数值范围有限,不能满足较大规模的高精度数值计算,因此需要利用其他方法实现高精度数值的计算,于是产生了大数运算。大数运算主要有加、减、乘三种方法。

简介
大数运算,顾名思义,就是很大的数值的数进行一系列的运算。
我们知道,在数学中,数值的大小是没有上限的,但是在计算机中,由于字长的限制,计算机所能表示的范围是有限的,当我们对比较小的数进行运算时,如:1234+5678,这样的数值并没有超出计算机的表示范围,所以可以运算。但是当我们在实际的应用中进行大量的数据处理时,会发现参与运算的数往往超过计算机的基本数据类型的表示范围,比如说,在天文学上,如果一个星球距离我们为100万光年,那么我们将其化简为公里,或者是米的时候,我们会发现这是一个很大的数。这样计算机将无法对其进行直接计算。
可能我们认为实际应用中的大数也不过就是几百位而已,实际上,在某些领域里,甚至可能出现几百万位的数据进行运算,这是我们很难想象的。如果没有计算机,那么计算效率可想而知。
既然在计算机中无法直接表示,那么大数到底如何进行运算呢,学习过数据结构的都知道线性表,将大数拆分然后存储在线性表中,不失为一个很好的办法。
在算法竞赛中经常遇到的主要是乘法和除法运算两类,其中除法运算可以通过递归或者循环结合着栈来进行求解,而大数乘法则主要是通过将大数拆分并存储进线性表中来进行求解。下面将给出两个例子来讨论分析如何进行大数乘法。



—— 分割线 ——



【蓝桥杯】 算法提高 P1001

问题描述
当两个比较大的整数相乘时,可能会出现数据溢出的情形。为避免溢出,可以采用字符串的方法来实现两个大数之间的乘法。具体来说,首先以字符串的形式输入两个整数,每个整数的长度不会超过15位,然后把它们相乘的结果存储在另一个字符串当中(长度不会超过30位),最后把这个字符串打印出来。例如,假设用户输入为:62773417和12345678,则输出结果为:774980393241726.

输入样例
62773417 12345678

输出样例
774980393241726

资源限制
时间限制:1.0s 内存限制:256.0MB


算法分析:
可以模拟两个多位数进行乘法运算时的过程,如下:
【算法与数据结构】—— 大数运算_第1张图片
不难发现当两个数进行乘法运算时,其规则是从两个数的最低位中选定一个置于下方(现实中为了计算便捷我们往往选择较短的那个,但对程序而已可以随意选定) ,然后将这个数的最低位依次与其上方的另一个数的所有位进行乘法运算并将结果置于横线下方,同时该结果的最低位要与选定进行乘法运算的数的当前位一致。接下来遍历这个数的所有位,并执行相同的操作。最终,将得到的所有乘法运算结果叠加即可得到最终的乘法结果。需要注意的是,在将这些乘法结果进行叠加时,要注意进位。
根据上面的运算过程,我们可以用一个双重for循环来遍历两个数字进而进行乘法运算。
在设计代码时,需要注意三点:

  1. 输入数据是字符串,在将字符串中某个数字字符转换为数字时可以用 c - ‘0’ 来得到;
  2. 字符串转换为数字数组时,为了方便后续进行乘法运算,应该要将其逆序,否则每次乘法运算导致进位时都需要把数组中的所有数据进行后移(数组索引是从0开始的,没有-1),这将加大时间开销;
  3. 得到最后的乘法结果后,要注意进位,进位算法如下:
for(i=0;i<30;i++)
	{
		ans[i+1]+=ans[i]/10;
		ans[i]%=10;
  }

下面给出本题的完整代码:

#include
#include
using namespace std;

int p[9];							//用于放置基本的9个数字
int cnt[10000005];					//打表用到的数组

int main()
{
    for(int i=0;i<9;i++)
        p[i]=i+1;
    int a,b,c,ans;
    do{
        for(int i=0;i<=6;i++)
            for(int j=i+1;j<=7;j++)
            {
            	a=b=c=ans=0;
                for(int k=0;k<=i;k++)
                {
                    a=a*10+p[k];
                }
                for(int k=i+1;k<=j;k++)
                {
                    b=b*10+p[k];
				}
                for(int k=j+1;k<=8;k++)
                {
                    c=c*10+p[k];
                }
                if(b%c==0)
                {
                    ans=a+b/c;
                    if(ans<1000000)
                    cnt[ans]++;
                }
            }
    }while(next_permutation(p,p+9));  //这是一个求一个排序的下一个排列的函数,可以遍历全排列,要包含头文件
    int n;
    cin>>n;
    cout<<cnt[n];    
}



—— 分割线 ——



【洛谷】 产生数 P1037

问题描述
两给出一个整数n(n<1030)以及k个变换规则(k≤15)。
规则:

 1. 一位数可变换成另一个一位数。
 2. 规则的右部不能为零。

例如:n=234。有规则(k=2):
2 -> 5
3 -> 6
上面的整数 234 经过变换后可能产生出的整数为(包括原数):
234
534
264
564
共 4 种不同的产生数。现在给出一个整数 n 和 k 个规则。求出经过任意次的变换(0次或多次),能产生出多少个不同整数。仅要求输出个数。

输入格式
第一行两个整数 n, k。
接下来 k 行,每行两个整数 xi, yi

输出格式
输出能生成的数字个数。

输入样例
234 2
2 5
3 6

输出样例
4

资源限制
时间限制:1.0s 内存限制:125.0MB


算法分析
这道题实际上是求在特定变换规则下,一个指定数能有多少种变换。而该变换规则实际上就是规定一个数字能变换为哪些目标数字(0除外)。就像上面的例子,由于2能变换为5(加上它自己本身,那么2就有两种变换:2->2、2->5),同理3也有两种(3->3、3->6);再看这个数字本身,其中含有1个2、1个3,那么不难算出其总的变换共有2╳2=4种。
所以,本题的总体求解思路如下:

  1. 根据输入的k个变换规则,得到每个数能有多少种变换。比如对于变换:1->2、1->3、2->4、2->5,这时候应得到:1有四个变换规则(1->2、1->3、1->4、1->5),2有两个变换规则(2->4、2->5);
  2. 统计出给出的整数 n 中,每个数字各有多少个,以便于计算最终的变换个数;
  3. 本题中 n 的取值范围最大可达1030,这已经超出了long 的长度,所以必须解决大数运算的问题。

问题1:求解每个数的可变换数量。
由于数字一共有10个,因此可以用一个10╳10的二维矩阵来存放数字之间的可达关系,之所以选用邻接矩阵作为存放可达关系的数据结构是因为后续可以通过佛洛伊德算法来将这些间接的可达关系全部转换为直接可达,进而得到每个数的可变换数量(如1->2 且2->3,那么1->3)。
注:佛洛伊德算法是一个利用动态规划的思想来寻找给定的加权图中多源点之间最短路径的算法。因此当对一个带有权值的二维数组进行佛洛伊德算法后,它将得到这个二维数组所反映的图中各点的距离,也就反映了各点之间的可达关系。下面给出通过佛洛伊德算法来将规格为10╳10的二维矩阵map中所有间接可达的点变换为直接可达的代码:

void Floyd()
{  
	for (int k = 0;k < 10;k++)
		for (int i = 0;i < 10;i++)
			for (int j = 0;j < 10;j++) 
				if(map[i][k]&&map[k][j])
					map[i][j]=1;
}

问题2:如何统计所有的变换个数?
实际上统计变换个数很简单,只需要一层循环去遍历n中的每个数字,然后依次叠乘每个数字的可变换个数就能得到最终的变换总数。但是这里有个问题,考虑一种极端情况,假如每个数字之间都能相互变换(即每个数都有10种变换),那么对于一个长度为30的数字,其总的变换个数就为1030,而这个数已经超过了long,因此我们必须自己设计一个大数运算的算法,而不是直接用系统定义的数据类型进行乘法运算。
由于本题在进行叠乘时,总是一个不大于10的数字乘以之前的数,因此这里的大数相乘可以直接用当前的可变换数依次与前面的大数的每位进行乘法运算,然后再进行进位操作。算法如下:

for (i=0;i<len;i++)		// len 表示当前输入的n的长度
{
	for(j=0;j<31;j++)	// 31是设置的最长的扫描距离,大于30即可
		    ans[j] *= change[n[i]-'0'];	// ans是最终的结果数组
	for(int j=0;j<31;j++)
	{
		    if(ans[j] > 9) 
		    {
				ans[j+1] += ans[j]/10;
				ans[j] %= 10;
		    }
	}
}

下面给出本题的完整代码:

#include
#include
using namespace std;

int map[10][10];		// 用于弗洛伊德算法求每个数的可变换数量
int change[10];			// 用于统计每个数的可变换个数
int const MAXN = 10;	// 最多可变换的数字
int const MAXL = 31;	// 最终答案的最大长度(大于30即可)

void Floyd()			// 弗洛伊德算法(求解每个数的可变换数量)
{  
	for (int k = 0;k < MAXN;k++)
		for (int i = 0;i < MAXN;i++)
			for (int j = 0;j < MAXN;j++) 
				if(map[i][k]&&map[k][j]) 
					map[i][j]=1;
}

int main()
{
	char n[MAXL];		// 由于输入的数据范围超过了C提供的最长数据类型,因此这里用字符串替代
	int k;				// 变换规则总数
	while(cin>>n>>k)
	{
		int i,j=0,x,y;
		for(i=0;i<k;i++)
		{
			cin>>x>>y;
			map[x][y]=1;			// 注意这里构建的map是有向的
		} 
		Floyd();					// 求出可达矩阵
		for(i=0;i<MAXN;i++)			// 找出每个数的可变化数量,并将其存进数组change中
        {
            map[i][i]=1;			// 注意自己和自己也算是一种可达
            for(j=0;j<MAXN;j++)
                if(map[i][j])
					change[i]++;
        }
		int len=strlen(n);			// 输入的字符串的长度 
		int ans[MAXL]={0};			// 用于贮存最后的变化总数(即最终存放结果的数组)
		ans[0]=1;					// 必须将ans[0]初始化为1
		for (i=0;i<len;i++) 		// 遍历输入的 n 以统计所有的变换个数
		{
			for(j=0;j<MAXL;j++) 	// 这里的循环结束条件只能是
				ans[j] *= change[n[i]-'0']; 
			for(j=0;j<MAXL;j++)		// 因为有可能中间某个数为0,此时若以sum[j]!=0作为循环结束 
			{						// 条件则会提前结束判断从而导致错误
				if(ans[j] > 9) 
				{
					ans[j+1] += ans[j]/10;		//进位
					ans[j] %= 10;
				}
}
		}
		i=MAXL-1;					// 重新将指针置于 ans 的最末尾之前(即最高位之前)
		while(!ans[i]) i--;			// 以找到第一个非零最高位
		while(i>-1)	cout<<ans[i--];	// 依次取出并输出
		cout<<endl;
	}
	return 0;
}

END


你可能感兴趣的:(算法与数据结构,洛谷试题题解,蓝桥杯试题题解,大数运算,大数乘法,蓝桥杯P1001,洛谷产生数P1037)