PAT OJ 刷题必备知识总结

前言:文章篇幅较长,仅仅是总结了一些比较基础的知识,读者可以按照目录快速访问自己想了解的内容。

文章目录

  • 1 int 与 long long
  • 2 字符常量
  • 3 最大整数
  • 4 scanf
  • 5 printf
    • 四舍六入五成双
  • 6 getchar 和 putchar
  • 7 typedef
  • 8 常用 math 函数
  • 9 数组
    • 初始化数组的方法
  • 10 gets() 和 puts()
  • 11 string.h 头文件下的常用函数
  • 12 sscanf 与 sprintf
  • 13 引用
  • 14 结构体
  • 15 cin 与 cout
  • 16 浮点数的比较
  • 17 快速读取输入并输出结果
  • 18 测试数据有多组
  • 19 闰年的判断
  • 20 进制转换
    • 20.1 P P P 进制转十进制
      • 秦久韶算法
    • 20.2 十进制转 P P P 进制数:
  • 21 链表
  • 22 HashTable 妙用
  • 23 最大公约数
  • 24 最小公倍数
  • 25 分数的表示和化简
    • 25.1 分数的表示
    • 25.2 分数的化简
    • 25.3 分数的四则运算
    • 25.4 分数的输出
  • 26 素数(质数)
    • 26.1 素数的判断
    • 26.2 获取素数表
      • 26.2.1 埃氏筛法
      • 26.2.2 欧拉筛法
  • 27 质因子分解
  • 28 求因子个数和因子之和
  • 29、大整数运算
    • 29.1 大整数的存储
    • 29.2 大整数的比较
    • 29.3 大整数的四则运算
      • 29.3.1 高精度加法
      • 29.3.2 高精度减法
      • 29.3.3 高精度与低精度的乘法
      • 29.3.4 高精度与低精度的除法

1 int 与 long long

  • 什么时候用 int?什么时候用 long long?

绝对值在 1 0 9 10^9 109 以内或32位整数用 int 定义,绝对值 1 0 18 10^{18} 1018 以内或64位整数用 long long 定义,此外,对 long long 型赋大于 2 31 − 1 2^{31}-1 2311 的初值时需要在最后面加上 LL。

int num;
long long bignum;
long long bignum = 1234567890123456LL; // 末尾加上 LL
  • long 是 long int 的简写,long long 是 long long int 的简写。

  • 不同位数操作系统下的区别:

16位的操作系统下 int 占2字节,long 占4字节,没有 long long;32位的操作系统下 int 与 long 都占4字节,long long 占8字节(分两次读取);64位的操作系统下 int 与 long 都占4字节,long long 占8字节(一次即可读取)。

2 字符常量

0 ~ 9 的 ASCII 码为 48 ~ 57、A ~ Z 的为 65 ~ 90、a ~ z 为 97 ~ 122。如果想初始化字符变量 a 的值为小写字母 z:

char a = z;		// 错误:编译器会把 z 当作一个字符变量而不是字符常量
char a = 'z';	// 正确:'z' 表明它是一个字符常量

为一个字符变量赋值一个字符常量时,字符常量必须用单引号标注起来,目的是区分其为字符变量还是字符常量

printf("%c %d", a, a); // 该语句输出结果是 z 122

在 printf 中,以 %c 格式输出的是字符,以 %d 格式输出的是字符 ASCII 码值。

3 最大整数

定义最大 int 型正整数的两种方法:本质都是定义值为 2 32 − 1 2^{32} - 1 2321 的变量。

const int INF = (1LL << 31LL) - 1;	// 利用位运算得到最大值
const int INF = 0x3fffffff;		// 直接定义最大的十六进制下的值

有一点需要注意,在1和31之后要加上 LL ,否则可能会出现如下报错。原因可能在于:尽管1和31没有用 int 型变量保存,但是在计算的过程中默认是按照 int 型变量运算,而当1左移31位后成了 2147483648,其值超过了 int 型变量存储的范围,也就是出现了溢出,所以会报错,所以需要加上 LL 来将其转为 long long 型变量运算。

warning: integer overflow in expression of type ‘int’ results in ‘2147483647’ [-Woverflow]

之所以左移31位,是因为 C/C++ 中的数据都是补码,左移32位后,int 型的 INF 就成了-2147483648,而 long long 型的 INF 才是真实值 2147483648。

print("%d", INF); // 输出的结果是 2147483647

4 scanf

容易出错的格式控制符:long long 为 %lld;float 为 %f;double 为 %lf;long double 为 %llf;数组或字符串不需要加取地址运算符 “&”。

另外 %c 能够读入空格符换行符,而 %s 通过空格换行来识别字符串的结束

scanf("%s", str);
printf("%s", str);

此处输入 abcd efg 后,打印出来的是 abcd。

5 printf

在 printf 中,double 型和 float 型的格式符都为 %f,这一点要和 scanf 区分开。long 和 long long 型的格式符分别为 %ld 和 %lld。

下面两条语句可以分别输出百分号 “%” 以及反斜杠 “\”

printf("%%");
printf("\\");

%md:使不足 m 位的 int 型变量以 m 位右对齐输出,高位(左边)用空格补齐;若变量本身超过 m 位则保持原样。

int a = 123, b = 1234567;
printf("%5d\n", a);
printf("%5d\n", b);
// 输出结果为
  123
1234567

%0md:当变量不足 m 位时用0而不是空格补齐。

int a = 123;
printf("%05d\n", a);
// 输出结果为
00123

%.mf:使浮点数按照“四舍六入五成双”保留 m 位小数输出。

四舍六入五成双

假如有数123.45,要求保留4位有效数字,从左往右数4位,第5位就是舍弃位,也称为被修约位,该位上的数字称为被修约数。下面的规则中都是保留4位有效数字。

规则:

  1. 被修约数小于5,直接舍去,比如123.44 = 123.4。
  2. 被修约数大于5,舍位进一,比如123.46 = 123.5。
  3. 被修约数等于5
  • 5后没有除0外任何数字时,则看5前一位的数,奇进偶舍,比如123.45 = 123.4,123.55 = 123.6。
  • 5后含有除0外任何数字时,不管5前是奇是偶都要进位,比如123.4501 = 123.5,123.5501 = 123.6。

从统计学的角度,“四舍六入五成双”比“四舍五入”要科学,在大量运算时,它使舍入后的结果误差的均值趋于零,而不是像四舍五入那样逢五就入,导致结果偏向大数,使得误差产生积累进而产生系统误差,“四舍六入五成双”使测量结果受到舍入误差的影响降到最低。

6 getchar 和 putchar

前者获取单个字符,后者从打印单个字符。getchar 可以识别并读取换行符

7 typedef

用来给某一对象取别名。

typedef long long LL;	// typedef 给 long long 取了个别名 LL
LL a = 123456789012345LL;

8 常用 math 函数

以下函数返回的均是 double 型的值。

