城中分支

onlyblues發表於2024-10-07

城中分支

題目描述

“城市處於建設中......”

預言之子的你獲得了一個擁有 $n$ 個元素的陣列。 天神賦予你 $q$ 次操作:

  1. $Modify$,$l$,$r$,$x$------對於每個 $i$ $(l \leq i \leq r)$,將 $a_i$ 乘以 $x$ 。
  2. $Query$,$l$,$r$------你需要回答 $\varphi(\prod_{i=l}^{r}{a_i})$ 取模 $10^9+7$,其中 $\varphi$ 表示尤拉函式。

正整數 $n$(表示為 $\varphi(n)$)的尤拉函式是滿足 $\gcd(n,x)=1$ 的整數 $x$ $(1 \leq x \leq n)$ 的數量。

$\prod_{i=l}^{r}{a_i}$ 表示 $a_l \times a_{l+1} \times \cdots \times a_r$。

輸入描述:

第一行包含兩個整數 $n$ 和 $q$ $(1 \leq n \leq 4 \cdot 10^5 ,1 \leq q \leq 2 \cdot 10^5)$ — 陣列中的元素數和查詢數。

第二行包含 $n$ 個整數 $a_1, a_2, \ldots ,a_n$ $(1 \leq a_i \leq 300)$ — 陣列 $a$ 的元素。

接下來是 $q$ 行以語句中給出的格式描述查詢。

$Modify$,$l$,$r$,$x$ $(1 \leq l \leq r \leq n, 1\leq x \leq 300)$ —表示修改操作。

$Query$,$l$,$r$ $(1 \leq l \leq r \leq n)$ ——表示對尤拉函式值的查詢操作。

資料保證至少有一個 $Query$ 查詢。

輸出描述:

對於每個 $Query$ 查詢,列印其答案對 $10^9+7$ 取模的結果。

示例1

輸入

4 4
5 9 1 2
Query 3 3
Query 3 4
Modify 4 4 3
Query 4 4

輸出

1
1
2

示例2

輸入

1 1
4
Query 1 1

輸出

2

解題思路

  程式碼巨長巨難寫,卡了快 3 個小時,不是 TLE 就是 MLE。

  首先有尤拉函式公式 $\varphi(x) = x \left(1 - \frac{1}{P_0}\right)\left(1 - \frac{1}{P_1}\right) \cdots \left(1 - \frac{1}{P_k}\right)$,其中 $x = P_{0}^{\alpha_{0}}P_{1}^{\alpha_{1}} \cdots P_{k}^{\alpha_{k}}$。因此有 $\varphi(x) = P_0^{\alpha_0} \left(1 - \frac{1}{P_0}\right) \cdot P_1^{\alpha_1} \left(1 - \frac{1}{P_1}\right) \cdots \cdot P_k^{\alpha_k} \left(1 - \frac{1}{P_k}\right) = \varphi(P_{0}^{\alpha_0}) \varphi(P_{1}^{\alpha_1}) \cdots \varphi(P_{k}^{\alpha_k})$。

  顯然需要用到線段樹,但我們既不可以直接維護區間的乘積(顯然爆 long long),又不可以對乘積取模(否則求不了尤拉函式)。注意到 $a_i$ 和 $x$ 都不超過 $300$,而 $300$ 內的質數只有 $62$ 個,意味著乘積的結果均可以表示成 $P_{0}^{\alpha_{0}}P_{1}^{\alpha_{1}} \cdots P_{61}^{\alpha_{61}}$,因此我們可以轉向去維護乘積結果的各個質因子的數量即 $\alpha_{i}$。

  定義線段樹節點資訊:

struct Node {
    int l, r;
    array<LL, 62> s, sum;
};

  當需要給節點維護的整個區間乘上 $x = P_{0}^{\alpha_{0}}P_{1}^{\alpha_{1}} \cdots P_{61}^{\alpha_{61}}$ 時,只需更新 $s_i \gets s_i + \alpha_{i}(r-l+1)$,$\text{sum}_i \gets \text{sum}_i + \alpha_i$ $(0 \leq i < 62)$($\text{sum}$ 是懶標記)。

  查詢時,我們累加詢問區間內每個質因子的次數,用大小為 $62$ 的陣列 $s$ 表示。乘積的尤拉函式就是 $\prod\limits_{i=0}^{61} [s_i > 0] \, P_i^{s_i} \left(1 - \frac{1}{P_i} \right)$。

  上述做法的時間複雜度為 $O\left( n \log{A} + q\log{n}(\log{A} + \pi(A)\log{(q\log{A})}) \right)$,空間複雜度為 $O(n \pi(A))$。然而實際上線段樹所需要的記憶體空間為 1k+ MB!上述做法肯定過不了。

  用不了線段樹然後我就投機取巧用分塊,結果空間是變小了但 TLE。做法和上面類似,每個塊維護各個質因子的數量,具體做法就不詳述了。分塊的時間複雜度為 $O\left( n \log{A} + q\sqrt{n}(\log{A} + \pi(A)\log{(q\log{A})}) \right)$,空間複雜度為 $O(n \pi(A))$ 但常數比上面的小非常多。

  分塊做法 TLE 程式碼如下:

