求约数
1. 求出 n n n 的所有约数 ------ O ( n ) O(\sqrt{n}) O(n)
- 我们如果想要找到 n n n 的所有约数,可以根据之前判断质因数学过的试除法,在 O ( n ) O(\sqrt{n}) O(n) 的时间内找到 n n n 的所有约数
void get_divisors(int n)
{
vector<int> v;//用vector来存 n 的所有因子
for(int i = 1; i <= n / i; i++)//只需要枚举到 n / i 即可
{
if(n % i ==0)
{
v.push_back(i);
if(i != n / i)v.push_back(n / i);//因数都是成对出现的,如果 d | n 则 (n / d) | n
// 如果 i * i == n 那就只加一次
}
}
//排序,从小到大输出
sort(v.begin(),v.end());
for(auto i:v) printf("%d ",i);
puts("");
}
很简单的一个方法,它能求出 n n n 的所有约数,但是它的时间复杂度也相对来说比较高,当我们不需要知道 n n n 的约数具体是多少的时候,同时又要知道它的约数的个数的时候,有没有一些更快的方法呢?请看接下来的方法!
2. 约数个数 ------ O ( l o g n ) ∼ O ( n ) O(log_n) \sim O(\sqrt{n}) O(logn)∼O(n)
如果我们开始按照之前的方法, f o r for for 枚举 1 ∼ n 1 \sim \sqrt{n} 1∼n 出现 n 的因子 ans++,这样无疑计算不了 1 e 16 ∼ 1 e 18 1e16 \sim 1e18 1e16∼1e18的数据量(比如有 t t t 个数,让你求这些数的乘积的约数的个数, t ≤ 1 e 5 , n ≤ 2 e 9 t \le 1e5,n \le 2e9 t≤1e5,n≤2e9 ,这么大的数据量就很容易超时,更别说 n n n 的几次幂或者 n n n 的阶乘等等), 那么我们应该采取什么样的方法呢?
让我们回忆一下唯一分解定理:
唯一分解定理:任何一个大于1的自然数 N {N} N ,如果 N {N} N不为质数,都可以唯一分解成有限个质数的乘积 N = P 1 α 1 ∗ P 2 α 2 ∗ ⋯ ∗ P n α n N = {P_1}^{\alpha_1}*{P_2}^{\alpha_2}* \cdots *{P_n}^{\alpha_n} N=P1α1∗P2α2∗⋯∗Pnαn ,这里 P 1 < P 2 < ⋯ < P n {P_1} < {P_2}<\cdots<{P_n} P1<P2<⋯<Pn 均为质数,其诸指数 α i {\alpha_i} αi 是正整数。
我们假设 d d d 为 N N N 的一个因子,则 d = P 1 β 1 ∗ P 2 β 2 ∗ ⋯ ∗ P n β n d = {P_1}^{\beta_1}*{P_2}^{\beta_2}* \cdots *{P_n}^{\beta_n} d=P1β1∗P2β2∗⋯∗Pnβn ( 0 ≤ β i ≤ α i 0 \le {\beta_i} \le {\alpha_i} 0≤βi≤αi) ,根据每一个数的质因数分解都是唯一的,我们可以知道 β i \beta_i βi 只要有一项不同,那么 d d d 都会不同,那么对于每一个 β i \beta_i βi 我们都可以选择 0 ∼ α i 0 \sim \alpha_i 0∼αi 的任何一个数,所以每一个 β i \beta_i βi 都有 α i + 1 \alpha_i + 1 αi+1 中选法,那么所有的选法就是 ( α 1 + 1 ) ∗ ( α 2 + 1 ) ∗ ⋯ ∗ ( α n + 1 ) (\alpha_1 +1) * (\alpha_2 +1) * \cdots *(\alpha_n + 1) (α1+1)∗(α2+1)∗⋯∗(αn+1) ,而每一种选法也就对应了每一个不同的因子,所以约数的个数也就是 ( α 1 + 1 ) ∗ ( α 2 + 1 ) ∗ ⋯ ∗ ( α n + 1 ) (\alpha_1 +1) * (\alpha_2 +1) * \cdots *(\alpha_n + 1) (α1+1)∗(α2+1)∗⋯∗(αn+1)。
接下来看代码(假设有 t t t 个数,让你求这些数的乘积的约数的个数, t ≤ 1 e 5 , n ≤ 2 e 9 t \le 1e5,n \le 2e9 t≤1e5,n≤2e9,结果对 1 e 9 + 7 1e9+7 1e9+7 取模)
int t,n;
scanf("%d", &t);// t 个数的乘积
while(t--)
{
scanf("%d", &n);
for (int i = 2; i <= n / i; i++)
while(n % i == 0)
{
n /= i;
mp[i] ++;//i 表示质因数 mp[i] 表示 质因数的质数
}
if(n != 1)mp[n]++;
//质因数分解,对于 a1 * a2 * ··· * an 的质因数分解,其实就相当于对于每一个数进行质因数分解,存到mp中
}
solve();
const int N = 1e5+10 ,mo = 1e9+7;
typedef long long LL;
unordered_map<int ,int>mp;
void solve()
{
LL ans = 1;
for(auto item: mp)
{
int alpha = item.second;//指数
//根据我们推出的结论,质因子分解后 (指数 + 1) 相乘即为 N 的约数个数
ans = ans * (alpha + 1) % mo;
}
printf("%lld\n", ans);
}
我们来算一下这个方法的时间复杂度,前面说到分解质因数的复杂度为 O ( l o g n ) ∼ O ( n ) O(log_n) \sim O(\sqrt{n}) O(logn)∼O(n) ,一般远小于 O ( n ) O(\sqrt{n}) O(n),所以这个代码的复杂度就大概在 O ( 1 e 5 ∗ 32 ) ∼ O ( 1 e 5 ∗ ( 4 e 5 ∼ 5 e 4 ) ) O(1e5\ *\ 32 ) \sim O(1e5\ *\ (4e5 \sim 5e4)\ ) O(1e5 ∗ 32)∼O(1e5 ∗ (4e5∼5e4) ) 但一般远小于 O ( 1 e 5 ∗ ( 4 e 5 ∼ 5 e 4 ) ) O(1e5\ *\ (4e5 \sim 5e4)\ ) O(1e5 ∗ (4e5∼5e4) ) ,所以就能够处理 t ≤ 1 e 5 , n ≤ 2 e 9 t \le 1e5,n \le 2e9 t≤1e5,n≤2e9 这样的数据。
3. 约数之和 ------ O ( l o g n ) ∼ O ( n ) O(log_n) \sim O(\sqrt{n}) O(logn)∼O(n)
我们先给出约数之和的公式
S
=
(
P
1
0
+
P
1
1
+
⋯
+
P
1
α
1
)
∗
(
P
2
0
+
P
2
1
+
⋯
∗
P
2
α
2
)
∗
⋯
∗
(
P
n
0
+
P
n
1
+
⋯
+
P
n
α
n
)
S = ({P_1}^{0}+{P_1}^{1}+ \cdots +{P_1}^{\alpha_1}) * ({P_2}^{0}+{P_2}^{1}+ \cdots *{P_2}^{\alpha_2}) * \cdots * ({P_n}^{0}+{P_n}^{1}+ \cdots +{P_n}^{\alpha_n})
S=(P10+P11+⋯+P1α1)∗(P20+P21+⋯∗P2α2)∗⋯∗(Pn0+Pn1+⋯+Pnαn)。
接下来我们来验证一下,这为什么是对的。
同样,我们的也是利用了唯一分解定理,我们依旧令 d d d 为 N N N 的一个因子, d = P 1 β 1 ∗ P 2 β 2 ∗ ⋯ ∗ P n β n d = {P_1}^{\beta_1}*{P_2}^{\beta_2}* \cdots *{P_n}^{\beta_n} d=P1β1∗P2β2∗⋯∗Pnβn ( 0 ≤ β i ≤ α i 0 \le {\beta_i} \le {\alpha_i} 0≤βi≤αi) , 我们可以发现对于 S 中的每一项, P 1 0 ∗ P 2 0 ∗ ⋯ ∗ P n 0 {P_1}^0 * {P_2}^0 * \cdots * {P_n}^0 P10∗P20∗⋯∗Pn0 即为 N N N 的一个因子,所以把 S S S 中的每一项拆开,根据乘法分配律,其每一项都是 N N N 的一个因子,所以 S S S 即为 N N N 的所有约数之和。
接下来看下代码怎么写(假设有 t t t 个数,让你求这些数的乘积的约数的和, t ≤ 1 e 5 , n ≤ 2 e 9 t \le 1e5,n \le 2e9 t≤1e5,n≤2e9,结果对 1 e 9 + 7 1e9+7 1e9+7 取模)
LL ans = 1;
for(auto item : mp)
{
int p = item.first , alpha = item.second;
LL sum = 0;
for(int i = 0; i <= alpha; i ++) sum = (sum * p + 1) % mo;
ans =ans * sum % mo;
}
printf("%lld\n", ans);
同样,先将每个数的质因数分解完,将其存入map中,p就是其质因数,alpha就是质因数的个数,这里我们采用 s u m = ( s u m ∗ p + 1 ) % m o sum = (sum * p + 1) \%mo sum=(sum∗p+1)%mo 的这种方法来求 P 1 0 + P 1 1 + ⋯ + P 1 α 1 {P_1}^{0}+{P_1}^{1}+ \cdots +{P_1}^{\alpha_1} P10+P11+⋯+P1α1 。可以来模拟一下
第 i 轮 | s u m sum sum |
---|---|
第零轮 | 0 0 0 |
第一轮 | s u m ∗ p + 1 = 1 sum * p +1 = 1 sum∗p+1=1 |
第二轮 | s u m ∗ p + 1 = p + 1 sum *p +1 =p+1 sum∗p+1=p+1 |
第三轮 | s u m ∗ p + 1 = p 2 + p + 1 sum*p+1=p^{2}+p+1 sum∗p+1=p2+p+1 |
第四轮 | s u m ∗ p + 1 = p 3 + p 2 + p + 1 sum*p+1=p^{3}+p^{2}+p+1 sum∗p+1=p3+p2+p+1 |
p s : ps: ps: 简直完美
注意这里如果直接用等比数列的公式求最后再进行取模操作,大概率会溢出,导致结果出错。
同样这里我们还是来算一下这个方法的时间复杂度,不难发现它的时间复杂度和求约数个数的复杂度是一样的,至于下面的 f o r for for 循环,与前面的分解质因数相比差距太大,就可以忽略不计了,也就说这个方法的时间复杂度就是 O ( 1 e 5 ∗ 32 ) ∼ O ( 1 e 5 ∗ ( 4 e 5 ∼ 5 e 4 ) ) O(1e5\ *\ 32 ) \sim O(1e5\ *\ (4e5 \sim 5e4)\ ) O(1e5 ∗ 32)∼O(1e5 ∗ (4e5∼5e4) ) 但一般远小于 O ( 1 e 5 ∗ ( 4 e 5 ∼ 5 e 4 ) ) O(1e5\ *\ (4e5 \sim 5e4)\ ) O(1e5 ∗ (4e5∼5e4) ) 。
4. 最大公约数 ------
1. 定义:指两个或多个整数共有约数中最大的一个,记为gcd(a,b),gcd —— Grand Central Dispatch 最大公约数
2. 那么怎么来求两个数的最大公约数呢?
- 根据最大公约数的定义,我们是不是得把 a a a 和 b b b 所有的的约数都求出来之后,去找它们中公有的最大的那个约数?
- 粗略估计一下,这种方法的时间复杂度,求出 a a a, b b b 所有的约数 — O ( n ) O(\sqrt{n}) O(n) , 其次我们还需要遍历一遍所有的约数,所以时间复杂度基本上是在 O ( n ) O(\sqrt{n}) O(n) 往上。那么我们知道 O ( n ) O(\sqrt{n}) O(n) 的时间复杂度,处理不了1e18的数据,那么是否有一种方法能够解决这个问题呢?
2.1 欧几里得算法 ( 辗转相除法 ) ------ O ( l o g ( a + b ) ) O(log(a+b)) O(log(a+b))
- 首先我们先定义 k k k 是 a a a 和 b b b 的公因子 ,那么就有 k ∣ a , k ∣ b k\ |\ a,\ k\ |\ b k ∣ a, k ∣ b( k k k 能整除 a a a 且 k k k 能整除 b b b ,即 a a a 能被 k k k 整除且 b b b 能被 k k k 整除)。
- 那么 k ∣ ( a + b ) k\ |\ (a+b) k ∣ (a+b) ,这个其实很好理解因为 k ∣ a k\ |\ a k ∣ a, k ∣ b k\ |\ b k ∣ b ,所以可以令 k 1 = a k k1 = \frac{a}{k} k1=ka, k 2 = b k k2 = \frac{b}{k} k2=kb,那么 a + b = k 1 ∗ k + k 2 ∗ k = ( k 1 + k 2 ) ∗ k a+b = k1 * k +k2*k = (k1+k2)*k a+b=k1∗k+k2∗k=(k1+k2)∗k,所以 k ∣ ( a + b ) k\ |\ (a+b) k ∣ (a+b)。
- 那么由此就可以简单的推出 k ∣ ( a ∗ x + b ∗ y ) , ( x ∈ Z , y ∈ Z ) k\ |\ (a*x+b*y)\ ,\ (x\in Z,y\in Z) k ∣ (a∗x+b∗y) , (x∈Z,y∈Z),这无外乎就是 k 1 = x ∗ a k k1 = x*\frac{a}{k} k1=x∗ka , k 2 = y ∗ b k k2 = y * \frac{b}{k} k2=y∗kb ,所以很明显 k k k 能整除 a ∗ x + b ∗ y a*x+b*y a∗x+b∗y, x x x 和 y y y 为任意整数。
- 另外我们还知道 a % b = a − ⌊ a b ⌋ ∗ b a\ \%\ b = a -\lfloor \frac{a}{b}\rfloor *b a % b=a−⌊ba⌋∗b ,那么我们可以把 ⌊ a b ⌋ \lfloor \frac{a}{b}\rfloor ⌊ba⌋ 看成一个整数,那么根据我们上面的结论,我们可以令 x = 1 , y = − ⌊ a b ⌋ x = 1,y=-\lfloor \frac{a}{b}\rfloor x=1,y=−⌊ba⌋ ,那么我们就可以得出 k ∣ ( a % b ) k \ |\ (a\ \%\ b) k ∣ (a % b) 。
- 因为以上的结论是对 " k k k 是 a a a 和 b b b 的公因子 " 这个条件成立的,所以任意的 k k k ,只要满足它是 a a a 和 b b b 的公因子,那么以上结论都成立。所以对于 k k k 只要它是 a a a 因子,同时也是 b b b 的因子,那么它就一定是 ( a % b ) (a\ \%\ b) (a % b) 的因子。同理,我们可以推出如果 k k k 是 b b b 的因子,同时也是 ( a % b ) (a\ \%\ b) (a % b) 的因子,那么它也是 a a a 的因子。
令 c = a % b = a − ⌊ a b ⌋ ∗ b c = a\ \%\ b = a -\lfloor \frac{a}{b}\rfloor *b c=a % b=a−⌊ba⌋∗b
若 k ∣ b & & k ∣ c k\ |\ b\ \&\&\ k\ |\ c k ∣ b && k ∣ c
那我们可以得出 k ∣ ( c ∗ x + b ∗ y ) , ( x ∈ Z , y ∈ Z ) k\ |\ (c*x+b*y)\ ,\ (x\in Z,y\in Z) k ∣ (c∗x+b∗y) , (x∈Z,y∈Z)
那么我们令 x = 1 , y = ⌊ a b ⌋ x=1,y = \lfloor \frac{a}{b}\rfloor x=1,y=⌊ba⌋ 则 ( c ∗ x + b ∗ y ) = a (c*x+b*y) = a (c∗x+b∗y)=a
所以 k ∣ a k\ |\ a k ∣ a
- 所以我们可以得出 a a a 和 b b b 的公因子的集合 = b b b 和 a % b a\ \%\ b 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)。
- 那么我们就可以通过递归的方式快速的求出 g c d ( a , b ) gcd(a,\ b) gcd(a, b)。
- 下面来看下代码
int gcd(int a,int b)
{
//gcd(a,0) = a;
return b ? gcd(b, a%b) : a;
/*如果 b 不为 0, 就递归gcd(b, a%b)
如果 b 为0, 根据定义gcd(a,0) 就等于 a
*/
}
- 我们可以看到,代码非常短,就只有一行,那么现在我们来算一下它的时间复杂度。
- 首先我们根据上面提到过的,一个数的约数总是成对这么一个结论,得出一个推论:如果 a > m a > m a>m 那么 a % m < a / 2 a\ \%\ m < a/2 a % m<a/2
简单证明一下
因为 a % m < m a\ \%\ m < m a % m<m,
所以若想要 a % m a\ \%\ m a % m 尽可能大,那么 m m m 也要尽可能大。
当 m > a / 2 m > a\ /\ 2 m>a / 2 时,我们可以发现 a % m a\ \%\ m a % m 随着 m m m 的增加, a % m a\ \%\ m a % m 反而越来越小,所以当 m > a / 2 m > a\ /\ 2 m>a / 2 时 m = a / 2 m = a\ /\ 2 m=a / 2 能使得 a % m a\ \%\ m a % m 最大, a % m = a / 2 − 1 < a / 2 a\ \%\ m = a\ /\ 2 - 1 < a\ /\ 2 a % m=a / 2−1<a / 2 。
当 m = a / 2 m = a\ /\ 2 m=a / 2 时, a % m = 0 a\ \%\ m = 0 a % m=0
当 m < a / 2 m < a\ /\ 2 m<a / 2 时,我们可以发现由于 a % m < m a\ \%\ m < m a % m<m 因为 m < a / 2 m < a\ /\ 2 m<a / 2 ,所以 a % m < a / 2 a\ \%\ m < a\ /\ 2 a % m<a / 2。
所以我们可以证明:当 a > m a > m a>m 时, a % m < a / 2 a\ \%\ m < a/2 a % m<a/2。
- 然后我们可以观察到 g c d ( a , b ) → g c d ( b , a % b ) gcd(a,b) \to gcd(b,a\ \%\ b) gcd(a,b)→gcd(b,a % b) 要分两种情况
- 如果 a > b a > b a>b , 那么迭代两次之后就会得到 g c d ( a % b , b % ( a % b ) ) gcd(a\ \%\ b,b\ \%\ (a\ \%\ b)) gcd(a % b,b % (a % b))。
由上面的推论,我们可以知道 a % b < a / 2 a\ \%\ b < a\ /\ 2 a % b<a / 2 ,而 a % b < b a\ \%\ b < b a % b<b ,则 b % ( a % b ) ) < b / 2 b\ \%\ (a\ \%\ b)) < b\ /\ 2 b % (a % b))<b / 2,也就是相当于两次迭代之后,数据总和会减小一半,那么时间复杂度为 O ( 2 ∗ l o g ( a + b ) ) O(2*log(a+b)) O(2∗log(a+b))- 如果 a > b a > b a>b , 那么迭代一次之后得到 g c d ( b , a % b ) gcd(b,a\ \%\ b) gcd(b,a % b) ,而我们知道 a % b < a / 2 < a a\ \%\ b < a\ /\ 2 < a a % b<a / 2<a,那么就又回到了我们的第一种情况。
所以这样就得到了欧几里得算法的时间复杂度为 O ( l o g ( a + b ) ) O(log(a+b)) O(log(a+b))。