Loading

算法笔记--简单数学1

数字黑洞

题目

给定任一个各位数字不完全相同的 4 位正整数,如果我们先把 4 个数字按非递增排序,再按非递减排序,然后用第 1 个数字减第 2 个数字,将得到一个新的数字。一直重复这样做,我们很快会停在有“数字黑洞”之称的 6174,这个神奇的数字也叫 Kaprekar 常数。

例如,我们从6767开始,将得到
7766 - 6677 = 1089
9810 - 0189 = 9621
9621 - 1269 = 8352
8532 - 2358 = 6174
7641 - 1467 = 6174
... ...
现给定任意 4 位正整数,请编写程序演示到达黑洞的过程。

输入与输出

输出格式:
如果 N 的 4 位数字全相等,则在一行内输出 N - N = 0000;否则将计算的每一步在一行内输出,直到 6174 作为差出现,输出格式见样例。注意每个数字按 4 位数格式输出。

输入样例 1:
6767
输出样例 1:
7766 - 6677 = 1089
9810 - 0189 = 9621
9621 - 1269 = 8352
8532 - 2358 = 6174
输入样例 2:
2222
输出样例 2:
2222 - 2222 = 0000

解题思路

思路
步骤1:
	写两个函数:int型整数转换成int型数组的to_array函数(即把每一位都当成数组的一个元素)、int型数组转换成int型整数的to_number函数。
步骤2:建立一个while循环,对每一层循环:
	用to_array函数将n转换为数组并递增排序,再用to_number函数将递增排序完的数组转换为整数MIN.
	将数组递减排序,再用to_number数将递减排序完的数组转换为整数MAX
	令 n = MAX - MIN为下一个数,并输出当前层的信息。
	如果得到的n为0或6174,退出循环。

代码

#include<cstdio>
#include<algorithm>
using namespace std;
bool cmp(int a, int b){
    return a > b;   // 从大到小排序
}

void to_array(int number, int myArray[]){	// 转换为数组
    for(int i = 0; i < 4; i++){
        myArray[i] = number % 10;			// 利用求余
        number /= 10;
    }
}
// 数组转换数字技巧 
//如 1234 = ((((0  * 10 + 1) * 10 + 2) * 10 + 3) * 10 + 4)		
int to_number(int myArray[]){				
    int number = 0;
    for(int i = 0; i < 4; i++){
        number = number * 10 + myArray[i];	
    }
    return number;
}

int main(){
    int number, MIN, MAX;
    int myArray[5];
    scanf("%d", &number);
    while(1){
        to_array(number, myArray);
        sort(myArray, myArray + 4);
        MIN = to_number(myArray);
        sort(myArray, myArray + 4, cmp);
        MAX = to_number(myArray);
        number = MAX - MIN;
        printf("%04d - %04d = %04d\n", MAX, MIN, number);
        if(number == 0 || number == 6174)break;
    }
    return 0;
}

最大公约数与最小公倍数

最大公约数

利用欧几里得算法辗转相除法)j计算最大公约数

169 = 48 * 3 + 25    	—— (169 , 48) = (48 , 25)

48 = 25 * 1 + 13		——(48 , 25) = (25 , 13)

25 = 13 * 1 + 12		——(25 , 13) = (13 , 12)

13 = 12 * 1 + 1			——(13 , 12) = (12 , 1)

12 = 1 * 12  + 0 		——(12 , 1 ) = (1 , 0) 此时 b = 0

故最大公约数为 1  

递归的关键步骤:

  • 🅰递归式:gcd(a, b) = gcd(b, a % b)
  • 🅱递归边界:gcd(a, 0) = a
int gcd(int a, int b){
    if(b == 0) return a;
    else return gcd(b, a % b);
}

题目: [codeup 1818]最大公约数

题目描述:
	输入两个正整数,求其最大公约数
输入:
	测试数据有多组,每组输入两个正整数
输出:
	对于每组输入,请输出其最大公约数
样例输入:
	49 14
样例输出:
	7

代码

#include<cstdio>

int gcd(int a, int b){
    if(b == 0) return a;
    else return gcd(b, a % b);
}

int main(){
    int a, b;
    while(scanf("%d%d", &a, &b) != EOF){
        printf("%d", gcd(a, b));
    }

    return 0;
}

最小公倍数

一般用lcm(a,b)来表示a和b的最小公倍数

最小公倍数的求解在最大公约数的基础上进行,当得到gcd(a, b) = d,可以马上得到a,b的最小公倍数是ab/d,但是ab计算可能会溢出,因此应写为a/bd

