zoukankan      html  css  js  c++  java
  • 拯救数学——莫比乌斯反演

    莫比乌斯反演

    本文符号说明

    ([A]=1) 代表表达式 (A) 是真,([A]=0) 代表其为假。

    (gcd(x,y)) 代表最大公约数,(lcm(x,y)) 代表最小公倍数。

    (Omega(x)) 代表 (x) 的质因子个数。

    (p) 如果没有特殊说明均指一个素数。

    前置芝士

    包括积性函数性质(线性筛),狄利克雷卷积以及数论分块,杜教筛,min_25筛的内容。如果已经会了的可以自行跳过。

    除了这些前置芝士你可能还需要会的知识:约数相关素数相关、组合数学、二项式定理。

    积性函数

    若一个数论函数(定义域和值域均为整数(这个定义可能不严谨,但不妨碍我们的推到))满足以下性质

    [gcd(x,y)=1 Rightarrow f(x)f(y) = f(xy) ]

    则称 (f(x)) 为数论函数。

    如果不用 (gcd(x,y)=1) 则称完全积性函数。

    一些常见的的积性函数

    欧拉函数

    (varphi (x)) 代表 ([1,x]) 中与 (x) 互质的数的个数,即 (varphi(x)=sumlimits_{i=1}^x [gcd(i,x)=1])

    通过类容斥原理我们可以的到欧拉函数的快速计算公式:

    (x=p_1^{c_1} imes p_2^{c_2} imes cdots imes p_m^{c_m}),则 (varphi(x)=x imes frac{p_1-1}{p_1} imes frac{p_2-1}{p_2} imes cdots imes frac{p_n-1}{p_n})

    证明:

    (p,q) 均为 (x) 的质因子,则每 (p) 个数中只有一个与 (x) 不互质,同理每 (q) 个数中也只有一个与 (x) 不互质。

    ((p,q)=1),所以互质的数一定不冲突,就有 (x imes frac{p-1}{p} imes frac{q-1}{q}) 个互质的数。显然这个可以推到 (m) 项。

    通过上面的例子我们还可以发现欧拉函数的一个性质:(varphi(p) = p - 1),其中 (p) 是质数。

    例题:求 (sumlimits_{i=1}^n i imes [gcd(n,i)=1])

    即求 (n) 以内所有与 (n) 互质的数的和。根据辗转相减法有 (gcd(a,b) =gcd(a,a-b)),所以互质的数一定是成对出现且和为 (n) 的。所以题目所求即为 (frac{varphi(n) imes n}{2})

    根据其求和式,显然欧拉函数为积性函数。根据积性函数的性质我们可以推出以下结论:

    对于一个质数 (p),若 ((x,p)=1),则 (varphi(xp)=varphi(x)varphi(p)=(p-1)varphi(x))。否则 (p|x),根据欧拉函数的计算公式 (varphi(xp)=pvarphi(x))

    根据上述两个性质我们又可以得到线性筛欧拉函数的方法,类似欧拉筛质数,还是用最小的质因子更新:

    int N;
    int prime[MAXN], phi[MAXN], cnt;
    bool mark[MAXN];
    void sieve() {
    	phi[1] = 1;
    	for (int i = 2; i <= N; i++) {
    		if (!mark[i]) {
    			prime[++cnt] = i;
    			phi[i] = i - 1;
    		}
    		for (int j = 1; j <= cnt && prime[j] * i <= N; j++) {
    			mark[i * prime[j]] = true;
    			if (i % prime[j] == 0) {
    				phi[i * prime[j]] = prime[j] * phi[i];
    				break;
    			} else {
    				phi[i * prime[j]] = (prime[j] - 1) * phi[i];
    			}
    		}
    	}
    }
    

    总结一下,如果是积性函数且素数情况易得,倍数情况易推导的函数即可线性筛。

    莫比乌斯函数

    通过欧拉函数我们已经简单了解了积性函数,现在我们请出我们今天的主角——莫比乌斯函数。

    有些教材会把容斥原理与莫比乌斯函数一起讲,原因是莫比乌斯函数其实就是容斥系数,我们先来看莫比乌斯函数的定义。

    莫比乌斯函数记作 (mu(x)),若 (x=1)(mu(x)=1);若 (x) 为若干素数乘积则 (mu(x)=(-1)^{Omega(x)});其它情况为 (0)

    先来证明其为积性函数:

    对于两个数 ((x,y)=1),若 (mu(x)=0)(mu(y)=0)(mu(xy)) 一定也为 (0),得证。若 (x=1)(y=1) 也显然成立。

    我们主要考虑 (mu(x)=(-1)^{Omega(x)},mu(y)=(-1)^{Omega(y)}) 这种情况。((x,y)=1)(Omega(xy)=Omega(x)Omega(y)),所以显然 (mu(xy)=mu(x)mu(y))

    为了线性筛出莫比乌斯函数所以我们还需找到 (mu(p)),显然 (mu(p)=-1)

    根据积性函数的性质若 ((p,x)=1),则 (mu(px)=-mu(x)),而当其为 (p) 的倍数时直接就是 (0)

    int N;
    int prime[MAXN], mu[MAXN], cnt;
    bool mark[MAXN];
    void sieve() {
    	mu[1] = 1;
    	for (int i = 2; i <= N; i++) {
    		if (!mark[i]) {
    			prime[++cnt] = i;
    			mu[i] = -1;
    		}
    		for (int j = 1; j <= cnt && prime[j] * i <= N; j++) {
    			mark[i * prime[j]] = true;
    			if (i % prime[j] == 0) {
    				mu[i * prime[j]] = 0;
    				break;
    			} else {
    				mu[i * prime[j]] = -mu[i];
    			}
    		}
    	}
    }
    
    约数个数与约数和函数

    题目中有可能会出现这里就简单介绍一下求和式,不过相信大家小学奥数应该也都学过。

    (n=p_1^{c_1} imes p_2^{c_2} imes cdots imes p_m^{c_m})

    约数个数函数:(d(n)=(c_1+1) imes (c_2+1) imes cdots imes (c_m+1))

    约数和函数:(sigma(n)=(sum_{i=0}^{c_1} p_1^i) imes cdots imes(sum_{i=0}^{c_m}p_m^i))

    性质的证明与线性筛的实现读者可以自己思考。

    一道练习题(数据还没造完)

    这里再提一嘴埃氏筛,这玩意虽然 (O(nlog{n})),但有的时候是即好想又好用,如果时间允许写这个比想破脑袋写线性筛好多了。其实就是用每个数去筛其倍数。

    上面那道题没卡埃氏筛。

    一些常见的完全积性函数

    常函数 (I(n)=1)

    恒等函数 (id(n)=n)

    单位函数 (varepsilon(n)=[n=1])

    狄利克雷卷积

    对于三个数论函数 (F(x),f(x),g(x)),若 (F(x)=sum_{d|x}f(x)g(frac{x}{d})),则称 (F)(f)(g) 的狄利克雷卷简记 (F=f*g)。显然 (f*g=g*f)

    性质1:(f * varepsilon=f)

    证明:由狄利克雷卷积的定义就可以证明。

    性质2:(mu * I=varepsilon)

    证明:稍微复杂,展开其实是 (sum_{d|n}mu(d)),当 (n=1)(mu(n)=1) 得证;而 (n e 1) 时,因为对于不是若干质数乘积的数 (mu)(0),所以我们只考虑是若干质数乘积的 (d),所以我们只用考虑选或不选某个质因子,从而转变为在 (Omega(n)) 个数中选 (0,1,2dotsOmega(n)) 个数的方案。

    所以有:

    [sum_{d|n}mu(d)=sum_{i=0}^{Omega(n)} (-1)^{i}C_{Omega(n)}^{i}=(1+(-1))^{Omega(n)}=0 ]

    得证。这条性质很重要,可能在做题时比一般的反演用得还多。反演本质。

    性质3:(varphi * I = id)

    证明:两种证明方法,这里用逆推法证,另外一种用积性函数证的可以参考李煜东的蓝书。

    即证:(sum_{d|n}varphi(d)=n)。我们把 ([1,n]) 的数与和 (n)(gcd) 分类,即 (n=sum_{i|n}sum_{j=1}^n [gcd(j,n)=i]),再根据 (gcd) 的性质,有 (sum_{i|n}sum_{j=1}^n [gcd(j,n)=i]=sum_{i|n}sum_{j=1}^{frac{n}{i}} [gcd(j,frac{n}{i})=1]=sum_{i|n} varphi(frac{n}{i})=sum_{i|n}varphi(i))

    得证。这条结论还被称为欧拉反演。

    另一种证明方法:

    (f = varphi * I),易得 (f) 是积性函数。(f(p^m)=sum_{i=1}^{m}varphi(p^i)=p^m),所以 (f(n)=n)(f=id)

    性质4:(mu*id=varphi)

    这其实已经是一个莫比乌斯反演的形式了。通过性质2和性质3可得:(mu*I*id=varepsilon*id=id=varphi*I),然后两边同时搞掉 (I) 就是性质4的式子了。

    性质5、6:

    做题可能会用到

    (I*I=d)

    (I*id=sigma)

    数论分块

    (sumlimits_{i=1}^nlfloorfrac{n}{i} floor)([i,n/(n/i)]) 的数都是相等的。最多只会有 (2sqrt{n}) 块,所以可以 (O(sqrt{n})) 解决。

    for (int i = 1, j; i <= n; i = j + 1) {
        j = n / (n / i);
        ans += (j - i + 1) * (n / i);
    }
    

    莫比乌斯反演

    终于到正题了qwq。

    莫比乌斯反演的约数形式:(F=f*I),则(f=F*mu)

    证明和性质4很像:(F*mu=f*mu*I=f*epsilon=f)

    倍数形式:(F(n)=sum_{n|d}f(d))(f(n)=sum_{n|d}F(d)mu(d/n))

    好了,没了,就这,就这!!!

    但是毒瘤出题人可以把莫比乌斯反演出到你怀疑人生。

    杜教筛

    我们在数论分块的时候经常要用到前缀和,而有的时候毒瘤出题人喜欢把 (n) 出到 (10^{10}) 或更高,现在线性筛也没用了,我们需要一个更快的求出积性函数前缀和的方法。

    假设我们现在想求 (f) 的前缀和,设为 (S),然后我们找到一个积性函数 (g) 使得 (f*g) 的前缀和很好求,(g) 的前缀和也很好求。假设我们现在已经找到了这么一个 (g)

    [sum_{i=1}^n(f*g)(i)=sum_{i=1}^{n}sum_{d|i}f(d)g(i/d) ]

    可以把 (d) 提到前面来转而枚举 (d)

    [sum_{d=1}^{n}g(d)sum_{i,d|i}f(i/d)=sum_{d=1}^{n}g(d)sum_{i=1}^{frac{n}{d}}f(i)=sum_{d=1}^{n}g(d)S(frac{n}{d}) ]

    我们现在想求 (g(1)S(n)),我们发现直接一减就是:

    [g(1)S(n)=sum_{d=1}^{n}g(d)S(frac{n}{d})-sum_{d=2}^{n}g(d)S(frac{n}{d})=sum_{i=1}^n(f*g)(i)-sum_{d=2}^{n}g(d)S(frac{n}{d}) ]

    前面的很好求(这是要求),后面的可以数论分块然后递归去做。

    时间复杂度的分析:

    [T(n)=Theta(sqrt{n})+sum_{i=2}^{n}(T(i)+T(n/i))=Theta(sqrt{n})+sum_{i=2}^{n}(Theta(sqrt{i})+Theta(sqrt{n/i}))=Theta(n^{frac{3}{4}}) ]

    如果通过线性筛预处理前面的一部分,假设是 (k)

    则为 (Theta(frac{n}{sqrt{k}})),显然 (k=n^{2/3}) 最优,复杂度为 (n^{2/3})

    【模板】杜教筛(Sum)

    看到 (mu) 想到 (mu*I=varepsilon),然后 (varepsilon)(I) 的前缀和都很好求,就做完了,注意记忆化。

    (varphi) 的自己思考。

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int MAXN = 2000000;
    int t;
    ll prime[MAXN], cnt;
    ll sum_mu[MAXN], sum_phi[MAXN];
    bool mark[MAXN];
    map<ll, ll> mp_mu, mp_phi;
    ll getsum_mu(ll n) {
        if (n < MAXN) return sum_mu[n];
        if (mp_mu.find(n) != mp_mu.end()) return mp_mu[n];
        ll ret = 1;
        for (ll i = 2, j; i <= n; i = j + 1) {
            j = n / (n / i);
            ret -= 1ll * (j - i + 1) * getsum_mu(n / i);
        }
        return mp_mu[n] = ret;
    }
    ll getsum_phi(ll n) {
        if (n < MAXN) return sum_phi[n];
        if (mp_phi.find(n) != mp_phi.end()) return mp_phi[n];
        ll ret = n * (n + 1) >> 1;
        for (ll i = 2, j; i <= n; i = j + 1) {
            j = n / (n / i);
            ret -= 1ll * (j - i + 1) * getsum_phi(n / i);
        }
        return mp_phi[n] = ret;
    }
    void sieve() {
        sum_mu[1] = sum_phi[1] = 1;
        for (int i = 2; i < MAXN; i++) {
            if (!mark[i]) {
                prime[++cnt] = i;
                sum_mu[i] = -1;
                sum_phi[i] = i -1 ;
            }
            for (int j = 1; j <= cnt && prime[j] * i < MAXN; j++) {
                mark[i * prime[j]] = 1;
                if (i % prime[j] == 0) {
                    sum_mu[i * prime[j]] = 0;
                    sum_phi[i * prime[j]] = sum_phi[i] * prime[j];
                    break;
                } else {
                    sum_mu[i * prime[j]] = -sum_mu[i];
                    sum_phi[i * prime[j]] = sum_phi[i] * (prime[j] - 1);
                }
            }
        }
        for (int i = 2; i < MAXN; i++) sum_mu[i] += sum_mu[i - 1], sum_phi[i] += sum_phi[i - 1];
    }
    int main() {
        sieve();
        read(t);
        while (t--) {
            ll n;
            read(n);
            printf("%lld %lld
    ", getsum_phi(n), getsum_mu(n));
        }
        return 0;
    }
    

    例题

    [POI2007]ZAP-Queries

    题目即求 (sum_{i=1}^asum_{j=1}^b[gcd(i,j)=d])

    这里介绍几种不同的算法,其实不同只在推到过程,最后的式子都是相同的。

    这里不妨设 (a ge b)

    算法1

    只用到了上述性质2。根据 (gcd) 的性质,有(gcd(i,j)=d Rightarrow gcd(i/d,j/d)=1)

    所以这题所求可以转换为

    [sum_{d|i}sum_{d|j}[gcd(i/d,j/d)=1]=sum_{i=1}^{[frac{a}{d}]}sum_{j=1}^{[frac{b}{d}]}[gcd(i,j)=1]=sum_{i=1}^{[frac{a}{d}]}sum_{j=1}^{[frac{b}{d}]}epsilon(gcd(i,j)) ]

    继续推为

    [sum_{i=1}^{[frac{a}{d}]}sum_{j=1}^{[frac{b}{d}]}sum_{k|gcd(i,j)}mu(k)=sum_{i=1}^{[frac{a}{d}]}sum_{j=1}^{[frac{b}{d}]}sum_{k=1}^{gcd(i,j)}mu(k) imes[k|gcd(i,j)] ]

    可以把 (mu(k)) 提到前面来:

    [sum_{k=1}^{[frac{b}{d}]}mu(k) imes sum_{i=1}^{[frac{a}{d}]}sum_{j=1}^{[frac{b}{d}]}[k|gcd(i,j)]=sum_{k=1}^{[frac{b}{d}]}mu(k) imes[frac{a}{dk}] imes [frac{b}{dk}] ]

    显然这个东西可以数论分块来搞,时间复杂度 (O(sqrt{min(a,b)}))

    算法2

    看完算法1我们发现这和莫比乌斯反演莫得关系啊。那来个有关系的?

    (f(d)) 为所求。我们需要找到一个合适的 (F(d)),这里我们发现若选取 (F(n)=sum_{n|d}f(d)=[frac{a}{n}] cdot[frac{b}{n}]),比较好搞。

    根据莫比乌斯反演的倍数公式 :

    [f(n)=sum_{n|d}mu(d/n)F(d)=sum_{n|d}mu(d/n)[frac{a}{n}] cdot[frac{b}{n}] ]

    于是我们得到了....和上面一样的东西!

    惊不惊喜,意不意外!

    但我觉得还是算法1更无脑一点,毕竟算法2需要选择合适的 (F)。不知道有没有神仙教教我怎么快速找 (F) 啊。

    不过你说算法1叫莫比乌斯反演也可,因为其用到了莫比乌斯反演最基础的公式(反演本质),而且最后一步也推到了类似莫比乌斯反演公式的东西。算法1最重要的核心是找到 (varepsilon),也就是 ([...=1]) 这种东西。

    对于看算法2爽的同学,对于这种 (gcd) 的题 (F) 一般都是这么选(倍数形式)的。

    这道题的代码实现:

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int MAXN = 50010;
    ll mu[MAXN], prime[MAXN], cnt, sum[MAXN];
    bool mark[MAXN];
    void sieve() {
    	mu[1] = sum[1] = 1;
    	for (int i = 2; i < MAXN; i++) {
    		if (!mark[i]) {
    			prime[++cnt] = i;
    			mu[i] = -1;
    		}
    		for (int j = 1; j <= cnt && prime[j] * i < MAXN; j++) {
    			mark[i * prime[j]] = 1;
    			if (i % prime[j] == 0) {
    				mu[i * prime[j]] = 0;
    				break;
    			} else {
    				mu[i * prime[j]] = -mu[i];
    			}
    		}
    	}
    	for (int i = 2; i < MAXN ; i++) sum[i] = mu[i] + sum[i - 1];
    }
    ll solve(int a, int b, int d) {
    	a /= d, b /= d;
    	if (a < b) swap(a, b);
    	ll ret = 0;
    	for (int i = 1, j; i <= b; i = j + 1) {
    		j = min(a / (a / i), b / (b / i));
    		ret += (sum[j] - sum[i - 1]) * (long long)(a / i) * (long long)(b / i);
    	}
    	return ret;
    }
    int main() {
    	sieve();
    	int n;
    	scanf("%d", &n);
    	while (n--) {
    		int a, b, d;
    		scanf("%d%d%d", &a, &b, &d);
    		printf("%lld
    ", solve(a, b, d));
    	}
    	return 0;
    }
    

    YY的GCD

    这题和上题类似,不过怕还有同学(比如说我)不会推式子就再推一遍。

    求:(sum_{pin Prime}sum_{i=1}^nsum_{j=1}^m[gcd(i,j)=p])

    [sum_{pin Prime}sum_{i=1}^nsum_{j=1}^m[gcd(i,j)=p]=sum_{pin Prime}sum_{i=1}^{lfloor frac{n}{p} floor}sum_{j=1}^{lfloor frac{m}{p} floor}[gcd(i,j)=1]=sum_{pin Prime}sum_{i=1}^{lfloor frac{n}{p} floor}sum_{j=1}^{lfloor frac{m}{p} floor}sum_{k|gcd(i,j)}mu(k) ]

    老套路,把 (k) 搞到前面来:

    [sum_{pin Prime}sum_{k=1}^{lfloor frac{n}{p} floor}mu(k)lfloor frac{n}{pk} floorlfloor frac{m}{pk} floor ]

    这里看 (pk) 不爽,再把它搞到前面去,其实也就是把 (p) 搞到里面去:

    [sum_{pk=1}^{n}lfloor frac{n}{pk} floorlfloor frac{m}{pk} floorsum_{p}mu(frac{pk}{p}) ]

    后面那玩意可以用埃氏筛求出来,时间复杂度约为:(O(frac{n}{ln{n}} imes ln{{n}})=O(n))。当然还可用标准的线性筛(那也是个积性函数),不过没有必要(上面有提到)。

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int MAXN = 10000010;
    int T;
    int N, M;
    int mu[MAXN], prime[MAXN], cnt;
    ll sum[MAXN];
    bool mark[MAXN];
    void sieve() {
    	mu[1] = 1;
    	for (int i = 2; i < MAXN; i++) {
    		if (!mark[i]) {
    			prime[++cnt] = i;
    			mu[i] = -1;
    		}
    		for (int j = 1; j <= cnt && prime[j] * i < MAXN; j++) {
    			mark[i * prime[j]] = 1;
    			if (i % prime[j] == 0) {
    				mu[i * prime[j]] = 0;
    				break;
    			} else {
    				mu[i * prime[j]] = -mu[i];
    			}
    		}
    	}
    	for (int i = 1; i <= cnt; i++)
    		for (int j = 1; j * prime[i] < MAXN; j++)
    			sum[j * prime[i]] += mu[j];
    	for (int i = 1; i < MAXN; i++) sum[i] += sum[i - 1];
    }
    ll solve(int n, int m) {
    	ll ret = 0;
    	if (n > m) swap(n, m);
    	for (int i = 1, j; i <= n; i = j + 1) {
    		j = min(n / (n / i), m / (m / i));
    		ret += (sum[j] - sum[i - 1]) * (n / i) * (m / i);
    	}
    	return ret;
    }
    int main() {
    	sieve();
    	scanf("%d", &T);
    	while (T--) {
    		scanf("%d%d", &N, &M);
    		printf("%lld
    ", solve(N, M));
    	}
    	return 0;
    }
    

    类似的题:[HAOI2011]Problem b(套上简单容斥)、[SDOI2014]数表(树状数组维护前缀和)

    [SDOI2015]约数个数和

    这题虽然也和上面类似,但是有一个引理还是很有意思的。

    引理:(d(ij)=sum_{a|i}sum_{b|j}[gcd(i,j)=1])

    证明:考虑一个质数 (p),设 (i=i' imes p^{k_i},j=j' imes p^{k_j})。由上面的约数个数公式可得 (p) 的贡献为 (k_i+k_j+1)。考虑等式右边,设 (a=a' imes p^{k_a},b=b' imes p^{k_b}),若有 (gcd(a,b)=gcd(a' imes p^{k_a},b' imes p^{k_b})=1),必有 (k_a=0)(k_b=0)。而前者和后者分别还有 (k_j+1)(k_i+1) 种,减去都为 (0) 的一种,共 (k_i+k_j+1) 种,刚好为左边。

    然后我们处理的式子即为:

    [sum_{i=1}^nsum_{j=1}^msum_{a|i}sum_{b|j}[gcd(a,b)=1] ]

    然后就是套路:

    [sum_{i=1}^nsum_{j=1}^msum_{a|i}sum_{b|j}sum_{d|gcd(a,b)}mu(d) ]

    然后 (a,b) 作为约数没用,提到前面去:

    [sum_{a=1}^nsum_{b=1}^msum_{a|i}sum_{b|j}sum_{d|gcd(a,b)}mu(d)=sum_{a=1}^nsum_{b=1}^m[frac{n}{a}][frac{n}{b}]sum_{d|gcd(a,b)}mu(d)=sum_{d=1}^nmu(d)sum_{a=1}^{[frac{n}{d}]}[frac{n}{da}]sum_{b=1}^{[frac{m}{d}]}[frac{m}{db}] ]

    后面那俩玩意可以预处理,然后就可以数论分块 (O(sqrt{n})) 做了。

    [国家集训队]Crash的数字表格

    根据小学数学 (lcm(a,b)=frac{a imes b}{gcd(a,b)}),然后套路地枚举 (gcd),得到小面的式子:

    [sum_{k=1}^nksum_{d=1}^{n/k}mu(d)d^2sum_{i=1}^{n/kd}isum_{j=1}^{m/kd}j ]

    发现是两个数论分块套一起,时间复杂度 (O(n))。注意一下二维数论分块时一定要判 (n,m) 大小关系。

    当然这题还有 (O(sqrt{n})) 的解法,具体来说枚举第二个分块的除数为 (T),然后后面搞出来的是个积性函数,可以线性筛。当你想降一下时间复杂度时,枚举第二个分块中的某一项再进行处理可能是一个好选择。

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int MAXN = 10000010;
    const ll mod = 20101009;
    int T;
    int prime[MAXN], mu[MAXN], cnt;
    ll sum[MAXN];
    bool mark[MAXN];
    void sieve() {
    	mu[1] = 1;
    	for (int i = 2; i < MAXN; i++) {
    		if (!mark[i]) {
    			prime[++cnt] = i;
    			mu[i] = -1;
    		}
    		for (int j = 1; j <= cnt && prime[j] * i < MAXN; j++) {
    			mark[i * prime[j]] = 1;
    			if (i % prime[j] == 0) {
    				mu[i * prime[j]] = 0;
    				break;
    			} else {
    				mu[i * prime[j]] = -mu[i];
    			}
    		}
    	}
    	for (ll i = 1; i < MAXN; i++) sum[i] = sum[i - 1] + mu[i] * i * i, sum[i] %= mod;
    }
    ll calc(ll a, ll b) {
    	return ((a + b) * (b - a + 1) >> 1) % mod;
    }
    ll count(int n, int m) {
    	ll ret = 0;
    	if (n > m) swap(n, m);
    	for (int i = 1, j; i <= n; i = j + 1) {
    		j = min(n / (n / i), m / (m / i));
    		ret += (sum[j] - sum[i - 1] + mod) * calc(1, n / i) % mod * calc(1, m / i) % mod;
    		ret %= mod;
    	}
    	return ret % mod;
    }
    ll solve(int n, int m) {
    	ll ret = 0;
    	if (n > m) swap(n, m);
    	for (int i = 1, j; i <= n; i = j + 1) {
    		j = min(n / (n / i), m / (m / i));
    		ret += calc(i, j) * count(n / i, m / i) % mod;
    		ret %= mod;
    	}
    	return ret % mod;
    }
    int main() {
    	sieve();
    	int n, m;
    	scanf("%d%d", &n, &m);
    	printf("%lld
    ", solve(n, m));
    	return 0;
    }
    

    简单的数学题

    欧拉反演×min_25筛或杜教筛

    发现这玩意一顿乱推需要我们求一个这样的东西的前缀和:(id^2varphi)。于是我们要找到一个合适的 (g),看到 (varphi) 我们想到 (I),但是还有个 (id^2) 所以我们也要再搞一个 (id^2)。于是我们的 (g) 就是 (id^2I)(((id^2varphi) * (id^2I))(n)=n^2(varphi*I)(n)=n^3),前缀和就是个立方和公式,相信大家小学奥数都学过。然后 (g) 的前缀和就是个平方和公式,也可以求,于是就可以杜教筛了。杜教筛的时间复杂度不变。

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int MAXN = 5000000;
    ll mod, inv2, inv6;
    int T;
    ll prime[MAXN + 1], phi[MAXN + 1], cnt;
    ll sum[MAXN + 1];
    bool mark[MAXN + 1];
    map<ll, ll> mp;
    ll ksm(ll a, ll b) {
    	ll ret = 1;
    	while (b) {
    		if (b & 1) ret = (ret * a) % mod;
    		a = (a * a) % mod;
    		b >>= 1;
    	}
    	return ret;
    } 
    void sieve() {
    	phi[1] = 1;
    	for (int i = 2; i < MAXN; i++) {
    		if (!mark[i]) {
    			prime[++cnt] = i;
    			phi[i] = i - 1;
    		}
    		for (int j = 1; j <= cnt && prime[j] * i < MAXN; j++) {
    			mark[i * prime[j]] = 1;
    			if (i % prime[j] == 0) {
    				phi[i * prime[j]] = phi[i] * prime[j];
    				break;
    			} else {
    				phi[i * prime[j]] = phi[i] * (prime[j] - 1);
    			}
    		}
    	}
    	for (ll i = 1; i < MAXN; i++) sum[i] = (sum[i - 1] + phi[i] * i % mod * i) % mod;
    }
    ll calc(ll n) {
    	n %= mod;
    	return n * (n + 1) % mod * inv2 % mod;
    }
    ll cal(ll n) {
    	n %= mod;
    	return n * (n + 1) % mod * (2 * n + 1) % mod * inv6 % mod;
    }
    ll get_sum(ll n) {
    	if (n < MAXN) return sum[n];
    	if (mp.find(n) != mp.end()) return mp[n];
    	ll ret = calc(n) * calc(n) % mod;
    	for (ll i = 2, j; i <= n; i = j + 1) {
    		j = n / (n / i);
    		ret -= (cal(j) - cal(i - 1) + mod) * get_sum(n / i) % mod;
    		ret = (ret + mod) % mod;
    	}
    	ret = (ret + mod) % mod;
    	return mp[n] = ret;
    }
    ll solve(int n) {
    	ll ret = 0;
    	for (int i = 1, j; i <= n; i = j + 1) {
    		j = n / (n / i);
    		ret += (get_sum(j) - get_sum(i - 1) + mod) * calc(n / i) % mod * calc(n / i) % mod;
    		ret %= mod;
    	}
    	return ret;
    }
    int main() {
    	ll p, n;
    	scanf("%lld%lld", &p, &n);
    	mod = p;
    	sieve();
    	inv2 = ksm(2, mod - 2); inv6 = ksm(6, mod - 2);
    	printf("%lld
    ", solve(n));
    	return 0;
    }
    

    最小公倍数之和

    这时不一定是 (1)(n) 了,可是值域很小,所以我们不妨看成这样:

    (c_i) 代表 (i) 出现的次数,然后求的即为 (sum_{i=1}^{50000}sum_{j=1}^{50000}c_ic_jlcm(i,j))。然后莫比乌斯反演套路就可以处理了。

    提交记录

    类似题目:[AGC038C] LCMs

    [SDOI2017]数字表格

    最后一题了。

    我们发现它求的是积,不过没有关系,还是一样的套路。

    [Pi_{i=1}^{n}Pi_{j=1}^mf_{gcd(i,j)}=Pi_{d=1}^{n}f_d^{sum_{i=1}^{n}sum_{j=1}^{m}[gcd(i,j)=d]}=Pi_{d=1}^nf_d^{sum_{k=1}^{[frac{n}{d}]}mu(k)[frac{n}{dk}][frac{m}{dk}]}=Pi_{T=1}^n(Pi_{d|T}f_d^{mu(frac{T}{d})})^{[frac{n}{dk}][frac{m}{dk}]} ]

    预处理 (Pi_{d|T}f_d^{mu(frac{T}{d})}) 的时间是 (O(nlog{n})) 的。然后可以数论分块,单次 (O(sqrt{n}))

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const ll mod = 1e9 + 7;
    const int MAXN = 1000010;
    template <typename T> void read(T &x) {
    	T f = 1;
    	char ch = getchar();
    	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
    	for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
    	x *= f;
    }
    int T;
    ll prime[MAXN], mu[MAXN], cnt;
    ll sum[MAXN], F[MAXN], invF[MAXN];
    bool mark[MAXN];
    ll ksm(ll a, ll b) {
    	ll ret = 1;
    	while (b) {
    		if (b & 1) ret = (ret * a) % mod;
    		a = (a * a) % mod;
    		b >>= 1;
    	}
    	return ret;
    }
    void sieve() {
    	for (int i = 1; i <= 1000000; i++) sum[i] = 1;
    	F[0] = 0;
    	F[1] = 1;
    	invF[1] = 1;
    	for (int i = 2; i <= 1000000; i++) F[i] = (F[i - 1] + F[i - 2]) % mod, invF[i] = ksm(F[i], mod - 2);
    	mu[1] = 1;
    	for (int i = 2; i <= 1000000; i++) {
    		if (!mark[i]) {
    			prime[++cnt] = i;
    			mu[i] = -1;
    		}
    		for (int j = 1; j <= cnt && prime[j] * i <= 1000000; j++) {
    			mark[i * prime[j]] = 1;
    			if (i % prime[j] == 0) break;
    			mu[i * prime[j]] = -mu[i];
    		}
    	}
    	sum[0] = 1;
    	for (int i = 1; i <= 1000000; i++) {
    		for (int j = i; j <= 1000000; j += i) {
    			if (mu[j / i] > -1) sum[j] = (sum[j] * ksm(F[i], mu[j / i])) % mod;
    			else sum[j] = (sum[j] * invF[i]) % mod;
    		}
    	}
    	for (int i = 2; i <= 1000000; i++) {
    		sum[i] = (sum[i - 1] * sum[i]) % mod;
    	}
    }
    ll solve(ll n, ll m) {
    	ll ret = 1;
    	if (n > m) swap(n, m);
    	for (int i = 1, j; i <= n; i = j + 1) {
    		j = min(n / (n / i), m / (m / i));
    		ret = (ret * ksm(sum[j] * ksm(sum[i - 1], mod - 2) % mod, (n / i) * (m / i))) % mod;
    	}
    	return ret;
    }
    int main() {
    	sieve();
    	read(T);
    	while (T--) {
    		ll n, m;
    		read(n); read(m);
    		printf("%lld
    ", solve(n, m));
    	}
    	return 0;
    }
    

    处理技巧

    1. (sum_{i=1}^nsum_{j=1}^m[gcd(i,j)=k]=sum_{i=1}^{n/k}sum_{j=1}^{m/k}[gcd(i,j)=1])
    2. 枚举约数不好搞,不如把约数提前枚举然后枚举倍数,因为倍数很容易算。(n) 以内 (x) 的倍数有 (lfloorfrac{n}{x} floor) 个。
    3. 对于一个整体 (xy),我们可以把它提到前面去,转而枚举其约数然后形成类卷积形式(一般都是积性函数)埃氏筛或线性筛都可搞。
    4. 求的是一个数列可以转换为出现次数
    5. 乘积转换为幂

    参考资料

    莫比乌斯反演+常见数论函数的性质+狄利克雷卷积+数论分块+杜教筛学习笔记 from birchtree

    莫比乌斯反演(函数)练习题单 from CYJian

    算法竞赛进阶指南 from 李煜东

    题解 P3455 【[POI2007]ZAP-Queries】 from pengym

    莫比乌斯反演初学总结 from _sys

    莫比乌斯反演-让我们从基础开始 from An_Account

    完结撒花❀

  • 相关阅读:
    BZOJ1057:[ZJOI2007]棋盘制作——题解
    洛谷4147:玉蟾宫——题解
    洛谷1578:[WC2002]奶牛浴场——题解
    BZOJ1926:[SDOI2010]粟粟的书架——题解
    BZOJ3123:[SDOI2013]森林——题解
    BZOJ1834:[ZJOI2010]网络扩容——题解
    BZOJ2668:[CQOI2012]交换棋子——题解
    BZOJ1070:[SCOI2007]修车——题解
    BZOJ1877:[SDOI2009]晨跑——题解
    在阿里,我们如何管理测试环境
  • 原文地址:https://www.cnblogs.com/zcr-blog/p/13534640.html
Copyright © 2011-2022 走看看