2024/9/10+11 分塊雜題三道 + 縫合題兩道

zsxuan發表於2024-09-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] 。
考慮 \(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]\) 的眾數次數。

這是縫合的第一個東西,包裝成不像人話。

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 的人提供教育價值的……雖然其實縫得挺不錯

相關文章