AtCoder Beginner Contest 355

~Lanly~發表於2024-05-26

A - Who Ate the Cake? (abc355 A)

題目大意

三人有偷吃蛋糕的嫌疑,現告訴兩個目擊證人的澄清物件。問誰偷吃蛋糕。

解題思路

兩個物件相同就不知道是誰,否則就是剩下的那一個了。

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int a, b;
    cin >> a >> b;
    if (a == b)
        cout << "-1" << '\n';
    else
        cout << 1 + 2 + 3 - a - b << '\n';

    return 0;
}



B - Piano 2 (abc355 B)

題目大意

給定兩個陣列\(a,b\),定義陣列 \(c\),為陣列 \(a,b\)拼接後排序。

\(c\)中相鄰兩個數,均在陣列 \(a\)中出現的數量。

解題思路

預處理出\(exist[i]\)表示數字 \(i\)是否在陣列 \(a\)出現過,然後列舉 \(c\)的相鄰數判斷即可。

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, m;
    cin >> n >> m;
    vector<int> a(n), b(m);
    for (auto& i : a)
        cin >> i;
    for (auto& i : b)
        cin >> i;
    vector<int> c = a;
    c.insert(c.end(), b.begin(), b.end());
    sort(c.begin(), c.end());
    vector<int> ina(*max_element(a.begin(), a.end()) + 1, 0);
    for (auto i : a)
        ina[i] = 1;
    bool ok = false;
    for (int i = 0; i < n + m - 1; ++i) {
        ok |= ina[c[i]] && ina[c[i + 1]];
    }
    if (ok)
        cout << "Yes" << endl;
    else
        cout << "No" << endl;

    return 0;
}



C - Bingo 2 (abc355 C)

題目大意

二維網格,有\(t\)次操作,每次操作塗黑一個格子。

問多少操作後,有一行或一列或對角線的格子全部被塗黑。

解題思路

分別記錄每行、每列、對角線中被塗黑的格子數量\(row[i], col[i], diag1, diag2\),每次操作後檢查該格子對應的行列對角線的黑格子數是否是 \(n\)即可。

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, t;
    cin >> n >> t;
    vector<int> row(n), col(n);
    int diag1 = 0, diag2 = 0;
    int ans = -1;
    auto tr = [&](int x) -> pair<int, int> { return {x / n, x % n}; };
    for (int i = 0; i < t; i++) {
        int x;
        cin >> x;
        --x;
        auto [r, c] = tr(x);
        row[r]++;
        col[c]++;
        if (r + c == n - 1)
            diag1++;
        if (r == c)
            diag2++;
        if (row[r] == n || col[c] == n || diag1 == n || diag2 == n) {
            ans = i + 1;
            break;
        }
    }
    cout << ans << '\n';

    return 0;
}



D - Intersecting Intervals (abc355 D)

題目大意

給定\(n\)個區間\([l_i, r_i]\),問倆倆區間有重疊的數量。交點重疊也算重疊。

解題思路

將這些區間按照左端點排序,然後依次列舉區間,考慮如何計算重疊數量\(cnt_j\)

假設當前列舉的第\(j\)個區間,我要求 \(i < j\)的數量,滿足第 \(i\)個區間與第 \(j\)個區間有重疊。

考慮滿足什麼條件算重疊,由於\(l_i \leq l_j\),所以只要\(r_i \geq l_j\),那麼第 \(i\)個區間就與第 \(j\)個區間重疊。

因此\(cnt_j\)就是滿足 \(i < j, r_i \geq l_j\)的數量。