函数名 返回的均是 double 型的值
ceil(double x) 返回向上取整后的数,比如4.9 = 5.0
floor(double x) 返回向下取整后的数,比如4.9 = 4.0
pow(double r, double p) 返回 r p r^p rp
sqrt(double x) 返回变量 x 的 算术平方根 x \sqrt x x
log(double x) 返回以 x 的自然对数 l n   x ln\ x ln x
sin(double x), cos(double x), tan(double x) 返回 x 的三角函数值
asin(double x), acos(double x), atan(double x) 返回 x 的反三角函数值
round(double x) 返回对 x 的第一位小数四舍五入后的取整值

9 数组

如果数组比较大(大概 1 0 6 10^6 106 级别),则需要将其定义在主函数外面,否则会使程序异常退出。

初始化数组的方法

定义数组 a 并初始化的两种方法:

  1. 方法一:利用 malloc() 和 memset(),用于赋同一个值。
#include	// malloc 函数的头文件
#include	// memset 函数的头文件

int *a;
a = (int*)malloc(sizeof(int) * 100);
memset(a, 0, sizeof(int) * 100);
void *memset(void *a, int c, unsigned long n);

memset() 将指针变量 a 所指向的前 n 个字节的内存单元用一个整数 c 的二进制补码替换,而且只拿最低的一位字节进行替换。而由于0的二进制补码为全0,-1的二进制补码为全1,所以用它们为数组赋值不会出错,而赋值其他的数则容易出错。例如266的二进制补码为00000001 00001010,那么用10进行赋值就只会给每个字节赋“00001010”:

#include 
#include	// malloc() 的头文件
#include	// memset() 的头文件
#include 	// bitset<>() 的头文件
using namespace std;

int main()
{
	int *a = (int*)malloc(sizeof(int) * 10);
	memset(a, 266, sizeof(int) * 10);

	// // bitset(n) 会将后面的数 n 转换成 s 位的二进制
	for (int i = 0; i < 10; ++i)
        cout << bitset<32>(a[i]) << " " << a[i] << endl;

	return 0;
}

打印的结果如下:
PAT OJ 刷题必备知识总结_第1张图片

如果要对数组赋其他值(例如1、2等)就使用 fill(),区别在于 memset() 执行速度更快,毕竟它是直接对内存进行操作的,是对较大的数组或结构体进行清零初始化的最快方法。

例如

  1. 方法二:利用循环,或直接初始化,或通过指针。

例如为数组中的每个元素赋其下标值:

int *p = a;
*p = 0;
for(int *p = a + 1; p < a + 100; ++p)
{
    *p = *(p - 1) + 1;
    printf("%d\n", *p);
}

// 上面的循环相当于
a[0] = 0;
for(int i = 1; i < 100; ++i)
{
    a[i] = a[i - 1] + 1;
    printf("%d\n", a[i]);
}

两个 int 型的指针相减,等价于求两个指针之间相差了几个 int。

10 gets() 和 puts()

gets() 用来获取一行字符串,并将其存于一维数组中。它以换行符 ‘\n’ 作为输入结束的标志,并将换行符作为数组的最后一个元素,也即 结束标志 ‘\0’ 之前。因此在用 scanf 读取完一个整数后,如果要需要使用 gets(),需要先用 getchar() 过滤掉换行符,否则会导致 gets 只获取一个换行符。

puts() 用来输出一个字符串,并紧跟一个换行。

注意,在 PAT 的题目中是不能使用 gets() 的,其等价函数为 fgets(),用法如下:

char *fgets(char *str, int n, FILE *stream); // n 是字符数组 str 的长度
// 常见用法
fgets(str, 100, stdin); // fgets() 会读入换行,因此 str 的最后一个元素是 '\n'

使用 cin 时推荐用 getline():

cin.getline(char *str, int n);	// n 是字符数组 str 的长度
getline(cin, string str);		// string 类型的版本

11 string.h 头文件下的常用函数

strlen(str):返回字符数组的长度(不包含结束符 ‘\0’)。

strcmp(str_1, str_2):返回两个字符数组比较大小后的结果。若前者小于后者返回负整数,等于返回0,大于返回正整数。

strcpy(str_1, str_2):用 str_2 的内容去覆盖 str_1。

strcat(str_1, str_2):把 str_2 接到 str_1 后面,会覆盖掉 str_1 的结束符 ‘\0’。

12 sscanf 与 sprintf

sscanf 的作用是把字符数组 str 中的内容以指定的格式写到其他变量中,顺序为从左至右,如:

int n;
double db;
char str1[100] = "2018:3.14,hello", str2[100];
sscanf(str1, "%d:%lf,%s", &n, &db, str2); // 把 str1 的内容按照指定的格式写入 str2 中
printf("n = %d, db = %.2f, str2 = %s\n", n, db, str2);

// 输出结果是
n = 2018, db = 3.14, str2 = hello

sprintf 的作用刚好相反,是把其他类型变量写到字符数组 str 中,如:

int n = 12;
double db = 3.1415;
char str1[100], str2[100] = "good";
sprintf(str1, "%d:%.2f,%s", n, db, str2);	// 把后面的变量按指定格式写入 str1 中
printf("str1 = %s\n", str1);

// 输出结果是
str1 = 12:3.14,good

13 引用

引用产生的是变量的别名,因此常量不可使用引用

14 结构体

通过重写构造函数可以实现对结构体变量的快速初始化,但如果重写过构造函数,就不能不经初始化就定义结构体变量。换句话说,重写构造函数会覆盖掉默认生成的构造函数,为了同时实现两者,需要将默认构造函数手动加上。如:

struct stu
{
    int id;
    char gender;
	// 手动写上默认构造函数,调用该函数就能不经过初始化定义结构体变量
    stu() {} 					
	// 重写的构造函数一,调用该函数需要提供一个参数进行初始化
    stu(char _gender) { gender = _gender; }
	// 重写的构造函数二,调用该函数需要提供两个参数进行初始化
    stu(int _id, char _gender)	
    {
        id = _id;
        gender = _gender;
    }
};

15 cin 与 cout

使用它俩需要加上如下代码:

#include
using namespace std;

cin 的输入不指定格式,也不需要加取地址运算符,直接写变量名即可,如:

cin >> n >> db >> c >> str;

如果想要读入一整行,则需要使用 getline 函数,如:

// 用 getline 读取字符数组
char str[100];
cin.getline(str, 100);
// 用 getline 读取 string
string str;
getline(cin, str);

cout 的输出运算符是 “<<”,输出空格需要手动加上,如:

cout << n << " " << db << " " << "heihei" << endl;

如果需要控制输出精度,需要加上如下头文件:

#include

cout << setiosflags(ios::fixed) << setprecision(2) << 123.4567 << endl;

如果输出需要指定格式用 cout 就很麻烦,此时还是建议使用 printf。另外要注意,cin 和 scanf 最好只选用其中一个而不要混用,cout 和 printf 则可以同时使用。

16 浮点数的比较

浮点数的比较不能使用传统的 “==”,因为数据在计算机中都是以二进制形式储存的,而用二进制很难精确的表示大部分含有小数的十进制数,所以使用另一种方法来比较浮点数:宏定义以及差值判定的方法。

