《算法竞赛进阶指南》0x32_1约数

约数

定义
若整数n除以d的余数是0,即d能整除n,则称d是n的约数,n是d的倍数,记为d|n。

算法基本定理的推论
在算法基本定理中,若正整数N被唯一分解为 N = p 1 c 1 p 2 c 2 . . . p m c m N=p_1^{c_1}p_2^{c_2}...p_m^{c_m} N=p1c1p2c2...pmcm,其中ci都是正整数,pi都是质数,且满足 p 1 < p 2 < . . . < p n p_1<p_2<...<p_n p1<p2<...<pn,则N的正约数集合可写作:
{ p 1 c 1 p 2 c 2 . . . p m c m } ,其中 0 ≤ b i ≤ c i \{p_1^{c_1}p_2^{c_2}...p_m^{c_m}\},其中0 \leq b_i \leq c_i {p1c1p2c2...pmcm},其中0bici
N的正约数个数是
( c 1 + 1 ) ( c 2 + 1 ) ∗ . . . ∗ ( c m + 1 ) = ∏ i = 1 m ( c i + 1 ) (c_1+1)(c_2+1)*...*(c_m+1)=\prod\limits_{i=1}^{m}(c_i+1) (c1+1)(c2+1)...(cm+1)=i=1m(ci+1)
所有正约数的和为
( 1 + p 1 + p 1 2 + . . . + p 1 c 1 ) ∗ . . . ∗ ( 1 + p m + p m 2 + . . . + p m c m ) = ∏ i = 1 m ( ∑ i = 1 c i ( p i ) j ) (1+p_1+p_1^2+...+p_1^{c_1})*...*(1+p_m+p_m^2+...+p_m^{c_m})=\prod_{i=1}^m(\sum\limits_{i=1}^{c_i}(p_i)^j) (1+p1+p12+...+p1c1)...(1+pm+pm2+...+pmcm)=i=1m(i=1ci(pi)j)

1.求N的正约数集合——试除法
d ≤ N d \leq \sqrt{N} dN 是N的约数,则 N / d ≤ N N/d \leq \sqrt{N} N/dN 也是N的约数。换言之,约数总是成对出现的。
因此只需要扫描 [ 1 , N ] [1,\sqrt{N}] [1,N ]中的数,尝试d能否整除N,若能整除,则N/d也是N的约数,时间复杂度为 O ( N ) O(\sqrt{N}) O(N )

int factor[1600], m = 0;
for (int i = 1; i * i <= n; i ++ )
{
	if (n % i == 0)
	{
		factor[ ++ m] = i;
		if (i != n / i) factor[ ++ m] = n/ i;
	}
}
for (int i = 1; i <= m; i ++ )
cout << factor[i] << endl;

试除法的推论
一个整数的约数个数上界为 2 N 2\sqrt{N} 2N

2.求1-N每个数的正约数集合——被除法
若用试除法求1-N所有书的正约数集合,时间复杂度为 O ( N N ) O(N\sqrt{N}) O(NN ),可以反过来考虑,对于每个数d,1-N中以d为约数的数就是d的倍数d,2d,3d,… ⌊ N / d ⌋ ∗ d \lfloor N/d \rfloor*d N/dd

vector<int> factor[500010];
for (int i = 1; i <= n; i ++ )
	for (int j = i; j <= n; j += i)
	factor[j].push_back(i);
for (int i = 1; i <= n; i ++ )
{
	for (int j = 0; j < factor[i].size(); j ++ )
	printf("%d ",factor[i][j]);
	put("");
}

时间复杂度 O ( N + N / 2 + N / 3 + . . . + N / N ) = O ( N l o g N ) O(N+N/2+N/3+...+N/N)=O(NlogN) O(N+N/2+N/3+...+N/N)=O(NlogN)

倍除法的推论
1-N中每个数的约数个数总和大约是NlogN。

acwing1291.轻拍牛头

倍除法模板题,变动在于每个数的数量不一定是1。

