用JAVA可以用BigInteger解决。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map; public class Main {
@SuppressWarnings("unchecked")
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BigInteger big = new BigInteger(br.readLine());
int sum = 0; BigInteger x = big.add(BigInteger.ZERO);
while (x.compareTo(BigInteger.ZERO) > 0) {
int digit = x.mod(BigInteger.valueOf(10l)).intValue();
sum += digit;
x = x.divide(BigInteger.valueOf(10l));
} Map<Integer, String> map = new HashMap<>();
map.put(0, "zero");
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.put(4, "four");
map.put(5, "five");
map.put(6, "six");
map.put(7, "seven");
map.put(8, "eight");
map.put(9, "nine"); String str = String.valueOf(sum); for (int j = 0; j < str.length(); j++) {
int digit = str.charAt(j) - '0';
if( j != 0){
System.out.print(" ");
}
System.out.print(map.get(digit));
}
System.out.println(); br.close();
} }

  

高精度计算

太长不看版:结尾自取模板……

定义

高精度计算(Arbitrary-Precision Arithmetic),也被称作大整数(bignum)计算,运用了一些算法结构来支持更大整数间的运算(数字大小超过语言内建整型)。

引入

高精度问题包含很多小的细节,实现上也有很多讲究。

所以今天就来一起实现一个简单的计算器吧。

任务

输入:一个形如 a <op> b 的表达式。

  • ab 分别是长度不超过  的十进制非负整数;
  • <op> 是一个字符(+-* 或 /),表示运算。
  • 整数与运算符之间由一个空格分隔。

输出:运算结果。

  • 对于 +-* 运算,输出一行表示结果;
  • 对于 / 运算,输出两行分别表示商和余数。
  • 保证结果均为非负整数。

存储

在平常的实现中,高精度数字利用字符串表示,每一个字符表示数字的一个十进制位。因此可以说,高精度数值计算实际上是一种特别的字符串处理。

读入字符串时,数字最高位在字符串首(下标小的位置)。但是习惯上,下标最小的位置存放的是数字的 最低位,即存储反转的字符串。这么做的原因在于,数字的长度可能发生变化,但我们希望同样权值位始终保持对齐(例如,希望所有的个位都在下标 [0],所有的十位都在下标 [1]……);同时,加、减、乘的运算一般都从个位开始进行(回想小学的竖式运算),这都给了「反转存储」以充分的理由。

此后我们将一直沿用这一约定。定义一个常数 LEN = 1004 表示程序所容纳的最大长度。

由此不难写出读入高精度数字的代码:

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void clear(int a[]) {
for (int i = 0; i < LEN; ++i) a[i] = 0;
} void read(int a[]) {
static char s[LEN + 1];
scanf("%s", s); clear(a); int len = strlen(s);
// 如上所述,反转
for (int i = 0; i < len; ++i) a[len - i - 1] = s[i] - '0';
// s[i] - '0' 就是 s[i] 所表示的数码
// 有些同学可能更习惯用 ord(s[i]) - ord('0') 的方式理解
}

输出也按照存储的逆序输出。由于不希望输出前导零,故这里从最高位开始向下寻找第一个非零位,从此处开始输出;终止条件 i >= 1 而不是 i >= 0 是因为当整个数字等于  时仍希望输出一个字符 0

1
2
3
4
5
6
7
void print(int a[]) {
int i;
for (i = LEN - 1; i >= 1; --i)
if (a[i] != 0) break;
for (; i >= 0; --i) putchar(a[i] + '0');
putchar('\n');
}

拼起来就是一个完整的复读机程序咯。

copycat.cpp

   

四则运算

四则运算中难度也各不相同。最简单的是高精度加减法,其次是高精度—单精度(普通的 int)乘法和高精度—高精度乘法,最后是高精度—高精度除法。

我们将按这个顺序分别实现所有要求的功能。

加法

高精度加法,其实就是竖式加法啦。

