莫比烏斯函數入門

2021-02-19 ACM算法日常
緣起

入坑數論中的重要的部分——莫比烏斯函數

分析

本文的例題來自  Hdu 6053 TrickGCD 莫比烏斯函數的入門題.

給定一個長度為n的數組A, zhu同學想知道有多少個不同的數組 B 滿足以下條件
1. 1 <= B_i <= A_i
2. 對任何滿足 1<=l<=r<=n 的(l, r), 我們有 gcd(b_l,...,b_r)>=2

【輸入】
多樣例, 第一行是樣例數, 每個樣例開始於 n, 然後一行是包含 n 個整數Ai,  1<=n, Ai<=1e5

【輸出】
對第 k 個詢問, 輸出 Case #k , 然後輸出答案, 因為答案太大了, 所以答案要 mod掉 1e9+7

【樣例輸入】
1
4
4 4 4 4

【樣例輸出】
Case #1: 17

【限制】
2500ms 256MB

【來源】
2017暑期多校訓練第二場

要解決這道題目,它要求莫比烏斯(Mobius)函數作為知識背景.  所以我們先來學習一下mobius函數.

mobius函數是一個數論函數,它是由德國著名天文學家以及數學家莫比烏斯(沒錯,就是發現莫比烏斯環這種反人類的拓撲結構的天才人物),它的定義如下

mobius函數是一個積性函數, 即

這裡順便說一下,如果把 gcd(m,n)=1這個條件去掉的話,則就是完全積性函數的定義.

既然介紹了mobius函數的定義,那麼我們自然要琢磨如何計算mobius函數.  但是這種計算分成兩類

給定 m, 要你一口氣計算 [1,...,m] 中所有整數的mobius函數值

其實這種事情我們在歐拉函數(也是一個數論函數)的時候也做過. 這裡對於 mobius函數也給出這兩類問題的模板.

問題1

顯然根據 mobius 函數的定義, 我們只需要將 m 分解素因子然後按mobius函數的定義直接擼就可以了.

ili mobius(int m)
{
    if (m == 1) return 1;
    int tot = 0; // tot 是 m 不同素因子的個數
    for (re i = 2, num; i * i <= m; i++)
    {
        if (m % i) continue;
        ++tot;
        num = 0; // num 是 m 的 i 素因子的個數
        while (m % i == 0)
        {
            m /= i;
            if (++num > 1) return 0;
        }
    }
    return tot & 1 ? -1 : 1;
}

問題2

在線性篩的過程中順便計算mobius函數即可.

先上模板再解釋該模板.

上面過程本質上來講就是

假設已經遍歷到了

那麼我們至少會有兩個疑問

疑問1: 23行為什麼當mobius函數值為0的話就結束本輪的for循環? 因為原本你可以使用 {i * prime[1], i * prime[2],...i * prime[tot]} 來篩掉更多的合數呀~ 但是現在你只使用了 {i * prime[1], i * prime[2],..., i * prime[j]}, 1 <= j <= tot 來篩掉合數啊~

疑問2: 其實是由疑問1產生的,因為你現在使用的並不是完整的 {i * prime[1], i * prime[2],...i * prime[tot]}   來篩掉合數. 所以又憑什麼斷定第8行 !isp[i] 成的話 i 就一定是素數呢?

鑑於疑問2由疑問1產生,所以這兩個疑問我們一起來回答.

或者更確切的說,我們試圖說明上面的mobius算法是正確的.

我們稱 發現的. 如果上面的偽代碼的 第15行發生了.