#include <iostream>
using namespace std;
#define N 1000010
int a[N], cnt[N], sum[N];
int n;
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ )
    {
        cin >> a[i];
        cnt[a[i]] ++ ;
    }
        
    for (int i = 1; i < N; i ++ )
    {
        if (cnt[i] == 0) continue;
        for (int j = i; j < N; j += i)
        sum[j] += cnt[i];
    }
    for (int i = 1; i <= n; i ++ )
    cout << sum[a[i]] - 1 << endl;
    return 0;
}

acwing1294.樱花

1 x + 1 y = 1 n ! \frac{1}{x}+\frac{1}{y}=\frac{1}{n!} x1+y1=n!1
x n ! + y n ! = x y xn!+yn!=xy xn!+yn!=xy
x ( y − n ! ) = y n ! x(y-n!)=yn! x(yn!)=yn!
x = y n ! ( y − n ! ) = n ! + ( n ! ) 2 y − n ! x=\frac{yn!}{(y-n!)}=n!+\frac{(n!)^2}{y-n!} x=(yn!)yn!=n!+yn!(n!)2
又因为x和y一定大于n!,因此将本问题转化为对于x求整数解的个数,也即 ( n ! ) 2 y − n ! \frac{(n!)^2}{y-n!} yn!(n!)2正整数解的个数,也即 ( n ! ) 2 (n!)^2 (n!)2约数的个数。

对于n!约数个数的求解可以采用分解质因数的方法,时间复杂度只有O(n), ( n ! ) 2 (n!)^2 (n!)2的约数个数求法同理。

#include <iostream>
using namespace std;
const int N = 1000010;
const int mod = 1e9 + 7;
typedef long long ll;
int prime[N];
bool st[N];
int n, cnt = 0;
void init(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) prime[cnt ++ ] = i;
        for (ll j = 0; prime[j] * i <= n; j ++ )
        {
            st[i * prime[j]] = 1;
            if (i % prime[j] == 0)break;
        }
    }
}
int main()
{
    cin >> n;
    ll res = 1;
    init(n);
    for(int i = 0; i < cnt; i ++ )
    {
        int p = prime[i];
        ll s = 0;
        for (ll j = p; j <= n; j *= p) s += n / j;
        res = res * (2 * s + 1) % mod;
    }
    cout << res;
    return 0;
}

acwing198.反素数
分析题目可以得到以下三个性质
1)不同的质因子个数最多有9个,即2,3,5,7,11,13,17,19,23,因为2*3*5*7*11*13*17*19*23*29大于了2e9,因此可能的质因子只有2到23。
2)约数的个数最多只有30,因为2^31刚好大于了2e9。
3)分解为 p 1 c 1 p 2 c 2 . . . p m c m p_1^{c_1}p_2^{c_2}...p_m^{c_m} p1c1p2c2...pmcm之后,一定有 c 1 ≤ c 2 ≤ . . . ≤ c m c_1 \leq c_2 \leq ... \leq c_m c1c2...cm,因为如果出现 c i > c j , i < j c_i >c_j,i <j ci>cj,i<j,那么就可以通过交换ci和cj的值,得到约数个数相同但是值更小的数,使得原先的数不符合反素数的定义。

采用dfs来做,依次枚举每一个可能的约数的次数,后面的约数次数不能大于此约数的次数,一旦找到约数个数更多的数,或者约束个数相同但是值更小的数则更新答案。

#include <iostream>
using namespace std;
typedef long long ll;
const int N = 2e9 + 7;
int prime[9] = {2, 3, 5, 7, 11, 13, 17, 19, 23};
int n;
int max_cnt = 0, number = n;
//当前枚举到了第u个数,可以枚举的指数最大到last
//当前枚举到的数构成的数为mul,当前约数个数为cnt
void dfs(int u, int last, int mul, int cnt)
{
    if (cnt > max_cnt || cnt == max_cnt && mul < number)
    {
        number = mul;
        max_cnt = cnt;
    }
    if (u == 9)return ;
    for (int i = 1; i <= last; i ++ )
    {
        if ((ll) mul * prime[u] > n) break;
        mul *= prime[u];
        dfs(u + 1, i, mul, cnt * (i + 1));
    }
}
int main()
{
    cin >> n;
    dfs(0, 30, 1, 1);
    cout << number;
    return 0;
}

