2024CCPC山東省賽補題記錄

TJUHuangTao發表於2024-10-07

前言

今天和隊友VP了24CCPC山東省賽,最後9題,但是賽中7題左右我就隱身了,賽後看題解發現E題不難,賽時過的人太少導致有點畏手畏腳,看到題解一下就懂了,幾分鐘寫好。這裡主要補一下E和L的題解,這場比賽學到了維護區間資訊,可以考慮把區間掛線上段樹節點上,以及動態維護樹直徑的典。

E 感測器 sensors

https://codeforces.com/gym/105385/problem/E

題意

給定 \(n\) 個球,初始都是紅色,有 \(m\) 個區間。一共 \(n\) 輪,每輪將其中一個紅球變藍,強制線上維護恰好有一個紅球的區間的編號平方和。

分析

一開始和隊友都是讀錯題,以為小球是從藍變紅,這樣就太簡單了,因為每個區間只會恰好被操作2次後就剃掉,所以直接分塊啥的亂搞就行。後來發現是從都是紅色慢慢減少,這樣每次小球變化,都得操作很多區間,但是很多操作是沒必要的,只需要在意可能導致區間和變1的那些操作。
然後一個很巧妙的操作就是,類似線段樹分治思想,把 \(m\) 個區間掛線上段樹的 \(mlogn\) 個節點裡,那麼一個操作有可能使某個區間和變1,僅當某個線段樹節點的區間和變成1或0.而這樣的變化只會有2次,總共又只有 \(mlogn\) 的關鍵節點,所以就可以標記那些關鍵節點,線段樹每個節點開vector,裡面存掛在這個節點的區間編號。之後若干次單點修改,push_up時候判斷一下限度桉樹節點的區間和,如果變為1或0,就遍歷一下對應vector裡面的所有編號,裡面對應區間的權值和要麼是減去 \(r - l + 1 - 1\)要麼減一,如果區間權值和變為1或0,就對應維護一下答案的編號平方和,程式碼很好寫。

點選檢視程式碼
#include <bits/stdc++.h>
#define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define tii tuple<int, int, int>
#define db double
#define all(a) a.begin(), a.end()
using namespace std;
const int maxn = 5e5 + 10;
const int mod = 998244353;
int val[maxn];
ll ans;
struct SegmentTree {
    struct Node {
        int sum;
        vector<int> vec;
    };
#define ls rt << 1
#define rs rt << 1 | 1
#define mid ((l + r) >> 1)
    vector<Node> t;
    SegmentTree (int n) {t.resize(n << 2);}
    void push_up(int rt) {t[rt].sum = t[ls].sum + t[rs].sum;}
    void build(int rt, int l, int r) {
        t[rt].vec.clear();
        if (l == r) {t[rt].sum = 1;return;}
        build(ls, l, mid), build(rs, mid + 1, r);
        push_up(rt);
    }
    void modify(int rt, int l, int r, int p, int q, int id) {
        if (p > r || q < l) return;
        if (p <= l && r <= q) {
            t[rt].vec.push_back(id);
            return;
        }
        modify(ls, l, mid, p, q, id);
        modify(rs, mid + 1, r, p, q, id);
    }
    void modify(int rt, int l, int r, int pos) {
        if (l == r) {
            t[rt].sum = 0;
            for (auto id : t[rt].vec) {
                val[id]--;
                if (val[id] == 1) ans += id * id;
                else if (val[id] == 0) ans -= id * id;
            }
            return;
        }
        if (pos <= mid) modify(ls, l, mid, pos);
        else modify(rs, mid + 1, r, pos);
        push_up(rt);
        if (t[rt].sum == 1) {
            for (auto id : t[rt].vec) {
                val[id] -= (r - l + 1) - 1;
                if (val[id] == 1) ans += id * id;
                else if (val[id] == 0) ans -= id * id;
            }
        } else if (t[rt].sum == 0) {
            for (auto id : t[rt].vec) {
                val[id]--;
                if (val[id] == 1) ans += id * id;
                else if (val[id] == 0) ans -= id * id;
            }
        }
    }
};
void solve() {
    int n, m; cin >> n >> m;
    SegmentTree seg(n);
    seg.build(1, 0, n - 1);
    for (int i = 1; i <= m; i++) {
        int l, r; cin >> l >> r;
        val[i] = r - l + 1;
        if (val[i] == 1) ans += i * i;
        seg.modify(1, 0, n - 1, l, r, i);
    }
    cout << ans << " ";
    for (int i = 1; i <= n; i++) {
        int x; cin >> x;
        int pos = (x + ans) % n;
        seg.modify(1, 0, n - 1, pos);
        cout << ans << " \n"[i == n];
    }
}
signed main() {
    // freopen("1.in", "r", stdin);
    // freopen("1.out", "w", stdout);
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) solve();
    return 0;
}

