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]\) ,询问

\[\sum_{l \leq i \leq j \leq r} min_{k = i}^{j} a_k \]

思路

这题能降紫我很不服气,虽然是已经是众所周知题……但能降紫还是很不服气。(非常自然而且经典)

一个非常自然的问题,询问一个区间的所有子区间的最小值之和。

单次询问的朴素做法是什么?
\(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]\) ,有

\[c[l][r] = c[l][r - 1] + \sum_{i = l}^{r} f(i, r) \]

考虑 \([l, r]\) 中最小值的位置为 \(p\) ,这可以倍增(ST 表)预处理出后 \(O(1)\) 查询,有

\[\begin{aligned} f(i, r) &= \min_{k = i}^{r} a_k = a_p \quad s.t. \ l \leq i \leq p \\ \Rightarrow c[l][r] &= c[l][r - 1] + (p - l + 1) \times a_p + \sum_{i = p + 1}^{r} f(i, r) \end{aligned} \]

于是定义 \([l, r - 1]\)\([l, r]\) 的增量为

\[\Delta_1 = (p - l + 1) \times a_p + \sum_{i = p + 1}^{r} f(i, r) \]

这里传递性并没有被优化下去,别急。

定义

\[dpl(i) = \sum_{j = 1}^{i} f(j, i) \]

可以递推

\[dpl(i) = dpl(pre_i) + \sum_{j = pre_i + 1}^{i} f(j, i) = dpl(pre_i) + (i - pre_i) \times a_{i} \]

\(O(n)\) 递推出 \(dp_{1 \sim n}\)

展开 \(dpl(r)\)

\[\begin{aligned} dpl(r) &= dpl(pre_i) + \sum_{j = pre_i + 1}^{r} f(j, r) \\ &= dpl(pre_{pre_i}) + \sum_{j = pre_{pre_i} + 1}^{r} f(j, r) \\ &\cdots \\ &= dpl(p) + \sum_{j = p + 1}^{r} f(j, r) \\ \end{aligned} \]

发现

\[\begin{aligned} &\Delta_1 - (p - l + 1) \times a_p = dpl(r) - dpl(p) = \sum_{i = p + 1}^{r} f(i, r) \\ \Rightarrow &\Delta_1 = dpl(r) - dpl(p) + (p - l + 1) \times a_p \end{aligned} \]

于是就可以 \([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]\)

\[c[l][r] = c[l + 1][r] + \sum_{i = l}^{r} f(l, i) \]

依旧考虑 \([l, r]\) 的最小值位置为 \(p\) ,有

\[\begin{aligned} f(l, i) &= min_{k = l}^{i} a_k = a_p \quad p \leq i \leq r \\ \Rightarrow c[l][r] &= c[l + 1][r] + (r - p + 1) \times a_p + \sum_{i = l}^{p - 1} f(l, i) \end{aligned} \]

定义 \([l + 1, r]\)\([l, r]\) 的增量为

\[\Delta_2 = (r - p + 1) \times a_p + \sum_{i = l}^{p - 1} f(l, i) \]

类似地,再定义

\[dpr(i) = \sum_{i}^{n} f(l, i) \]

且有递推

\[dpr(i) = dpr(nxt_{i}) + \sum_{i = l}^{nxt_{i} - 1} f(l, i) = dpr(nxt_{i}) + (nxt_{i} - i) \times a_{i} \]

\(O(n)\) 倒序递推出 \(dpr_{1 \sim n}\)

然后展开 \(dpr(l)\) 可以得到

\[dpr(l) = \sum_{j = 1}^{n} f(l, j) = dpr(p) + \sum_{j = l}^{p - 1} f(l, j) \]

注意到

\[\begin{aligned} &\Delta_2 - (r - p + 1) \times a_p = dpr(l) - dpr(p) = \sum_{i = l}^{p - 1} f(l, i) \\ \Rightarrow &\Delta_2 = dpr(l) - dpr(p) + (r - p + 1) \times a_p \\ \end{aligned} \]

于是 \([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 的人提供教育价值的……虽然其实缝得挺不错

posted @ 2024-09-11 06:49  zsxuan  阅读(11)  评论(0编辑  收藏  举报