高精实乃万恶之源
算法部分:
一、压位:
1.目的:
压位的目的在于优化时间、空间。
空间上,例如一个四位数1111,不压位则需要用4个变量存储每一位,而压位(比如压4位)后,就只需要用1个变量(比如int)了。
时间上,例如计算1111*1111,不压位则按位相乘共需计算4*4=16次,而压位(比如压4位)后,就只需要计算一次。
综上,优化是显然的。
2.具体实现细节:
要压位,需要满足以下条件:记压的位数为k,则存储数位的变量应能够在任何运算情况下,表示出任意一个x位的数。
For example,如果我要压4位,那么在任意情况下,存储数位的变量应能够表示出0~9999。
那么现在看一下实际情况,如果我们要用int来存储数位,那么最多只能压4位。
因为int最大是2147483647,如果要压5位,那么考虑乘法的极端情况,临时计算结果会达到10位,而int显然是没法表示2147483648~9999999999的,
而如果压4位,则表示出99999999即可,对此int则绰绰有余,所以最多压4位。
在特殊情况下,比如没有乘法,那么我们也完全可以用int压9位。
当然,也可以用long long压9位(带乘法的一般情况)。
理论上来讲,压位压的越多,那么空间和时间效率的优化越明显;
但是long long占的空间为int的两倍,而且在时间上还会带来额外的常数开销(有可能抵消压位压得更多所节省的时间),所以说如何取舍还要依实际情况而定。
二、输入及存储:
通常情况下,大整数以字符串形式读入,以数组形式存储。
在字符串中,数字从高位到低位对应字符串下标从低到高(默认如此,无法改变);
而在数组中,如何存储是可以人为改变的,我们通常采用数字从高位到低位对应数组下标从高到低,与字符串相反。
原因:如果采用数字从高位到低位对应数组下标从低到高的存储方式,考虑发生进位的情况,我们需要新建若干个更高的数位,那么就需要将原来的数组元素统统挪动一遍来腾出空间给新出现的高位,会产生较大的时间开销,而采用数字从高位到低位对应数组下标从高到低的存储方式则不会,因为直接在原先的数组元素后新建高位即可,不必挪动原来的元素。
将字符串存储的数字写入数组时,按照字符串下标从高到低,数组下标从低到高的顺序,每四个字符对应的数字写入一个数组元素(因为我压的是4位),在数字最高的几位那里特殊处理一下。
三、输出:
数组的最高位直接输出,其后的各位则需要补齐前导0。
四、加法:
1.按位相加并进位;
2.记相加后两加数中较长的一个的长度为l,则结果的长度为l或l+1(考虑极限情况,如9999+9999或10000+1),视情况赋值即可。
五、减法:
1.按位相减;
2.从低位到高位按位检查,若当前位为负,则向高一位借位(一位)。
合理性证明:
①为何不从高位到低位检查:考虑如下情况,有某数位为0而其低一位为负,当检查至此位时,此位不向高一位借位,而检查至低一位时,低一位向此位借位,导致此位变为负且无法再次修改,生成错误结果。
②为何借位时只借一位:考虑极端情况,当被减数某位为0而减数对应位为9999时,此位相减得-9999,为相减所能得出的最小值,借一位(10000)完全可满足需求。
3.记被减数长度为l,将l减至结果第l位恰好刚刚出现正值时停止,将此时的l记为结果的长度。注意:①减法的l不可以类比加法只减一次,如考虑极端情况(10000-9999=1,应该减4位);②只有当l>1时才能将其减小,因为l最小值为1,即“有的可减”,防止结果为0时将l减至0甚至更小,引发错误(下面各个需要减小l的时候也是如此)。
六、乘法:
1.按位相乘并随时进位,因为乘法的情况下,结果的每一位都要进行多次累加,且每次累加的结果都较大(位数约为压位位数的2倍),如不及时进位,则容易发生溢出。
2.记两因数长度之和为l,则结果的长度为l或l-1(考虑极限情况,如100*100或999*999),视情况赋值即可。
七、左移:
如果不知道位运算是什么,请出门左转自行学习(下面右移也是如此)。
写左移的目的在于利用此来实现除法,因为除法的逐位相减写法我不会(果然还是太蒻了555),所以只好曲线救国,选择倍增写法来实现除法(下面右移也是如此)。
在这里,我的写法是将左移x位视为执行x次左移一位来处理(因为我怕左移位数太多的时候,一次性处理容易溢出)。
对于每一次左移一位,对每一位左移一位后进位,并同时更新结果位数。
八、右移:
右移的情况下,对于每一位,其右移一位和借位必须同时处理,不能像左移一样使用两次循环来分开处理。
原因:如果分开处理,那么对于存储的数值为奇数的某位,其右移之后产生的余数1将无处安放。
具体实现起来,可以从高位到低位处理,也可以从低位到高位,两种写法有所差异,我采用的是从高位到低位的写法。实现细节如下。
从高位到低位:对于某一位,将其右移一位,若其原先为奇数,则低一位加上10000。
从低位到高位:对于某一位,将其右移一位,若其原先为奇数,则低一位加上5000。
二者产生差别的原因在于后效性。前者是因为借位在低一位进行右移之前,而后者则恰恰相反。
后者的正确性证明(为什么不会溢出):处理某一位时,低一位已经右移,至多剩下4999,若发生借位,则第一位至多为9999,符合要求。
每一次右移的同时更新结果位数。
九、除法:
高精的重头戏。
上面说过,我只会倍增写法,那就讲一讲倍增写法。
如果不知道倍增是什么,请出门左转自行学习。建议学习过快速幂、ST表等经典倍增算法后再阅读此节内容。
实现细节:
1.将除数不断*2并记录除数相对于原来的倍数,直至除数刚刚大于被除数。
2.将除数不断/2直至除数变回原来的大小,在此过程中,每当除数恰好小于等于被除数时,将被除数减去除数,并将此时除数对应的倍数累加入答案。此过程完毕后,答案即为所求。
原理:将结果转化为二进制数,那么上面的操作就是在求出二进制数的每一位(联想一下十进制数转二进制数,有助于理解此节原理),将各个二进制位乘以权值累加即得答案。
被除数减除数时,对于每个倍数下的除数,至多减一次。原因:二进制数每一位至多是1。
十、取模:
基本与除法相同,唯一的不同之处是,最后返回结果不返回累加的答案,而返回所有操作完毕后剩余的被除数(即余数)。
语法部分:
首先强调一个灰常重要的问题:要分清“结构体”和“结构体变量”!!!!!
一、成员函数:
成员函数即某个结构体独有的函数。我们可将普通函数视为“全局函数”,而将成员函数视为“局部函数”(纯属口胡)
1.声明:具体写法与普通函数基本一致。
有何异同:
①在引用本结构体(即*this,后面章节会详细讲解)内元素时,不需要加以.引导的前缀,直接写元素名即可;
②引用全局元素方式与普通函数相同;
③引用其他结构体变量内的元素时,则必须加以.引导的前缀。
2.引用:
①在本结构体内引用,直接写函数名即可;
②在任何外部区域引用,则必须加以.引导的前缀。
③若存在指向本结构体的指针(设本结构体为a,指针为p,存在结构体成员x和成员函数fun()),则亦可用指针引用本结构体内元素,写法为 指针->引用元素,如a.x与p->x、a.fun()与p->fun()分别等效。
二、函数重载:
如果不知函数重载为何物,请出门左转学习。
与普通函数一样,成员函数亦可重载。
三、构造函数:
构造函数是一种特殊的成员函数,其用途是:在声明某个结构体变量的同时对其进行初始化,每次声明时自动引用。
(悄悄说一句,如果不喜欢这个东西,也完全阔以写个普通成员函数用于初始化,在声明结构体变量之后随即引用一下即可。纯属口胡)
1.声明:没有返回值类型,函数名即为结构体名,参数和函数内容随意。示例详见代码。
2.引用:
①如果构造函数无参数,则声明结构体变量时自动引用,无需再写任何其他内容。
②如果有参数,则在声明的结构体变量名后加括号,括号内写上各参数即可。示例见“四、函数参数赋初值”。
③还有一种神奇的用法:(假设此处已经声明了下一节“栗子”中的结构体和结构体变量a)
1 a=abc(2,3);
很容易猜出来这句是啥意思,实在猜不出就试验求证一下好了。
括号内的常数也阔以换成变量(这点很明显吧)。
另外,这种用法也阔以写在声明结构体变量的时候,比如这样:
1 abc a=abc(2,3);
不得不说,这实在是一个鸡肋的操作
不过这种用法在除了变量声明之外的场合,还是很有用的。
四、函数参数赋初值:
令我初次看到的时候百思不得姐的写法
对,没有看错,函数参数是阔以赋初值的。
然后很自然就会有这样的想法:如果这样,函数参数岂不是变成常量了?
非也,非也。
我们来举一个栗子:
1 struct abc{ 2 int x,y; 3 abc(int a=1,int b=2){ 4 x=a; 5 y=b; 6 } 7 void print(){ 8 printf("%d %d\n",x,y); 9 } 10 } a;
先声明如上结构体变量,然后在主函数中执行如下语句:
1 a.print();
输出为1 2
But。。。如果在声明时这样写:
1 abc a(2,3);
然后输出结果就变为了2 3
明白了吗?参数赋过初值之后,在引用函数时可以不写参数,此时函数将认为是代入了值为初值的参数。如果仍写参数,则函数将代入手动引用的参数。
注意:
1 struct abc{ 2 int x,y; 3 abc(){ 4 x=1; 5 y=2; 6 } 7 abc(int a,int b){ 8 x=a; 9 y=b; 10 } 11 void print(){ 12 printf("%d %d\n",x,y); 13 } 14 };
这种写法与上面的写法等效。
但是:
1 struct abc{ 2 int x,y; 3 abc(){ 4 x=1; 5 y=2; 6 } 7 abc(int a=1,int b=2){ 8 x=a; 9 y=b; 10 } 11 void print(){ 12 printf("%d %d\n",x,y); 13 } 14 };
这种写法是错误的。
想一想这是为什么?
五、this指针:
this指针只能在成员函数中使用,指向本结构体变量,即this所在的成员函数所属的结构体变量。请参照代码加深理解。
顺便一提const的一种用法:const可写在成员函数的花括号之前,用于防止修改*this。
六、作用域运算符:
作用域运算符是::(两个冒号),表示其后的内容对其前的内容的隶属关系。
作用域运算符前的内容可以为struct名,class名或namespace名(仅对结构体而言,就是应为结构体名而不是结构体变量名)。
坦白说,这个东西我不是很懂,在这里只能讲它很有限的一点应用。
举个栗子,比如我们定义了如下的结构体:
1 struct abc{ 2 int x,y; 3 int max(){ 4 if(x>y)return x;else return y; 5 } 6 };
我们可以对成员函数max()作出如下改写,两种写法完全等效:
1 struct abc{ 2 int x,y; 3 int max(); 4 }; 5 6 int abc::max(){ 7 if(x>y)return x;else return y; 8 }
(这种先声明再补全的写法,有木有似曾相识的感觉?)
将成员函数写在结构体外,除了函数首部要作修改(具体指函数名前加结构体名::),其余部分与写在结构体内的写法无异。
对比上下两例,作用域运算符的作用不难体会到:即,让写在结构体外的成员函数,仍能像写在结构体内时一样,“受管辖”和“无障碍享受内部资源”。
七、运算符重载:
1.前言:
运算符是一种特殊的函数,所以运算符重载可以类比一下函数重载进行理解。
我们重载函数,目的之一是让函数能够处理多种类型的参数,而重载运算符的目的也正在于此。
举个栗子:
假设定义了高精度结构体bignum并声明了两个结构体变量a,b;
现在我们想要实现a+b的操作;
而C++中自带的+只允许两个加数(从更一般的意义上来说,是两个参数)的数据类型是int,long long之类的基本数据类型;
那么我们就只能写个普通函数(话说这操作真low)来实现a+b的操作了吗?
并不是。只要我们把+重载一下,就能实现像基本数据类型一样直接写a+b的操作了。
2.声明(对于相同的运算参数,以下各写法完全等效):
写法一、写为普通函数:
1 返回值类型 operator 运算符(参数表){ 2 ...... 3 }
写法二、写为成员函数:
1 返回值类型 operator 运算符(参数表){ 2 ...... 3 }
写法三、使用::改写写法二(结构体名::加在operator前),由于其与写法二基本一致,故不再赘述。
有没有发现写法一、二的格式一样?别急,其实是不一样的,详见下面章节。
3.规则:
①重载的运算符只能为C++中已有的运算符,不能自己创造一个运算符然后重载。
②只有一目运算符、二目运算符可以重载,三目运算符及一些特殊运算符不能重载(具体是哪些,请自行百度)。
③对于上面的写法一:
如果是一目运算符,则参数表*有一个参数,即运算符的对应参数。
如果是二目运算符,则参数表*有两个参数,分别为运算符的左、右参数。
示例(此时加法的含义是将a,b的x,y分别对应相加):
1 struct abc{ 2 int x,y; 3 }; 4 5 abc operator +(abc a,abc b){ 6 abc ans; 7 ans.x=a.x+b.x; 8 ans.y=a.y+b.y; 9 return ans; 10 }
④对于写法二:
与写法一的不同之处:*this是要参与运算的,即将本结构体变量也作为一个运算参数。
如果是一目运算符,则参数表为空,*this即为运算符的对应参数。
如果是二目运算符,则参数表*有一个参数,*this为左参数,参数表中的参数为右参数。
示例(同上):
1 struct abc{ 2 int x,y; 3 4 abc operator +(abc b){ 5 abc ans; 6 ans.x=x+b.x; 7 ans.y=y+b.y; 8 return ans; 9 } 10 };
综上:写法二的参数比写法一少一个,因为在写法二中*this也要参与运算。
终于写完了,好累哦~
最后奉上完整代码:
1 #include<cstdio> 2 #include<cstring> 3 4 using namespace std; 5 6 inline int max(int x,int y){ 7 if(x>y)return x;else return y; 8 } 9 10 struct bignum{ //.引用的成员函数、构造函数、赋值写在结构体内,其余运算符重载写在结构体外 11 int c[5010],l; 12 13 void clear(){ //置0,函数内引用则置于最底层 14 memset(c,0,sizeof(c)); 15 l=1; 16 } 17 18 bignum(){ //构造函数(置0)(最底层) 19 clear(); 20 } 21 22 bignum operator =(char *s){ //字符串赋值,传进的串0位无意义,正文从1位开始(最底层) 23 clear(); 24 int p=strlen(s+1); 25 l=0; 26 while(p>=4){ 27 l++; 28 c[l]=(s[p]-'0')+(s[p-1]-'0')*10+(s[p-2]-'0')*100+(s[p-3]-'0')*1000; 29 p-=4; 30 } 31 if(p)l++; 32 for(int i=1;i<=p;i++)c[l]=c[l]*10+s[i]-'0'; 33 if(!l)l=1; 34 return *this; 35 } 36 37 bignum operator =(int x){ //小数字赋值 38 char s[20]; 39 sprintf(s+1,"%d",x); 40 *this=s; 41 return *this; 42 } 43 44 bignum(char *s){ //构造函数(字符串),传进的串正文应从1位开始 45 *this=s; 46 } 47 48 bignum(int x){ //构造函数(小数字) 49 *this=x; 50 } 51 52 void read(){ //输入 53 char s[10010]; 54 scanf("%s",s+1); 55 *this=s; 56 } 57 58 void print(){ //输出 59 printf("%d",c[l]); 60 for(int i=l-1;i>=1;i--){ 61 if(c[i]<10)printf("000"); 62 else if(c[i]<100)printf("00"); 63 else if(c[i]<1000)printf("0"); 64 printf("%d",c[i]); 65 } 66 } 67 }; 68 69 bool operator <(bignum &a,bignum &b){ //小于 70 if(a.l!=b.l)return a.l<b.l; 71 for(int i=a.l;i>=1;i--)if(a.c[i]!=b.c[i])return a.c[i]<b.c[i]; 72 return 0; 73 } 74 75 bool operator >(bignum &a,bignum &b){ //大于 76 return b<a; 77 } 78 79 bool operator ==(bignum &a,bignum &b){ //等于 80 return (!(a<b))&&(!(b<a)); 81 } 82 83 bool operator !=(bignum &a,bignum &b){ //不等于 84 return (a<b)||(b<a); 85 } 86 87 bool operator <=(bignum &a,bignum &b){ //小于等于 88 return !(b<a); 89 } 90 91 bool operator >=(bignum &a,bignum &b){ //大于等于 92 return !(a<b); 93 } 94 95 bignum operator +(bignum &a,bignum &b){ //加 96 bignum ans; 97 ans=a; 98 for(int i=1;i<=b.l;i++)ans.c[i]+=b.c[i]; 99 ans.l=max(a.l,b.l); 100 for(int i=1;i<=ans.l;i++) 101 if(ans.c[i]>=10000){ 102 ans.c[i]-=10000; 103 ans.c[i+1]++; 104 } 105 if(ans.c[ans.l+1])ans.l++; 106 return ans; 107 } 108 109 bignum operator -(bignum &a,bignum &b){ //减 110 bignum ans; 111 ans=a; 112 for(int i=1;i<=b.l;i++)ans.c[i]-=b.c[i]; 113 for(int i=1;i<ans.l;i++) 114 if(ans.c[i]<0){ 115 ans.c[i]+=10000; 116 ans.c[i+1]--; 117 } 118 while(!ans.c[ans.l] && ans.l>1)ans.l--; 119 return ans; 120 } 121 122 bignum operator *(bignum &a,bignum &b){ //乘 123 bignum ans; 124 for(int i=1;i<=a.l;i++) 125 for(int j=1;j<=b.l;j++){ 126 ans.c[i+j-1]+=a.c[i]*b.c[j]; 127 ans.c[i+j]+=ans.c[i+j-1]/10000; 128 ans.c[i+j-1]%=10000; 129 } 130 if(ans.c[a.l+b.l])ans.l=a.l+b.l;else ans.l=a.l+b.l-1; 131 return ans; 132 } 133 134 bignum operator <<(bignum &x,int e){ //左移 135 bignum ans; 136 ans=x; 137 for(int k=1;k<=e;k++){ 138 for(int i=1;i<=ans.l;i++)ans.c[i]<<=1; 139 for(int i=1;i<=ans.l;i++) 140 if(ans.c[i]>=10000){ 141 ans.c[i]-=10000; 142 ans.c[i+1]++; 143 } 144 if(ans.c[ans.l+1])ans.l++; 145 } 146 return ans; 147 } 148 149 bignum operator >>(bignum &x,int e){ //右移 150 bignum ans; 151 ans=x; 152 for(int k=1;k<=e;k++){ 153 for(int i=ans.l;i>=1;i--){ 154 if(ans.c[i]&1)ans.c[i-1]+=10000; 155 ans.c[i]>>=1; 156 } 157 if(!ans.c[ans.l] && ans.l>1)ans.l--; 158 } 159 return ans; 160 } 161 162 bignum operator /(bignum a,bignum b){ //除 163 bignum e=1,ans; 164 while(a>=b){ 165 b=b<<1; 166 e=e<<1; 167 } 168 while(e.l>1 || e.c[1]){ 169 if(a>=b){ 170 a=a-b; 171 ans=ans+e; 172 } 173 b=b>>1; 174 e=e>>1; 175 } 176 return ans; 177 } 178 179 bignum operator %(bignum a,bignum b){ //模 180 bignum e=1,ans; 181 while(a>=b){ 182 b=b<<1; 183 e=e<<1; 184 } 185 while(e.l>1 || e.c[1]){ 186 if(a>=b){ 187 a=a-b; 188 ans=ans+e; 189 } 190 b=b>>1; 191 e=e>>1; 192 } 193 return a; 194 } 195 196 bignum a,b,c; 197 198 int main(){ 199 a.read(); 200 b.read(); 201 c=a+b; 202 c.print(); 203 printf("\n"); 204 if(a<b){ 205 printf("-"); 206 c=b-a; 207 }else c=a-b; 208 c.print(); 209 printf("\n"); 210 c=a*b; 211 c.print(); 212 printf("\n"); 213 c=a/b; 214 c.print(); 215 printf("\n"); 216 c=a%b; 217 c.print(); 218 printf("\n"); 219 220 return 0; 221 }