L Intersection of Paths

https://codeforces.com/gym/105385/problem/L

題意

給定一棵樹,每條邊有邊權,多次詢問,每次詢問臨時改變一條邊的邊權,並給出引數 \(k\), 要求在樹上選 \(k\) 條端點各不相同的路徑,使得所有路徑的交集部分的邊權和最大。

分析

注意到對於一次詢問的特定的 \(k\), 能選擇的邊,得滿足它兩側連通塊的點數都得大等於 \(k\), 才能選對應路徑,包括上這邊。那麼把所有的合法邊連起來,會形成一棵樹。容易發現最終路徑的交集肯定只能是一條鏈,所以問題等價於去找這棵樹的最大邊權和的鏈,也就是樹的直徑。然後關注到一個關鍵性質,這棵樹的大小會隨著 \(k\) 的變化單調變化,所以可以離線下來,將詢問排序,對詢問的 \(k\) 從小到大處理,等價於初始一棵完整的樹,不斷把邊的 \(k\) 較小的刪掉,然後維護樹的直徑,可以dfs把邊的k也預處理出來,對邊排序,然後用個指標維護,每次問到一個詢問,判斷指標處的邊的 \(k\) 是否滿足條件,不滿足則刪掉這條邊,指標右移。

動態維護樹直徑CF1192B

然後就變成這個典題(也是看到題解說了才知道有這個典),直接看題解,大概就是利用了尤拉序性質,在邊權都正的前提下,樹的直徑等價於 \(max(dep[l] + dep[r] - 2 \times dep[a]), l \leq a \leq r\) ,然後用線段樹維護這個東西,合併時候按照區間最大最小值,以及 \(dep[l , r] - 2 \times dep[a]\) 的最大值來合併,整棵樹的直徑就是線段樹1號節點的對應答案。然後邊權的變化,就是尤拉序,讓兒子的子樹裡深度全部加上一個變化量,即可。

1192B程式碼

