Level Up

Yorg發表於2024-10-17

演算法

又是 Monocarp

較複雜演算法(學習思路)

暴力

觀察到對於每一個 \(k\) , 其最大升級次數為 \(\left\lfloor{\frac{n}{k}}\right\rfloor\)
對於所有的 \(k \in [1, n]\), 我們可以知道其升級次數為 \(\displaystyle\sum_{i = 1}^{n} {\left\lfloor{\frac{n}{k}}\right\rfloor} = n \ln n\)

考慮對 \(\text{each } k = x\), 我們求出其每一個每次升級後, 保持這個等級的升級段 \([L, R]\) , 其中 \(R\) 顯然二分可求, 考慮 \([L, R]\) 中等級為 \(level\) , 那麼這個升級段必須滿足

\[\sum_{i = l}^{r} \left[{a_i > level}\right] \geq k \]

使用主席樹等資料結構可求 \(L\)

於是求右端點為 \(\mathcal{O}(\log n)\) 左端點也為 \(\mathcal{O}(\log n)\) 一共有 \(\mathcal{O}(n \ln n)\) 個升級段
總時間複雜度約為 \(\mathcal{O}(n \log^3 n)\)

總結 \(1\)

對於這種典型的調和級數類問題, 考慮每一級單獨運算, 這屬於一個典型套路

暴力最佳化

\(\log\) 演算法無法透過, 考慮最佳化掉找 \(L\)\(\log\)
容易想到字首和最佳化, 令 \(sum_{i, j}\) 表示前 \(i\) 個數, 怪物等級 \(\leq j\) 的怪物個數, 那麼可以預處理 \(a_i\) 值域較小的情況 (即 \(k\) 偏大)
對於 \(a_i\) 值域較大的情況, 顯然此時 \(k\) 偏小, 於是可以直接列舉數列進行一個模擬

這個有點像根號分治
假設

  • \(k \geq B\) 時, 使用 \(\mathcal{O}(\frac{n ^ 2}{B})\) 的預處理, \(\mathcal{O}(n \log^2 n)\) 的回答詢問

  • \(k < B\) 是, 使用 \(\mathcal{O}(nB)\) 的暴力

均值不等式求得 \(B = \sqrt{n}\), 於是總時間複雜度為 \(\mathcal{O}(n \sqrt{n} + n \log^2 n)\)

程式碼

總結 \(2\)

對於一種最佳化技巧, 可以使用根號分治使其達到最優

較簡單演算法

感性發現, \(k\) 增大時, 顯然有升級更困難
於是考慮對於每一個怪物 \(i\), 找到其閾值 \(T_i\) (當 \(k \geq T_i\) 時迎戰, \(k < T_i\) 時跑路)
這個 \(T_i\) 顯然可以使用二分求得, 總時間複雜度是 \(\mathcal{O} (n ^ 2 \log n)\), 而 \(check\) 函式的時間複雜度是 \(\mathcal{O}(n)\)

觀察 \(check\) 函式, 發現其本質為找前面下標, 有多少已經滿足條件
於是可以開一個 樹狀陣列 , 記錄當前 \(k\) 值對應滿足條件的下標數, 那麼每次計算之後, 從 \(T_i \rightarrow n\) 整體 \(+ 1\) (後面的顯然滿足閾值)
時間複雜度最佳化到 \(\mathcal{O}(n \log^2 n)\)

程式碼

#include <iostream>
#include <tuple>
using namespace std;
const int N = 2e5 + 10;
int a[N], n, q, tr[N], idx = 1, l, r, mid, req[N];
inline void update(int x, int v)
{
    while (x < N)
    {
        tr[x] += v;
        x += (x & -x);
    }
}
inline int query(int x)
{
    int res = 0;
    while (x)
        res += tr[x], x -= (x & -x);
    return res;
}
inline bool check(int x, int v) // xth monster will fl, x=v
{
    return 1ll * a[x] * v <= query(v);
}
int main()
{
    scanf("%d%d", &n, &q);
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", a + i);
    }
    for (int i = 1; i <= n; i++)
    {
        l = 1, r = n;
        while (l < r)
        {
            mid = (l + r) >> 1;
            if (check(i, mid))
                l = mid + 1;
            else
                r = mid;
        }
        update(l, 1);
        req[i] = l;
    }
    for (int i = 1, x, k; i <= q; i++)
    {
        scanf("%d%d", &x, &k);
        puts(k < req[x] ? "NO" : "YES");
    }
}

總結

當一個操作涉及到其之前的區間, 考慮用區間類資料結構 \(n \rightarrow \log n\)

相關文章