int lcm(int a, int b){
    int d = gcd(a, b);
    return a / d * b;
}

分数的四则运算

给定两个分数的分子和分母,求他们加减乘除的结果

分数的表示和化简

分数的表示

对于一个分数来说,最简洁的写法就是写成假分数的形式,无论分子分母大或者小,都保留其原数

struct Fraction{	// 分数
    int up, down;	// 分子 分母
}
  • 1️⃣使用down为非负数。如果分数为负,那么令分子up为负即可
  • 2️⃣如果该分数恰好为0,则令分子为0,分母为1
  • 3️⃣分子和分母没有除了1以外的公约数

分数的化简

  • 🟡如果分母down为负数,那么令分子up和分母down都变为相反数
  • 🟢如果分了up为0,那么令分母down为1
  • 🔵约分:求出分子绝对值分母绝对值最大公约数d,然后令分子分母同时除以d
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; 
}

分数的四则运算

  1. 分数的加法

\[result = \frac{f1.up * f2.down + f2.up * f1.down}{f1.down * f2.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);		// 结果注意化简
}
  1. 分数的减法

\[result = \frac{f1.up * f2.down - f2.up * f1.down}{f1.down * f2.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);
}
  1. 分数的乘法

\[result = \frac{f1.up * f2.up }{f1.down * f2.down} \]

Fraction add(Fraction f1, Fraction f2){
    Fraction result;
    result.up = f1.up *  f2.up;
    result.down = f1.down * f2.down;
    return reduction(result);
}
  1. 分数的除法

\[result = \frac{f1.up * f2.down }{f1.down * f2.up} \]

Fraction add(Fraction f1, Fraction f2){
    Fraction result;
    result.up = f1.up *  f2.down;
    result.down = f1.down * f2.up;
    return reduction(result);
}