點選檢視程式碼
#include <bits/stdc++.h>
#define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define tii tuple<int, int, int>
#define db double
#define all(a) a.begin(), a.end()
using namespace std;
const int maxn = 5e5 + 10;
const int mod = 998244353;
vector<pii> G[maxn];
int dfn[maxn], ncnt, dep[maxn], L[maxn], R[maxn], fa[maxn];
tii edge[maxn];
void dfs(int u, int f) {
    dfn[++ncnt] = u;
    L[u] = R[u] = ncnt;
    fa[u] = f;
    for (auto [v, w] : G[u]) {
        if (v == f) continue;
        dep[v] = dep[u] + w;
        dfs(v, u);
        dfn[++ncnt] = u;
        R[u] = ncnt;
    }
}
struct SegmentTree {
#define ls rt << 1
#define rs rt << 1 | 1
#define mid ((l + r) >> 1)
    struct Node {
        int ans, mx, mn, rm, lm, laz;
    };
    vector<Node> t;
    SegmentTree(int n) {t.resize(n << 2);}
    void push_up(int rt) {
        t[rt].ans = max({t[ls].ans, t[rs].ans, t[ls].lm + t[rs].mx, t[rs].rm + t[ls].mx});
        t[rt].mx = max(t[ls].mx, t[rs].mx);
        t[rt].mn = min(t[ls].mn, t[rs].mn);
        t[rt].lm = max({t[ls].lm, t[rs].lm, t[ls].mx - 2 * t[rs].mn});
        t[rt].rm = max({t[ls].rm, t[rs].rm, t[rs].mx - 2 * t[ls].mn});
    }
    void fun(int rt, int l, int r, int k) {
        t[rt].mx += k, t[rt].mn += k;
        t[rt].lm -= k, t[rt].rm -= k;
        t[rt].laz += k;
    }
    void push_down(int rt, int l, int r) {
        if (t[rt].laz) {
            fun(ls, l, mid, t[rt].laz);
            fun(rs, mid + 1, r, t[rt].laz);
            t[rt].laz = 0;
        }
    }
    void build(int rt, int l, int r) {
        if (l == r) {
            t[rt].ans = 0;
            t[rt].mx = t[rt].mn = dep[dfn[l]];
            t[rt].rm = t[rt].lm = -dep[dfn[l]];
            return;
        }
        build(ls, l, mid), build(rs, mid + 1, r);
        push_up(rt);
    }
    void modify(int rt, int l, int r, int p, int q, int k) {
        if (p > r || q < l) return;
        if (p <= l && r <= q) {
            fun(rt, l, r, k);
            return;
        }
        push_down(rt, l, r);
        modify(ls, l, mid, p, q, k), modify(rs, mid + 1, r, p, q, k);
        push_up(rt);
    }
};
void solve() {
    int n, q, w; cin >> n >> q >> w;
    for (int i = 0; i < n - 1; i++) {
        int u, v, w; cin >> u >> v >> w;
        G[u].emplace_back(v, w);
        G[v].emplace_back(u, w);
        edge[i] = tii(u, v, w);
    }
    dfs(1, 0);
    SegmentTree seg(ncnt);
    seg.build(1, 1, ncnt);
    int lst = 0;
    for (int i = 1; i <= q; i++) {
        int d, e; cin >> d >> e;
        d = (d + lst) % (n - 1);
        e = (e + lst) % w;
        auto& [u, v, prew] = edge[d];
        if (fa[v] == u) swap(u, v);
        int delta = e - prew;
        prew = e;
        seg.modify(1, 1, ncnt, L[u], R[u], delta);
        lst = seg.t[1].ans;
        cout << lst << "\n";
    }
}
signed main() {
    // freopen("1.in", "r", stdin);
    // freopen("1.out", "w", stdout);
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    int t = 1;
    // cin >> t;
    while (t--) solve();
    return 0;
}

回到L,轉化後跟這個典就沒啥大區別了,比較巧妙的一個是刪邊等價於讓邊權等0,然後要注意的是,詢問裡臨時改變邊權,如果是已經刪掉的邊,那麼不能改邊權,否則可能讓直徑偏大。

L程式碼