首先定义精度和圆周率(用不到圆周率,但是补充一下):

const double eps = 1e-8;		// 精度:10的-8次方
const double Pi = acos(-1.0); 	// 圆周率

接着书写宏定义:

#define Equ(a, b) ((fabs((a) - (b))) < (eps))	// 相等的宏定义

#define More(a, b) (((a) - (b)) > (eps)) 		// 大于的宏定义

#define Less(a, b) (((a) - (b)) < (-eps))		// 小于的宏定义

#define More(a, b) (((a) - (b)) >= (eps)) 		// 大于等于的宏定义

#define Less(a, b) (((a) - (b)) <= (-eps)) 		// 小于等于的宏定义

解释一下上面的宏定义。当两个数的差值在一个绝对值范围之内时(第一条),可以近似的认为这两个数就是相等的,前提是范围足够小,这个范围就是精度。所以如果 a − b a - b ab 的差值比精度的正值大,则说明 a > b a>b a>b;反之如果比精度的负值小,则说明 a < b aa<b。大于等于和小于等于以此类推。

17 快速读取输入并输出结果

在 .cpp 所在文件夹下新建 “input.txt”,将要输入的数据复制到 txt 文本中,然后在代码的第一行加上如下语句,就不用手动输入测试数据,而会自动从文本文件中读取输入。

#include // 所必需的头文件

freopen("input.txt", "r", stdin);

18 测试数据有多组

利用如下语句实现循环读取多组数据:

while (scanf("%d", &n) != EOF)
{
	...
}
// 或者
while (cin >> n)
{
	...
}

19 闰年的判断

闰年的判断是:“能被4整除但不能被100整除 ”或者 “能被400整除”,即:

if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
    return 1;
else
	return 0;

整除的概念:如果 b % a == 0(读作 b 取模 a,整个等式的意思是 b 取模 a 的结果为0) 或者说 b / a = k(读作 b 除以 a),其中 k 是一个整数,则说明 b 能被 a 整除,b 是被除数,a 是除数。

20 进制转换

20.1 P P P 进制转十进制

若有 P P P 进制数 x = a 1 a 2 ⋅ ⋅ ⋅ a n − 1 a n x = a_1a_2···a_{n-1}a_n x=a1a2⋅⋅⋅an1an(无小数部分), a k a_k ak 表示各个数位上的数,则 x x x 转换为十进制数 y y y 的公式是:
y = a 1 ∗ P n − 1 + a 2 ∗ P n − 2 + . . . + a n − 1 ∗ P 1 + a n ∗ P 0 y = a_1*P^{n - 1} + a_2*P^{n - 2}+...+a_{n-1} * P^1 + a_n * P^0 y=a1Pn1+a2Pn2+...+an1P1+anP0代码实现如下:

int y = 0, product = 1; 		// product 每次循环都乘 P,得到 P、P^2、P^3...
while (x != 0)
{
	y = y + (x % 10) * product; // x % 10 是为了每次获取 x 的个位数
	x = x / 10; 				// 去掉 x 的个位
	product = product * P;
}

秦久韶算法

在这里介绍一下秦久韶算法求解多项式,假设有二进制数101101,现在要转化为十进制数,其计算公式如下:
1 × 2 5 + 0 × 2 4 + 1 × 2 3 + 1 × 2 2 + 0 × 2 1 + 1 × 2 0 1\times 2^5+0\times 2^4+1\times 2^3+1\times 2^2+0\times 2^1+1\times 2^0 1×25+0×24+1×23+1×22+0×21+1×20

不停地提取公因子2可以得到如下过程:

1 × 2 5 + 0 × 2 4 + 1 × 2 3 + 1 × 2 2 + 0 × 2 1 + 1 × 2 0 1\times 2^5+0\times 2^4+1\times 2^3+1\times 2^2+0\times 2^1+1\times 2^0 1×25+0×24+1×23+1×22+0×21+1×20
= ( 1 × 2 4 + 0 × 2 3 + 1 × 2 2 + 1 × 2 1 + 0 × 2 0 ) × 2 + 1 = ( ( 1 × 2 3 + 0 × 2 2 + 1 × 2 1 + 1 × 2 0 ) × 2 + 0 ) × 2 + 1 = ( ( ( 1 × 2 2 + 0 × 2 1 + 1 × 2 0 ) + 1 ) × 2 + 0 ) × 2 + 1 = ( ( ( ( 1 × 2 1 + 0 × 2 0 ) × 2 + 1 ) × 2 + 1 ) × 0 ) × 2 + 1 = ( ( ( ( 1 × 2 + 0 ) × 2 + 1 ) × 2 + 1 ) × 0 ) × 2 + 1 =(1\times 2^4+0\times 2^3+1\times 2^2+1\times 2^1+0\times 2^0)\times 2+1\\=((1\times 2^3+0\times 2^2+1\times 2^1+1\times 2^0)\times 2+0)\times 2+1\\=(((1\times 2^2+0\times 2^1+1\times 2^0)+1)\times 2+0)\times 2+1\\=((((1\times 2^1+0\times 2^0)\times 2+1)\times 2+1)\times 0)\times 2+1\\=((((1\times 2+0)\times 2+1)\times 2+1)\times 0)\times 2+1 =(1×24+0×23+1×22+1×21+0×20)×2+1=((1×23+0×22+1×21+1×20)×2+0)×2+1=(((1×22+0×21+1×20)+1)×2+0)×2+1=((((1×21+0×20)×2+1)×2+1)×0)×2+1=((((1×2+0)×2+1)×2+1)×0)×2+1

假设有 P P P 进制数 x = a 1 a 2 ⋅ ⋅ ⋅ a n x = a_1a_2···a_n x=a1a2⋅⋅⋅an 依次存放于下标从0开始的数组 d d d 中,且 d [ 0 ] = a 1 , d [ 1 ] = a 2 d[0] = a_1, d[1] = a_2 d[0]=a1,d[1]=a2 等等,则可以按照如下算法求出其对应的十进制数 y y y

int y = 0;
for (int i = 0; i < d.length(); ++i)
	y = y * p + d[i];
// 或者直接从最高位的次位开始计算
int y = d[0];
for (int i = 1; i < d.length(); ++i)
	y = y * p + d[i];

20.2 十进制转 P P P 进制数:

将十进制数 y y y 转换为 Q Q Q 进制数 z z z 采用除基取余法。将待转换数除以 Q Q Q,得到的余数作为低位存储在数组中,而商则继续除以 Q Q Q 并重复进行上面的操作,直到商为0为止,然后将所有位从高到低输出就可以得到 z z z

int z[40], num = 0; 	// 数组 z 存放 Q 进制数 y 的每一位,num 为位数
do {
	z[num++] = y % Q;	// 除基取余
	y = y / Q;
} while(y != 0); 		// 当商不为0时进行循环,不要漏了分号

