演算法
又是 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\) , 那麼這個升級段必須滿足
使用主席樹等資料結構可求 \(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\)