记一点笔记
算法笔记 第5章 入门篇(3)——数学问题
5.1 简单数学
略
5.2 最大公约数和最小公倍数
5.2.1 最大公约数
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);
}
5.2.2 最小公倍数
int lcm(int a, int b) {
int d = gcd(a, b);
return a / d * b;
}
5.3 分数的四则运算
5.3.1 分数的表示和化简
1. 分数的表示
struct Fraction {
int up, down;
};
制定三项规则:
- 使down为非负数
- 如果该分数恰为0,规定其分子为0,分母为1
- 分子和分母没有除了1以外的公约数
2. 分数的化简
Fraction reduction(Fraction result) {
if(result.down < 0) {
result.up = -result.up;
result.down = -result.down;
}
if(result.up = 0) {
result.down = 1;
} else {
int d = gcd(abs(result.up), abs(result.down));
result.up /= d;
result.down /= d;
}
return result;
}
5.3.2 分数的四则运算
1. 分数的加法
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);
}
2. 分数的减法
Fraction minu(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);
}
3. 分数的乘法
Fraction multi(Fraction f1, Fraction f2) {
Fraction result;
result.up = f1.up * f2.up;
result.down = f1.down * f2.down;
return reduction(result);
}
4. 分数除法
- 如果除数为0(f2.up == 0),那么应当直接特判输出题目要求的输出语句(例如输出Error、Inf之类)。只有当除数不为0时,才能使用下面的函数进行计算。
Fraction divide(Fraction f1, Fraction f2) {
Fraction result;
result.up = f1.up * f2.down;
result.down = f1.down * f2.up;
return reduction(result);
}
5.3.3 分数的输出
几个注意点:
- 输出分数前,需要先对其进行化简
- 如果分数 r 的分母 down 为1,说明该分数是整数,一般来说题目会要求直接输出分数,而省略分母的输出
- 如果分数 r 的分子 up 的绝对值大于分母 down,说明该分数是假分数,此时应按带分数的形式输出,即整数部分为
r.up/r.down
,分子部分为abs(r.up)%r.down
,分母部分为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("%d %d/%d", r.up / r.down, abs(r.up) % r.down, r.down);
else
printf("%d/%d", r.up, r.down);
}
- 由于分数的乘法和出发的过程中可能使分子或分母超过
int
型的标识范围,因此一般情况下,分子和分母应当使用long long
型来存储
5.4 素数
5.4.1 素数的判断
bool isPrime(int n) {
if(n <= 1) return false; //特判
int sqr = (int)sqrt(1.0 * n); //根号n
for(int i = 2; i <= sqr; i++) {
if(n % i == 0) return false;
}
return true;
}
5.4.2 素数表的获取
const int maxn = 101; //表长
int prime[maxn], pNum = 0 //prime数组存放所有素数,pNum为素数个数
bool p[maxn] = {0}; //p[i] == true表示i是素数
void Find_Prime() {
for(int i = 1; i < maxn; i++) {
if(isPrime(i) == true) {
prime[pNum++] = i;
p[i] = true;
}
}
}
- 埃式筛法:从小到大枚举所有数,对每一个素数,筛去它的所有倍数,剩下的就都是素数了。
const int maxn = 101; //表长
int prime[maxn], pNum = 0 //prime数组存放所有素数,pNum为素数个数
bool p[maxn] = {0}; //p[i] == false表示i是素数
void Find_Prime() {
for(int i = 2; i < maxn; i++) {
if(p[i] == false) { //如果i是素数
prime[pNum++] = i;
for(int j = i + i; j < maxn; j += i) {
//筛去所有i的倍数
p[j] = true;
}
}
}
}
5.5 质因子分解
- 由于最后都要归结到若干不同素数的乘积,因此不妨先把素数表打印出来。
- 如果有些题目中要求对 1 进行处理,那么视题目条件而定来进行特判处理。
- 由于每个质因子都可以布置出现一次,因此不妨定义结构体
factor
,用来存放指引及其个数。
struct factor {
int x, cnt; //x为质因子,cnt为其个数
} fac[10];
- 对一个正整数 n 来说,如果它存在 [2,n] 范围内的质因子,要么这些质因子全部小于等于 sqrt(n),要么只存在一个大于 sqrt(n) 的质因子,而其余质因子全部小于等于 sqrt(n)。
质因子分解:
- 枚举 1 ~ sqrt(n) 范围内的所有质因子 p,判断 p 是否是 n 的因子。
//如果是,进行以下操作;如果不是,则跳过
if(n % prime[i] == 0) {
fac[num].x = prime[i]; //记录该因子
fac[num].cnt = 0;
while(n % prime[i] == 0) { //计算个数
fac[num].cnt++;
n /= prime[i];
}
num++;
}
- 如果在上面步骤结束后 n 仍大于1,说明 n 有且仅有一个大于 sqrt(n) 的质因子(有可能是 n 本身),这时需要把这个质因子加入fac数组,并令其个数为1。
if(n != 1) {
fac[num].x = n;
fac[num++].cnt = 1;
}
5.6 大整数(高精度整数)运算
5.6 大整数的存储
- 用数组存储。整数的高位存储在数组的高位,整数的地位存储在数组的地位。
- 把整数按字符串读入的时候,需要在另存为至整数数组的时候反转一下。
struct bign {
int d[1000];
int len;
bign() { //构造函数,用以初始化结构体
memset(d, 0, sizeof(d));
len = 0;
}
};
- 输入大整数时,一般都是先用字符串读入,然后再把字符串另存为至
bign
结构体。
bign change(char str[]) { //把整数转换为bign
bign a;
a.len = strlen(str); //bign的长度就是字符串的长度
for(int i = 0; i < a.len; i++) {
a.d[i] = str[a.len - i - 1] - '0'; //逆着赋值
}
return a;
}
//比较a和b大小,a大、相等、a小分别返回1、0、-1
int compare(bign a, bign b) {
if(a.len > b.len) return 1;
else if(a.len < b.len) return -1;
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;
}
}
5.6.2 大整数的四则运算
1. 高精度加法
bign add(bign a, bign b) {
bign c;
int carry = 0; //进位
for(int i = 0; i < a.len || i < b.len; i++) {
int temp = a.d[i] + b.d[i] + carry;
c.d[c.len++] = temp % 10;
carry = temp / 10;
}
if(carry != 0) {
c.d[c.len++] = carry;
}
return c;
}
- 这样写法的条件时两个对象都是非负整数。如果有一方是负的,可以在转换到数组这一步时去掉其负号,然后采用高精度减法;如果两个都是负的,就都去掉负号后用高精度加法,最后再把负号加回去即可。
2. 高精度减法
bign sub(bign a, bign b) {
bign c;
for(int i = 0; i < a.len || i < b.len; i++) {
if(a.d[i] < b.d[i]) { //如果不够减
a.d[i+1]--; //向高位借位
a.d[i] += 10; //当前位加10
}
c.d[c.len++] = a.d[i] - b.d[i];
}
while(c.len - 1 >= 1 && c.d[c.len - 1] == 0) {
c.len--; //去除高位的0,同时至少保留一位最低位
}
}
- 使用
sub
函数前要比较两个数的大小,如果被减数小于减数,需要交换两个变量,然后输出负号,再使用sub
函数
3. 高精度与低精度乘法
bign multi(bign a, int b) {
bign c;
int carry = 0; //进位
for(int i = 0; i < a.len; i++) {
int temp = a.d[i] * b + carry;
c.d[c.len++] = temp % 10;
carry /= 10;
}
while(carry != 0) { //和加法不一样,乘法的进位可能不止一位,因此用while
c.d[c.len++] = carry % 10;
carry /= 10;
}
return c;
}
- 如果 a 和 b 中存在负数,需要先记录下其负号,然后取他们的绝对值带入函数。
4. 高精度与低精度除法
bign divide(bign a, bign b, int &r) { //r为余数
bign c;
c.len = a.len; //被除数的每一位和商的每一位是一一对应的,因此先令长度相等
for(int i = a.len - 1; i >= 0; i--) { //从高位开始
r = r * 10 + a.d[i]; //和上一位遗留的余数结合
if(r < b) c.d[i] = 0; //不够除,该位为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;
}
5.7 扩展欧几里得算法
略
5.8 组合数
略