2024/9/10+11 分块/莫队杂题三道 + 缝合题两道
洛谷 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] 。
考虑 \(i\) 可以贡献给多少区间,左边可以扩展 \([pre[i + 1], i]\) ,右边可以扩展 \([i, nxt[i - 1]]\) ,于是贡献为 \(a_i \times (i - pre_{i + 1}) \times (nxt_{i - 1} - i)\) 。单次询问复杂度 \(O(n)\) 。
考虑区间传递性。
考虑 \(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]\) 的众数次数。
这里缝合的似乎是“任意长度 \(\geq k\) 的子区间的第 \(k\) 大之和(这是个扫描线 + 链表的经典题)”这个的题描述。
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\) 。
\(q\) 次询问区间内所有子区间的最小值之和。
多次区间询问,所有子区间的区间最值之和。经典问题“序列”,原题。
RMQ + DP + 差分 + 莫队处理。时间复杂度 \(O(m \sqrt{m} + m \log m)\) 。\(n, m\) 同阶。
G1/2/3 作为缝合题缝合了至少 4 个常见问题的技巧处理(包括多道省选级别的题)。据说是给 d4 的人提供教育价值的……虽然其实缝得挺不错