分数的输出

  • 输出分数前,需要先对其进行化简。
  • 如果分数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("%d", 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型来存储。

素数

素数又称为质数,是除了1和本身之外,不能被其他数整数的一类数

1既不是质数,也不是合数

素数的判断

  • 如果利用N能否被2, 3, ..., N - 1整除,其时间复杂度为O(n)
  • 可以利用N能否被2, 3, ..., (int)(sqrt(N))的一个整数整除,其时间复杂度为O(sqrt(n))
#include<math.h>
#include<stdio.h>
bool isPrime(int n){
    if(n <= 1) return false;
    int sqr = (int)sqrt(1.0 * n); // sqrt的参数为浮点数
    for(int i = 2; i < sqr; i++){
        if(n % i == 0) return false;
    }
    return true;
}

素数表的获取

  • 🅰枚举法

要打印1到n的素数,即从1到n进行枚举,判断每个数是否是素数,如果是则加入素数表。

枚举的时间复杂度为O(n),判断素数的时间复杂度为O(sqrt(n)),所以,总的时间复杂度为O(n*sqrt(n)),这个复杂度对n不超过\(10^5\)的大小是没有问题的

const int maxn = 101;
int prime[maxn], pNum = 0;
bool p[maxn] = {0};
void FindPrime(){
    for(int i = 1; i < maxn; i++){
        if(isPrime(i) == true){
            prime[pNum++] = i;
            p[i] = true;
        }
    }
}

求解100以内的素数(枚举法)

#include<stdio.h>
#include<math.h>
const int MAXN = 101;
int pNum = 0;
int prime[MAXN];
bool p[MAXN] = {false};
bool isPrime(int n){
    if(n <= 1) return false;
    int sqr = (int)sqrt(1.0 * n);
    for(int i = 2; i <= sqr; i++){
        if(n % i == 0) return false;
    }
    return true;
}

void FindPrime(){
    for(int i = 1; i < MAXN; i++){
        if(isPrime(i) == true){
            prime[pNum++] = i;
            p[i] = true;
        }
    }
}

int main(){
    FindPrime();
    for(int i = 0; i < pNum; i++)
        printf("%d\t", prime[i]);
    return 0;
}

🅱筛选法

例子:筛选出1~15中的所有素数

Snipaste_2020-03-24_11-44-41

代码

const int MAXN = 101;
int prime[maxn], pNum = 0;
bool p[MAXN] = {0};
void FindPrime(){
    for(int i = 2; i < MAXN; i++){
        if(p[i] ==  false){						  // 如果是素数
            prime[pNum++] = i;
            for(int j = i + i; j < MAXN; j += i){ //筛去所有i的倍数
                p[j] = true;					  // 循环条件不能写成j <= MAXN
            }
        }
    }
}

求解100以内的素数(筛选法)

#include<stdio.h>
const int MAXN = 101;
int prime[MAXN], pNum = 0;
bool p[MAXN] = {0};
void FindPrime(){
    for(int i = 2; i < MAXN; i++){
        if(p[i] ==  false){						  // 如果是素数
            prime[pNum++] = i;
            for(int j = i + i; j < MAXN; j += i){ //筛去所有i的倍数
                p[j] = true;					  // 循环条件不能写成j <= MAXN
            }
        }
    }
}
int main(){
    FindPrime();
    for(int i = 0; i < pNum; i++){
        printf("%d\t", prime[i]);
    }
}

[PAT B1013] 数素数

令Pi为第i个素数,在一行中输入正整数M和N(M<=N<=10000),以空格分隔,输出PM到PN之间的所有素数,每10个数字占1行,其间以空格分隔,行末不得有多余空格。

输入样例:
5 27
输出样例:
11 13 17 19 23 29 31 37 41 43 
47 53 59 61 67 71 73 79 83 89 
97 101 103
#include<cstdio>
const int MAXN = 10000001; // 不知道第10000个素数大小,所以MAXN设大点
int pNum = 0, prime[MAXN];
bool p[MAXN] = {0}; // false表示素数

void FindPrime(int n){
    for(int i = 2; i < MAXN; i++){
        if(p[i] == false){
            prime[pNum++] = i;
            if(pNum >= n)break;
            for(int j = i + i; j < MAXN; j += i){
                p[j] = true;
            }
        }
    }
}

int main(){
    int m, n, count = 0;
    scanf("%d%d", &m, &n);
    FindPrime(n);
    for(int i = m; i <= n; i++){
        printf("%d", prime[i - 1]); // 下标从0开始
        count ++;
        if(count % 10 != 0 && i < n)printf(" ");
        else printf("\n");
    }
    return 0;
}

注意

⭕1不是素数。
⭕素数表长至少要比n大1
⭕在FindPrime()函数中要特别留意i<MAXN不能写成i≤MAXN
main函数中要记得调用FindPrime(),不然不会出结果

质因子分解

质因子的分解就是指将一个正整数n写成一个或者多个质数的乘积的形式,如180 = 2*2*3*3*5,或者写成\(180 = 2^2 * 3^2 * 5^1\)

注意:由于1本身不是素数,因此它没有质因子,如果有些题目要求对1进行处理,那么视题目条件而定来进行特判处理

定义结构体factor来存放质因子及其个数

struct factor{
    int x, cnt;
}fac[10];

fac[]数组存放的就是给定的正整数n的所有质因子,如180:

fac[0].x = 2;
fac[0].cnt = 2;
fac[1].x = 3;
fac[1].cnt = 2;
fac[2].x = 5;
fac[2].cnt = 1;

因为2*3*5*7*11*13*17*19*23*29已经超过int范围,所以对一个int型的数来说,fac数组的大小只要开到10就够够的了

对一个整数n来说,如果它存在[2,n]范围内的质因了,要么这些质因子全部小于等于sqrt(n),要么只存在一个大于sqrt(n)的质因了,而其余质因子全部小于等于sqrt(n)这就给进行质因了分
解提供了一个很好的思路:

  • 🅰枚举1~sqrt(n)范围内的所有质因子p,判断p是否是n的因子

    如果p是n的因子,则执行如下:

if(n % prime[i] == 0){		// 如果prime[i]是n的因子
    fac[num].x = prime[i];	// 记录该因子
    fac[num].cnt = 0;		// 初始化其个数为0
    while(n % prime[i] == 0){ // 计算出质因子prime[i]的个数
        fac[num].cnt++;		// 只要p还是n的因子,就让n不断除以p,每次操作令p的个数加1
        n /= prime[i];		// 直到p不再是n的因子为止
    }
    num++;
}

如果p不是n的因子,就直接跳过

  • 🅱如果在上面步骤结束后n仍然大于1,说明n有且仅有一个大于sqrt(n)的质因子(有可能是n本身),这时需要把这个质因了加入fac数组,并令其个数为1
if(n != 1){				//如果无法被根号n以内的质因子除尽
    fac[num].x = n;		// 那么一定有一个大于根号n的质因子
    fac[num++].cnt = 1;
}

至此,fac数组中存放的就是质因子分解的结果,时间复杂度为O(sqrt(n))

例题Prime Factors

题目描述:
Given any positive integer N, you are supposed to find all of its prime factors, and write them in the format N = p1^k1 * p2^k2 *…*pm^km.

输入样例:
97532468
输出样例:
97532468 = 2^2 * 11 * 17 * 101  * 1291

思路

利用上面学习的内容,要在前面先把素数表打印出来,然后进行质因子分解操作

代码

#include<cstdio>
#include<cmath>
const int MAXN = 100005;
int pNum = 0, prime[MAXN];
int p[MAXN] = {0};
bool isPrime(int n){    // 判断是否是素数
    if(n == 1) return false;
    int sqr = (int)sqrt(1.0 * n);
    for(int i = 2; i <= sqr; i++){
        if(n % i == 0) return false;
    }
    return true;
}

void FindPrime(){       // 构建素数表
    for(int i = 1; i < MAXN; i++){
        if(isPrime(i) ==  true){
            prime[pNum++] = i;
            for(int j = i + i; j < MAXN; j += i){
                p[j] = true;
            }
        }
    }
}

struct factor{  // 质因子的结构体
    int x, cnt; // x表示质数,cnt表示个数
}fac[10];

int main(){
    FindPrime();
    int n, num = 0;
    scanf("%d", &n);
    if(n == 1)printf("1 = 1");
    else{
        printf("%d = ", n);
        int sqr = (int)sqrt(1.0 * n);
        // 枚举根号n以内的质因子
        for(int i = 0; i < pNum && prime[i] <= sqr; i++){
            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++;
            }
            if(n == 1)break;
        }
            if(n != 1){
                fac[num].x = n;
                fac[num++].cnt = 1;
            }
            for(int i = 0; i < num; i++){
                if(i > 0)printf("*");
                printf("%d", fac[i].x);
                if(fac[i].cnt > 1){
                    printf("^%d", fac[i].cnt);
                }
            }
        }

    return 0;
}