這是一個二維偏序問題,但與之前不同的是條件\(l_j\)是單調遞增的。因此我們可以維護一個小根堆的優先佇列,將 \(i < j\)\(r_i\)丟進去, 一旦堆頂的\(r_i < l_j\),說明不重疊,之後也不會重疊,就丟棄。 \(cnt_j\)就是優先佇列裡的元素個數。

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vector<array<int, 2>> a(n);
    for (auto& x : a) {
        cin >> x[0] >> x[1];
    }
    sort(a.begin(), a.end());
    priority_queue<int, vector<int>, greater<int>> team;
    LL ans = 0;
    for (int i = 0; i < n; i++) {
        while (!team.empty() && team.top() < a[i][0]) {
            team.pop();
        }
        ans += team.size();
        team.push(a[i][1]);
    }
    cout << ans << '\n';

    return 0;
}



E - Guess the Sum (abc355 E)

題目大意

互動題。

有一個長度為\(2^n\)的隱藏陣列\(a\),你需要用最少的詢問,問出 \(\sum_{i=l}^{r} a_i\)的和。對 \(100\)取模。

每次詢問給定\(i,j\),回答 \(l=2^ij, r = 2^i(j+1),\sum_{i=l}^{r-1} a_i\)的和,對 \(100\)取模。

解題思路

一開始參考abc349d,結果\(wa\)了,應該是詢問的次數不是最少。

可以考慮一反例,詢問 \([1,7]\),如果按照上面的思路,則是詢問 \([1,1] + [2, 3] + [4,7]\) 共三個詢問,即\((0, 1)(1,1)(2,1)\)

而實際上可以做到兩個:詢問\([0,7] - [0, 0]\) 。即\((3,0)(0,0)\)

因此此處除了區間加,還有可能區間減。換句話說就像是先退一步(\(1 \to 0\)),才能走的更遠 (\(0 \to 7\))。

由此我們可以假想在一張有\(2^n\)個點的無向圖上,我們從點\(l\)出發,花最小的步數到達點 \(r+1\)。點與點之間透過詢問連邊。比如我們可以詢問 \((3,0)\),即區間 \([0,8)\),因此點\(0\)和點 \(8\)就連一條無向邊。而詢問數只有 \(O(n2^n)\),也即邊數,點數有 \(O(2^n)\),邊權均為 \(1\)。因此在該圖上直接從 點\(l\)進行一次 \(BFS\),記錄轉移點,然後詢問即可。

按上面的為例,我們從點 \(1\)出發,最終到達點\(8\),一個最短的方案是 \(1 \to 0 \to 8\),第一條就是詢問 \([0,1)\),要減去該值,第二條就是詢問 \([0,8)\),要加上該值。

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, l, r;
    cin >> n >> l >> r;
    int up = (1 << n);
    vector<vector<int>> edge(up + 1);
    for (int i = 0; i <= n; ++i) {
        for (int l = 0; l < up; l += (1 << i)) {
            int r = l + (1 << i);
            edge[l].push_back(r);
            edge[r].push_back(l);
        }
    }
    queue<int> team;
    vector<int> pre(up + 1, -1);
    pre[l] = 0;
    team.push(l);
    while (!team.empty()) {
        int cur = team.front();
        team.pop();
        if (cur == r + 1) {
            break;
        }
        for (int next : edge[cur]) {
            if (pre[next] == -1) {
                pre[next] = cur;
                team.push(next);
            }
        }
    }
    vector<array<int, 2>> query;
    for (int i = r + 1; i != l; i = pre[i]) {
        query.push_back({pre[i], i});
    }
    reverse(query.begin(), query.end());

    int ans = 0;
    for (auto& [a, b] : query) {
        int sign = 1;
        if (a > b) {
            sign = -1;
            swap(a, b);
        }
        int i = __builtin_ctz(b - a);
        int j = a >> i;
        cout << "? " << i << " " << j << endl;
        int sum;
        cin >> sum;
        ans += sum * sign;
    }
    ans = (ans % 100 + 100) % 100;

    cout << "! " << ans << endl;

    return 0;
}



F - MST Query (abc355 F)

題目大意

給定一棵樹,邊有邊權,不斷加邊,求每次加邊後的最小生成樹的邊權和。

邊權$ \leq 10$。

解題思路

