【算法笔记】第五章:数学问题
标签(空格分隔):【算法笔记】
第五章:入门篇(3)——数学问题
文章目录
5.1 简单数学
5.2 最大公约数与最小公倍数
5.2.1 最大公约数
欧几里得算法(辗转相除法):设a、b为两个整数(假设a>b),则gcd(a,b) = gcd(a, a%b);
5.2.2 最小公倍数
最小公倍数的求解是建立在 最大公约数 上的。
当得到 a 和 b 的最大公约数 d 之后,可以得到a与b的最小公倍数为 ab/d.
5.3.1 分数的表示和化简
分数的表示:对于一个分数来说,最简洁的写法就是写成 假分数 的形式,即无论分子和分母的大小如何,均保留其原数。
可以用结构体来存储这种只有分子和分母的分数。
struct Fraction{
int up;
int down;
}
这样表示,可以制定三项规则:1. 分母down不可以为负数。若分数为负数,则分子up为负数。2. 如果分数为0, 则规定分子up 为 0, 分母down 为1. 3. 分子和分母没有除了1之外的公约数。
分数的化简:主要看约分:求出分子绝对值与分母绝对值的最大公约数d,然后令二者同时除以d.
5.3.2 分数的四则运算
分数的加减法:对于给定的两个分数 f1 和 f2,其加减法公式为:
分数的乘法:对于给定的两个分数 f1 和 f2,其乘法公式为:
分数的除法:对于给定的两个分数 f1 和 f2,其除法公式为:
以上代码实现略。
5.3.3 分数的输出
5.4 素数
素数(质数):除了 1 和其本身外,不能被其他数整除的一类数。
特殊:1既不是素数,也不是合数。
5.4.1 素数的判断
最直接的方法:一个整数n要被判断为素数,首先判断n是否能被2,3,…,n-1中的一个整除。只有2,3,…,n-1都不能整除n,n才能判定为素数,否则n为合数。该方法时间复杂度为O(n),对于实际问题而言,是比较耗时的。
改进方法:如果 2 ~ n-1中存在n的约数,不妨设为k,即 n%k == 0, 那么由 k *(n/k) == n ,可知 n/k 也是n的一个约数。所以,我们只需要判定n是否能被 2,3,…
中的一个整除,即可判定n是否为素数。该方法运算时间为O(sqrt(n)).
代码如下:
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;
}
筛选法:其中最容易理解的一种筛选法为 埃氏晒法(eratoshenes筛法),其时间复杂度为O(nloglogn).
算法从小到大枚举所有的数字,对于每一个素数,晒去它的所有倍数,剩下的就是素数了。
例如:求 1 ~ 15 中所有的素数。
首先 2 是素数,所以晒去2的倍数。
第二个数字是3,由于它没有被晒去,所以3是素数。
依次类推即可。
代码:
const int maxsize =101;
int prime[maxsize], pNum = 0;//素数表长至少比n大1.
bool p[maxsize] = {0};
void find_Prime(){
for( int i = 2; i < maxsize; i++){//不能写成<=
if( p[i] == flase;){
prime[pNum++] = i;
for( int j = i + i; j < maxSize; j+=i)
p[j] = true;
}
}
}
5.5 质因子分解
质因子分解:指的是将一个正整数n写成一个或者多个质数的乘积的形式,例如 6 = 2 * 3, 8 = 2 * 2 * 2…注意,由于1本身不是素数,所以它没有质因子。
由于每一个质因子可能出现不止一次,所以可以定义结构体factor,用来存放质因子以及其个数。
struct factor{
int x;
int count;
}fac[ num];
5.6 大整数的运算
5.6.1 大整数的存储
A+B这样的运算永远是最简单的,但是如果A和B是1000位的数字呢?
计算机是无法存储这么大的数字的,因此需要进行特殊处理。
最简单的处理办法是使用 数组,可以让整数的每一位分别存储到对应数组的位置上。整数的高位存储在数组的高位,整数的低位存储在数组的低位。(有时候需要反转一下)
5.6.2 大整数的四则运算
高精度加法、高精度减法、高精度乘法、高精度除法。
以上代码实现较为简单,知道思想即可。
5.7 组合数
5.7.1 一个关于 n! 的问题
关于 n! 的问题:求 n! 中有多少个质因子p。
方法一: 最直观的想法是计算 1~n 中每个数的总共有多少质因子p,然后结果相加。该算法运算时间为O(nlogn).
显然,对于足够大的数字,以上放上是不够实用的。
方法二:公式:n! 中有 $ \frac{n}{p} + \frac{n}{p^2} + \frac{n}{p^3} +…$ 个质因子p,其中除法均为向下取整。该算法的运算时间为O(logn).
代码如下:
int cal( int n, int p){
int ans = 0;
while( n){
ans += n/p;
n /= p;
}
return ans;
}
扩展:计算n!的末尾有多少个零?
由于末尾0的个数等于n!中因子10的个数,而这又等于n!中质因子5的个数,因此只需要代入cal(n,5)即可。
5.7.2 组合数的计算
组合数$ \binom{n}{m}
C_n^m $, 其定义为
性质:1. 组合数满足$ C_n^m = C_n^{n-m} $. 2.
.
计算组合数的方法:
方法一: 通过定义直接计算。
由
,可先计算 n!, 再计算m!(n-m)!.
但是,由于阶乘数十分庞大,很容易发生溢出。因此不推荐这种方式。
方法二: 通过递推式:
由组合数的性质可知,$ C_n^m $代表从n个数字中选择m个数。可以分解为两部分,分别是 一,选最后一个数字,从前n-1个数字中选择m-1个数字;二,不选最后一个数字,从前n-1个数字中选择m个数字。
即 $ C_n^m = C_{n-1}^m + C_{n-1}^{m-1}$
注意,该方法写代码时不建议使用递归(因为会造成重复计算),可以使用二位矩阵代替。
方法三: 通过定义式的变形来计算
$C_n^m = \frac{n!}{m!(n-m)!} = \frac{ (n - m +1)(n- m + 2)…(n-m+m)}{123*…*m} $
可以观察到,上式的分子和分母的项数均为m项,可以按照以下方式计算:
上式唯一需要证明的每次除法都是整数。这个很简单,因为上式就是把$ C_{n-m+i}^{i}
C_{n-m+i}^{i}$必然是个整数。
代码:
long long C( long long n, long long m){
long long ans = 1;
for( int i = 1; i <=m; i++)
ans = ans * (n - m + i) /i; // 先乘再除
}
一般而言,组合数的求解会发生溢出。在很多问题中,一般让运算结果对一个正整数p进行取模。