前言:文章篇幅较长,仅仅是总结了一些比较基础的知识,读者可以按照目录快速访问自己想了解的内容。
绝对值在 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 231−1 的初值时需要在最后面加上 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字节(一次即可读取)。
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 码值。
定义最大 int 型正整数的两种方法:本质都是定义值为 2 32 − 1 2^{32} - 1 232−1 的变量。
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
容易出错的格式控制符:long long 为 %lld;float 为 %f;double 为 %lf;long double 为 %llf;数组或字符串不需要加取地址运算符 “&”。
另外 %c 能够读入空格符跟换行符,而 %s 通过空格或换行来识别字符串的结束。
scanf("%s", str);
printf("%s", str);
此处输入 abcd efg 后,打印出来的是 abcd。
在 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位有效数字。
规则:
从统计学的角度,“四舍六入五成双”比“四舍五入”要科学,在大量运算时,它使舍入后的结果误差的均值趋于零,而不是像四舍五入那样逢五就入,导致结果偏向大数,使得误差产生积累进而产生系统误差,“四舍六入五成双”使测量结果受到舍入误差的影响降到最低。
前者获取单个字符,后者从打印单个字符。getchar 可以识别并读取换行符。
用来给某一对象取别名。
typedef long long LL; // typedef 给 long long 取了个别名 LL
LL a = 123456789012345LL;
以下函数返回的均是 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 的第一位小数四舍五入后的取整值 |
如果数组比较大(大概 1 0 6 10^6 106 级别),则需要将其定义在主函数外面,否则会使程序异常退出。
定义数组 a 并初始化的两种方法:
#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;
}
如果要对数组赋其他值(例如1、2等)就使用 fill(),区别在于 memset() 执行速度更快,毕竟它是直接对内存进行操作的,是对较大的数组或结构体进行清零初始化的最快方法。
例如
例如为数组中的每个元素赋其下标值:
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。
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 类型的版本
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’。
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
引用产生的是变量的别名,因此常量不可使用引用。
通过重写构造函数可以实现对结构体变量的快速初始化,但如果重写过构造函数,就不能不经初始化就定义结构体变量。换句话说,重写构造函数会覆盖掉默认生成的构造函数,为了同时实现两者,需要将默认构造函数手动加上。如:
struct stu
{
int id;
char gender;
// 手动写上默认构造函数,调用该函数就能不经过初始化定义结构体变量
stu() {}
// 重写的构造函数一,调用该函数需要提供一个参数进行初始化
stu(char _gender) { gender = _gender; }
// 重写的构造函数二,调用该函数需要提供两个参数进行初始化
stu(int _id, char _gender)
{
id = _id;
gender = _gender;
}
};
使用它俩需要加上如下代码:
#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 则可以同时使用。
浮点数的比较不能使用传统的 “==”,因为数据在计算机中都是以二进制形式储存的,而用二进制很难精确的表示大部分含有小数的十进制数,所以使用另一种方法来比较浮点数:宏定义以及差值判定的方法。
首先定义精度和圆周率(用不到圆周率,但是补充一下):
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 a−b 的差值比精度的正值大,则说明 a > b a>b a>b;反之如果比精度的负值小,则说明 a < b aa<b。大于等于和小于等于以此类推。
在 .cpp 所在文件夹下新建 “input.txt”,将要输入的数据复制到 txt 文本中,然后在代码的第一行加上如下语句,就不用手动输入测试数据,而会自动从文本文件中读取输入。
#include // 所必需的头文件
freopen("input.txt", "r", stdin);
利用如下语句实现循环读取多组数据:
while (scanf("%d", &n) != EOF)
{
...
}
// 或者
while (cin >> n)
{
...
}
闰年的判断是:“能被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 是除数。
若有 P P P 进制数 x = a 1 a 2 ⋅ ⋅ ⋅ a n − 1 a n x = a_1a_2···a_{n-1}a_n x=a1a2⋅⋅⋅an−1an(无小数部分), 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=a1∗Pn−1+a2∗Pn−2+...+an−1∗P1+an∗P0代码实现如下:
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];
将十进制数 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 语句将使循环直接跳出,导致结果出错。
当元素个数小于 1 0 5 10^5 105 时使用静态链表,定义的时候注意不要把结构体类型名和结构体变量名取成相同的名字。
// 定义静态链表
struct Node
{ // Node是结构体类型名
typename data; // 数据域
int next; // 指针域,也即下一个结点的数组下标
} node[size]; // node 是数组名,也是结构体变量名
如果要统计一个字符串中所有字符各自出现的次数或者是否出现过,可以利用 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 数组)。
两个整数 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)。
证明:
由上面这个定理可知,如果 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);
}
两个整数 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)=a∗b/gcd(a,b)。由于 a ∗ b a * b a∗b 在实际计算时有可能溢出,因此更恰当的写法是 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。
首先用结构体来存储分数:
要注意的是,由于分数的乘法和除法的过程中可能使分子或分母超过 int 型表示范围,因此一般情况下,分子和分母应当使用 long long 型来存储。
struct Fraction // 分数结构体
{
long long up, down; // up 是分子,down 是分母
};
约定三个规则:
分三步:
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;
}
加法公式: 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.down∗f2.downf1.up∗f2.down+f2.up∗f1.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.down∗f2.downf1.up∗f2.down−f2.up∗f1.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.down∗f2.downf1.up∗f2.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.down∗f2.upf1.up∗f2.down
Fraction divide(Fraction f1, Fraction f2) // 分数除法
{
Fraction result;
result.up = f1.up * f2.down;
result.down = f1.down * f2.up;
return reduction(result); // 返回化简后的结果
}
四个注意点:
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); // 真分数
}
素数指除了1和本身之外不能被其他数整除(不包含其他因子)的一类数。用代码逻辑来说,即对于比1大比 n 小的任意一个整数 a(1 < a < n),都有 n % a != 0 成立,那么就称 n 是素数;如果存在 n % a == 0 则称 n 为合数。特别注意,1既不是素数也不是合数。
要判断 n 是否为素数,如果从2逐个枚举到 n - 1,当题目数据比较大时算法的时间复杂度也会很大。不妨假设在2 ~ (n - 1) 中存在 n 的因子 k,即有 n % k == 0,由 k ∗ n k = n k * \frac{n}{k} = n k∗kn=n 可以知道,n / k 也是 k 的一个约数,而这两者必有一个 ≤ n \leq \sqrt {n} ≤n。所以若 n 不是素数,则至少存在一个因子 a( 1 ≤ a ≤ n 1\leq a \leq \sqrt{n} 1≤a≤n),所以只需要判断 2 ∼ n 2 \sim \sqrt{n} 2∼n 的数即可。
由于 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;
}
经过上一小节的处理,求素数的时间复杂度可以降低到 O ( n ) O(\sqrt{n}) O(n),如果在一开始就需要获取某个范围内的所有素数,该算法时间复杂度依然显得有些过大。下面介绍基于两个更优思想下,获取范围 n 内所有素数(素数表)的算法。
两个思想的思想都是创造一个更加高效的筛选算法,而筛选的核心步骤是标记,所以你可以理解为,筛选的过程就是将某个数标记为“是否为素数”的过程。
核心思想:从2开始,将每个质数的倍数都标记成合数,以达到筛选素数的目的。时间复杂度可以降低到 O ( n l o g l o g n ) O(nloglogn) O(nloglogn)。
对第二层循环从 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;
}
}
}
当数据范围超过 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=Factormax∗P 若令 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 是合数,由合数定理可知其可以由有限个素数相乘得到,故可以写成 P = P 1 ∗ P 2 ∗ P 3 ∗ . . . ∗ P k P = P_1*P_2*P_3*...*P_k P=P1∗P2∗P3∗...∗Pk,其中 P 1 ≤ P 2 ≤ . . . ≤ P k P_1 \leq P_2 \leq ... \leq P_k P1≤P2≤...≤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=Factormax∗P=Factormax∗P1∗P2∗...∗Pk=Factornew∗P2∗...∗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=Factormax∗P1,由此可知,存在 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=P1∗P2∗P3∗...∗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=P∗P2∗P3∗...∗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[i1∗prime[j+1]]=true,即数 P = i 1 ∗ p r i m e [ j + 1 ] P = i_1 * prime[j + 1] P=i1∗prime[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=i1∗prime[j+1]=prime[j]∗k∗prime[j+1]
设 i 2 i_2 i2 满足 i 2 ≤ m a x n i_2 \leq maxn i2≤maxn 且 i 2 = k ∗ p r i m e [ j + 1 ] i_2 = k * prime[j + 1] i2=k∗prime[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]∗k∗prime[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=i1∗prime[j+1]=i2∗prime[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 增大后,被真正的最小质因子筛去。这样就能保证每个合数只被筛去一次。
本节讲的就是上一节中的整数惟一分解定理:任何一个数都可以用有限个素数的积来表示。所谓质因子分解就是指求出数 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) 范围内的质因子,它们一定满足以下两种情况之一:
所以思路便是:
解释一下 (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;
}
}
求一个正整数 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=1∏k(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=p1e1∗p2e2∗...∗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)=1−p11−p1e1+1∗1−p21−p2e2+1∗...∗1−pk1−pkek+1
大整数也称高精度整数,是用基本数据类型无法存储的整数。比如一千位的整数,已不能再用普通的数据类型来定义并进行相应的运算了,而需要利用四则运算的基本原理来实现大整数的运算(也称高精度运算)。
用一个数组 d 存储大整数的各个数位上的数,整数高位存储在数组的高位,整数低位存储在数组的低位。而整数高位是在数的左侧,而数组高位是下标更大的一侧,所以大整数在数组中是反过来的,如下所示。因为四则运算都是从整数的低位开始的,这种顺位存储符合这种计算的思维。
#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;
}
// 比较 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
}
// 高精度 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;
}
// 高精度 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;
}
低精度整数是指可以用基本数据类型存储的整数,乘法的思想类似于高精度的加法。
// 高精度 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;
}
联系整数的除法来理解下面的步骤:
将余数作为引用传入是因为函数只能返回一个值,如果题目要求返回余数,可以通过引用的方式得到结果。
// 高精度 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;
}
希望本篇博客能对你有所帮助,也希望看官能动动小手点个赞哟~~。