做一般性的考慮, 令 ,其中,  {

那麼根據偽代碼第24行,

這是理解上述模板的核心觀察.

好, 現在基於該核心觀察來回答疑問 1 和 疑問 2.  首先,任何一個[1,..,m]中的合數都會被篩掉,或者說,任何一個合數

於是,我們講清楚了mobius函數的線性篩算法. 該算法的複雜度是 O(n) 的.

有了mobius函數的基礎知識,現在我們來看看本題. 題目要求的其實是計數

那麼,自然的想法就是枚舉

而因為

特別的,如果

那麼我們就要求出 d至少是 i 個素數乘積的情況 這一計數結果.  而這是可以通過問題2的模板 O(n) 預處理出來的. 即通過mobius函數的線性篩法我們可以知道 [1,1e5]內所有的數的 mobius 函數值. 對於是 i 個不同素數乘積的數,它的mobius值恰好就是

於是,我們可以切一版代碼出來

//#include "stdafx.h"
//#define LOCAL
#pragma GCC optimize(2)
#pragma G++ optimize(2)
#pragma comment(linker, "/STACK:1024000000,1024000000")
#include <stdio.h>
#include <iostream>
#include <string>
#include <ctype.h>
#include <string.h>
#include <math.h>
#include <map>
#include <algorithm>
#include <vector>
#include <stack>
#include <queue>
#include <set>
#include <time.h>
#include <stdlib.h>
using namespace std;
#define int long long
#define re register int
#define ci const int
typedef pair<int, int> P;
#define FE(cur) for(re h = head[cur], to, len; ~h; h = g[h].nxt)
#define ilv inline void
#define ili inline int
#define ilc inline char
#define ild inline double
#define ilp inline P
#define LEN(cur) (hjt[cur].r - hjt[cur].l)
#define MID(cur) (hjt[cur].l + hjt[cur].r >> 1)
typedef vector<int>::iterator vit;
typedef set<int>::iterator sit;
typedef map<int, int>::iterator mit;
namespace fastio
{
 const int BUF = 1 << 21;
 char fr[BUF], fw[BUF], *pr1 = fr, *pr2 = fr;int pw;
 ilc gc() { return pr1 == pr2 && (pr2 = (pr1 = fr) + fread(fr, 1, BUF, stdin), *pr2 = 0, pr1 == pr2) ? EOF : *pr1++; }
 ilv flush() { fwrite(fw, 1, pw, stdout); pw = 0; }
 ilv pc(char c) { if (pw >= BUF) flush(); fw[pw++] = c; }
 ili read(int &x)
 {
  x = 0; int f = 1; char c = gc(); if (!~c) return EOF;
  while(!isdigit(c)) { if (c == '-') f = -1; c = gc(); }
  while(isdigit(c)) x = (x << 3) + (x << 1) + (c ^ 48), c = gc();
  x *= f; return 1;
 }
 ili read(double &x) 
 {
  int xx = 0; double f = 1.0, fraction = 1.0; char c = gc(); if (!~c) return EOF;
  while (!isdigit(c)) { if (c == '-') f = -1.0; c = gc(); }
  while (isdigit(c)) { xx = (xx << 3) + (xx << 1) + (c ^ 48), c = gc(); }
  x = xx;
  if (c ^ '.') { x = f * xx; return 1; }
  c = gc();
  while (isdigit(c)) x += (c ^ 48) * (fraction /= 10), c = gc();
  x *= f; return 1;
 }
 ilv write(int x) { if (x < 0) pc('-'), x = -x; if (x > 9) write(x / 10); pc(x % 10 + 48); }
 ilv writeln(int x) { write(x);pc(10); }
 ili read(char *x)
 {
  char c = gc(); if (!~c) return EOF;
  while(!isalpha(c) && !isdigit(c)) c = gc();
  while (isalpha(c) || isdigit(c)) *x++ = c, c = gc();
  *x = 0; return 1;
 }
 ili readln(char *x)
 {
  char c = gc(); if (!~c) return EOF;
  while(c == 10) c = gc();
  while(c >= 32 && c <= 126) *x++ = c, c = gc();
  *x = 0; return 1;
 }
 ilv write(char *x) { while(*x) pc(*x++); }
 ilv writeln(char *x) { write(x); pc(10); }
 ilv write(char c) { pc(c); }
 ilv writeln(char c) { write(c); pc(10); }
} using namespace fastio;
const int maxn = 1e5+5, mod = 1e9+7, inf = 1ll<<60;
int n, a[maxn], mu[maxn], prime[maxn], isp[maxn], tot, cmin;

ilv mobius(int m)
{
 mu[1] = 1;
 for (re i = 2; i<= m; i++)
 {
  if (!isp[i])
  {
   prime[++tot] = i;
   mu[i] = -1;
  }
  for (re j = 1, t; j <= tot && i * prime[j] <= m; j++)
  {
   t = prime[j] * i;
   isp[t] = 1;
   if (i % prime[j])
   {
    mu[t] = -mu[i];
   }
   else
   {
    mu[t] = 0;
    break;
   }
  }
 }
}

ili gx(int d)
{
 int ans = 1;
 for (re i = 1; i <= n; i++)
 {
  ans *= a[i] / d;
  ans %= mod;
 }
 return ans;
}

ili kk()
{
 int ans = 0;
 for (re d = 2; d <= cmin; d++)
 {
  if (mu[d])
  {
   ans -= mu[d] * gx(d); // -mu[d] 才是容斥原理中的係數
   ans = (ans + mod) % mod;
  }
 }
 return ans;
}

signed main()
{
#ifdef LOCAL
 freopen("d:\\data.in", "r", stdin);
// freopen("d:\\my.out", "w", stdout);
#endif
 mobius(100000);
 int kase; read(kase);
 for (re i = 1; i <= kase; i++)
 {
  read(n);
  cmin = inf;
  for (re j = 1; j <= n; j++) read(a[j]), cmin = min(cmin, a[j]);
  write("Case #"), write(i), write(": "), writeln(kk());
 }
 flush();
 return 0;
}

但是遺憾的是,上面的代碼被T掉了. 被T掉的原因也是很顯然的,因為 gx 的複雜度是 O(n) 啊, 所以跑一個case的最壞複雜度達到了

那麼怎麼優化呢?  也就是要優化

的計算. 不要O(n) 計算. 咱們換個角度來看這個問題,其實

注意,上述形式裡面已經出現了冪次, 也就是我們可以使用快速冪來優化. 在此之前,我們需要知道

於是此題就破了~

//#include "stdafx.h"
//#define LOCAL
#pragma GCC optimize(2)
#pragma G++ optimize(2)
#pragma comment(linker, "/STACK:1024000000,1024000000")
#include <stdio.h>
#include <iostream>
#include <string>
#include <ctype.h>
#include <string.h>
#include <math.h>
#include <map>
#include <algorithm>
#include <vector>
#include <stack>
#include <queue>
#include <set>
#include <time.h>
#include <stdlib.h>
using namespace std;
#define int long long
#define re register int
#define ci const int
typedef pair<int, int> P;
#define FE(cur) for(re h = head[cur], to, len; ~h; h = g[h].nxt)
#define ilv inline void
#define ili inline int
#define ilc inline char
#define ild inline double
#define ilp inline P
#define LEN(cur) (hjt[cur].r - hjt[cur].l)
#define MID(cur) (hjt[cur].l + hjt[cur].r >> 1)
typedef vector<int>::iterator vit;
typedef set<int>::iterator sit;
typedef map<int, int>::iterator mit;
namespace fastio
{
    const int BUF = 1 << 21;
    char fr[BUF], fw[BUF], *pr1 = fr, *pr2 = fr;int pw;
    ilc gc() { return pr1 == pr2 && (pr2 = (pr1 = fr) + fread(fr, 1, BUF, stdin), *pr2 = 0, pr1 == pr2) ? EOF : *pr1++; }
    ilv flush() { fwrite(fw, 1, pw, stdout); pw = 0; }
    ilv pc(char c) { if (pw >= BUF) flush(); fw[pw++] = c; }
    ili read(int &x)
    {
        x = 0; int f = 1; char c = gc(); if (!~c) return EOF;
        while(!isdigit(c)) { if (c == '-') f = -1; c = gc(); }
        while(isdigit(c)) x = (x << 3) + (x << 1) + (c ^ 48), c = gc();
        x *= f; return 1;
    }
    ili read(double &x) 
    {
        int xx = 0; double f = 1.0, fraction = 1.0; char c = gc(); if (!~c) return EOF;
        while (!isdigit(c)) { if (c == '-') f = -1.0; c = gc(); }
        while (isdigit(c)) { xx = (xx << 3) + (xx << 1) + (c ^ 48), c = gc(); }
        x = xx;
        if (c ^ '.') { x = f * xx; return 1; }
        c = gc();
        while (isdigit(c)) x += (c ^ 48) * (fraction /= 10), c = gc();
        x *= f; return 1;
    }
    ilv write(int x) { if (x < 0) pc('-'), x = -x; if (x > 9) write(x / 10); pc(x % 10 + 48); }
    ilv writeln(int x) { write(x);pc(10); }
    ili read(char *x)
    {
        char c = gc(); if (!~c) return EOF;
        while(!isalpha(c) && !isdigit(c)) c = gc();
        while (isalpha(c) || isdigit(c)) *x++ = c, c = gc();
        *x = 0; return 1;
    }
    ili readln(char *x)
    {
        char c = gc(); if (!~c) return EOF;
        while(c == 10) c = gc();
        while(c >= 32 && c <= 126) *x++ = c, c = gc();
        *x = 0; return 1;
    }
    ilv write(char *x) { while(*x) pc(*x++); }
    ilv writeln(char *x) { write(x); pc(10); }
    ilv write(char c) { pc(c); }
    ilv writeln(char c) { write(c); pc(10); }
} using namespace fastio;
const int maxn = 1e5+5, mod = 1e9+7, inf = 1ll<<60;
int n, a[maxn], mu[maxn], prime[maxn], isp[maxn], tot, cmin, cmax, cnt[maxn << 1], pref[maxn << 1];

ilv mmax(int &a, int b)
{
    if (a < b)
    {
        a = b;
    }
}

ilv mmin(int &a, int b)
{
    if (a > b)
    {
        a = b;
    }
}

ilv mobius(int m)
{
    mu[1] = 1;
    for (re i = 2; i<= m; i++)
    {
        if (!isp[i])
        {
            prime[++tot] = i;
            mu[i] = -1;
        }
        for (re j = 1, t; j <= tot && i * prime[j] <= m; j++)
        {
            t = prime[j] * i;
            isp[t] = 1;
            if (i % prime[j])
            {
                mu[t] = -mu[i];
            }
            else
            {
                mu[t] = 0;
                break;
            }
        }
    }
}

ili ksm(int a, int b)
{
    int ans = 1;
    while (b)
    {
        if (b & 1)
        {
            ans = ans * a;
            ans %= mod;
        }
        if (b > 1)
        {
            a = a * a;
            a %= mod;
        }
        b >>= 1;
    }
    return ans;
}

ili gx(int d)
{
    int ans = 1;
    for (re i = 1; i * d <= cmax; i++) // [i * d, (i + 1) * d) 範圍
    {
        ans *= ksm(i, pref[(i + 1) * d - 1] - pref[i * d - 1]); // 快速冪優化gx的計算
        ans %= mod;
    }
    return ans;
}

ili kk()
{
    int ans = 0;
    for (re d = 2; d <= cmin; d++)
    {
        if (mu[d])
        {
            ans -= mu[d] * gx(d); 
            ans = (ans + mod) % mod;
        }
    }
    return ans;
}

signed main()
{
#ifdef LOCAL
    freopen("d:\\data.in", "r", stdin);
    freopen("d:\\my.out", "w", stdout);
#endif
    mobius(1e5);
    int kase; read(kase);
    for (re i = 1; i <= kase; i++)
    {
        read(n);
        cmax = 0, cmin = inf;
        memset(cnt, 0, sizeof(cnt));
        for (re j = 1; j <= n; j++) read(a[j]),mmin(cmin, a[j]), mmax(cmax, a[j]), ++cnt[a[j]];
        for (re i = 1; i <= cmax << 1; i++) pref[i] = pref[i - 1] + cnt[i];
        write("Case #"), write(i), write(": "), writeln(kk());
    }
    flush();
    return 0;
}

ac情況

accepted 280ms 8988kB

小結

mobius函數的作用就在於它O(n)時間預處理出了所有恰好由若干兩兩不同的質數相乘得到的數. 而這些數恰好是我們在容斥原理中要用到的數.

相關焦點

  • 狄利克雷函數與黎曼函數簡介(高等數學入門系列拓展閱讀)
    閱讀更多「高等數學入門」系列文章,歡迎關注數學若只如初見!在高等數學中,有些問題須要用一些特殊的函數加以說明,狄利克雷函數和黎曼函數就是這樣兩個重要的函數,用它們可以澄清一些概念上的問題。本節我們主要介紹狄利克雷函數和黎曼函數的定義,並介紹它們在連續性方面的一些獨特性質。(由於公式較多,故正文採用圖片形式給出。)
  • 高中數學函數入門篇(中)
    高中數學函數入門篇(中)教學內容:本次課的主要內容是繼續第一次課的函數入門篇深入來講解函數到底是什麼,什麼樣的圖像不是函數,其和初中階段學習的一次函數之間的聯繫到底是什麼,區別又在哪裡,通過數形結合將函數入門知識講解到位,讓學生能夠一目了然地快速入門函數相關的知識及其考點!為下面的課程的深入講解做鋪墊!
  • 高等數學入門——反函數的求導法則及反三角函數的導數公式總結
    閱讀更多「高等數學入門」系列文章,歡迎關注數學若只如初見!本節我們介紹反函數的求導法則,由於中學階段對反函數及反三角函數的要求不高,本節我們先複習一些這方面的基礎知識,再介紹反函數的求導法則,並利用其推導四個常用反三角函數的導數公式。(由於公式較多,故正文採用圖片形式給出。)
  • 高一數學函數入門課程之函數的單調性詳解
    函數的單調性,這樣理解,才算真正入門了hello,大家好,這裡是尖子生數理化教育,很高興在這裡跟大家見面了。最近很多學生反應函數不知道怎麼學,不知道怎麼才能入門,一會函數定義域一會函數值域,一會周期函數,一會奇偶函數,一會函數單調性,整得有點崩潰了。
  • 高中數學入門篇之函數(上)
    高中數學入門篇之函數(上),尖子生數理化教育函數是高一新生數學學習的入門課。可是很多學生學了三年,發現函數是啥都不清楚,這就是很尷尬的一件事情了。那就說明其根本沒有學懂數學。不知道數學是在幹啥,那麼其最後的成績也是可想而知了!高中數學,三年的知識都是圍繞函數來進行講解的。那麼究竟函數是什麼?如何才能入門,這節課我們來講一下。函數是兩個集合之間的映射,這兩個集合是特殊的集合:數集。
  • 高等數學入門——函數微分的概念及其與導數的聯繫
    閱讀更多「高等數學入門」系列文章,歡迎關注數學若只如初見!本節我們開始介紹函數的微分,這是一個與導數密切相關的概念,本節我們先給出函數微分的定義,然後從邏輯上推導出它與導數之間的「等價」關係,並總結一元函數微分中相關概念之間的聯繫。(由於公式較多,故正文採用圖片形式給出。)
  • Python零基礎入門教程,如何使用函數?
    大綱函數語法格式及調用參數:默認值、元組和字典可變參數的使用全局變量和局部變量作用域,局部變量如何升級為全局變量函數是可重複使用的,實現單一功能的代碼塊。可以把項目中某一功能想像成積木模型,函數是組成模型的大大小小積木塊。
  • 九年級數學一元二次函數基礎知識點講解,輕鬆入門一元二次函數
    九年級數學一元二次函數基礎知識點講解,輕鬆入門一元二次函數本文我們主要通過圖像和函數的性質進行一元二次函數基礎考點的講解,希望同學們在應用一元二次函數相關的知識時,能夠記住這些基礎相關的考點,順利突破難點!
  • 什麼是莫比烏斯指環?莫比烏斯環的詛咒
    導語:每當我們提起十分火熱的恐怖驚悚電影或者電視劇時都會想到莫比烏斯指環的詛咒,莫比烏斯指環是在1858年被德國兩位學者發現的,那麼究竟什麼是莫比烏斯指環呢
  • 高等數學入門——計算乘積函數高階導數的萊布尼茲公式
    閱讀更多「高等數學入門」系列文章,歡迎關注數學若只如初見!上一節中我們推導了一些常見初等函數的n階導數公式,並在末尾提到了如何計算兩個函數之和的高階導數,但兩個函數之積的高階導數就不是那麼容易計算的了,本節我們來具體介紹計算乘積函數高階導數的萊布尼茲公式。(由於公式較多,故正文採用圖片形式給出。)
  • 實變函數的入門簡介
    實變函數學十遍,比「彙編語言不會編」還是難很多的。至少我彙編就練得不錯:(但實變就只懂一點點,做那湯松大牛的課後題是白搭,但是在需要的時候拿來懟一下深度神經網絡DNN還是可以的(捂臉1,可數,可數是實變函數的入門核心概念,一般都會放在第一章的開頭。
  • 大學生計算機二級考試C語言中的函數入門詳解
    C語言計算機二級考試必考考點之函數入門詳解一般來說理科生的大學生有一門必修課是編程,而想要從事軟體開發的人員,沒有C語言基礎是不行的。而C語言中比較重要的部分就是函數。函數是實現各種軟體開發功能的途徑,如果你對函數了如指掌,那麼軟體開發將是一件很簡單的事情了。
  • 神奇的莫比烏斯環
    莫比烏斯回到辦公室,裁出紙條,把紙的一端扭轉180°,再將一端的正面和背面粘在一起,這樣就做成了特殊的紙圈兒。接著,莫比烏斯捉了一隻小甲蟲,放在上面讓它爬。結果,小甲蟲不翻越任何邊界就爬遍了圓圈兒的所有部分。莫比烏斯激動地說:「公正的小甲蟲,你無可辯駁地證明了這個圈兒只有一個面。」
  • 高等數學入門——二元函數的累次極限及其與二重極限的關係
    例如用ε-δ語言證明函數極限,以及教材中多數定理的詳細證明過程,這些內容高等數學課程通常不要求掌握,因此在這個系列文章中不作過多介紹。相應地,我們補充了一些類似」利用泰勒公式推導二項式定理」、「零點定理的妙用「等具有一定趣味性的內容,作為對傳統教材內容適度拓展。本系列文章適合作為大一新生初學高等數學時的課堂同步輔導,也可作為高等數學期末複習以及考研第一輪複習時的參考資料。
  • Python零基礎入門教程,如何使用lambda、filter和map函數?
    大綱函數類型定義及特性lambda函數定義及使用filter函數定義及使用map函數定義及使用引入函數類型概念函數類型定義:python中任意一個函數都有數據類型,這種數據類型是function(函數類型)
  • python入門基礎之lambda匿名函數詳解
    python入門基礎之lambda匿名函數詳解剛開始學習python的時候很多人可能對於lambda函數不了解,感覺和def很混亂,下面我來介紹一下lambda函數我從一下幾個方面來介紹lambda:,而def定義的函數必須有一個名字。
  • 金基德《莫比烏斯》為啥限制上映-金基德,導演,韓國,上映,莫比烏斯...
    但在隨後的坎城電影節市場放映中,未完成剪輯版《莫比烏斯》受到了各國片商的關注,電影節還未結束,已經售出多個歐洲國家的版權。有買家說「金基德導演尋找到了自己的語言」、也有說「是金基德電影中最為深奧的作品」。相隔不久,第70屆威尼斯電影節組委會發布官方消息,稱《莫比烏斯》成為第一部列入角逐金獅獎的影片,這也是金基德繼《漂流欲室》、《收件人不詳》、《空房間》以及《聖殤》後,第五次收到威尼斯電影節的邀請。
  • 話題| 莫比烏斯陷阱
    今晚,在這一課裡,我也把這裡的告誡,命名為「莫比烏斯陷阱」。人們能否避免「愛」轉向「害」?怎樣防止「熱烈」導致「自焚」?如何止步於「恰如其分」而不至於「適得其反」?人民群眾能否識別「變革」與「革命」的差別?能否警告「變革」之蟲不可輕率地爬向「革命」?答案是:往往不能。
  • 永遠無限循環的莫比烏斯環
    這是我們一起探索的第82個實驗公元1858年德國數學家莫比烏斯發現把一個紙條一頭扭轉180°後再將兩頭粘接起來而不必跨過它的邊緣我們把這種由莫比烏斯發現的神奇的單面紙帶稱為「莫比烏斯環」今天的實驗我們就一起做一個神秘的「莫比烏斯環」
  • 行走在莫比烏斯環上
    想念沒有終結就像死亡沒有盡頭一樣這個夢同樣也是我從小到大一直做的一個夢反反覆覆不是噩夢但覺得很累夢的內容很簡單我在一條莫比烏斯環上行走事實上有兩種不同的莫比烏斯帶鏡像他們相互對稱如果把紙帶順時針旋轉再粘貼就會形成一個右手性的莫比烏斯帶反之亦類似