也就是从最低位开始,将两个加数对应位置上的数码相加,并判断是否达到或超过 。如果达到,那么处理进位:将更高一位的结果上增加 ,当前位的结果减少 

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void add(int a[], int b[], int c[]) {
clear(c); // 高精度实现中,一般令数组的最大长度 LEN 比可能的输入大一些
// 然后略去末尾的几次循环,这样一来可以省去不少边界情况的处理
// 因为实际输入不会超过 1000 位,故在此循环到 LEN - 1 = 1003 已经足够
for (int i = 0; i < LEN - 1; ++i) {
// 将相应位上的数码相加
c[i] += a[i] + b[i];
if (c[i] >= 10) {
// 进位
c[i + 1] += 1;
c[i] -= 10;
}
}
}

试着和上一部分结合,可以得到一个加法计算器。

adder.cpp

   

减法

高精度减法,也就是竖式减法啦。

从个位起逐位相减,遇到负的情况则向上一位借 。整体思路与加法完全一致。

 1
2
3
4
5
6
7
8
9
10
11
12
13
void sub(int a[], int b[], int c[]) {
clear(c); for (int i = 0; i < LEN - 1; ++i) {
// 逐位相减
c[i] += a[i] - b[i];
if (c[i] < 0) {
// 借位
c[i + 1] -= 1;
c[i] += 10;
}
}
}

将上一个程序中的 add() 替换成 sub(),就有了一个减法计算器。

subtractor.cpp

   

试一试,输入 1 2——输出 /9999999,诶这个 OI Wiki 怎么给了我一份假的代码啊……

事实上,上面的代码只能处理减数  大于等于被减数  的情况。处理被减数比减数小,即  时的情况很简单。

要计算  的值,因为有 ,可以调用以上代码中的 sub 函数,写法为 sub(b,a,c)。要得到  的值,在得数前加上负号即可。

乘法

高精度—单精度

高精度乘法,也就是竖……等会儿等会儿!

先考虑一个简单的情况:乘数中的一个是普通的 int 类型。有没有简单的处理方法呢?

一个直观的思路是直接将  每一位上的数字乘以 。从数值上来说,这个方法是正确的,但它并不符合十进制表示法,因此需要将它重新整理成正常的样子。

重整的方式,也是从个位开始逐位向上处理进位。但是这里的进位可能非常大,甚至远大于 ,因为每一位被乘上之后都可能达到  的数量级。所以这里的进位不能再简单地进行  运算,而是要通过除以  的商以及余数计算。详见代码注释,也可以参考下图展示的一个计算高精度数  乘以单精度数  的过程。

当然,也是出于这个原因,这个方法需要特别关注乘数  的范围。若它和 (或相应整型的取值上界)属于同一数量级,那么需要慎用高精度—单精度乘法。

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void mul_short(int a[], int b, int c[]) {
clear(c); for (int i = 0; i < LEN - 1; ++i) {
// 直接把 a 的第 i 位数码乘以乘数,加入结果
c[i] += a[i] * b; if (c[i] >= 10) {
// 处理进位
// c[i] / 10 即除法的商数成为进位的增量值
c[i + 1] += c[i] / 10;
// 而 c[i] % 10 即除法的余数成为在当前位留下的值
c[i] %= 10;
}
}
}

高精度—高精度

如果两个乘数都是高精度,那么竖式乘法又可以大显身手了。

回想竖式乘法的每一步,实际上是计算了若干  的和。例如计算 ,计算的就是 

于是可以将  分解为它的所有数码,其中每个数码都是单精度数,将它们分别与  相乘,再向左移动到各自的位置上相加即得答案。当然,最后也需要用与上例相同的方式处理进位。

注意这个过程与竖式乘法不尽相同,我们的算法在每一步乘的过程中并不进位,而是将所有的结果保留在对应的位置上,到最后再统一处理进位,但这不会影响结果。

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void mul(int a[], int b[], int c[]) {
clear(c); for (int i = 0; i < LEN - 1; ++i) {
// 这里直接计算结果中的从低到高第 i 位,且一并处理了进位
// 第 i 次循环为 c[i] 加上了所有满足 p + q = i 的 a[p] 与 b[q] 的乘积之和
// 这样做的效果和直接进行上图的运算最后求和是一样的,只是更加简短的一种实现方式
for (int j = 0; j <= i; ++j) c[i] += a[j] * b[i - j]; if (c[i] >= 10) {
c[i + 1] += c[i] / 10;
c[i] %= 10;
}
}
}