上述代码中使用 do…while 语句而不是 while 语句的原因是:如果十进制数 y y y 恰好等于0,正确结果应该是数组 z z z 中存放了 z [ 0 ] = 0 z[0] = 0 z[0]=0;而使用 while 语句将使循环直接跳出,导致结果出错。

21 链表

当元素个数小于 1 0 5 10^5 105 时使用静态链表,定义的时候注意不要把结构体类型名结构体变量名取成相同的名字。

// 定义静态链表
struct Node
{ 	// Node是结构体类型名
	typename data; 	// 数据域
	int next; 		// 指针域,也即下一个结点的数组下标
} node[size]; 		// node 是数组名,也是结构体变量名

22 HashTable 妙用

如果要统计一个字符串中所有字符各自出现的次数或者是否出现过,可以利用 HashTable 数组,如下所示。因为所有的字符都有唯一的 ASCII 码,因此可以将它们的 ASCII 码当作数组下标(索引)来使用。

int hashTable[128] = {0};
bool hashTable[128] = {false};

但是要注意的是,这样定义只适用于初始值为全0或全 false 的数组,使用下面代码中的做法只会将 hashTable[0] 赋值为1或 true。原因在于数组初始化时,若列表中的元素个数小于指定的数组长度时,不足的元素补以默认值。而在 C/C++ 中默认值是0和 false。

int hashTable[128] = {1};
bool hashTable[128] = {true};

可以使用 memset() 来实现字符数组或者 bool 型数组的初始化,例如下面的代码:

// 初始化 bool 型数组,赋值 true 实际上是赋值1
bool hashTable[128];
memset(hashTable, true, sizeof(hashTable));
// 初始化字符数组
char hashTable[128];
memset(hashTable, 'a', sizeof(hashTable));

memset() 的功能可以参考本篇文章的(9 数组)。

23 最大公约数

两个整数 a a a b b b 的公约数(又称公因数)中最大的那个称为最大公约数,一般用 g c d ( a , b ) gcd(a, b) gcd(a,b) 来表示,常用求解方法为欧几里得算法(辗转相除法),其基本原理是 g c d ( a , b ) = g c d ( b , a % b ) gcd(a, b) = gcd(b, a \% b) gcd(a,b)=gcd(b,a%b)

证明:

  1. a = k b + r a = kb + r a=kb+r k k k r r r 分别为 a   /   b a\ /\ b a / b 的商和余数,则有 r = a − k b = a % b r = a - kb = a \% b r=akb=a%b
  2. d d d a a a b b b 的任意一个公约数,因为 r d = a − k b d = a d − k b d \frac{r}{d} = \frac{a - kb}{d} = \frac{a}{d}-k \frac{b}{d} dr=dakb=dakdb 是一个整数,所以 d d d 也是 r r r 的约数,由此可知 d d d 也是 b b b r r r 的公约数。
  3. 又因为 r = a % b r = a \% b r=a%b,所以 d d d b b b a % b a \% b a%b 的公约数,由 d d d 的任意性可知, a a a b b b 的公约数都是 b b b a % b a \% b a%b 的公约数。
  4. 同理可证 b b b a % b a \% b a%b 的公约数都是 a a a b b b 的公约数,因此 g c d ( a , b ) = g c d ( b , a % b ) gcd(a, b) = gcd(b, a \% b) gcd(a,b)=gcd(b,a%b)

由上面这个定理可知,如果 a < b,那么定理的结果就是将 a 和 b 交换;如果 a > b,通过这个定理可以使数据规模快速减小。利用递归式可以实现欧几里得算法,递归的边界为 b == 0,代码如下:

int gcd(int a, int b)
{
    if (b == 0) return a;
    else return gcd(b, a % b);
}
// 或者简洁点
int gcd(int a, int b)
{
    return !b ? a : gcd(b, a % b);
}

24 最小公倍数

两个整数 a a a b b b 的公倍数中最小的那个称为最小公倍数,它在最大公约数的基础上求得,一般用 l c m ( a , b ) lcm(a, b) lcm(a,b) 来表示,其中 l c m ( a , b ) = a ∗ b / g c d ( a , b ) lcm(a, b) = a * b / gcd(a, b) lcm(a,b)=ab/gcd(a,b)。由于 a ∗ b a * b ab 在实际计算时有可能溢出,因此更恰当的写法是 l c m ( a , b ) = a / g c d ( a , b ) ∗ b lcm(a, b) = a / gcd(a, b) * b lcm(a,b)=a/gcd(a,b)b

25 分数的表示和化简

25.1 分数的表示

首先用结构体来存储分数:

要注意的是,由于分数的乘法和除法的过程中可能使分子或分母超过 int 型表示范围,因此一般情况下,分子和分母应当使用 long long 型来存储。

struct Fraction // 分数结构体
{
    long long up, down; // up 是分子,down 是分母
};

约定三个规则:

  • 分母 down 为非负数。若分数是负数,令分子 up 为负数即可;
  • 如果分数为0,则分子为0,分母为1;
  • 分子和分母没有除了1以外的公约数。

25.2 分数的化简

分三步:

  • 如果分母为负数,令分子 up 和分母 down 变为相反数;
  • 如果分子 up 为0,令分母 down 为1;
  • 约分:求出 up 的绝对值和 dwon 的绝对值的最大公约数 d,然后令分子分母同时除以 d。
Fraction reduction(Fraction result)	// 该函数实现分数的化简
{
    if (result.down < 0) 			// 分母为负,分子分母变相反数
    {
        result.up = -result.up;
        result.down = -result.down;
    }
    else if (result.up == 0) 		// 分子为0,分母变1
        result.down = 1;
    else 
    {	// 约分
        int d = gcd(abs(result.up), abs(result.down));	// 求分子分母的最大公约数
        result.up = result.up / d; 						// 约去最大公约数
        result.down = result.down / d;
    }

    return result;
}

25.3 分数的四则运算

加法公式: r e s u l t = f 1. u p ∗ f 2. d o w n + f 2. u p ∗ f 1. d o w n f 1. d o w n ∗ f 2. d o w n result = \frac{f1.up * f2.down + f2.up * f1.down}{f1.down * f2.down} result=f1.downf2.downf1.upf2.down+f2.upf1.down

Fraction add(Fraction f1, Fraction f2) 		// 分数加法
{
    Fraction result;
    result.up = f1.up * f2.down + f2.up * f1.down;
    result.down = f1.down * f2.down;
    return reduction(result);				// 返回化简后的结果
}

减法公式: r e s u l t = f 1. u p ∗ f 2. d o w n − f 2. u p ∗ f 1. d o w n f 1. d o w n ∗ f 2. d o w n result = \frac{f1.up * f2.down - f2.up * f1.down}{f1.down * f2.down} result=f1.downf2.downf1.upf2.downf2.upf1.down

Fraction minus(Fraction f1, Fraction f2)	// 分数减法
{
    Fraction result;
    result.up = f1.up * f2.down - f2.up * f1.down;
    result.down = f1.down * f2.down;
    return reduction(result);				// 返回化简后的结果
}

