数论 --- 约数

求约数

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} 1n 出现 n 的因子 ans++,这样无疑计算不了 1 e 16 ∼ 1 e 18 1e16 \sim 1e18 1e161e18的数据量(比如有 t t t 个数,让你求这些数的乘积的约数的个数, t ≤ 1 e 5 , n ≤ 2 e 9 t \le 1e5,n \le 2e9 t1e5n2e9 ,这么大的数据量就很容易超时,更别说 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α1P2α2Pnα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β1P2β2Pnβ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 t1e5n2e9,结果对 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  (4e55e4) ) 但一般远小于 O ( 1 e 5   ∗   ( 4 e 5 ∼ 5 e 4 )   ) O(1e5\ *\ (4e5 \sim 5e4)\ ) O(1e5  (4e55e4) ) ,所以就能够处理 t ≤ 1 e 5 , n ≤ 2 e 9 t \le 1e5,n \le 2e9 t1e5n2e9 这样的数据。


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β1P2β2Pnβ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 P10P20Pn0 即为 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 t1e5n2e9,结果对 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=(sump+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 sump+1=1
第二轮 s u m ∗ p + 1 = p + 1 sum *p +1 =p+1 sump+1=p+1
第三轮 s u m ∗ p + 1 = p 2 + p + 1 sum*p+1=p^{2}+p+1 sump+1=p2+p+1
第四轮 s u m ∗ p + 1 = p 3 + p 2 + p + 1 sum*p+1=p^{3}+p^{2}+p+1 sump+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  (4e55e4) ) 但一般远小于 O ( 1 e 5   ∗   ( 4 e 5 ∼ 5 e 4 )   ) O(1e5\ *\ (4e5 \sim 5e4)\ ) O(1e5  (4e55e4) )


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=k1k+k2k=(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  (ax+by) , (xZ,yZ),这无外乎就是 k 1 = x ∗ a k k1 = x*\frac{a}{k} k1=xka k 2 = y ∗ b k k2 = y * \frac{b}{k} k2=ykb ,所以很明显 k k k 能整除 a ∗ x + b ∗ y a*x+b*y ax+by x x x y y y 为任意整数。
  • 另外我们还知道 a   %   b = a − ⌊ a b ⌋ ∗ b a\ \%\ b = a -\lfloor \frac{a}{b}\rfloor *b a % b=abab ,那么我们可以把 ⌊ a b ⌋ \lfloor \frac{a}{b}\rfloor ba 看成一个整数,那么根据我们上面的结论,我们可以令 x = 1 , y = − ⌊ a b ⌋ x = 1,y=-\lfloor \frac{a}{b}\rfloor x=1y=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=abab
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  (cx+by) , (xZ,yZ)
那么我们令 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 (cx+by)=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 / 21<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(ba % b) 要分两种情况
  1. 如果 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(2log(a+b))
  2. 如果 a > b a > b a>b , 那么迭代一次之后得到 g c d ( b , a   %   b ) gcd(b,a\ \%\ b) gcd(ba % 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))

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值