洛谷 P1997 faebdc 的煩惱
題意簡述:
\(q\) 次詢問 \([l, r]\) ,詢問區間眾數出現次數。
思路:
考慮是否滿足區間加性?不滿足於是不能線段樹。
考慮區間是否具有傳遞性?
具有傳遞性,\([l, r]\) 的答案可以快速傳遞到 \([l, r + 1]\) 或 \([l + 1, r]\) 的答案。
樸素想法是維護 freq[v] 表示 \(v\) 出現頻率,使用 multiset \(\mathbb{S}\) 維護當前所有頻率。
auto add = [&] (int x) {
if (S.find(freq[a[x]] != S.end()) S.erase(S.find(freq[a[x]]));
freq[a[x]]++;
S.insert(freq[a[x]]);
};
auto add = [&] (int x) {
if (S.find(freq[a[x]] != S.end()) S.erase(S.find(freq[a[x]]));
freq[a[x]]--;
S.insert(freq[a[x]]);
};
區間可以 \(O(\log n)\) 傳遞。眾數次數為最大頻率 \(*S.rbegin()\) 。
注意到 freq 如果出現,只會逐漸遞增。如果消失,只會逐漸遞減。只需使用 cnt[freq[x]] 統計所有頻率出現過的次數。
眾數次數可以用歷史最大頻率更新 mx = max(mx, freq[v]) 。單次更新複雜度為 \(O(q)\) ,總的更新複雜度為 \(O(q)\) 。
當然歷史頻率會降低,但不會憑空消失,而是往下遞減。於是有另一個更新 while(cnt[mx] == 0) { mx--; } 總的更新複雜度為 \(O(q)\),均攤複雜度為 \(O(1)\) 。
於是區間只需要 \(O(1)\) 傳遞。
auto add = [&] (int x) {
cnt[freq[a[x]]]--;
freq[a[x]]++;
cnt[freq[a[x]]]--;
mx = std::max(mx, freq[a[x]]);
};
auto add = [&] (int x) {
cnt[freq[a[x]]]--;
freq[a[x]]--;
cnt[freq[a[x]]]--;
while (cnt[mx] == 0) { mx--; assert(mx >= 1); };
};
然後莫隊離線一下就做完了。時間複雜度 \(O((n + q )\sqrt{n} + n \log n)\) 。
強制線上怎麼做?能離線就別搞線上,送了。
Code
int n, q; std::cin >> n >> q;
std::vector<int> a(n + 1);
for (int i = 1; i <= n; i++) {
std::cin >> a[i]; a[i] += 100001;
}
std::vector<std::array<int, 3> > que(q + 1);
for (int i = 1; i <= q; i++) {
int l, r; std::cin >> l >> r;
que[i] = {l, r, i};
}
const int M = (int)std::sqrt(n) + 2;
std::sort(que.begin() + 1, que.end(), [&](std::array<int, 3> A, std::array<int, 3> B){
if (A[0] / M != B[0] / M) return A[0] / M < B[0] / M;
else return ~ (A[0] / M) & 1 ? A[1] < B[1] : A[1] > B[1];
});
std::vector<int> cnt(n + 1);
cnt[0] = 1 << 30;
int mx = 0;
auto add = [&] (int x) {
--cnt[freq[a[x]]];
++freq[a[x]];
++cnt[freq[a[x]]];
mx = std::max(mx, freq[a[x]]);
};
auto del = [&] (int x) {
--cnt[freq[a[x]]];
--freq[a[x]];
++cnt[freq[a[x]]];
while (cnt[mx] == 0) { --mx; }
};
std::vector<int> ans(q + 1);
int L = 1, R = 0;
for (int i = 1; i <= q; i++) {
while (R < que[i][1]) R++, add(R);
while (L > que[i][0]) L--, add(L);
while (R > que[i][1]) del(R), R--;
while (L < que[i][0]) del(L), L++;
ans[que[i][2]] = mx;
}
for (int i = 1; i <= q; i++) std::cout << ans[i] << "\n";
洛谷 P3203 [HNOI2010] 彈飛綿羊
題意簡述:
\(n\) 個裝置動能為 \(a_i\) 。在第 \(i\) 個裝置開始,會被彈到第 \(i + a_i\) 個裝置,繼續被彈到第 \(i + a_i + a_{i + a_{i}}\) 個裝置……直到超過 \(n\) 。
\(q\) 次操作:
- 從第 \(x\) 個裝置開始,需要幾次彈出界。
- 修改第 \(x\) 個裝置動能為 \(y\) 。
思路:
考慮按塊長 \(M\) 分塊,得到 \(\frac{n}{M}\) 塊。
自然經典地維護塊的基礎資訊:每個下標在哪塊 block[i] ,每個塊的左右端點下標 left[block[i]] 、 right[block[i]] 。
考慮維護每個塊內的答案:\(x\) 從 \(block[x]\) 剛好跳到右邊一個塊的答案。
for(int i = n; i >= 1; --i) {
if(i + a[i] > right[pos[i]]) {
step[i] = 1;
to[i] = i + a[i];
}
else {
step[i] = step[i + a[i]] + 1;
to[i] = to[i + a[i]];
}
}
這裡是 \(O(n)\) 維護出所有塊的答案。
考慮從 \(x\) 起跳,暴力向右跳最多 \(n / M\) 個塊,最後一段不能越界的位置,最壞要暴力跑兩個塊計算答案。單次詢問複雜度 \(T(\frac{n}{M} + 2M) = O(\frac{n}{M} + M)\) 。當 \(M = \sqrt{M}\) 為 \(T(3 \sqrt{n}) = O(\sqrt{n})\) 。
int L = x, R = n;
i64 res = 0;
while (to[L] <= R) {
res += step[L];
L = to[n];
}
res += step[L];
這題最後一段不越界的位置只需要 \(O(1)\) 計算答案,單次詢問的時間複雜度為 \(T(\frac{n}{M}) = O(\frac{n}{M})\) 。
考慮修改 \(a_x = y\) ,暴力修改 \(block_{a_x}\) 這個塊即可。
for(int i = right[block[a[x]]]; i >= left[block[a[x]]]; --i) {
if(i + a[i] > right[pos[i]]) {
step[i] = 1;
to[i] = i + a[i];
}
else {
step[i] = step[i + a[i]] + 1;
to[i] = to[i + a[i]];
}
}
單次修改複雜度 \(O(M)\) 。
總時間複雜度 \(O(n + q (\frac{n}{M} + M))\) ,當 \(M = \sqrt{n}\) 時間複雜度為 \(O(q \sqrt{n})\) 。
Code
void solve() {
int n; std::cin >> n;
const int M = std::sqrt(n) + 2;
std::vector<int> a(n + 1);
std::vector<int> pos(n + 1), L(n + 1, 1 << 30), R(n + 1);
std::vector<int> step(n + 1), to(n + 1);
for(int i = 1; i <= n; i++) {
std::cin >> a[i];
pos[i] = i / M;
}
for(int i = 1; i <= n; i++) {
L[pos[i]] = std::min(L[pos[i]], i);
R[pos[i]] = std::max(R[pos[i]], i);
}
auto update = [&] (int l, int r) {
for(int i = r; i >= l; --i) {
if(i + a[i] > R[pos[i]]) {
step[i] = 1;
to[i] = i + a[i];
}
else {
step[i] = step[i + a[i]] + 1;
to[i] = to[i + a[i]];
}
}
};
update(1, n);
auto ask = [&] (int x) -> int {
int res = 0;
while (to[x] <= n) {
res += step[x];
x = to[x];
}
res += step[x];
return res;
};
int qc; std::cin >> qc;
for (int q = 1; q <= qc; q++) {
int opt, x; std::cin >> opt >> x; x++;
if(opt == 1) {
std::cout << ask(x) << "\n";
}
else {
int y; std::cin >> y;
a[x] = y;
update(L[pos[x]], R[pos[x]]);
}
}
}
洛谷 P3246 [HNOI2016] 序列
題意簡述:
\(q\) 次詢問 \([l, r]\) ,詢問
思路
這題能降紫我很不服氣,雖然是已經是眾所周知題……但能降紫還是很不服氣。(非常自然而且經典)
一個非常自然的問題,詢問一個區間的所有子區間的最小值之和。
單次詢問的樸素做法是什麼?
\(O(n^{2})\) 列舉區間 + \(O(n)\) 暴力檢驗?時間複雜度 \(O(n^{3})\) 。
\(O(n^{2})\) 列舉區間 + \(O(1)\) \(RMQ\) 檢查?時間複雜度 \(O(n^{2}) + n \log n\) 。
考慮區間傳遞性。
令 \(f(l, r) = min_{k = l}^{r} a_k\) 。
- 注意 \([l, r - 1] \rightarrow [l, r]\) ,增加貢獻 \(\sum_{i = 1}^{r} f(i, r)\) 。
- 注意 \(\forall i < j, f(i, r) \leq f(j, r)\) 。
- 注意 \(\forall i < j, f(l, i) \geq f(l, j)\) 。
考慮使用單調棧, \(O(n)\) 處理出每個 \(i\) 前一個和後一個更小值的位置 pre[i] 、 nxt[i] 。
考慮 \(c[l][r - 1]\) 的答案如何傳遞到 \(c[l][r]\) ,有
考慮 \([l, r]\) 中最小值的位置為 \(p\) ,這可以倍增(ST 表)預處理出後 \(O(1)\) 查詢,有
於是定義 \([l, r - 1]\) 到 \([l, r]\) 的增量為
這裡傳遞性並沒有被最佳化下去,別急。
定義
可以遞推
先 \(O(n)\) 遞推出 \(dp_{1 \sim n}\) 。
展開 \(dpl(r)\)
發現
於是就可以 \([l, r - 1]\) 和 \([l, r]\) 可以互相 \(O(1)\) 轉移了。
這裡不使用 add、del 維護轉移,而是使用莫隊的另一種轉移 moveRight、moveLeft :
auto moveRight = [&] (int L, int R) -> i64 {
int p = getMivp(L, R);
return f[R] - f[p] + 1LL * (p - L + 1) * a[p];
};
考慮 \([l + 1, r]\) 怎麼轉移到 \([l, r]\) 。
依舊考慮 \([l, r]\) 的最小值位置為 \(p\) ,有
定義 \([l + 1, r]\) 到 \([l, r]\) 的增量為
類似地,再定義
且有遞推
先 \(O(n)\) 倒序遞推出 \(dpr_{1 \sim n}\) 。
然後展開 \(dpr(l)\) 可以得到
注意到
於是 \([l + 1, r]\) 和 \([l, r]\) 之間可以 \(O(1)\) 轉移。
有
auto moveLeft = [&] (int L, int R) -> i64 {
int p = getMivp(L, R);
return f[L] - f[p] + (R - p + 1) * a[p];
};
然後莫隊離線就解決了。其中維護答案的時候如下:
int L = 1, R = 0;
for (int i = 1; i <= q; i++) {
while (R < que[i][1]) R++, res += moveRight(L, R);
while (L > que[i][0]) L--, res += moveLeft(L, R);
while (R > que[i][1]) res -= moveRight(L, R), R--;
while (L < que[i][0]) res -= moveLeft(L, R), L++;
ans[que[i][2]] = res;
}
時間複雜度 \(O((n + q)\sqrt{n} + n \log n)\) 。
Code
void solve() {
int n, q; read<int>(n); read<int>(q);
std::vector<i64> a(n + 1);
const int LOGN = 31 - __builtin_clz(n);
std::vector<std::vector<int> > fmiv(LOGN + 1, std::vector<int>(n + 1, 1 << 30));
std::vector<std::vector<int> > mivp(LOGN + 1, std::vector<int>(n + 1));
for (int i = 1; i <= n; i++) {
read<i64>(a[i]);
fmiv[0][i] = a[i];
mivp[0][i] = i;
}
std::vector<int> stk(n + 1); int top = 1;
std::vector<int> nxt(n + 1);
for (int i = 1; i <= n; i++) {
if (top == 1 || a[i] >= a[stk[top - 1]]) stk[top++] = i;
else {
while (top > 1 && a[i] < a[stk[top - 1]]) {
nxt[stk[top - 1]] = i;
top--;
}
stk[top++] = i;
}
}
while (top > 1) nxt[stk[top-- - 1]] = n + 1;
top = 1;
std::vector<int> pre(n + 1);
for (int i = n; i >= 1; --i) {
if (top == 1 || a[i] >= a[stk[top - 1]]) stk[top++] = i;
else {
while (top > 1 && a[i] < a[stk[top - 1]]) {
pre[stk[top - 1]] = i;
top--;
}
stk[top++] = i;
}
}
while (top > 1) pre[stk[top-- - 1]] = 0;
std::vector<i64> f(n + 2), g(n + 2);
for (int i = 1; i <= n; i++) {
f[i] = f[pre[i]] + 1LL * (i - pre[i]) * a[i];
}
for (int i = n; i >= 1; --i) {
g[i] = g[nxt[i]] + 1LL * (nxt[i] - i) * a[i];
}
for (int j = 1; j <= LOGN; j++) {
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
if (fmiv[j - 1][i] < fmiv[j - 1][i + (1 << j - 1)]) {
fmiv[j][i] = fmiv[j - 1][i];
mivp[j][i] = mivp[j - 1][i];
}
else {
fmiv[j][i] = fmiv[j - 1][i + (1 << j - 1)];
mivp[j][i] = mivp[j - 1][i + (1 << j - 1)];
}
}
}
std::vector<int> LG2(n + 1);
LG2[1] = 0;
for (int i = 2; i <= n; i++) LG2[i] = LG2[i / 2] + 1;
auto getMip = [&] (int l, int r) -> int {
assert(l <= r);
int L = LG2[r - l + 1];
if (fmiv[L][l] < fmiv[L][r - (1 << L) + 1]) return mivp[L][l];
else return mivp[L][r - (1 << L) + 1];
};
std::vector<std::array<int, 3> > que(q + 1);
for (int i = 1; i <= q; i++) {
int l, r; read<int>(l); read<int>(r);
que[i] = {l, r, i};
}
const int M = (int)std::sqrt(n) + 2;
std::sort(que.begin() + 1, que.end(), [&](std::array<int, 3> A, std::array<int, 3> B){
if (A[0] / M != B[0] / M) return A[0] / M < B[0] / M;
else return ~(A[0] / M) & 1 ? A[1] < B[1] : A[1] > B[1];
});
std::vector<i64> ans(q + 1);
i64 res = 0;
int L = 1, R = 0;
auto moveRight = [&] (int L, int R) -> i64 {
int p = getMip(L, R);
return f[R] - f[p] + a[p] * (p - L + 1);
};
auto moveLeft = [&] (int L, int R) -> i64 {
int p = getMip(L, R);
return g[L] - g[p] + a[p] * (R - p + 1);
};
for (int i = 1; i <= q; i++) {
while (R < que[i][1]) R++, res += moveRight(L, R);
while (L > que[i][0]) L--, res += moveLeft(L, R);
while (R > que[i][1]) res -= moveRight(L, R), R--;
while (L < que[i][0]) res -= moveLeft(L, R), L++;
ans[que[i][2]] = res;
// std::cout << que[i][0] << " " << que[i][1] << " " << res << "\n";
}
for (int i = 1; i <= q; i++) {
write<i64>(ans[i]); puts("");
}
}
CF 上的縫合題
https://codeforces.com/contest/2009 G1/2/3
先給題意。
給 \(a_1, a_2, \cdots, a_n\) 。
定義 \(f(l, r)\) 為 \([l, r]\) 中,改變任意個最少的數,使 \([l, r]\) 出現一段長為 \(k\) 的連續子序列,滿足這段子序列是嚴格逐加序列,\(\forall i \in [j, j + k - 1], a_i + 1 = a_{i + 1}\) 。
顯然是包裝的東西,考慮解包裝,讓 \(\forall i, a_i += n - i + 1\) (通常最好加一個偏移常量)。
\(f(l, r)\) 的重定義為 \([l, r]\) 中, \(k\) 減去“一段連續 \(k\) 子序列的最多眾數次數”。
繼續重定義為 \(f(l, r) = min_{i = l}^{r - k + 1} c_i\) ,其中 \(c_i\) 為 \([i, i + k - 1]\) 的眾數次數。
這是縫合的第一個東西,包裝成不像人話。
G1
欽定 \(r = l + k - 1\) ,詢問 \(q\) 次 \(f(l, r)\) 。
限制了 \(r - l + 1 = k\) ,滑動視窗划過去即可。時間複雜度 \(O(n)\) 。
G2
詢問 \(q\) 次 \(\sum_{i = l + k - 1}^{r} f(l, i)\) 。
那麼就是詢問 \(\sum_{i = l}^{r - k + 1} min_{k = l}^{i} c_k\) 。
目光只需放在 \(c\) 陣列,長度為 \(m\) 。順便定義 \(r := r - k + 1\) 。
單次回答\(\sum_{i = l}^{r} min_{k = l}^{i} c_k\) 。
單次詢問,暴力解答是 \(O(m^{2})\) 。
預處理 \(nxt_{i}\) 為 \(c_i\) 下一個更小值的位置。
單次詢問,可以以 \(O(m)\) 解答。具體為從 \(i\) 跳到 \(nxt_i\) ,貢獻會增加 \((nxt_{i} - i) \times c_i\) 。
想到向單向跳的區間詢問,經典問題是“彈飛綿羊”。
按根號長度分塊,維護每個塊內的點剛好跳到下一個塊的答案。\(O(1)\) 計算。
考慮單次詢問,最多跳 \(\frac{m}{\sqrt{m}} = \sqrt{m}\) 次,最後將要越界時暴力跑 \(O(\sqrt{m})\) 。
於是可以 \(q \sqrt{m}\) 解決問題。\(n, m\) 同階。
G3
不妨目光只需放在 \(c\) 陣列,長度為 \(m\) 。順便定義 \(r := r - k + 1\) 。
單次回答\(\sum_{i = l}^{r} min_{k = l}^{i} c_k\) 。
詢問 \(\sum_{i = l}^{r} \sum_{j = 1}^{r} min_{k = i}^{r} c_k\) 。
區間內所有子區間的最小值之和。
經典問題“序列”,原題。
RMQ + DP + 差分 + 莫隊處理。時間複雜度 \(O(m \sqrt{m} + m \log m)\) 。\(n, m\) 同階。
G1/2/3 作為縫合題縫合了至少 4 個常見問題的技巧處理(包括多道省選級別的題)。據說是給 d4 的人提供教育價值的……雖然其實縫得挺不錯