乘法公式: r e s u l t = f 1. u p ∗ f 2. u p f 1. d o w n ∗ f 2. d o w n result = \frac{f1.up * f2.up}{f1.down * f2.down} result=f1.downf2.downf1.upf2.up

Fraction multiply(Fraction f1, Fraction f2)	 // 分数乘法
{
    Fraction result;
    result.up = f1.up * f2.up;
    result.down = f1.down * f2.down;
    return reduction(result);				// 返回化简后的结果
}

除法公式: r e s u l t = f 1. u p ∗ f 2. d o w n f 1. d o w n ∗ f 2. u p result = \frac{f1.up * f2.down}{f1.down * f2.up} result=f1.downf2.upf1.upf2.down

Fraction divide(Fraction f1, Fraction f2) 	// 分数除法
{
    Fraction result;
    result.up = f1.up * f2.down;
    result.down = f1.down * f2.up;
    return reduction(result);				// 返回化简后的结果
}

25.4 分数的输出

四个注意点:

  • 输出前先化简;
  • 若分母 down 为1,说明其是整数。视题目而定要不要省略分母;
  • 分子 up 的绝对值大于分母 down 时,说明其为假分数,需要按带分数的形式输出。整数部分为 r.up / r.down(因为 r.up 可能为负数,故用除法可以保证负号给到假分数的整数部分),分子部分为 abs(r.up) % r.down,分母部分不变;
  • 以上均不满足时说明分数 r 是真分数,按原样输出即可。
void showResult(Fraction r)
{
    r = reduction(r);	// 化简
    
    if (r.down == 1) 							// 整数
        printf("%lld", r.up);
    else if (abs(r.up) > r.down) 				// 假分数
        printf("%lld %lld / %lld", r.up / r.down, abs(r.up) % r.down, r.down);
    else
        printf("%lld / %lld", r.up, r.down);	// 真分数
}

26 素数(质数)

素数指除了1和本身之外不能被其他数整除(不包含其他因子)的一类数。用代码逻辑来说,即对于比1大比 n 小的任意一个整数 a(1 < a < n),都有 n % a != 0 成立,那么就称 n 是素数;如果存在 n % a == 0 则称 n 为合数。特别注意,1既不是素数也不是合数。

26.1 素数的判断

要判断 n 是否为素数,如果从2逐个枚举到 n - 1,当题目数据比较大时算法的时间复杂度也会很大。不妨假设在2 ~ (n - 1) 中存在 n 的因子 k,即有 n % k == 0,由 k ∗ n k = n k * \frac{n}{k} = n kkn=n 可以知道,n / k 也是 k 的一个约数,而这两者必有一个 ≤ n \leq \sqrt {n} n 。所以若 n 不是素数,则至少存在一个因子 a( 1 ≤ a ≤ n 1\leq a \leq \sqrt{n} 1an ),所以只需要判断 2 ∼ n 2 \sim \sqrt{n} 2n 的数即可。

由于 sqrt 函数返回 double 类型的数据,故需要强制转换类型。同时 sqrt 函数的形参为 double 类型,故将 n 乘以1.0转为浮点数。

bool isPrime(int n)
{
    if (n <= 1) return false;		// 特判
    int sqr = (int)sqrt(1.0 * n);	// sqr 存储根号 n
    for (int i = 2; i <= sqr;  ++i) // 遍历2 ~ 根号 n
        if (n % i == 0)
            return false; 			// 存在 n 的因子,n 不是素数,返回 false
    return true; 					// n 是素数,返回 true
}

// 更简单的写法,不需要使用 sqrt 函数
bool isPrime(int n)
{
    if (n <= 1) return false;
    for (int i = 2; i * i <= n;  ++i)
        if (n % i == 0)
            return false;
    return true;
}

26.2 获取素数表

经过上一小节的处理,求素数的时间复杂度可以降低到 O ( n ) O(\sqrt{n}) O(n ),如果在一开始就需要获取某个范围内的所有素数,该算法时间复杂度依然显得有些过大。下面介绍基于两个更优思想下,获取范围 n 内所有素数(素数表)的算法。

两个思想的思想都是创造一个更加高效的筛选算法,而筛选的核心步骤是标记,所以你可以理解为,筛选的过程就是将某个数标记为“是否为素数”的过程。

26.2.1 埃氏筛法

核心思想:从2开始,将每个质数的倍数都标记成合数,以达到筛选素数的目的。时间复杂度可以降低到 O ( n l o g l o g n ) O(nloglogn) O(nloglogn)

  • 定义数组 prime 存放所有的素数,定义布尔数组 p,p[i] = false 表明数 i 是素数;
  • 第一层循环,从2枚举到 n - 1;
  • i = x(2 <= x <= n - 1) 时,若 p[i] = false,说明 i 是素数, 将 i 存入数组 prime 中;
  • 第二层循环,从 i * i 开始,令数组 p 中所有下标值为 i 的倍数的元素的值为 true;
  • 重复上述步骤,直到枚举结束。

对第二层循环从 i * i 开始做一个解释:假如 i = 5,那么循环就是从 5*5 开始。你且看 5*2、5*3、5*4 这几个数,其实已经在2的倍数、3的倍数和4的倍数时就被标记为 true 了,所以无需再标记一遍。

const int maxn = 500; 		// 预设的表长
int prime[maxn], pNum = 0; 	// prime 数组存放素数,pNum 统计素数的个数
bool p[maxn] = {false};		// p 数组是素数表,p[i] = false 表明 i 是素数

void eratosthenes()
{
	for (int i = 2; i < maxn; ++i)	// 枚举 2 ~ (n - 1)
    {
        if (p[i] == false)			// 如果 i 是素数
        {
            prime[pNum++] = i; 		// 将素数存入 prime 数组中
            for (int j = i * i; j < maxn; j += i)
                p[j] = true;
        }
    }
}

26.2.2 欧拉筛法

当数据范围超过 1 0 6 10^6 106 时,即使是埃氏筛法都不一定能满足较低的时间要求了。因为在埃氏筛法中存在重复筛选的问题:对于某一个合数来说,多次执行了 p[j] = true。比如合数 12 = 2 * 6 = 3 * 4,会被素数2和3同时筛选一次,欧拉筛法用于解决这一问题。欧拉筛法的核心思想在于,每一个合数都只能被其最小质因子(即最小的素因子)筛去。而最小质因子是唯一的,因此每个合数都只被筛选一次,从而使得算法的时间复杂度降为 O ( n ) O(n) O(n)。下面是欧拉筛法的原理。

首先需要知道整数惟一分解定理:任何一个大于1的自然数都可以用有限个素数的积来表示,1不属于素数。因此对于每一个合数来说,都可以分解成有限个质因子的乘积,其中一定有一个最小的质因子。通过最小质因子,就可以保证每个合数都只被筛选一次。对于合数 n n n 来说,有一个重要的结论: n = F a c t o r m a x ∗ P n = Factor_{max} * P n=FactormaxP 若令 F a c t o r m a x Factor_{max} Factormax 表示合数 n n n 最大的因数(不等于 n n n 且不论是不是素数),则 P P P 一定满足以下两个条件:

  1. P P P 是素数。
  2. P P P 是最小的因数(1除外)。