除法

高精度除法的一种实现方式就是竖式长除法。

竖式长除法实际上可以看作一个逐次减法的过程。例如上图中商数十位的计算可以这样理解:将  减去三次  后变得小于 ,不能再减,故此位为 

为了减少冗余运算,我们提前得到被除数的长度  与除数的长度 ,从下标  开始,从高位到低位来计算商。这和手工计算时将第一次乘法的最高位与被除数最高位对齐的做法是一样的。

参考程序实现了一个函数 greater_eq() 用于判断被除数以下标 last_dg 为最低位,是否可以再减去除数而保持非负。此后对于商的每一位,不断调用 greater_eq(),并在成立的时候用高精度减法从余数中减去除数,也即模拟了竖式除法的过程。

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 被除数 a 以下标 last_dg 为最低位,是否可以再减去除数 b 而保持非负
// len 是除数 b 的长度,避免反复计算
bool greater_eq(int a[], int b[], int last_dg, int len) {
// 有可能被除数剩余的部分比除数长,这个情况下最多多出 1 位,故如此判断即可
if (a[last_dg + len] != 0) return true;
// 从高位到低位,逐位比较
for (int i = len - 1; i >= 0; --i) {
if (a[last_dg + i] > b[i]) return true;
if (a[last_dg + i] < b[i]) return false;
}
// 相等的情形下也是可行的
return true;
} void div(int a[], int b[], int c[], int d[]) {
clear(c);
clear(d); int la, lb;
for (la = LEN - 1; la > 0; --la)
if (a[la - 1] != 0) break;
for (lb = LEN - 1; lb > 0; --lb)
if (b[lb - 1] != 0) break;
if (lb == 0) {
puts("> <");
return;
} // 除数不能为零 // c 是商
// d 是被除数的剩余部分,算法结束后自然成为余数
for (int i = 0; i < la; ++i) d[i] = a[i];
for (int i = la - lb; i >= 0; --i) {
// 计算商的第 i 位
while (greater_eq(d, b, i, lb)) {
// 若可以减,则减
// 这一段是一个高精度减法
for (int j = 0; j < lb; ++j) {
d[i + j] -= b[j];
if (d[i + j] < 0) {
d[i + j + 1] -= 1;
d[i + j]+=10;}}// 使商的这一位增加 1
c[i]+=1;// 返回循环开头,重新检查
}}}

入门篇完成!

将上面介绍的四则运算的实现结合,即可完成开头提到的计算器程序。

calculator.cpp

   

压位高精度

引入

在一般的高精度加法,减法,乘法运算中,我们都是将参与运算的数拆分成一个个单独的数码进行运算。

例如计算  时,如果按照高精度乘高精度的计算方式,我们实际上算的是 

在位数较多的时候,拆分出的数也很多,高精度运算的效率就会下降。

有没有办法作出一些优化呢?

注意到拆分数字的方式并不影响最终的结果,因此我们可以将若干个数码进行合并。

过程

还是以上面这个例子为例,如果我们每两位拆分一个数,我们可以拆分成 

这样的拆分不影响最终结果,但是因为拆分出的数字变少了,计算效率也就提升了。

从 进位制 的角度理解这一过程,我们通过在较大的进位制(上面每两位拆分一个数,可以认为是在  进制下进行运算)下进行运算,从而达到减少参与运算的数字的位数,提升运算效率的目的。

这就是 压位高精度 的思想。

下面我们给出压位高精度的加法代码,用于进一步阐述其实现方法:

压位高精度加法参考实现

   

压位高精下的高效竖式除法

在使用压位高精时,如果试商时仍然使用上文介绍的方法,由于试商次数会很多,计算常数会非常大。例如在万进制下,平均每个位需要试商 5000 次,这个巨大的常数是不可接受的。因此我们需要一个更高效的试商办法。