當給樹加了一條邊,此時就出現了個環,而只要去除環上邊權最大的邊,就又變回一棵樹,同時也是一個最小生成樹。

因此我們要動態維護一棵樹,維護倆點之間的邊權最大值,這是Link-Cut Tree所擅長的——動態連邊、斷邊、快速求出一條路徑的權值最大值。

LCT
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

const int maxn = 1e6;
const int inf = 1e9 + 7;

struct Splay {
    int ch[maxn][2], fa[maxn], tag[maxn], val[maxn], maxx[maxn];

    void clear(int x) {
        ch[x][0] = ch[x][1] = fa[x] = tag[x] = val[x] = maxx[x] = 0;
    }

    int getch(int x) { return ch[fa[x]][1] == x; }

    int isroot(int x) { return ch[fa[x]][0] != x && ch[fa[x]][1] != x; }

    void maintain(int x) {
        if (!x)
            return;
        maxx[x] = x;
        if (ch[x][0]) {
            if (val[maxx[ch[x][0]]] > val[maxx[x]])
                maxx[x] = maxx[ch[x][0]];
        }
        if (ch[x][1]) {
            if (val[maxx[ch[x][1]]] > val[maxx[x]])
                maxx[x] = maxx[ch[x][1]];
        }
    }

    void pushdown(int x) {
        if (tag[x]) {
            if (ch[x][0])
                tag[ch[x][0]] ^= 1, swap(ch[ch[x][0]][0], ch[ch[x][0]][1]);
            if (ch[x][1])
                tag[ch[x][1]] ^= 1, swap(ch[ch[x][1]][0], ch[ch[x][1]][1]);
            tag[x] = 0;
        }
    }

    void update(int x) {
        if (!isroot(x))
            update(fa[x]);
        pushdown(x);
    }

    void print(int x) {
        if (!x)
            return;
        pushdown(x);
        print(ch[x][0]);
        printf("%d ", x);
        print(ch[x][1]);
    }

    void rotate(int x) {
        int y = fa[x], z = fa[y], chx = getch(x), chy = getch(y);
        fa[x] = z;
        if (!isroot(y))
            ch[z][chy] = x;
        ch[y][chx] = ch[x][chx ^ 1];
        fa[ch[x][chx ^ 1]] = y;
        ch[x][chx ^ 1] = y;
        fa[y] = x;
        maintain(y);
        maintain(x);
        if (z)
            maintain(z);
    }

    void splay(int x) {
        update(x);
        for (int f = fa[x]; f = fa[x], !isroot(x); rotate(x))
            if (!isroot(f))
                rotate(getch(x) == getch(f) ? f : x);
    }

    void access(int x) {
        for (int f = 0; x; f = x, x = fa[x])
            splay(x), ch[x][1] = f, maintain(x);
    }

    void makeroot(int x) {
        access(x);
        splay(x);
        tag[x] ^= 1;
        swap(ch[x][0], ch[x][1]);
    }

    int find(int x) {
        access(x);
        splay(x);
        while (ch[x][0])
            x = ch[x][0];
        splay(x);
        return x;
    }

    void link(int x, int y) {
        makeroot(x);
        fa[x] = y;
    }

    void cut(int x, int y) {
        makeroot(x);
        access(y);
        splay(y);
        ch[y][0] = fa[x] = 0;
        maintain(y);
    }
} st;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, q;
    cin >> n >> q;
    int ans = 0;
    for (int i = 1; i <= n; ++i)
        st.val[i] = -inf, st.maintain(i);
    int id = n + 1;
    vector<array<int, 2>> edge;
    for (int i = 0; i < n - 1; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        st.val[id] = w;
        st.maintain(id);
        st.link(u, id);
        st.link(id, v);
        edge.push_back({u, v});
        ++id;
        ans += w;
    }
    while (q--) {
        int u, v, w;
        cin >> u >> v >> w;
        edge.push_back({u, v});
        st.val[id] = w;
        st.maintain(id);
        st.makeroot(u);
        st.access(v);
        st.splay(v);
        int x = st.maxx[v];
        int maxx = st.val[x];
        auto [l, r] = edge[x - n - 1];
        if (w < maxx) {
            st.cut(l, x);
            st.cut(x, r);
            st.link(u, id);
            st.link(id, v);
            ans -= maxx;
            ans += w;
        }
        ++id;
        cout << ans << '\n';
    }

    return 0;
}


