2023牛客寒假演算法基礎集訓營3 A-I+K

空白菌 發表於 2023-01-24
演算法

A

題解

知識點:貪心。

把所有正偶數除成奇數,即可。

(人傻了沒加 \(x>0\) WA2

時間複雜度 \(O(n)\)

空間複雜度 \(O(1)\)

程式碼

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

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    cin >> n;
    ll ans = 0;
    for (int i = 1;i <= n;i++) {
        int x;
        cin >> x;
        while (x > 0 && x % 2 == 0) x /= 2;
        ans += x;
    }
    cout << ans << '\n';
    return 0;
}

B

題解

知識點:數學,構造。

特判 \(n=2\) 無解。

可以先放邊長為 \(\left\lceil \dfrac{n}{2} \right\rceil\) 正方形,隨後邊長每增加 \(1\) 需要最少 \(3\) 塊,直到邊長為 \(2 \cdot \left\lceil \dfrac{n}{2} \right\rceil\) 後,邊長每增加 \(1\) 需要最少 \(5\) 塊。以此類推,當邊長為 \(\left[(k-1)\cdot\left\lceil \dfrac{n}{2} \right\rceil,k\cdot\left\lceil \dfrac{n}{2} \right\rceil \right),k \in \N^+\) 時,邊長每增加 \(1\) 需要 \(2k-1\) 塊積木。

顯然,擺完第一輪邊長為 \(\left\lceil \dfrac{n}{2} \right\rceil\) 後,剩下的 \(n - \left\lceil \dfrac{n}{2} \right\rceil\) 個積木,而 \(\left\lfloor \dfrac{n - \left\lceil \dfrac{n}{2} \right\rceil}{3} \right\rfloor \leq \left\lceil \dfrac{n}{2} \right\rceil\) ,因此不可能擺到需要 \(5\) 個積木的情況。

綜上,邊長最大值為 \(\left\lceil \dfrac{n}{2} \right\rceil + \left\lfloor \dfrac{n - \left\lceil \dfrac{n}{2} \right\rceil}{3} \right\rfloor\)

本題也可以用二分邊長做。