最大公约数

定义
若自然数d同时是自然数a和b的约数,则称d是a和b的公约数,在所有a和b的公约数里最大的一个,成为a和b的最大公约数,记为gcd(a, b)。
若自然数m同时是自然数a和b的倍数,则称m是a和b的公倍数,在所有a和b的公倍数里最小的一个,成为a和b的最小公倍数,记为lcm(a, b)。

定理
∀ a , b ∈ N , g c d ( a , b ) ∗ l c m ( a , b ) = a ∗ b \forall a,b \in \mathbb{N}, gcd(a,b)*lcm(a,b)=a*b a,bN,gcd(a,b)lcm(a,b)=ab
证明:
d = g c d ( a , b ) , a 0 = a / d , b 0 = b / d d=gcd(a,b),a0=a/d,b0=b/d d=gcd(a,b),a0=a/d,b0=b/d。根据最大公约数的定义,又 g c d ( a 0 , b 0 ) = 1 gcd(a0,b0)=1 gcd(a0,b0)=1.再根据最小公倍数的定义,有 l c m ( a 0 , b 0 ) = a 0 ∗ b 0 lcm(a0,b0)=a0*b0 lcm(a0,b0)=a0b0
于是 l c m ( a , b ) = l c m ( a 0 ∗ d , b 0 ∗ d ) = d ∗ l c m ( a 0 , b 0 ) = a 0 ∗ b 0 ∗ d = a ∗ b / d lcm(a,b)=lcm(a0*d,b0*d)=d*lcm(a0,b0)=a0*b0*d=a*b/d lcm(a,b)=lcm(a0d,b0d)=dlcm(a0,b0)=a0b0d=ab/d
证毕。

更相减损数
∀ a , b ∈ N , a ≥ b , 有 g c d ( a , b ) = g c d ( b , a − b ) = g c d ( a , a − b ) \forall a,b \in \mathbb{N},a\geq b,有gcd(a,b)=gcd(b,a-b)=gcd(a,a-b) a,bN,ab,gcd(a,b)=gcd(b,ab)=gcd(a,ab)
∀ a , b ∈ N , 有 g c d ( 2 a , 2 b ) = 2 g c d ( a , b ) \forall a,b \in \mathbb{N},有gcd(2a,2b)=2gcd(a,b) a,bN,gcd(2a,2b)=2gcd(a,b)

欧几里得算法

∀ a , b ∈ N , b ! = 0 , 有 g c d ( a , b ) = g c d ( b , a   m o d   b ) \forall a,b \in \mathbb{N},b!=0,有gcd(a,b)=gcd(b,a\ mod \ b) a,bN,b!=0,gcd(a,b)=gcd(b,a mod b)

证明:
a < b , 则 g c d ( b , a   m o d   b ) = g c d ( b , a ) = g c d ( a , b ) ,显然成立 a<b,则gcd(b,a\ mod \ b)=gcd(b,a)=gcd(a,b),显然成立 a<b,gcd(b,a mod b)=gcd(b,a)=gcd(a,b),显然成立
a ≥ b , 不妨设 a = q ∗ b + r , 其中 0 ≤ r < b , 显然 r = a   m o d   b , 对于 a , b 的任意公约数 d ,因为 d ∣ a , d ∣ q ∗ b , 故 d ∣ ( a − q ∗ b ) , 即 d ∣ r , 因此 d 也是 b , r 的公约数。 a \geq b,不妨设a=q*b+r,其中0 \leq r < b,显然r=a \ mod \ b,对于a,b的任意公约数d,因为d|a,d|q*b,故d|(a-q*b),即d|r,因此d也是b,r的公约数。 ab,不妨设a=qb+r,其中0r<b,显然r=a mod b,对于ab的任意公约数d,因为da,dqb,d(aqb),dr,因此d也是br的公约数。
故a,b的公约数集合和b,r的公约数集合相同,最大公约数自然也相同。