不過這題有個特別的地方就是邊權\(\leq 10\),會有不需要 \(lct\)的做法。

由於邊權個數只有\(10\),我們可以考慮每個邊權選擇的數量之類的,這樣可以算出生成樹邊權,但刪邊的話無從下手,因為不知道加邊的兩個點之間邊權。

或許可以維護邊權為 \(i\)的邊所形成的連通性, 這樣加一條邊權為\(i\)時,看有沒有邊權 \(> i\)的使得那兩點連通,從而決定刪除哪條邊。但會發現不好維護,邊權為 \(i\)的邊所形成的連通性會依賴於邊權\(< i\)的選擇結果。

為了不依賴其他情況,我們可以維護邊權 \(\leq i\)的邊所形成的連通性情況,這用一個並查集\(d_i\)維護,這樣我們就建立 \(10\)個這樣的並查集,並查集之間的依賴關係就沒有了。

每加一條邊就只用更新這\(O(10)\)個並查集的連通性,而如何計算最小生成樹的邊權呢?關鍵是求出每個邊權的使用個數。

由於每加一條邊,連通塊的個數就會少一。要求出邊權為\(i\)使用的個數,那就看 \(d_i\)維護的連通塊數量與\(d_{i-1}\)的連通塊數量,兩者的差值就是邊權為\(i\)的使用個數。

這樣求答案的時間也是 \(O(10)\)

神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

class dsu {
  public:
    vector<int> p;
    int n;
    int block;

    dsu(int _n) : n(_n), block(_n) {
        p.resize(n);
        iota(p.begin(), p.end(), 0);
    }

    inline int get(int x) { return (x == p[x] ? x : (p[x] = get(p[x]))); }

    inline bool unite(int x, int y) {
        x = get(x);
        y = get(y);
        if (x != y) {
            p[x] = y;
            --block;
            return true;
        }
        return false;
    }
};

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, q;
    cin >> n >> q;
    vector<dsu> d(10, dsu(n));
    vector<array<int, 3>> edge(n - 1);
    for (auto& [u, v, w] : edge) {
        cin >> u >> v >> w;
        --u;
        --v;
    }
    sort(edge.begin(), edge.end(),
         [](auto& a, auto& b) { return a[2] < b[2]; });
    for (int i = 0; i < 10; ++i) {
        int up = i + 1;
        auto& di = d[i];
        for (auto& [u, v, w] : edge) {
            if (w > up)
                break;
            di.unite(u, v);
        }
    }
    while (q--) {
        int u, v, w;
        cin >> u >> v >> w;
        --u, --v, --w;
        for (int i = w; i < 10; ++i) {
            d[i].unite(u, v);
        }
        int ans = 0, la = 0;
        for (int i = 0; i < 10; ++i) {
            int use = n - d[i].block;
            ans += (use - la) * (i + 1);
            la = use;
        }
        cout << ans << '\n';
    }

    return 0;
}



G - Baseball (abc355 G)

題目大意

給定一個關於\(n\)的排列\(p\),高橋從中取\(k\)個數\(x_i\)。青木則以一定機率取一個數\(y \in [1,n]\),取的\(y=i\)的機率是 \(\frac{p_i}{\sum p_j}\)

青木的分數即為\(\min |x_i - y|\)

求高橋的最優策略,使得青木的期望分數最小。輸出對應的最小分數的\(\sum p_j\)倍。

解題思路

<++>

神奇的程式碼