我们可以把 double 作为媒介。假设被除数有 4 位,是 ,除数有 3 位,是 ,那么我们只要试一位的商:使用  进制,用式子  来估商。而对于多个位的情况,就是一位的写法加个循环。由于除数使用 3 位的精度来参与估商,能保证估的商 q' 与实际商 q 的关系满足 ,这样每个位在最坏的情况下也只需要两次试商。但与此同时要求  在 double 的有效精度内,即 ,所以在运用这个方法时建议不要超过 32768 进制,否则很容易因精度不足产生误差从而导致错误。

另外,由于估的商总是小于等于实际商,所以还有再进一步优化的空间。绝大多数情况下每个位只估商一次,这样在下一个位估商时,虽然得到的商有可能因为前一位的误差造成试商结果大于等于 base,但这没有关系,只要在最后再最后做统一进位便可。举个例子,假设 base 是 10,求 ,试商计算步骤如下:

  1. 首先试商计算得到 ,于是 ,这一步出现了误差,但不用管,继续下一步计算。
  2. 对余数 98801 继续试商计算得到 ,于是 ,这就是最终余数。
  3. 把试商过程的结果加起来并处理进位,即  便是准确的商。

方法虽然看着简单,但具体实现上很容易进坑,所以以下提供一个经过多番验证确认没有问题的实现供大家参考,要注意的细节也写在注释当中。

压位高精度高效竖式除法参考实现

   

Karatsuba 乘法

记高精度数字的位数为 ,那么高精度—高精度竖式乘法需要花费  的时间。本节介绍一个时间复杂度更为优秀的算法,由前苏联(俄罗斯)数学家 Anatoly Karatsuba 提出,是一种分治算法。

考虑两个十进制大整数  和 ,均包含  个数码(可以有前导零)。任取 ,记

其中 。可得

观察知

于是要计算 ,只需计算 ,再与  相减即可。

上式实际上是 Karatsuba 算法的核心,它将长度为  的乘法问题转化为了  个长度更小的子问题。若令 ,记 Karatsuba 算法计算两个  位整数乘法的耗时为 ,则有 ,由主定理可得 

整个过程可以递归实现。为清晰起见,下面的代码通过 Karatsuba 算法实现了多项式乘法,最后再处理所有的进位问题。

karatsuba_mulc.cpp

   

关于 new 和 delete

但是这样的实现存在一个问题:在  进制下,多项式的每一个系数都有可能达到  量级,在压位高精度实现中可能造成整数溢出;而若在多项式乘法的过程中处理进位问题,则  与  的结果可能达到 ,增加一个位(如果采用  的计算方式,则不得不特殊处理负数的情况)。因此,需要依照实际的应用场景来决定采用何种实现方式。

基于多项式的高效大整数乘法

如果数据规模达到了  或更大,普通的高精度乘法可能会超时。本节将介绍用多项式优化此类乘法的方法。

对于一个  位的十进制整数 ,可以将它看作一个每位系数均为整数且不超过  的多项式 。这样,我们就将两个整数乘法转化为了两个多项式乘法。

普通的多项式乘法时间复杂度仍是 ,但可以用多项式一节中的 快速傅里叶变换快速数论变换 等算法优化,优化后的时间复杂度是 

 