用反证法证明如下:

证明1:假设 P P P 是合数,由合数定理可知其可以由有限个素数相乘得到,故可以写成 P = P 1 ∗ P 2 ∗ P 3 ∗ . . . ∗ P k P = P_1*P_2*P_3*...*P_k P=P1P2P3...Pk,其中 P 1 ≤ P 2 ≤ . . . ≤ P k P_1 \leq P_2 \leq ... \leq P_k P1P2...Pk 且它们均为素数。此时有, n = F a c t o r m a x ∗ P = F a c t o r m a x ∗ P 1 ∗ P 2 ∗ . . . ∗ P k = F a c t o r n e w ∗ P 2 ∗ . . . ∗ P k n = Factor_{max} * P = Factor_{max} * P_1*P_2*...*P_k = Factor_{new}*P_2*...*P_k n=FactormaxP=FactormaxP1P2...Pk=FactornewP2...Pk 其中 F a c t o r n e w = F a c t o r m a x ∗ P 1 Factor_{new} = Factor_{max} * P_1 Factornew=FactormaxP1,由此可知,存在 F a c t o r n e w > F a c t o r m a x Factor_{new} > Factor_{max} Factornew>Factormax,使得其与 F a c t o r m a x Factor_{max} Factormax 是合数 n n n 最大的因数” 产生矛盾,假设不成立,假设的反面成立,所以 P P P 不是合数而是素数。

证明2:假设 P P P 不是最小的因数,令 F a c t o r m a x = P 1 ∗ P 2 ∗ P 3 ∗ . . . ∗ P k Factor_{max} = P_1*P_2*P_3*...*P_k Factormax=P1P2P3...Pk ,且 P > P 1 P > P_1 P>P1,则由证明1的推导可知 F a c t o r n e w = P ∗ P 2 ∗ P 3 ∗ . . . ∗ P k > F a c t o r m a x Factor_{new} = P * P_2*P_3*...*P_k > Factor_{max} Factornew=PP2P3...Pk>Factormax,其与 F a c t o r m a x Factor_{max} Factormax 是合数 n 最大的因数” 产生矛盾,假设不成立,假设的反面成立,所以 P P P 是最小的因数。

由此便证明了 P P P 是最小的质因数,接下来的问题就是确定合数 n n n 的最大因数。在枚举 2 ~ n - 1 时,每次都将 i 当做最大的因数,将 prime 中的素数当做最小质因子。

void euler()
{
    for (int i = 2; i < maxn; ++i)		// 枚举 2 ~ (n - 1)
    {
        if (p[i] == false)				// 如果 i 是素数
            prime[pNum++] = i; 			// 将素数存入 prime 数组中
        for (int j = 0; i * prime[j] <= maxn; ++j)
        { 	// 将 i 当做最大因数,prime[j] 当做最小质因子来筛选合数
            p[i * prime[j]] = true;		// 数 i * prime[j] 被质因子 prime[j] 筛掉,换句话说,被标记
            if (i % prime[j] == 0)
                break;
        }
    }
}

算法的核心语句在于 if (i % prime[j] == 0) 判定为真后就跳出内层循环(执行 break 语句),证明:

假设 i % p r i m e [ j ] = = 0 i \% prime[j] == 0 i%prime[j]==0 为真时不跳出内层循环。设此时的 i = i 1 i = i_1 i=i1 ,则有 i 1 % p r i m e [ j ] = 0 i_1 \% prime[j] = 0 i1%prime[j]=0,不妨令 i 1 = p r i m e [ j ] ∗ k i_1 = prime[j] * k i1=prime[j]k。因为不跳出内层循环,所以在下一次内层循环中将执行语句 p [ i 1 ∗ p r i m e [ j + 1 ] ] = t r u e p[i_1 * prime[j + 1]] = true p[i1prime[j+1]]=true,即数 P = i 1 ∗ p r i m e [ j + 1 ] P = i_1 * prime[j + 1] P=i1prime[j+1] 被标记为合数,从而可以说 P P P 能被最小质因子 p r i m e [ j + 1 ] prime[j + 1] prime[j+1] 筛掉,此时的最大因数是 i 1 i_1 i1

由于, P = i 1 ∗ p r i m e [ j + 1 ] = p r i m e [ j ] ∗ k ∗ p r i m e [ j + 1 ] P=i_1 * prime[j + 1] = prime[j] * k * prime[j + 1] P=i1prime[j+1]=prime[j]kprime[j+1]

i 2 i_2 i2 满足 i 2 ≤ m a x n i_2 \leq maxn i2maxn i 2 = k ∗ p r i m e [ j + 1 ] i_2 = k * prime[j + 1] i2=kprime[j+1],则有, P = p r i m e [ j ] ∗ k ∗ p r i m e [ j + 1 ] = p r i m e [ j ] ∗ i 2 P=prime[j] * k * prime[j + 1] = prime[j] * i_2 P=prime[j]kprime[j+1]=prime[j]i2

综合可得, P = i 1 ∗ p r i m e [ j + 1 ] = i 2 ∗ p r i m e [ j ] P = i_1 * prime[j+1] = i_2 * prime[j] P=i1prime[j+1]=i2prime[j]

故也可以这么说: P P P 能被最小质因子 p r i m e [ j ] prime[j] prime[j] 筛掉,此时的最大因数是 i 2 i_2 i2。很明显,不论是 p r i m e [ j + 1 ] prime[j + 1] prime[j+1] 还是 p r i m e [ j ] prime[j] prime[j] 都可筛去数 P P P,但因为 p r i m e prime prime 是递增数组,所以实际上 p r i m e [ j ] prime[j] prime[j] 才是更小的质因子而不是 p r i m e [ j + 1 ] prime[j+1] prime[j+1] 。故在 i % p r i m e [ j ] = = 0 i \% prime[j] == 0 i%prime[j]==0 为真时需要跳出内层循环,以防止数 P P P 在下次内层循环中被质因子 p r i m e [ j + 1 ] prime[j+1] prime[j+1] 筛掉。不用担心,因为数 P P P 必定会 i i i 增大后,被真正的最小质因子筛去。这样就能保证每个合数只被筛去一次。

27 质因子分解

本节讲的就是上一节中的整数惟一分解定理:任何一个数都可以用有限个素数的积来表示。所谓质因子分解就是指求出数 n 的所有质因子。由于每一个质因子不唯一,因此定义结构体 factor:

struct factor
{
	int x, cnt; // x 为质因子,cnt 为该质因子的个数
} fac[10];

