1. 程式人生 > 其它 >題解 [六省聯考2017] 相逢是問候

題解 [六省聯考2017] 相逢是問候

題目連結

Luogu
Loj

題目描述

維護一個序列,支援區間求和,以及將區間內每一個數 \(a_i\) 變成 \(c^{a_i}\) (c為給定的常數), 並要求結果對 \(p\) 取模 ( \(p\) 不一定是質數 )
數列元素個數 \(n \leq 10^5\), 操作次數 \(m \leq 10^5\), \(1 \leq c < p \leq 10^8\)
注 : 以下稱 “將區間內每一個數 \(a_i\) 變成 \(c^{a_i}\)” 為 “操作”

分析

一道看著就很線段樹的題目 ...
首先我們發現區間和似乎並不是那麼好維護:假定我們維護了 \(a_i\) 的區間和,但我們並不能很快的推出 \(c^{a_i}\)

的區間和,因為從 \(a_i\)\(c^{a_i}\), 區間內的每一個元素的變化幾乎完全沒有相關性,並不像區間加或區間和一樣能方便的維護。
但除了線段樹外,我們應該還會很自然的想到擴充套件尤拉定理

\[a^b \equiv a^{(b\bmod\varphi(p)) + \varphi(p) } \pmod p, b \ge \varphi(p) \]

那麼很顯然的,當 \(a_i\) 對應的模數 \(\varphi(\varphi(\varphi(\varphi(...p)))) = 1\) 時,不論再怎麼操作 \(a_i\) 也不會變化了
而對一個數 \(p\) 不停地取 \(\varphi\)

,在取了 \(\log p\) 次後這個數必然會變成 \(1\).
那麼對於序列中所有的數,他們的總操作次數就應該不超過 \(n \log p\).
也就是說,對於每一個區間操作,我們只需要操作其中沒有操作到 \(\log p\) 次的數, 其他的數可以不管他.
那顯然可以考慮預處理出每個數在前 \(\log p\) 次操作下分別會變成多少,操作時單點修改即可,複雜度就是 \(O(n\log n\log p)\).
這樣做還有個的好處:我們可以用樹狀陣列代替掉線段樹.
至於如何在區間操作中快速找到哪些數的操做次數小於 \(\log p\), 我們可以考慮使用逐漸變形成並查集的單向連結串列,具體而言就是,我們有一個 \(next\)
陣列, \(next_i\) 表示下標大於等於 i 的數中第一個操作次數小於 \(\log p\) 的數 (即繼續操作可能會改變數值的數) 的下標,並有一個 \(time\) 陣列記錄每個數當前的操作次數.
顯然初始時 \(next_i = i + 1\).
那麼查詢下標大於等於 i 的第一個操作次數小於 \(\log p\) 的數可以這樣實現:

int get(int i) {
    if (time[i] < logp)
        return i;
    else
        return next[i] = get(next[i]);
}

這應該比較直觀的體現了他為什麼是"逐漸變形成並查集的單向連結串列"......

程式碼

#include <bits/stdc++.h>
#define lowbit(x) x&-x
using namespace std;
typedef long long LL;

const int N = 50010;
const int lam = 10;

LL n, m, p, c, logcp, top, nxt[N], tree[N], a[N], tms[N], phi[N], val[lam + 10][N];

inline int read()
{
    int x = 0, f = 1;
    char c = getchar();
    while(c < '0' || c > '9') { if(c == '-') f = -1; c = getchar(); }
    while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}

LL ksm(LL x, LL p)
{
    LL cur = c, res = 1;
    for(; x; x >>= 1, cur = cur * cur % p) if(x & 1) res = res * cur % p;
    return res;
}

int get_phi(int x) //暴力求尤拉函式
{
    LL res = 1;
    for(int i = 2; i * i <= x; i++){
        if(x % i == 0){
            res *= (i - 1), x /= i;
            while(x % i == 0) res *= i, x /= i;
        }
    }
    return (x == 1) ? res : res * (x - 1);
}

int dfs(int x, int num, int t)//深搜擴充套件尤拉定理
{
    if(phi[t] == 1) return 1;
    if(!num) return a[x];
    if(num < top) return val[num][x]; //特判不能用擴充套件尤拉定理的部分
    return ksm(dfs(x, num - 1, t + 1), phi[t]) + phi[t];
}

void prework()//預處理出a[i]操作b次後會變成多少,即val[b][i]
{
    LL o = 1;
    if(c == 1) logcp = 1e9;
    else while(o < p) logcp++, o = o * c;
    //logcp 就是以c為底p的對數
    int cur = p; phi[0] = p;
    for(int i = 1; i <= lam; i++){
        int now = get_phi(cur);
        phi[i] = cur = now;
    }
    for(int j = 1; j <= n; j++){
        val[0][j] = a[j];
        bool flag = true;
        for(int i = 1; i <= lam; i++)
        if(val[i - 1][j] < logcp && flag) val[i][j] = ksm(val[i - 1][j], p); //特判不能用擴充套件尤拉定理的部分
        else{
            if(flag) flag = false, top = i;
            val[i][j] = dfs(j, i, 0) % p;
        }
    }

}

void add(int pos, int val)
{
    for(; pos < N; pos += lowbit(pos))
        tree[pos] = (tree[pos] + val + p) % p;
}

LL que(int pos)
{
    LL res = 0;
    for(; pos; pos -= lowbit(pos)) res = (res + tree[pos]) % p;
    return res;
}

int get(int now)
{
    if(tms[now] < lam) return now;
    else return nxt[now] = get(nxt[now]);
}

int main()
{
    n = read(), m = read(), p = read(), c = read();
    for(int i = 1; i <= n; i++) a[i] = read(), nxt[i] = i + 1;
    for(int i = 1; i <= n; i++) add(i, a[i]);
    prework();

    for(int i = 1, op, l, r; i <= m; i++){
        op = read(), l = read(), r = read();
        if(op) printf("%lld\n", (que(r) - que(l - 1) + p) % p);
        else{
            int cur = get(l);
            while(cur <= r){
                LL x = val[tms[cur] + 1][cur], de = x - a[cur];
                add(cur, de);
                a[cur] = x, tms[cur] ++, cur = get(nxt[cur]);
            }
        }
    }
    return 0;
}

順便一提,雖然說理論上每個數需要操作 \(\log p\) 次才不會變化,但這題資料比較水,每個數操作 5 次就可以不管他了.
還有就是一定要注意擴充套件尤拉定理需要滿足指數不小於 \(\varphi(p)\).