int gcd(int a, int b)
{
	return b ? gcd(b, a % b) : a;
}

时间复杂度为 O ( l o g ( a + b ) ) O(log(a+b)) O(log(a+b))

acwing200.Hankson的趣味题

因为x和b0的最小公倍数是b1,所以x一定是b1的约数,所以可以考虑直接枚举b1的约数,并判断是否符合这两个条件,均满足的话则答案加1。
分析时间复杂度,首先两个判断的时间复杂度均为log(n)级别,对于每一个b1求约数的时间复杂度为 b 1 \sqrt{b1} b1 ,结合已知结论, 1 0 9 以内的数最大的约数个数为 1536 10^9以内的数最大的约数个数为1536 109以内的数最大的约数个数为1536,因此总体时间复杂度为
O ( T ∗ b 1 + T ∗ 1536 ∗ l o g n ) O(T*\sqrt{b1}+T*1536*logn) O(Tb1 +T1536logn),前半部分大大小为2000*5*10^4=10^8,会超时,考虑优化求约数的过程。
优化思路:
预处理出[1,50000]内的所有质数,质数个数大约为 n / l n n \sqrt{n}/ln\sqrt{n} n /lnn ,然后对每一个数在 n / l n n \sqrt{n}/ln\sqrt{n} n /lnn 的时间复杂度为分解质因数,分解之后可以通过一遍dfs枚举出所有的约数,dfs的时间复杂度约取决于数个数,不超过1536。
因此优化后的时间复杂度为 O ( n + T ∗ ( n / l n n + 1536 ) + T ∗ 1536 ∗ l o g n ) 。 O(\sqrt{n}+T*(\sqrt{n}/ln\sqrt{n}+1536)+T*1536*logn)。 O(n +T(n /lnn +1536)+T1536logn)

#include <iostream>
#include <cstring>
#include <cmath>
using namespace std;
#define N 500010
struct Data{
    int p, c;
}factor[N];
int prime[N];
bool st[N];
int divider[N];
typedef long long ll;
//cnt 记录素数个数,cnt2记录一个数可以分解为的不同质因子个数
//cnt3记录一个数的约数个数
int n, cnt = 0, cnt2 = 0, cnt3 = 0;
int a0, a1, b0, b1;
void init(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i])prime[cnt ++ ] = i;
        for (int j = 0; prime[j] * i <= n; j ++ )
        {
            st[i * prime[j]] = 1;
            if (i % prime[j] == 0)break;
        }
    }
}
void divide(int x)
{
    cnt2 = 0;
    for (int i = 0; prime[i] <= sqrt(x); i ++ )
    {
        int p = prime[i];
        if (x % p == 0)
        {
            int s = 0;
            while (x % p == 0)
            {
                s += 1;
                x /= p;
            }
            factor[cnt2 ++ ] = {p, s};
        }
    }
    if(x > 1) factor[cnt2 ++ ] ={x, 1};
}
void dfs(int u, int p)
{
    if (u == cnt2)
    {
        divider[cnt3 ++ ] = p;
        return ;
    }
    for (int i = 0; i <= factor[u].c; i ++ )
    {
        dfs(u + 1, p);
        p *= factor[u].p;
    }
    return ;
}
int gcd(int x, int y)
{
    return y ? gcd(y, x % y) : x;
}
ll lcm(int x, int y)
{
    return (ll) x * y / gcd(x, y);
}
int main()
{
    init(N - 1);
    cin >> n;
    while(n -- )
    {
        cin >> a0 >> a1 >> b0 >> b1;
        divide(b1);
        cnt3 = 0;
        dfs(0, 1);
        int res = 0;
        for (int i = 0; i < cnt3; i ++ )
        {
            if(gcd(a0, divider[i]) == a1 && lcm(b0, divider[i]) == b1)
            res ++ ;
        }
        cout << res << endl;
    }
    return 0;
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值