fac 数组的大小视题目情况而定,例如:如果只会出现 int 范围内的整数,那么只需要开到10即可,因为 2*3*5*7*11*13*17*19*23*29 才刚刚超过 int 的范围。由(26.1 素数的判断)中的讨论可知,对于一个正整数 n 来说,它的因子对中的两个数一定是在 n \sqrt n n 的左右成对出现,例如 16 = 1 * 16 = 2 * 8 = 4 * 4 而 16 = 4 \sqrt{16} = 4 16 =4。由此,如果一个正整数存在 [2, n) 范围内的质因子,它们一定满足以下两种情况之一:

  • 全部质因子都小于等于 n \sqrt n n
  • 只存在一个大于 n \sqrt n n 的质因子,其余质因子全部小于等于 n \sqrt n n

所以思路便是:

  1. 枚举 1 ~ n \sqrt n n 内的所有素数 p,判断其是否是 n 的因子。该步骤的前提是利用(26 素数)中的方法求出了素数表
  2. 若 p 是 n 的因子,则在 fac 数组中添加 p,令其 cnt = 0。然后用 p 不断整除 n,每整除一次 cnt 加1,该操作是为了求出该质因子的个数;若不能整除则枚举下一个素数,直至 n 为1或 1 ~ n \sqrt n n 内的素数都枚举完。
  3. 前两步执行完后,若 n 不为1,说明 n 符合第二种情况,有且只有一个大于 n \sqrt n n 的质因子,此时把这个质因子加入 fac 数组,令 cnt 为1;若为1,说明已经找出其所有质因子,算法结束。

解释一下 (int)sqrt(n * 1.0):因为 prime[i] 是 int 型,而 sqrt 函数返回浮点型数据,不同类型数据无法比较,所以进行强制类型转换;同样,sqrt 函数只接受浮点型的参数,因此将 n 乘上1.0转换为浮点型进行运算。

int num = 0; // num 表示数组 fac 中元素个数,也表示质因子的个数
void decomposed(int n)
{
    int temp = n, sqr = (int)sqrt(n * 1.0);	// 用 temp 暂存整数 n 的值
    // 依据素数表,枚举1到根号 n 内的所有素数
    for (int i = 0; i < pNum && prime[i] <= sqr; ++i)	
    {	 // 循环的条件要多注意一下
        if (temp % prime[i] == 0) 			// 如果 prime[i] 是 temp 的质因子
        {
            fac[num].x = prime[i]; 			// 记录该质因子
            fac[num].cnt = 0;				// 初始计数值为0
            while (temp % prime[i] == 0) 	// 计算出质因子 prime[i] 的个数
            {
                ++fac[num].cnt;				// 计数值+1
                temp /= prime[i];
            }
            ++num; 							// 不同质因子个数加1
        }
        if (temp == 1) break; 				// 及时退出循环节省时间
    }

    if (temp != 1) 			// 如果无法被根号 n 以内的质因子除尽
    {
        fac[num].x = temp;	// 那么一定有一个大于根号 n 的质因子
        fac[num++].cnt = 1;
    }
}

28 求因子个数和因子之和

求一个正整数 n 的因子个数(是指所有的因子,包括1和本身),需要首先对其进行质因子分解。假设得到的质因子为 p i p_i pi,个数为 e i e_i ei,那么 n 的因子个数就是: ∏ i = 1 k ( e i + 1 ) = ( e 1 + 1 ) ∗ ( e 2 + 1 ) ∗ . . . ∗ ( e k + 1 ) \prod_{i = 1}^{k}(e_i + 1) = (e_1 + 1) * (e_2 + 1)*...*(e_k + 1) i=1k(ei+1)=(e1+1)(e2+1)...(ek+1)这个就是约数个数定理

证明:由整数惟一分解定理可以知道,若 n 是质数,则其因子个数为2,即1和本身。若 n 是合数,则有: n = p 1 e 1 ∗ p 2 e 2 ∗ . . . ∗ p k e k n = p_1^{e_1} * p_2^{e_2} * ... * p_k^{e_k} n=p1e1p2e2...pkek

对于数 p 1 e 1 p_1^{e_1} p1e1 来说,其因子有 p 1 0 , p 1 1 , p 1 2 , . . . , p 1 e 1 p_1^{0}, p_1^{1}, p_1^{2},...,p_1^{e_1} p10,p11,p12,...,p1e1,共 e 1 + 1 e_1 + 1 e1+1 个;同理 p 2 e 2 p_2^{e_2} p2e2 的因子有 p 2 0 , p 2 1 , p 2 2 , . . . , p 2 e 2 p_2^{0}, p_2^{1}, p_2^{2},...,p_2^{e_2} p20,p21,p22,...,p2e2 ,共 e 2 + 1 e_2 + 1 e2+1 个;······; p k e k p_k^{e_k} pkek 的因子有 p k 0 , p k 1 , p k 2 , . . . , p k e k p_k^{0},p_k^{1}, p_k^{2},...,p_k^{e_k} pk0,pk1,pk2,...,pkek,共 e k + 1 e_k + 1 ek+1 个。由乘法定理可知 n 的因子个数为 ( e 1 + 1 ) ∗ ( e 2 + 1 ) ∗ . . . ∗ ( e k + 1 ) (e_1 + 1) * (e_2 + 1)*...*(e_k + 1) (e1+1)(e2+1)...(ek+1)

例如12 = 2 * 2 * 3,因子个数为 (2 + 1) * (1 + 1) = 6,而12所有的因子是1, 2, 3, 4, 6, 12,符合要求。

最后,由上面的证明可以知道 n 的所有因子之和应为: ( 1 + p 1 + p 1 2 + . . .   + p 1 e 1 ) ∗ ( 1 + p 2 + p 2 2 + . . .   + p 2 e 2 ) ∗ . . . ∗ ( 1 + p k + p k 2 + . . .   + p k e k ) = 1 − p 1 e 1 + 1 1 − p 1 ∗ 1 − p 2 e 2 + 1 1 − p 2 ∗ . . . ∗ 1 − p k e k + 1 1 − p k (1 + p_1 + p_1^2 +...\ +p_1^{e_1}) * (1 + p_2 + p_2^2 +...\ +p_2^{e_2}) * ... * (1 + p_k + p_k^2 +...\ +p_k^{e_k})\\=\\\frac{1-p_1^{e_1 + 1}}{1 - p_1} * \frac{1-p_2^{e_2 + 1}}{1 - p_2}*...*\frac{1-p_k^{e_k + 1}}{1 - p_k} (1+p1+p12+... +p1e1)(1+p2+p22+... +p2e2)...(1+pk+pk2+... +pkek)=1p11p1e1+11p21p2e2+1...1pk1pkek+1

29、大整数运算

大整数也称高精度整数,是用基本数据类型无法存储的整数。比如一千位的整数,已不能再用普通的数据类型来定义并进行相应的运算了,而需要利用四则运算的基本原理来实现大整数的运算(也称高精度运算)。

29.1 大整数的存储

用一个数组 d 存储大整数的各个数位上的数,整数高位存储在数组的高位,整数低位存储在数组的低位。而整数高位是在数的左侧,而数组高位是下标更大的一侧,所以大整数在数组中是反过来的,如下所示。因为四则运算都是从整数的低位开始的,这种顺位存储符合这种计算的思维。
PAT OJ 刷题必备知识总结_第2张图片