PAT 甲级1005【1005 Spell It Right】的更多相关文章

  1. 【PAT甲级】1005 Spell It Right (20 分)

    题意: 给出一个非零整数N(<=10^100),计算每位之和并用英文输出. AAAAAccepted code: #include<bits/stdc++.h> using name ...

  2. PAT甲级题解(慢慢刷中)

    博主欢迎转载,但请给出本文链接,我尊重你,你尊重我,谢谢~http://www.cnblogs.com/chenxiwenruo/p/6102219.html特别不喜欢那些随便转载别人的原创文章又不给 ...

  3. PAT甲级代码仓库

    大道至简,知易行难.希望能够坚持刷题. PAT甲级真题题库,附上我的代码. Label Title Score Code Level 1001 A+B Format 20 1001 * 1002 A+ ...

  4. PAT甲级1014. Waiting in Line

    PAT甲级1014. Waiting in Line 题意: 假设银行有N个窗口可以开放服务.窗前有一条黄线,将等候区分为两部分.客户要排队的规则是: 每个窗口前面的黄线内的空间足以包含与M个客户的一 ...

  5. PAT甲级1013. Battle Over Cities

    PAT甲级1013. Battle Over Cities 题意: 将所有城市连接起来的公路在战争中是非常重要的.如果一个城市被敌人占领,所有从这个城市的高速公路都是关闭的.我们必须立即知道,如果我们 ...

  6. PAT甲级1131. Subway Map

    PAT甲级1131. Subway Map 题意: 在大城市,地铁系统对访客总是看起来很复杂.给你一些感觉,下图显示了北京地铁的地图.现在你应该帮助人们掌握你的电脑技能!鉴于您的用户的起始位置,您的任 ...

  7. PAT甲级1127. ZigZagging on a Tree

    PAT甲级1127. ZigZagging on a Tree 题意: 假设二叉树中的所有键都是不同的正整数.一个唯一的二叉树可以通过给定的一对后序和顺序遍历序列来确定.这是一个简单的标准程序,可以按 ...

  8. PAT甲级1123. Is It a Complete AVL Tree

    PAT甲级1123. Is It a Complete AVL Tree 题意: 在AVL树中,任何节点的两个子树的高度最多有一个;如果在任何时候它们不同于一个,则重新平衡来恢复此属性.图1-4说明了 ...

  9. PAT甲级1119. Pre- and Post-order Traversals

    PAT甲级1119. Pre- and Post-order Traversals 题意: 假设二叉树中的所有键都是不同的正整数.一个唯一的二进制树可以通过给定的一对后序和顺序遍历序列来确定,也可以通 ...

  10. PAT甲级1114. Family Property

    PAT甲级1114. Family Property 题意: 这一次,你应该帮我们收集家族财产的数据.鉴于每个人的家庭成员和他/她自己的名字的房地产(房产)信息,我们需要知道每个家庭的规模,以及他们的 ...

随机推荐

  1. Kafka-如何重设消费者位移(重设OFFSET)

    1. 为什么要重设消费者组位移? 我们知道,Kafka 和传统的消息引擎在设计上是有很大区别的,其中一个比较显著的区别就是,Kafka 的消费者读取消息是可以重演的(replayable). 像 Ra ...

  2. Linux shell的while循环

    while循环 #!/bin/bash #其中":"表示while循环的条件永远为真的意思 while : do read -p "Enter a number [1-5 ...

  3. Python 中获取文件名

    Python 获取文件名import osimport sys # ①获取当前文件名os.path.basename(__file__)# ②获取程序启动文件名os.path.basename(sys ...

  4. 轻松玩转Makefile | 基础用法

    前言 本文通过几个简单的示例,可以快速了解Makefile的基本使用方法,适用于编译我们平时练习所编写的小量代码. 1. make命令 Makefile文件内容: all为目标,这里没有依赖的文件,这 ...

  5. MySQL案例-并行复制乱序提交引起的同步异常

    现象描述 Slave在开启并行复制后, 默认会乱序提交事务, 可能会引起同步中断; Slave端表现为同步的SQL线程抛出异常, 为主键重复, 修改的数据行不存在等; GTID信息类似于: 9a2a5 ...

  6. Git实战系列教程

    介绍 本文详细记录了Git一系列核心概念和工作中常用的操作命令,通篇以实际出发拒绝过度理论,值得典藏:). 概念 版本管理系统 版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的 ...

  7. Java 使用SimpleDateFormat格式化日期

    Java 使用SimpleDateFormat格式化日期,这里只涉及最实用的方面. 用途 用于格式化日期和解析日期类型字符串. formatting (date -> text), parsin ...

  8. ghost方式批量安装win7

    1.   需求介绍: 最近工作中需要给几百台PC安装win7操作系统,同时需要安装系统驱动和一些办公软件.刚开始是使用U盘制作的win7启动盘安装,发现效率太低,因为中间需要人监控安装进度以待安装好系 ...

  9. ysoserial CommonsCollections1 分析

    /* Gadget chain: ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() Map(Proxy). ...

  10. CGI, FastCGI, WSGI, uWSGI, uwsgi一文搞懂

    中间件 1.服务器中间件:nginx,apache 2.数据库中间件:介于应用程序和数据库之前的,MyCat 3.消息队列中间件:kafka,rabbitmq,Rocketmq CGI 1.CGI是一 ...