(沒考慮 \(n - \left\lceil \dfrac{n}{2} \right\rceil\) 大小,傻了吧唧的算了通式,不過可以出題了qwq

考慮 \(n\) 塊積木,給定 \(m\) ,每塊積木大小為 \(1 \times k,k \in \left[ 1,\left\lceil \dfrac{m}{2} \right\rceil \right]\) ,求能擺成正方形的邊長最大值。

時間複雜度 \(O(1)\)

空間複雜度 \(O(1)\)

程式碼

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

bool solve() {
    ll n;
    cin >> n;
    if (n == 2) return false;
    ll a = (n + 1) / 2;
    ll ans = a + (n - a) / 3;
    cout << ans << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

C

題解

知識點:構造。

\(n \leq 3\)\(n = 7\) 時無解。

考慮 \(n\bmod 4 = 0,1,2,3\) 的情況。

  1. \(n \bmod 4 = 0\) 時顯然形如構造 \(3,4,1,2\) 的迴圈即可。
  2. \(n \bmod 4 = 1\) 時,前 \(5\) 項構造成 \(4,5,1,2,3\) ,其餘仿照 \(n \bmod 4 = 0\) 情況。
  3. \(n \bmod 4 = 2\) 時,前 \(6\) 項構造成 \(4,5,6,1,2,3\) ,其餘仿照 \(n \bmod 4 = 0\) 情況。
  4. \(n \bmod 4 = 3\) 時,前 \(11\) 項構造分為 \(5\) 項和 \(6\) 項兩組仿照 \(n \bmod 4 = 1,2\) 情況,其餘仿照 \(n \bmod 4 = 0\) 情況。

時間複雜度 \(O(n)\)

空間複雜度 \(O(1)\)

程式碼

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

bool solve() {
    int n;
    cin >> n;
    if (n <= 3 || n == 7) return false;
    int m = 1;
    if (n % 4 == 1) {
        cout << "4 5 1 2 3" << ' ';
        m = 6;
    }
    else if (n % 4 == 2) {
        cout << "4 5 6 1 2 3" << ' ';
        m = 7;
    }
    else if (n % 4 == 3) {
        cout << "4 5 1 2 3 9 10 11 6 7 8" << ' ';
        m = 12;
    }
    for (int i = m;i <= n;i += 4) {
        cout << i + 2 << ' ' << i + 3 << ' ' << i << ' ' << i + 1 << ' ';
    }
    cout << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    //cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

D

題解

知識點:博弈論。

這類題需要先對局面分類,每種局面考慮找到一組平衡的操作,即對於其中一人,無論另一人如何操作,他都可以在下一次操作後回到原來的局面。

考慮將 \(n\) 分奇偶情況:

  1. \(n\) 為偶數,小紅每次可以選 \(1\) ,隨後數變為奇數局面,小紫只有奇數因子能選,數又變為偶數局面。到最後,必然是小紫讓數變為 \(0\) ,因為只有小紫能讓數變為偶數。因此,偶數局面小紅必勝。
  2. \(n\) 為奇數,根據 \(n\) 為偶數的推理,發現奇數局面小紅必敗。

時間複雜度 \(O(1)\)

空間複雜度 \(O(1)\)

程式碼

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

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    ll n;
    cin >> n;
    if (n & 1) cout << "yukari" << '\n';
    else cout << "kou" << '\n';
    return 0;
}

E

題解

知識點:計算幾何。

\(A(x_A,y_A),B(x_B,y_B),C(x_C,y_C)\) 構成等腰直角三角形,其中 \(C\) 為頂點且在 \(AB\) 右側,滿足方程:

\[\left\{ \begin{aligned} x_C+y_C = x_A + y_B\\ x_C-y_C = x_B - y_A \end{aligned} \right. \]

方程可以透過全等三角形證明。

顯然 \(C\)\(C\) 關於 \(AB\) 的對稱點同時是或不是整數點,解出 \(C(x_C,y_C)\) 後判斷是否為整數即可。

(平面幾何永遠的痛,並且以為無解輸出-1收穫WA

時間複雜度 \(O(1)\)

空間複雜度 \(O(1)\)

程式碼

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

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    ll a, b, c, d;
    cin >> a >> b >> c >> d;
    ll x = a + d + c - b;
    ll y = a + d + b - c;
    if (x & 1 || y & 1)cout << "No Answer!" << '\n';
    else cout << x / 2 << ' ' << y / 2 << '\n';

    return 0;
}

F

題解

知識點:宇宙的終極答案。

透過你高超的中文流讀取技術,發現這是營銷號特有的文案。

本打算對此嗤之以鼻的你,閱讀完樣例後逐漸理解了一切,確信 \(42\) 就是宇宙的終極答案。

時間複雜度 \(O(\infin)\)

空間複雜度 \(O(\infin)\)

程式碼

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

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cout << 42 << '\n';
    return 0;
}

G

題解

知識點:模擬,列舉,dfs。

很簡單(痛苦)的模擬。

dfs列舉每個 ? 的三種可能即可,注意快速冪前把底數模一下,因為可能炸 long long

可以選擇預處理數字後邊列舉邊求值,也可以考慮列舉完再求值。注意,邊列舉邊求值不太適用於有優先順序表示式。

(被表示式求值整了一頓,碼力太差了QAQ

時間複雜度 \(O(3^{12} \cdot n)\)

空間複雜度 \(O(n)\)

程式碼

邊列舉邊求值

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

ll qpow(ll a, ll k, ll P) {
    ll ans = 1;
    while (k) {
        if (k & 1) ans = ans * a % P;
        k >>= 1;
        a = a * a % P;
    }
    return ans;
}

int ans;
vector<int> num;
vector<char> op(20);
bool dfs(int step = 1, ll cur = num[0]) {
    if (step == num.size()) return cur == ans;
    op[step] = '+';
    if (dfs(step + 1, cur + num[step])) return true;
    op[step] = '-';
    if (dfs(step + 1, cur - num[step])) return true;
    if (cur > 0 && num[step] > 0) {
        op[step] = '#';
        if (dfs(step + 1, qpow(cur % num[step], cur, num[step]))) return true;
    }
    return false;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    string s;
    cin >> s;
    for (int i = 0;i < s.size();i++) {
        if (isdigit(s[i])) ans = ans * 10 + s[i] - '0';
        else num.push_back(ans), ans = 0;
    }
    if (dfs()) {
        cout << num[0];
        for (int i = 1;i < num.size();i++) cout << op[i] << num[i];
        cout << '=' << ans << '\n';
    }
    else cout << -1 << '\n';
    return 0;
}

列舉完求值,用到表示式計算。

這裡給了一個模板,可以修改map的優先順序,支援帶括號的二元運算,以及偽負號運算(指負號運算必須打括號)。

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

ll qpow(ll a, ll k, ll P) {
    ll ans = 1;
    while (k) {
        if (k & 1) ans = ans * a % P;
        k >>= 1;
        a = a * a % P;
    }
    return ans;
}

map<char, int> mp = { {'+',0},{'-',0},{'#',0},{'=',0} };
bool calc(string s) {
    vector<ll> num = { 0 };
    vector<char> op;
    for (auto ch : s) {
        if (ch >= '0' && ch <= '9') num.back() = num.back() * 10 + ch - '0';
        else {
            while (op.size() && mp[ch] <= mp[op.back()]) {
                char ope = op.back();
                op.pop_back();
                ll x = num.back();
                num.pop_back();
                if (ope == '+') num.back() += x;
                else if (ope == '-') num.back() -= x;
                else if (ope == '#') {
                    if (x <= 0) return false;
                    num.back() = qpow(num.back() % x, num.back(), x);
                }
            }
            if (ch == '#' && num.back() <= 0) return false;
            op.push_back(ch);
            num.push_back(0);
        }
    }
    return num[0] == num[1];
}

string s;
bool dfs(int step = 0) {
    if (step == s.size()) return calc(s);
    if (s[step] == '?') {
        s[step] = '+';
        if (dfs(step + 1)) return true;
        s[step] = '-';
        if (dfs(step + 1)) return true;
        s[step] = '#';
        if (dfs(step + 1)) return true;
        s[step] = '?';
    }
    else {
        while (step < s.size() && s[step] != '?') step++;
        if (dfs(step)) return true;
    }
    return false;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin >> s;
    if (dfs()) cout << s << '\n';
    else cout << -1 << '\n';
    return 0;
}

H

題解

知識點:機率dp。

可能重要的前置知識(大佬請跳過)

對於一維期望dp,在第 \(i\) 步時,其向其他狀態轉移的起點只有 \(f_i\) 一種,因此在第 \(i\) 步起點是狀態 \(f_i\) 的機率是百分百的,變化的期望直接加就行。

例如,有期望狀態 \(f_i\) 。操作有兩種,機率分別為 \(\dfrac{1}{4},\dfrac{3}{4}\),兩種操作的貢獻分別是 \(1,3\) 。那麼可以有轉移方程:

\[f_{i+1} = \dfrac{1}{4}(f_i+1) + \dfrac{3}{4}(f_i+3) \]

但是,如果在一維期望dp的基礎上,設每一步都有多個不同的狀態,那麼轉移時的期望就不是簡單加法了。

具體的說,在第 \(i\) 步時,其向其他狀態轉移的起點如果是 \(f_{i,j}\)\(j\) 種,那麼顯然這 \(j\) 種狀態都有機率成為起點,滿足機率的總和為百分百。因此考慮 \(f_{i,j}\) 為起點做轉移時,變化的期望需要乘上其作為起點的機率,表示這步操作在 \(f_{i,j}\) 作為起點的機率下的期望。當然,期望 \(f_{i,j}\) 本身不需要再乘一遍機率,因為求這個期望時已經考慮了到這個狀態的機率,同時我們還可以知道這 \(j\) 種期望的總和就是第 \(i\) 步的總期望。

例如,有期望狀態 \(f_{i,0/1/2}\) ,設 \(f_{i,j}\) 的機率為 \(g_{i,j}\) 。操作有兩種,機率分別為 \(\dfrac{1}{4},\dfrac{3}{4}\) ,我們假設從 \(f_{i,0/2}\) 都可以透過兩種操作轉移到 \(f_{i+1,0}\) ,兩種操作的貢獻對於兩種狀態分別是 \(1,2\)\(5,6\) 。那麼對於 \(f_{i+1,0}\) 可以有轉移方程:

\[\begin{aligned} f_{i+1,0} &= \dfrac{1}{4}(f_{i,0} + 1\cdot g_{i,0}) + \dfrac{3}{4}(f_{i,0} + 2 \cdot g_{i,0})\\ &+\dfrac{1}{4}(f_{i,2} + 5\cdot g_{i,2})+\dfrac{3}{4}(f_{i,2} + 6 \cdot g_{i,2}) \end{aligned} \]

接下來就可以輕鬆(真的嗎)做這道題了。

\(f_{i,j,k}\) 為執行到第 \(i\) 步且滿足串首狀態為 \(j\) 、串尾狀態為 \(k\) 的期望個數。其中,\(j = 0/1\) 表示串首是 rededr\(k = 0/1\) 同理。

\(g_{i,j,k}\) 為對應的 \(f_{i,j,k}\) 發生的機率。注意,除了四種串首尾的狀態,還有一種空串的狀態,這裡沒有標記到陣列裡,但是每步還是得自己手動加上去的,我們記 \(prob\) 為空串的機率,空串的期望為 \(0\) 不需要考慮。

因此有轉移方程:

\[\left\{ \begin{aligned} f_{i+1,0,0} &= \frac{1}{3} \cdot ((0 + 1 \cdot prob) + (f_{i,0,0} + 1 \cdot g_{i,0,0})+(f_{i,0,1}+1\cdot g_{i,0,1}))\\ &+ \frac{1}{3} \cdot 0\\ &+ \frac{1}{3} \cdot (10 \cdot f_{i,0,0})\\ f_{i+1,0,1} &= \frac{1}{3} \cdot 0\\ &+ \frac{1}{3} \cdot ((f_{i,0,0} + 0 \cdot g_{i,0,0})+(f_{i,0,1}+1 \cdot g_{i,0,1}))\\ &+ \frac{1}{3} \cdot (10\cdot f_{i,0,1})\\ f_{i+1,1,0} &=\frac{1}{3} \cdot ((f_{i,1,0} + 1 \cdot g_{i,1,0})+(f_{i,1,1}+1 \cdot g_{i,1,1}))\\ &+\frac{1}{3} \cdot 0\\ &+\frac{1}{3} \cdot (10 \cdot f_{i,1,0})\\ f_{i+1,1,1} &= \frac{1}{3} \cdot 0\\ &+ \frac{1}{3} \cdot ((0 + 0 \cdot prob) + (f_{i,1,0} + 0 \cdot g_{i,1,0})+(f_{i,1,1}+1 \cdot g_{i,1,1}))\\ &+ \frac{1}{3} \cdot (10 \cdot f_{i,1,1} + 9 \cdot g_{i,1,1})\\ g_{i+1,0,0} &= \frac{1}{3} \cdot (prob + g_{i,0,0} + g_{i,0,1}) + \frac{1}{3} \cdot 0 + \frac{1}{3} \cdot g_{i,0,0}\\\ g_{i+1,0,1} &= \frac{1}{3} \cdot 0 + \frac{1}{3} \cdot (g_{i,0,0} + g_{i,0,1}) + \frac{1}{3} \cdot g_{i,0,1}\\\ g_{i+1,1,0} &= \frac{1}{3} \cdot (g_{i,1,0} + g_{i,1,1}) + \frac{1}{3} \cdot 0 + \frac{1}{3} \cdot g_{i,1,0}\\\ g_{i+1,1,1} &= \frac{1}{3} \cdot 0 + \frac{1}{3} \cdot (prob + g_{i,1,0} + g_{i,1,1}) + \frac{1}{3} \cdot g_{i,1,1}\\\ prob' &= \frac{1}{3} \cdot prob \end{aligned} \right. \]

寫的很詳細了,三個 \(\dfrac{1}{3}\) 對應三種操作,分別算一下機率和期望轉移就行。特別注意,\(f_{i+1,1,1}\) 的操作三轉移可以產生十倍加九的期望。

程式碼用滾動陣列壓縮了一維, \(f_{j,k,0/1}\) 代表第 \(i\) 步的各種機率/期望, \(g_{j,k,0/1}\) 代表第 \(i+1\) 步的各種機率/期望。並且,程式碼轉移時是用子狀態刷表,而非如上述轉移方程填表,因為寫起來比較方便。填表也能寫,本質都是一樣的,很好理解。

推薦使用 Modint 不然開 long long 也救不了打 % 打到手痠。

時間複雜度 \(O(k)\)

空間複雜度 \(O(1)\)

程式碼

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

const int P = 1e9 + 7;
struct Modint {
    int val;
    Modint(int _val = 0):val(_val %P) { format(); }
    Modint(ll _val):val(_val %P) { format(); }

    //if val in [-P,2P)
    //maybe slower than global version
    Modint &format() {
        if (val < 0) val += P;
        if (val >= P) val -= P;
        return *this;
    }
    Modint inv()const { return qpow(*this, P - 2); }

    Modint &operator+=(const Modint &x) { val += x.val;return format(); }
    Modint &operator-=(const Modint &x) { val -= x.val;return format(); }
    Modint &operator*=(const Modint &x) { val = 1LL * val * x.val % P;return *this; }
    Modint &operator/=(const Modint &x) { return *this *= x.inv(); }
    friend Modint operator-(const Modint &x) { return { -x.val }; }
    friend Modint operator+(Modint a, const Modint &b) { return a += b; }
    friend Modint operator-(Modint a, const Modint &b) { return a -= b; }
    friend Modint operator*(Modint a, const Modint &b) { return a *= b; }
    friend Modint operator/(Modint a, const Modint &b) { return a /= b; }

    friend Modint qpow(Modint a, ll k) {
        Modint ans = 1;
        while (k) {
            if (k & 1) ans = ans * a;
            k >>= 1;
            a = a * a;
        }
        return ans;
    }

    friend istream &operator>>(istream &is, Modint &x) {
        ll _x;
        is >> _x;
        x = { _x };
        return is;
    }
    friend ostream &operator<<(ostream &os, const Modint &x) { return os << x.val; }
};
/*
f[0/1][0/1][0]:機率
f[0/1][0/1][1]:期望
00  red-red
01  red-edr
10  edr-red
11  edr-edr
注意還有一種空串情況
需要每次操作前手動加
*/
int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int k;
    cin >> k;
    array<array<array<Modint, 2>, 2>, 2> f = {};
    Modint inv3 = Modint(3).inv();
    Modint prob = 1;
    for (int i = 1;i <= k;i++) {
        //prod指空串機率,空串期望始終為0不需要管
        array<array<array<Modint, 2>, 2>, 2> g = {};
        g[0][0][0] = inv3 * prob;
        g[1][1][0] = inv3 * prob;
        g[0][0][1] = inv3 * prob;
        //空串到g[1][1]的期望還是0,不用管
        for (auto i : { 0,1 }) {
            for (auto j : { 0,1 }) {
                g[i][0][0] += inv3 * f[i][j][0];//操作1後的機率
                g[i][1][0] += inv3 * f[i][j][0];//操作2後的機率
                g[i][j][0] += inv3 * f[i][j][0];//操作3後的機率

                g[i][0][1] += inv3 * f[i][j][1];//操作1後的期望
                g[i][0][1] += inv3 * f[i][j][0];
                g[i][1][1] += inv3 * f[i][j][1];//操作2後的期望
                if (j == 1) g[i][1][1] += inv3 * f[i][j][0];//只有?1才能加
                g[i][j][1] += 10 * inv3 * f[i][j][1];//操作3後的期望
                if (i == 1 && j == 1) g[i][j][1] += 9 * inv3 * f[i][j][0];//只有11能加9個
            }
        }
        prob *= inv3;
        f = g;
    }
    Modint sum = 0;
    for (auto i : { 0,1 })for (auto j : { 0,1 }) sum += f[i][j][1];
    cout << sum << '\n';
    return 0;
}

I

題解

知識點:數論,構造。

  1. 偶數情況,若 \(x-1\) 是素數構造 \(n = (x-1)^2\) ,則 \(f(n) = 1+x-1=x\) ; 若 \(x-3\) 是素數構造 \(n = 2(x-3)\) ,則 \(f(n) = 1+2+x-3=x\)

  2. 奇數情況,因為一定存在 \(1\) 因子,我們考慮使其他因子的和湊出一個偶數 \(x-1\) 。考慮最簡單的素數情況,因為哥德巴赫猜想,一個大於等於 \(4\) 的偶數可以被分解成兩個素數之和,我們只要找到兩個不同的素數 \(p,q\) 使得 \(p+q = x-1\) ,那麼構造 \(n = pq\) ,則 \(f(n) = 1+p+q=x\)

    注意,哥德巴赫猜想所述是兩個素數之和,不是兩個不同的素數。經過測試 int 範圍內,大於等於 \(8\) 的偶數都可以被分解為兩個不同的素數之和,因此大於等於 \(9\) 的奇數我們無腦無解即可。

    我們需要特判 \(x = 1,3,7\) ,因為這些情況確實有解,但不能透過哥德巴赫猜想構造。

時間複雜度 \(O(x)\)

空間複雜度 \(O(x)\)

程式碼

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

const int N = 1e6 + 7;
bool vis[N];
vector<int> prime;
void get_prime(int n) {
    for (int i = 2;i <= n;i++) {
        if (!vis[i]) prime.push_back(i);
        for (int j = 0;j < prime.size() && i * prime[j] <= n;j++) {
            vis[i * prime[j]] = 1;
            if (!(i % prime[j])) break;
        }
    }
}

bool solve() {
    int x;
    cin >> x;
    if (x == 1) {
        cout << 2 << '\n';
        return true;
    }
    if (x == 3) {
        cout << 4 << '\n';
        return true;
    }
    if (x == 7) {
        cout << 8 << '\n';
        return true;
    }
    if (x & 1) {
        for (int i = 0;i < prime.size() && 2 * prime[i] < x - 1;i++) {
            if (!vis[x - 1 - prime[i]]) {
                cout << 1LL * prime[i] * (x - 1 - prime[i]) << '\n';
                return true;
            }
        }
    }
    else {
        if (!vis[x - 1]) cout << 1LL * (x - 1) * (x - 1) << '\n';
        else cout << 1LL * 2 * (x - 3) << '\n';
        return true;
    }
    return false;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    get_prime(1e6);
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

K

題解

知識點:貪心,數學。

給你前 \(n\) 種素數,每個素數有 \(a_i\) 個。

\(size = \sum_{i=1}^n a_i\) 。現在將這 \(size\) 個素數排成一個序列,設 \(f(i)\) 為序列中 \([1,i]\) 的數的乘積的因子的數量。現在求 \([1,size]\)\(f(i)\) 的和,即 \(\sum_{i=1}^{size} f(i)\) ,的最大值。

顯然,我們需要儘可能讓前面的 \(f(i)\) 的越大越好。我們知道,乘積的因子數量等於各個素數個數加 \(1\) 的乘積,透過一些嘗試很容易發現均攤素數的個數,比連續安排同一種素數得到的結果要大很多,因此我們每次安排還能安排的素數中出現次數最小的那個素數。

我們設 \(cnt_i\) 表示出現至少 \(i\) 次的素數個數,我們發現 \(f(i)\) 的結果呈現 \(2,2^2,\cdots,2^{cnt_1},2^{cnt_1-1} \cdot 3,\cdots,2^{cnt_1-cnt_2} \cdot 3^{cnt_2},\cdots\) 。直接求和要加 \(size\) 次是不可行的,因此我們先用差分維護好 \(cnt_i\) ,隨後用等比公式對每 \(cnt_i\) 個數直接求和。

\(pre\)\(cnt_{i-1}\) 段的最後一個數字,那麼 \(cnt_i\) 段的總和為 \(pre \cdot \dfrac{1-\frac{i+1}{i}^{cnt_i}}{1-\frac{i+1}{i}}\) ,最後一個數字 \(pre' = pre \cdot \dfrac{i+1}{i}^{cnt_i}\) ,於是就可以遞推求和了。

時間複雜度 \(O(2 \cdot 10^5 + n)\)

空間複雜度 \(O(2 \cdot 10^5)\)

程式碼

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

const int P = 1e9 + 7;
ll qpow(ll a, ll k) {
    ll ans = 1;
    while (k) {
        if (k & 1) ans = ans * a % P;
        k >>= 1;
        a = a * a % P;
    }
    return ans;
}

ll inv(ll a) { return qpow(a, P - 2); }

int cnt[200007];
int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    cin >> n;
    for (int i = 1;i <= n;i++) {
        int x;
        cin >> x;
        cnt[1]++;
        cnt[x + 1]--;
    }
    for (int i = 1;i <= 2e5;i++) cnt[i] += cnt[i - 1];
    int pre = 1;
    int ans = 0;
    for (int i = 1;i <= 2e5;i++) {
        int f = 1LL * (i + 1) * inv(i) % P;
        int g = qpow(f, cnt[i]);
        ans = (ans + 1LL * pre * f % P * (1 - g + P) % P * inv(1 - f + P) % P) % P;
        pre = 1LL * pre * g % P;
    }
    cout << ans << '\n';
    return 0;
}