大数据乘法最容易想到的就是用数组或者字符串,string来模拟,由于数字无法用一个整形变量存储,很自然的想到用字符串来表示一串数字。然后按照乘法的运算规则,用一个乘数的每一位乘以另一个乘数,然后将所有中间结果按正确位置相加得到最终结果。可以分析得出如果乘数为A和B,A的位数为m,B的位数为n,则乘积结果为m+n-1位(最高位无进位)或m+n位(最高位有进位)。因此可以分配一个m+n的辅存来存储最终结果。为了节约空间,所有的中间结果直接在m+n的辅存上进行累加。最后为了更符合我们的乘法运算逻辑,可以讲数字逆序存储,这样数字的低位就在数组的低下标位置,进行累加时确定下标位置较容易些。
举个直观的例子:
整个乘法过程就可以大致分成两部分:
第一部分对应位相乘再加上乘法进位MulFlag,结果是2位数(最大是9*9=81,则最加起来的最大值只有81+9),2位数中十位则是下一轮循环的乘法进位MulFlag,个位数是当前的一个临时中间结果,例如上图的6*4,当前MulFlag=0,则有24+0,十位的2是下一轮的MulFlag,4是当前中间结果。同样地,4*4+MulFlag(2)=18,十位的1是新的MulFlag,而8是当前的临时中间结果。
第二部分当前对应位结果+第一步的临时中间结果+AddFlag(加法进位),个位数为新的当前对应位结果,十位数为新的AddFlag.
例如:当前对应位(个位)结果是0,第一步的临时中间结果是4,AddFlag=0,则结果是4,个位数即为当前新的对应位结果,十位数为新的AddFlag=0。
具体的思路从代码中体会:
#include<iostream> #include<string> using namespace std; void Reverse(string& str,int len) { for(int i=0,j=len-1;i<j;++i,--j) swap(str[i],str[j]); } string MulLarge(string str1,string str2) { string strResult(str1.length()+str2.length(),'0'); // 构造一个str1.length+str2.length的字符串 int len1 = str1.length(); int len2 = str2.length(); Reverse(str1,len1); Reverse(str2,len2); for( int i=0 ; i<len1 ;++i ) { int MulFlag = 0 ; int AddFlag = 0 ; for( int j=0 ; j<len2 ; ++j ) { int MulResult = (str1[i]-'0') * (str2[j]-'0') + MulFlag ; //乘积的结果,是一个两位数 MulFlag = MulResult / 10 ; //MulResult的十位数即为乘法进位 MulResult = MulResult % 10 ; //着眼于个位数的运算(做加法) int AddResult = (strResult[i+j]-'0') + MulResult + AddFlag ; //AddFlag进位是前一次迭代进位上来的。 strResult[i+j] = AddResult % 10 + '0'; //个位数即为当前结果 AddFlag = AddResult / 10 ; //加法进位,为1或为0 } strResult[i+len2] += MulFlag + AddFlag ; //注意是+=,不是=,最高位是乘法进位和加法进位加起来的结果 } Reverse(strResult,len1+len2); return strResult; } void main() { string str1="352"; string str2="120"; cout<<MulLarge(str1,str2)<<endl; }另外一种更好理解的代码:基于下图的方式。(输出的时候应该忽略开始的0,这里不处理这个细节问题)
#include<iostream> #include<string> using namespace std; void reserve(string& str,int len) { for(int i=0,j=len-1;i<j;++i,--j) swap(str[i],str[j]); } string Mul(string str1,string str2) { int len1=str1.length(); int len2=str2.length(); reserve(str1,len1); //反转字符串,符合人类习惯 reserve(str2,len2); string strResult(len1+len2,'0');//实例化一个len1+len2长度的字符串 for(int i=0; i<len1; ++i) { for( int j=0 ; j<len2 ; ++j ) { strResult[i+j] += (str1[i]-'0')*(str2[j]-'0') ; // 累加乘积的和,最后再求进位 } } for( int j=0 ; j<len1+len2 ; ++j ) { strResult[j+1] += (strResult[j]-'0') / 10 ; //将十位上的数向前进位,并加上原来这个位上的数 strResult[j] = (strResult[j]-'0') % 10 +'0' ;//将剩余的数存原来的位置上 } reserve(strResult,len1+len2); return strResult; } void main() { string str1="16",str2="16"; cout<<Mul(str1,str2); }
http://acm.hdu.edu.cn/showproblem.php?pid=1042
思路:模拟手算,一位一位往上进位.(一次AC)
#include<stdio.h> #include<string.h> int A[70000],numLen=1; void Factorial(int N) { int i=0, index=0, carry=0; memset(A,0,sizeof(A)); A[0]=1; for(i=1;i<=N;++i)//从 1 到 N { carry=0; for(index=0;index<numLen;++index)//模拟手算,一位一位地向前进位 { carry=A[index]*i+carry; A[index]=carry % 10 ; carry = carry / 10 ; } while(carry)//如果前边的进位>1,继续向前进位 { A[index++]=carry % 10 ; carry = carry / 10 ; } if( index > numLen )//更新数据长度 numLen = index ; } } int main() { int n; while(scanf("%d",&n)!=EOF) { numLen=1; Factorial(n); for(int i=numLen-1;i>=0;--i) printf("%d",A[i]); printf("\n"); } return 0; }
思路可以模拟手算,思路简单,但是,细节较多。可以有两种方向,一种是将相应位累加起来存放在result数组中,最后再统一进位,这种思路比较直观简便,同时需要注意中途细节不要出错,第二种方式可以是相应位相加再加上加法进位addflag.同样需要注意细节问题。
#include<iostream> using namespace std; void reserve(char* str) { int len=strlen(str); for(int i=0,j=len-1;i<j;++i,--j) swap(str[i],str[j]); } char* add1(char* str1,char* str2) { reserve(str1) ; reserve(str2) ; int len1=strlen(str1) ; int len2=strlen(str2) ; char* result=new char[max(len1,len2)+1] ; result[max(len1,len2)+1]='\0' ; int addflag=0 ; int i=0 ; //case1:在共公范围内 for( ; i<len1 && i<len2 ; ++i) { result[i] = ((str1[i]-'0')+(str2[i]-'0') + addflag) % 10 + '0' ; addflag = ((str1[i]-'0')+(str2[i]-'0')) / 10 ; } //case2:不在公共范围内 ,3个for可以用一个for合并在一起,中间加上if控制语句 for( ; i<len1 ;++i) { result[i] = (str1[i]-'0' + addflag) % 10 + '0' ; addflag = (str1[i]-'0') / 10 ; } for( ; i<len2 ;++i) { result[i] = (str2[i]-'0' + addflag) % 10 + '0' ; addflag = (str2[i]-'0') / 10 ; } result[i]='0' + addflag; reserve(result); return result[0] == '0' ? result+1 :result ; } char* add2(char* str1,char* str2) { reserve(str1) ; reserve(str2) ; int len1=strlen(str1) ; int len2=strlen(str2) ; char* result=new char[max(len1,len2)+1] ; memset( result,'0',sizeof(char)*(max(len1,len2)+1) ) ; result[max(len1,len2)+1]='\0' ; for(int i=0 ; i<len1 || i<len2 ; ++i) { if(i< len1 && i<len2) //如果在公共范围内,求和并保存在result中 result[i] = ((str1[i]-'0')+(str2[i]-'0')) ; //先将整型数存储在result中,下一步计算进位再变成字符型 else if(i < len1) //如果不在公共范围内,将长的一段保存在result中 result[i] = (str1[i]-'0'); else result[i] = (str2[i]-'0'); } //统一处理进位问题 for(int i=0 ; i<len1 || i<len2 ;++i)//i<len1 || i<len2 等价于 i<max(len1,len2) { result[i+1] += result[i] / 10 ; result[i] = result[i] % 10 +'0'; } reserve(result); return result[0] == '0' ? result+1 :result ; } void main() { char str1[]="139" ; char str2[]="961" ; cout<<add1(str1,str2)<<endl; //cout<<add2(str1,str2)<<endl; }
#include <iostream> #include <string> using namespace std; string Add(string fNum,string sNum) { if( fNum.length() < sNum.length() ) fNum.swap(sNum); string A = "0"; A += fNum; for ( int i=1;i<=fNum.length() && i<=sNum.length();i++) A[A.length()-i]+=sNum[sNum.length()-i]-'0'; for( int i=1;i<A.length();i++) { if(A[A.length()-i]>'9') { A[A.length()-i]-=10; A[A.length()-i-1]+=1; } } while(A[0]=='0') A.erase(0,1); return A; } string F[1001] = { "0", "1", "1" }; void setNum () { for ( int i = 3; i != 1001; ++ i ) { F[i] = Add ( F[i-1] , F[i-2] ) ; } } int main () { int T; setNum (); while ( cin >> T ) { while ( T -- ) { int N; cin >> N; cout << F[N] << endl; } } return 0; }
http://acm.hdu.edu.cn/showproblem.php?pid=1047
经过N次错误之后,终于搞定,用的是上面的模板.#include<iostream> #include<string> using namespace std; void Add(string& Dst, string& Src) { if( Dst.length() < Src.length() ) //令Dst的长度最长 Dst.swap(Src); Dst = "0" + Dst; //前边补0,防止进位 int i; //判断条件是取length最小 for ( i=1; i<=Dst.length() && i<=Src.length(); ++i) Dst[Dst.length()-i]+=Src[Src.length()-i]-'0'; for( i=1;i<Dst.length();i++) { if(Dst[Dst.length()-i]>'9') { Dst[Dst.length()-i]-=10; Dst[Dst.length()-i-1]+=1; } } while(Dst.length()!=1 && Dst[0]=='0') Dst.erase(0,1); } int main() { string dst,src; int T; cin>>T; while(T--) { dst="0"; while(cin>>src && src != "0") { Add(dst,src); } if( T != 0 ) cout<<dst<<endl<<endl; else cout<<dst<<endl; } return 0; }
:http://acm.hdu.edu.cn/showproblem.php?pid=1002
这道题要求十分简单,实现两个大数相加,需要的是仔细,耐心
先给出几个测试用例
0001 1000
0 0
000 0000
9999 1
1 9999
99900 00999
00999 99900
发现上面的代码在处理000+0000是出问题的,这个好解决,重写一个print函数即可。处理完了print函数之后,我以为OK了,提交发现Runtime Error (ACCESS_VIOLATION),网上搜了下,这个错误一般是数组出现了问题,于是看了下数组部分,果然是数组开小了,改成max(len1,len2)+2之后,以后这下可以了。再提交,发现还是挂了,这次是Presentation Error,这个就完全蒙了,搞了半天才发现,原来两个solution之间要留下一个空行,这个真是无语了。下面是AC代码,从上面的代码改进而来。
#include<stdio.h> #include<string.h> int max(int a,int b) { return a > b ? a : b; } void reserve(char* str) { int len=strlen(str); for(int i=0,j=len-1;i<j;++i,--j) { char temp=str[i];str[i]=str[j];str[j]=temp; } } char* add2(char* str1,char* str2) { reserve(str1) ; reserve(str2) ; int len1=strlen(str1) ; int len2=strlen(str2) ; char* result=new char[max(len1,len2)+1] ; memset( result,'0',sizeof(char)*(max(len1,len2)+1) ) ; result[max(len1,len2)+1]='\0' ; for(int i=0 ; i<len1 || i<len2 ; ++i) { if(i< len1 && i<len2) //如果在公共范围内,求和并保存在result中 result[i] = ((str1[i]-'0')+(str2[i]-'0')) ; //先将整型数存储在result中,下一步计算进位再变成字符型 else if(i < len1) //如果不在公共范围内,将长的一段保存在result中 result[i] = (str1[i]-'0'); else result[i] = (str2[i]-'0'); } //统一处理进位问题 for(int i=0 ; i<len1 || i<len2 ;++i)//i<len1 || i<len2 等价于 i<max(len1,len2) { result[i+1] += result[i] / 10 ; result[i] = result[i] % 10 +'0'; } reserve(result); return result[0] == '0' ? result+1 :result ; } void printans(char ans[],int anslen) { int i; for( i=0;i<anslen && ans[i] == '0';++i) ; printf("%s\n",ans+i); } int main() { char str1[1002];//="112233445566778899"; char str2[1002];//="998877665544332211"; int n; scanf("%d",&n); for(int i=1;i<=n;++i) { scanf("%s%s",str1,str2); printf("Case %d:\n%s + %s = ",i,str1,str2); char* result=add2(str1,str2); //printf("%s\n",); printans(result,strlen(result)-1); if( i != n) printf("\n"); } return 0; }
注意,上面的代码00+000=000,不符合人类的习惯,我们需要写一个print函数将0000格式化成0,这个比较好解决)
http://acm.hdu.edu.cn/showproblem.php?pid=1753
思路:题意十分简单明了,就是求两个小数之和.实际编程比较麻烦.分为几个频骤想.
1.将小数分割成小数部分和整数部分,两个小数就可以分成4个部分,然后分别求小数部分之和与整数部分之和.最好是先和小数部分,因为需要考虑进位问题.
2.小数部分的求和模拟手算,从小数点以后需要对齐,然后,模拟手算加法,进位.
3.整数部分求和参见上面的模板.然后,组合整数和小数部分即可.
#include<iostream> #include<string> using namespace std; void DecimalAdd(string& dst,string& src) { string DecDst,DecSrc; int dot; if( dst.find('.') !=string::npos) { dot = dst.find('.'); DecDst.assign(dst,dot+1,dst.size()-dot); //dst小数部分 dst.erase(dot); //dst整数部分 } if( src.find('.') != string::npos) { dot = src.find('.'); DecSrc.assign(src,dot+1,src.size()-dot); //src小数部分 src.erase(dot); //src整数部分 } //小数部分相加 if(DecDst.size() < DecSrc.size()) DecDst.swap(DecSrc); DecDst = "0"+DecDst; int i,sz=DecSrc.size(); for(i=sz-1;i>=0;--i) { DecDst[i+1] += DecSrc[i]-'0'; if( DecDst[i+1] > '9') { DecDst[i+1] -= 10; DecDst[i] +=1 ; } } if( DecDst[0]>'0' )//有进位,至多进1,因为9+9=18 { dst[dst.size()-1] +=1; } //DecDst.erase(0,1); //删除进位,无论1,0 DecDst[0]='.'; //用.覆盖高位的进位 //整数部分相加 if( dst.size() < src.size()) dst.swap(src); //dst.len >= src.size dst = "0"+dst; //前边添0 int srcsz=src.size(),dstsz=dst.size(); for( i=0; i<srcsz; ++i) dst[dstsz-1-i] += src[srcsz-1-i]-'0'; for( i=dstsz-1; i>=1; --i) { if( dst[i]>'9') { dst[i] -=10; dst[i-1] +=1; } } for( i=0; dst[i] == '0'; ++i ) ; dst.erase(0,i); //清除前端0 for( i=DecDst.size()-1; i>=0 && DecDst[i]=='0'; --i ) ; DecDst.erase(i+1);//清除后端0 if( dst.empty()) dst="0"; cout<<dst; if( DecDst != ".") cout<<DecDst; cout<<endl; } int main() { string dst,src; while(cin>>dst>>src) { DecimalAdd(dst,src); } return 0; }
测试用例:
1.1 2.9 1.1111111111 2.3444323343 1 1.1 1.00000000000003 .43 34345.34 32425345 8523400000 777.700 3435 4554 0.000 0.0000 99999 1 1.0001 2.9999 1.235262578623 2.29375824758243527200 23546756.345326547567454 .2142356754225653425346 输出 4 3.4555434454 2.1 1.43000000000003 32459690.34 8523400777.7 7989 0 100000 4 3.529020826205435272 23546756.5595622229900193425346
http://acm.hdu.edu.cn/showproblem.php?pid=1316
暴力打表,然后找出[a,b)的区间元素个数.
#include<iostream> #include<string> using namespace std; string Add(string dst,string src) { if( dst.size() < src.size()) dst.swap(src); string res="0"+dst; int i,rlen=res.size(),slen=src.size(); for(i=0;i<slen;++i) { res[rlen-1-i] += src[slen-1-i]-'0'; } for(i=1;i<rlen;++i) { if(res[rlen-i] > '9') { res[rlen-i] -= 10; //不要减9 res[rlen-i-1] += 1; } } for(i=0; i<rlen && res[i]=='0'; ++i); res.erase(0,i); return res; } string result[500]; //保存fib数,打表 void Fibinaci(int n) { result[0]="1",result[1]="2"; int i; for(i=3;i<=n;++i) { result[i-1] = Add(result[i-2],result[i-3]); } } bool isSmaller(const string& str1,const string& str2) { if( str1.size() == str2.size()) return str1 < str2 ; else return str1.size() < str2.size(); } int main() { Fibinaci(500); string a,b; int beg,end; while(cin>>a>>b && !(a=="0" && b=="0")) { for(beg=0;isSmaller(result[beg],a);++beg)// result[beg] < a ; // result[beg]>=a; for(end=beg; !isSmaller(b,result[end]);++end)// b >= result[end] ; // result[end] > b; cout<<end-beg<<endl; } return 0; } /* 1 2 3 5 8 13 21 34 55 [2 , 6] */
#include<iostream> #include<string> using namespace std; bool isLess(const string& str1,const string& str2) // 小于 { if( str1.length() == str2.length() ) //如果位数相同,则相应位比较 return str1 < str2 ; else //如果位数不同,则位数多的数更大 return str1.length() < str2.length(); } string Sub(string Minuend ,string Subtrahend) { if( Minuend == Subtrahend ) //if equal return "0"; bool isNegative=false; //默认是正数 if( isLess(Minuend , Subtrahend) ) { isNegative=true; Minuend.swap(Subtrahend); } int i; for(i=0; i<Subtrahend.size(); ++i) { if( Minuend[Minuend.size()-1-i] >= Subtrahend[Subtrahend.size()-1-i] )//==号不能省略,否则出错 { Minuend[Minuend.size()-1-i] -= (Subtrahend[Subtrahend.size()-1-i]-'0'); } else { Minuend[Minuend.size()-1-i] =Minuend[Minuend.size()-1-i] +10 - (Subtrahend[Subtrahend.size()-1-i]-'0'); //模拟手算,从高位借位 Minuend[Minuend.size()-2-i] -= 1;//不必担心越界,因为不会向Minuend的最高位借位 } } while(Minuend[0]=='0') Minuend.erase(0,1); if( isNegative ) Minuend = '-' + Minuend; return Minuend; } int main() { char Minuend[]="134",Subtrahend[]="132"; while(cin>>Minuend>>Subtrahend) cout<<Sub(Minuend,Subtrahend)<<endl; return 0; }
大数相乘不需要reserve:
#include <iostream> #include <string> using namespace std; string mul( string str1, string str2 ) { int i,j,result,addcarry = 0,mulcarry = 0; int len1 = str1.size(),len2 = str2.size(); string str(len1+len2,'0'); for(i=len1-1; i>=0; --i) { addcarry = mulcarry = 0; for(j=len2-1; j>=0; --j) { result = (str1[i]-'0')*(str2[j]-'0') + mulcarry; mulcarry = result / 10; result %= 10; result += (str[i+j+1]-'0') + addcarry; addcarry = result / 10; result = result % 10; str[i+j+1] = result + '0'; } str[i] += addcarry+mulcarry; } while(str[0] == '0') str.erase(0,1); return str; } int main() { string str1 = "99",str2 = "99"; cout<<mul(str1,str2)<<endl; return 0; }
总结:往往看起来简单的东西做起来远没有想象中那么简单,就像上面的加法与乘法一样,平时觉得理所当然,但是,实现起来的细节还是有很多值得注意的地方,这种模拟手算的方式效率其实并不高,迟一些还需要写一篇用递归的方法求大数运算的博客(续)。