大整数的运算

大整数又称为高精度整数,其含义是用基本数据类型无法存储其精度的整数

大整数的存储

23513存储在数组中,则

d[0] = 3;
d[1] = 1;
d[2] = 5;
d[3] = 3;
d[4] = 2;

🉐整数的高位存储在数组的高位,整数的低位存储在数组的低位

但是,整数按照字符串%s读入时,实际上是逆位存储的,即

str[0] = '2';
str[1] = '3';
str[2] = '5';
str[3] = '1';
str[4] = '3';

🉐在读入后需要在另存至d[]数组时反转一下

定义大整数结构体

struct bign{
    int d[1000];
    int len;		// 记录长度
};

在定义结构体变量后,需要马上初始化结构体,所以有:

struct bign{
    int d[1000];
    int len;
    bign(){
        memset(d, 0, sizeof(d));
        len = 0;
    }
};

在输入大整数时,先用字符串读入然后再把字符串另存为bign结构体

bign change(char str[]){
    bign a;
    a.len = strlen(str);
    for(int i = 0; i < a.len; i++){
        a.d[i] = str[a.len - i - 1] - '0';	// 逆向赋值
    }
    return a;
}

比较两个bign大小

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;	// 两数相等
    }
}

大整数的四则运算

  • 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){							// 如果最后进位不是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]){
            b.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,同时至少保留一位最低位
    }
    return c;
}

​ 最后需要指出,使用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 = temp / 10;			// 高位部分作为新的进位
    }
    while(carry != 0){			// 进位可能不止一位,所以用循环
        c.d[c.len++] = carry % 10;
        carry /= 10;
    }
    return c;
}

​ 如果a和b中存在负数,需要先记录下负号,然后取它们的绝对值代入函数。

  • 4️⃣高精度与低精度除法

    ​ 上一步的余数乘以10加上该步的位,得到该步临时的被除数,将其与除数比较:如果不够除,则该位的商为0; 如果够除,则商即为对应的商,余数即为对应的余数。最后一步要注意减法后高位可能有多余的0,要忽视它们,但也要保证结果至少有一位数。

bign divide(bign a, int 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[len - 1] == 0){
        c.len--;					// 去除高位的0,同时至少保留一位最低位
    }
    return c;
}

由于引用就是对原变量进行修改的,所以r的值就是最终的余数

Write by Gqq

posted @ 2020-03-24 18:20  ZHGQCN  阅读(188)  评论(0)    收藏  举报