#include 	// memset 函数的头文件

const int maxn = 1000;

struct bign 		// 定义大整数 big number 的结构体
{
    int d[maxn]; 	// 数组 d 存储大整数的各个数位
    int len; 		// len 表示大整数的位数
    bign() 			// 初始化结构体的构造函数,定义结构体变量时自动对该变量初始化
    {
        memset(d, 0, sizeof(d));
        len = 0;
    }
};

C 语言标准库的头文件之一,包含了一些字符串/内存处理相关的函数(如 strcpy,memcpy 等)。
< cstring> 是 C++ 语言标准库的头文件之一,基本上就是 的 C++ 版本,当编写C++ 程序时如果需要使用 ,则应当用 < cstring> 代替。
< string> 是 C++ 语言标准库的头文件之一,主要包含了 string 模板及其相关函数。

大整数是以字符串的形式输入的,所以不能直接存入结构体,需要进行相应的处理。同时,由于顺位存储,需要从字符串的末尾开始储存。

bign change(char str[]) 	// 将大整数字符串存入结构体,参数传入 string str 也可以
{
    bign a;
    a.len = strlen(str); 	// a 的长度就是 str 的长度
    for (int i = a.len - 1; i >= 0; --i)
        a.d[i] = str[a.len - 1 - i] - '0'; // 从字符串末尾开始存储

    return a;
}

29.2 大整数的比较

  1. 先比较整数的长度,len 更大的大整数更大;
  2. 如果 len 相等,则从高位往低位比较,数位大者更大。
    由前面知道,大整数的高位是存储在数组 d 的高位的,所以在循环的时候要从数组的最后一位开始比较。
// 比较 a 和 b 的大小,a 大、相等、a 小分别返回1、0、-1
int compare(bign a, bign b) 
{
    if (a.len > b.len) return 1; 				// a 大
    else if (a.len < b.len) return -1;			// a 小
    else
    {
        for (int i = a.len - 1; i >= 0; --i)	// 从大整数高位往低位比较
        {
            if (a.d[i] > b.d[i]) return 1;
            else if (a.d[i] < b.d[i]) return -1;
        }
    }
    return 0;	// 循环正常结束,说明两者完全相等,返回0
}

29.3 大整数的四则运算

29.3.1 高精度加法

  1. 大整数的低位往高位逐位相加;
  2. 将得到的结果取个位数作为该位相加后的结果,取十位数作为进位。
// 高精度 a + b
bign add(bign a, bign b)
{
    bign c;
    int temp, carry = 0; 	// temp 暂存数位的乘积,carry 表示进位
    for (int i = 0; i < a.len || i < b.len; ++i)	// 以较长的大整数为界限
    {
        temp = a.d[i] + b.d[i] + carry; 			// 对应位与进位相加
        c.d[c.len++] = temp % 10; 					// 个位作为该位相加的结果
        carry = temp / 10; 							// 十位作为新的进位
    }
    if (carry != 0) // 如果最后的进位不为0,则直接复制结果给 c 的最高位
        c.d[c.len++] = carry;

    return c;
}

29.3.2 高精度减法

  1. 大整数的低位往高位相减;
  2. 如果不够减,则从高位借1,该位加10后再进行减法;
  3. 最后要注意,做完减法后最高位可能为0(因为减掉了),需要去除多余的0,但也要保证结果至少有一位数,比如相同的大整数相减结果为0。
  4. 默认做减法时 a 比 b 大。
// 高精度 a - b
bign substract(bign a, bign b)
{
    bign c = a;
    for (int i = 0; i < a.len || i <b.len; ++i) // 以较长的大整数为界限
    {
        if (c.d[i] < b.d[i])		// 如果不够减
        {
            --c.d[i + 1];			// 向高位借一位,高位减一
            c.d[i] += 10;			// 当前位加10
        }
        c.d[i] = c.d[i] - b.d[i];	// 减法结果为当前位结果
    }
    // 如果最高位数是0且大整数长度大于2,最高位被减掉了
    while (c.d[c.len - 1] == 0 && c.len - 1 >= 1)
        --c.len;	// 去除最高位的0,同时至少保留一位最低位

    return c;
}

29.3.3 高精度与低精度的乘法

低精度整数是指可以用基本数据类型存储的整数,乘法的思想类似于高精度的加法。

  1. 从大整数的低位往高位逐位与低精度的数相乘;
  2. 取个位作为该位相乘后的结果,其余的都作为进位,在下一步循环中加到高位与低精度的数相乘的结果上去;
  3. 重复上述步骤,要注意的是如果最后进位不为0,需要进行循环进位直至进位为0为止。
// 高精度 a * 低精度 b
bign multiply(bign a, int b) // 高精度 a 与低精度 b 的乘法
{
    bign c;
    int temp, carry = 0; 				// carry 是进位
    for (int i = 0; i < a.len; ++i)
    {
        temp = a.d[i] * b  + carry; 	// 相乘后要加上低位的进位
        c.d[c.len++] = temp % 10; 		// 取余数(个位)更新该位数值
        carry = temp / 10; 				// 取所有剩余部分作为新的进位
    }
    while (carry != 0) // 乘法的进位有可能含有多位,故需要用到循环
    {
        c.d[c.len++] = carry % 10;
        carry /= 10;
    }

    return c;
}

29.3.4 高精度与低精度的除法

联系整数的除法来理解下面的步骤:

  1. 从大整数的高位往低位逐位除以低精度数;
  2. 每一位做除法之前,首先要加上前面的数做除法后的余数*10
  3. 如果不够除(小于低精度的数),则该位商为0,新的余数为余数*10加上该位;如果够除,则该位的商即为对应的商,余数即为对应的余数,乘10加到下一位去。
  4. 最后依然要注意去除高位的0,同时至少保留一位数。

将余数作为引用传入是因为函数只能返回一个值,如果题目要求返回余数,可以通过引用的方式得到结果。

// 高精度 a / 低精度 b,r 为余数
bign divide(bign a, int b, int &r) 
{
    bign c;
    c.len = a.len;	// 被除数的每一位和商的每一位是一一对应的,因此先令长度相等
    int r = 0;		// 余数
    for (int i = a.len - 1; i >=0; --i) // 除法从高位开始计算
    {
        r = r * 10 + a.d[i];	// 余数乘10加到该位上
        if (r < b)				// 不够除,该位商为0
            c.d[i] = 0;
        else					// 够除
        {
            c.d[i] = r / b;		// 求出该位的商
            r = r % b;			// 计算余数
        }
    }
    while (c.len - 1 >= 1 && c.d[c.len - 1] == 0)
        --c.len;	// 去除高位的0,同时至少保留一位最低位

    return c;
}

希望本篇博客能对你有所帮助,也希望看官能动动小手点个赞哟~~。

你可能感兴趣的:(PTA,PTA,算法,数据结构,OJ,笔记)