點選檢視程式碼
#include <bits/stdc++.h>
#define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define tii tuple<int, int, int>
#define db double
#define all(a) a.begin(), a.end()
using namespace std;
const int maxn = 1e6 + 10;
const int mod = 998244353;
struct EDGE {
    int u, v, w, k;
} edge[maxn];
vector<pii> G[maxn];
int ncnt, n, q, L[maxn], R[maxn], dfn[maxn], siz[maxn], dep[maxn], fa[maxn];
void dfs(int u, int f) {
    siz[u] = 1, fa[u] = f;
    dfn[++ncnt] = u;
    L[u] = R[u] = ncnt;
    for (auto[v, id] : G[u]) {
        if (v == f) continue;
        dep[v] = dep[u] + edge[id].w;
        dfs(v, u);
        dfn[++ncnt] = u;
        R[u] = ncnt;
        siz[u] += siz[v];
        edge[id].k = min(siz[v], n - siz[v]);
    }
}
struct SegmentTree {
#define ls rt << 1
#define rs rt << 1 | 1
#define mid ((l + r) >> 1)
    struct Node {
        int ans, mx, mn, lm, rm, laz;
    };
    vector<Node> t;
    SegmentTree (int n) {t.resize(n << 2);}
    void push_up(int rt) {
        t[rt].ans = max({t[ls].ans, t[rs].ans, t[ls].lm + t[rs].mx, t[rs].rm + t[ls].mx});
        t[rt].mx = max(t[ls].mx, t[rs].mx);
        t[rt].mn = min(t[ls].mn, t[rs].mn);
        t[rt].lm = max({t[ls].lm, t[rs].lm, t[ls].mx - 2 * t[rs].mn});
        t[rt].rm = max({t[ls].rm, t[rs].rm, t[rs].mx - 2 * t[ls].mn});
    }
    void fun(int rt, int l, int r, int k) {
        t[rt].mx += k, t[rt].mn += k;
        t[rt].lm -= k, t[rt].rm -= k;
        t[rt].laz += k;
    }
    void push_down(int rt, int l, int r) {
        if (t[rt].laz) {
            fun(ls, l, mid, t[rt].laz);
            fun(rs, mid + 1, r, t[rt].laz);
            t[rt].laz = 0;
        }
    }
    void build(int rt, int l, int r) {
        if (l == r) {
            t[rt].ans = 0;
            t[rt].mx = t[rt].mn = dep[dfn[l]];
            t[rt].lm = t[rt].rm = -dep[dfn[l]];
            return;
        }
        build(ls, l, mid), build(rs, mid + 1, r);
        push_up(rt);
    }
    void modify(int rt, int l, int r, int p, int q, int k) {
        if (p > r || q < l) return;
        if (p <= l && r <= q) {
            fun(rt, l, r, k);
            return;
        }
        push_down(rt, l, r);
        modify(ls, l, mid, p, q, k), modify(rs, mid + 1, r, p, q, k);
        push_up(rt);
    }
};
bool cmp(EDGE a, EDGE b) {return a.k < b.k;}
struct Query {
    int a, b, k, id;
} query[maxn];
int ans[maxn];
tii preedge[maxn];
void solve() {
    cin >> n >> q;
    for (int i = 1; i < n; i++) {
        int u, v, w; cin >> u >> v >> w;
        G[u].emplace_back(v, i);
        G[v].emplace_back(u, i);
        edge[i] = EDGE{u, v, w, 0};
        preedge[i] = tii(u, v, w);
    }
    dfs(1, 0);
    SegmentTree seg(ncnt);
    seg.build(1, 1, ncnt);
    sort(edge + 1, edge + n, cmp);
    for (int i = 1; i <= q; i++) {
        int a, b, k; cin >> a >> b >> k;
        query[i] = Query{a, b, k, i};
    }
    sort(query + 1, query + 1 + q, [](Query a, Query b) {return a.k < b.k;});
    int lst = 1;
    for (int i = 1; i <= q; i++) {
        auto[a, b, k, id] = query[i];
        while (lst < n && edge[lst].k < k) {
            // 刪除這條邊
            auto [u, v, w, k2] = edge[lst];
            if (fa[v] == u) swap(u, v);
            seg.modify(1, 1, ncnt, L[u], R[u], -w);
            lst++;
        }
        auto[u, v, prew] = preedge[a];
        if (fa[v] == u) swap(u, v);
        if (min(siz[u], n - siz[u]) >= k) {
            seg.modify(1, 1, ncnt, L[u], R[u], -prew);
            seg.modify(1, 1, ncnt, L[u], R[u], b);
        }
        ans[id] = seg.t[1].ans;
        if (min(siz[u], n - siz[u]) >= k) {
            seg.modify(1, 1, ncnt, L[u], R[u], prew);
            seg.modify(1, 1, ncnt, L[u], R[u], -b);
        }
    }
    for (int i = 1; i <= q; i++)
        cout << ans[i] << "\n";
}
signed main() {
    // freopen("1.in", "r", stdin);
    // freopen("1.out", "w", stdout);
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    int t = 1;
    // cin >> t;
    while (t--) solve();
    return 0;
}

相關文章