檢視程式碼
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int N = 4e5 + 5, M = 305, B = 640, mod = 1e9 + 7;

int a[N][62];
int prime[M], mp[M], minp[M], cnt;
bool vis[M];
int id[N];
LL s[B][M], sum[B][M], c[M];

void get_prime(int n) {
    for (int i = 2; i <= n; i++) {
        if (!vis[i]) {
            prime[cnt] = i;
            mp[i] = cnt++;
            minp[i] = i;
        }
        for (int j = 0; prime[j] * i <= n; j++) {
            vis[prime[j] * i] = true;
            minp[prime[j] * i] = prime[j];
            if (i % prime[j] == 0) break;
        }
    }
}

int qmi(int a, LL k) {
    int ret = 1;
    while (k) {
        if (k & 1) ret = 1ll * ret * a % mod;
        a = 1ll * a * a % mod;
        k >>= 1;
    }
    return ret;
}

void modify(int l, int r, int x) {
    if (id[l] == id[r]) {
        for (int i = l; i <= r; i++) {
            int t = x;
            while (t > 1) {
                a[i][mp[minp[t]]]++;
                s[id[i]][mp[minp[t]]]++;
                t /= minp[t];
            }
        }
        
    }
    else {
        for (int i = l; id[i] == id[l]; i++) {
            int t = x;
            while (t > 1) {
                a[i][mp[minp[t]]]++;
                s[id[i]][mp[minp[t]]]++;
                t /= minp[t];
            }
        }
        for (int i = r; id[i] == id[r]; i--) {
            int t = x;
            while (t > 1) {
                a[i][mp[minp[t]]]++;
                s[id[i]][mp[minp[t]]]++;
                t /= minp[t];
            }
        }
        for (int i = id[l] + 1; i < id[r]; i++) {
            int t = x;
            while (t > 1) {
                s[i][mp[minp[t]]] += B;
                sum[i][mp[minp[t]]]++;
                t /= minp[t];
            }
        }
    }
}

int query(int l, int r) {
    memset(c, 0, sizeof(c));
    if (id[l] == id[r]) {
        for (int i = l; i <= r; i++) {
            for (int j = 0; j < cnt; j++) {
                c[j] += a[i][j] + sum[id[i]][j];
            }
        }
    }
    else {
        for (int i = l; id[i] == id[l]; i++) {
            for (int j = 0; j < cnt; j++) {
                c[j] += a[i][j] + sum[id[i]][j];
            }
        }
        for (int i = r; id[i] == id[r]; i--) {
            for (int j = 0; j < cnt; j++) {
                c[j] += a[i][j] + sum[id[i]][j];
            }
        }
        for (int i = id[l] + 1; i < id[r]; i++) {
            for (int j = 0; j < cnt; j++) {
                c[j] += s[i][j];
            }
        }
    }
    int ret = 1;
    for (int i = 0; i < cnt; i++) {
        if (c[i]) ret = ret * (prime[i] - 1ll) % mod * qmi(prime[i], c[i] - 1) % mod;
    }
    return ret;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m;
    cin >> n >> m;
    get_prime(M - 1);
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        id[i] = (i - 1) / B + 1;
        while (x > 1) {
            a[i][mp[minp[x]]]++;
            s[id[i]][mp[minp[x]]]++;
            x /= minp[x];
        }
    }
    while (m--) {
        string s;
        cin >> s;
        if (s[0] == 'M') {
            int l, r, x;
            cin >> l >> r >> x;
            modify(l, r, x);
        }
        else {
            int l, r;
            cin >> l >> r;
            cout << query(l, r) << '\n';
        }
    }
    
    return 0;
}

  下面給出正解了,還是用到線段樹。細想一下,最開始的線段樹做法會 MLE,正是因為我們給每個節點都開了大小為 $62$ 的陣列去統計各個質因子的數量,而我們有必要去記錄質因子的數量嗎?回想尤拉函式 $\varphi(x) = x \left(1 - \frac{1}{P_0}\right)\left(1 - \frac{1}{P_1}\right) \cdots \left(1 - \frac{1}{P_k}\right)$,我們只要分別知道對應區間乘積的結果(可以取模),以及乘積結果(不取模)所含有的質因子,就可以計算尤拉函式。可以發現我們並不需要統計各個質因子的數量,只需統計出現了哪些質因子即可,這個可以用一個 long long 變數來狀態壓縮統計。

  為此重新定義線段樹節點資訊:

struct Node {
    int l, r;
    int p, prod;
    LL s, sum;
};

  其中 $p$ 是區間乘積取模後的結果,$\text{prod}$ 是對應的區間乘懶標記。$s$ 是狀態壓縮表示區間乘積(沒取模)包含的質因子,$\text{sum}$ 是對應的區間加懶標記。節點更新以及懶標記下傳請參考程式碼,這裡就不過多贅述了。

  AC 程式碼如下,時間複雜度為 $O(n \log{A} + q (\log{n} + \pi(A)))$,空間複雜度為 $O(n)$:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int N = 4e5 + 5, M = 305, B = 640, mod = 1e9 + 7;

int a[N][62];
int prime[M], mp[M], minp[M], cnt;
bool vis[M];
int id[N];
LL s[B][M], sum[B][M], c[M];

void get_prime(int n) {
    for (int i = 2; i <= n; i++) {
        if (!vis[i]) {
            prime[cnt] = i;
            mp[i] = cnt++;
            minp[i] = i;
        }
        for (int j = 0; prime[j] * i <= n; j++) {
            vis[prime[j] * i] = true;
            minp[prime[j] * i] = prime[j];
            if (i % prime[j] == 0) break;
        }
    }
}

int qmi(int a, LL k) {
    int ret = 1;
    while (k) {
        if (k & 1) ret = 1ll * ret * a % mod;
        a = 1ll * a * a % mod;
        k >>= 1;
    }
    return ret;
}

void modify(int l, int r, int x) {
    if (id[l] == id[r]) {
        for (int i = l; i <= r; i++) {
            int t = x;
            while (t > 1) {
                a[i][mp[minp[t]]]++;
                s[id[i]][mp[minp[t]]]++;
                t /= minp[t];
            }
        }
        
    }
    else {
        for (int i = l; id[i] == id[l]; i++) {
            int t = x;
            while (t > 1) {
                a[i][mp[minp[t]]]++;
                s[id[i]][mp[minp[t]]]++;
                t /= minp[t];
            }
        }
        for (int i = r; id[i] == id[r]; i--) {
            int t = x;
            while (t > 1) {
                a[i][mp[minp[t]]]++;
                s[id[i]][mp[minp[t]]]++;
                t /= minp[t];
            }
        }
        for (int i = id[l] + 1; i < id[r]; i++) {
            int t = x;
            while (t > 1) {
                s[i][mp[minp[t]]] += B;
                sum[i][mp[minp[t]]]++;
                t /= minp[t];
            }
        }
    }
}

int query(int l, int r) {
    memset(c, 0, sizeof(c));
    if (id[l] == id[r]) {
        for (int i = l; i <= r; i++) {
            for (int j = 0; j < cnt; j++) {
                c[j] += a[i][j] + sum[id[i]][j];
            }
        }
    }
    else {
        for (int i = l; id[i] == id[l]; i++) {
            for (int j = 0; j < cnt; j++) {
                c[j] += a[i][j] + sum[id[i]][j];
            }
        }
        for (int i = r; id[i] == id[r]; i--) {
            for (int j = 0; j < cnt; j++) {
                c[j] += a[i][j] + sum[id[i]][j];
            }
        }
        for (int i = id[l] + 1; i < id[r]; i++) {
            for (int j = 0; j < cnt; j++) {
                c[j] += s[i][j];
            }
        }
    }
    int ret = 1;
    for (int i = 0; i < cnt; i++) {
        if (c[i]) ret = ret * (prime[i] - 1ll) % mod * qmi(prime[i], c[i] - 1) % mod;
    }
    return ret;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m;
    cin >> n >> m;
    get_prime(M - 1);
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        id[i] = (i - 1) / B + 1;
        while (x > 1) {
            a[i][mp[minp[x]]]++;
            s[id[i]][mp[minp[x]]]++;
            x /= minp[x];
        }
    }
    while (m--) {
        string s;
        cin >> s;
        if (s[0] == 'M') {
            int l, r, x;
            cin >> l >> r >> x;
            modify(l, r, x);
        }
        else {
            int l, r;
            cin >> l >> r;
            cout << query(l, r) << '\n';
        }
    }
    
    return 0;
}

參考資料

  瀋陽化工大學第十一屆程式設計瀋陽區競賽 thisislike_fan 提交的程式碼:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=71790420

相關文章