AtCoder Beginner Contest 350

~Lanly~發表於2024-04-20

A - Past ABCs (abc350 A)

題目大意

給定一個形如 ABCXXX的字串。

XXX是否是\(001 \to 349\)之間,且不能是 \(316\)

解題思路

將後三位轉換成數字後判斷即可。

神奇的程式碼
a = int(input().strip()[3:])
if a >= 1 and a <= 349 and a != 316:
    print("Yes")
else:
    print("No")


B - Dentist Aoki (abc350 B)

題目大意

給定\(n\)\(01\)序列。

進行\(q\)次操作,每次操作反轉某一位上的 \(01\)

問最後 \(1\)的個數。

解題思路

反轉操作的複雜度是\(O(1)\),因此直接模擬反轉即可,最後求和得到答案。

神奇的程式碼
#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, q;
    cin >> n >> q;
    vector<int> a(n, 1);
    while (q--) {
        int x;
        cin >> x;
        --x;
        a[x] ^= 1;
    }
    int ans = accumulate(a.begin(), a.end(), 0);
    cout << ans << '\n';

    return 0;
}



C - Sort (abc350 C)

題目大意

給定一個\(1 \to n\)的排序,透過最多\(n-1\)次操作以下操作將其變得有序。

操作為,交換任意兩個數。

輸出任意可行的操作次數及其對應的操作步驟。

解題思路

\(i = 1 \to n\),依次考慮將 \(i\)交換到第 \(i\)位。經過 \(n-1\)次操作後則必定有序。

因此需要記錄 \(pos[i]\)表示數字 \(i\)所在的位置,每次交換第\(i, pos[i]\) 位,就將\(i\)交換到第 \(i\)位,重複執行\(n-1\)次即可。

神奇的程式碼
#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<int> pos(n);
    vector<int> a(n);
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        --x;
        pos[x] = i;
        a[i] = x;
    }
    vector<array<int, 2>> ans;
    for (int i = 0; i < n; i++) {
        if (a[i] == i)
            continue;
        ans.push_back({i, pos[i]});
        swap(a[i], a[pos[i]]);
        swap(pos[a[i]], pos[a[pos[i]]]);
    }
    cout << ans.size() << '\n';
    for (auto& p : ans) {
        cout << p[0] + 1 << ' ' << p[1] + 1 << '\n';
    }

    return 0;
}



D - New Friends (abc350 D)

題目大意

給定一張無向圖,若三點\(x,y,z\),存在:

  • \(x,y\)有連邊
  • \(y,z\)有連邊
  • \(x,z\)無連邊

則連邊\(x,z\)

問最多能連多少次邊。

解題思路

考慮最簡單的一條鏈的情況\(1 \to 2 \to 3 \to 4\),容易發現可以連的邊有

  • \(1 \to 2, 2 \to 3\) => \(1 \to 3\)
  • \(1 \to 3, 3 \to 4\) => \(1 \to 4\)
  • \(2 \to 3, 3 \to 4\) => \(2 \to 4\)

觀察\(1\)的新增邊的情況,會發現它可以和所有能到達的點連邊,即最終情況下,一個連通塊內的任意兩點都會連邊,即變成一張完全圖。

因此\(BFS\)得到每個連通塊的點數 \(cp\)和邊數 \(ce\),最終情況下該連通塊會有 \(\frac{cp * (cp - 1)}{2}\) 條邊,而已經有\(ce\)條(無向)邊,因此可以連 \(\frac{cp * (cp - 1)}{2} - ce\)條邊。

所有連通塊的連邊次數相加即為答案。

神奇的程式碼
#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<vector<int>> edge(n);
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        --u, --v;
        edge[u].push_back(v);
        edge[v].push_back(u);
    }
    LL ans = 0;
    vector<int> vis(n, 0);
    for (int i = 0; i < n; ++i) {
        if (vis[i])
            continue;

        queue<int> team;
        team.push(i);
        vis[i] = 1;
        int cp = 1, ce = 0;
        while (!team.empty()) {
            int u = team.front();
            team.pop();
            for (auto v : edge[u]) {
                ++ce;
                if (vis[v])
                    continue;
                vis[v] = 1;
                team.push(v);
                cp++;
            }
        }

        ans += 1ll * cp * (cp - 1) - ce;
    }
    ans /= 2;
    cout << ans << '\n';

    return 0;
}



E - Toward 0 (abc350 E)

題目大意

給定一個數字\(n\)\(x,y,a\)。透過兩類操作,使得\(n\)變為 \(0\)

  • 操作一,花費代價\(x\),使得 \(n = \lfloor \frac{n}{a} \rfloor\)
  • 操作二,花費代價\(y\),擲骰子,等機率擲出\(1 \to 6\)中的一個\(b\),使得 \(n = \lfloor \frac{n}{b} \rfloor\)

問最優情況下,最小期望花費。

解題思路

期望題,根據定義,當前的期望值是所有後繼情況的期望值的機率加權。

\(dp[i]\)表示當前數字為 \(x\),將其變為 \(0\)的最小期望花費。

邊界條件很明顯就是 \(dp[0] = 0\)

雖然是期望,但它問的是最優情況下的最小花費,那就是一個決策最優問題,考慮我的決策是什麼。

很顯然,決策就是操作一還是操作二,如果我決定執行操作一,會有一個期望值,執行操作二,會有另一個期望值,這兩個期望值取最小,就是我做出的最優決策。因此需要分別求出操作一和操作二的期望花費。

根據定義,當前的期望值是所有後繼情況的期望值的機率加權。

當我執行操作一後,後繼情況只有一個,那就是\(dp[ \lfloor \frac{n}{a} \rfloor ]\),達到這個情況的機率是 \(1\)。因此操作一的期望花費\(cost1 = dp[ \lfloor \frac{n}{a} \rfloor ] + x\)

當我執行操作二後,後繼情況有\(6\)個:

  • \(dp[ \lfloor \frac{n}{1} \rfloor ]\)
  • \(dp[ \lfloor \frac{n}{2} \rfloor ]\)
  • \(dp[ \lfloor \frac{n}{3} \rfloor ]\)
  • \(dp[ \lfloor \frac{n}{4} \rfloor ]\)
  • \(dp[ \lfloor \frac{n}{5} \rfloor ]\)
  • \(dp[ \lfloor \frac{n}{6} \rfloor ]\)

到達每一個後繼情況的機率都是\(\frac{1}{6}\)

根據期望定義,可以得到操作二的期望花費 \(dp[n] = \frac{\sum_{i=1}^{6} dp[ \lfloor \frac{n}{i} \rfloor ]}{6} + y\)

但注意到\(i=1\)那一項是 \(dp[n]\),與左式是一樣的,這會造成迴圈求值,這裡我們將右邊的 \(dp[n]\) 移到左邊,合併同類項,就可以得到真正的\(cost2 = \frac{\sum_{i=2}^{6} dp[ \lfloor \frac{n}{i} \rfloor ]}{5} + \frac{6}{5}y\)

得到兩個操作的期望花費\(cost1,cost2\)後,接下來就是做決策——取花費最小的,作為 \(dp[n]\)的值。這樣是轉移了。

雖然 \(n\)\(O(10^{18})\),但由於每次都至少\(/2\),最多除以 \(\log\)次就變成 \(0\),總的狀態數其實很少,只有\(O(\log_2 * \log_3 * \log_4 * \log_5 * \log_6)\),大概就\(4e7\)的數量級。

從上面的分析可以看出,期望\(dp\)\(dp\)之間的區別僅僅是計算轉移代價時需要用到期望定義來算,最終還是根據不同操作之間的代價取最優,還是個決策問題。

神奇的程式碼
#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);
    LL n;
    int a, x, y;
    cin >> n >> a >> x >> y;
    map<LL, double> dp;
    auto dfs = [&](auto dfs, LL u) -> double {
        if (u == 0)
            return 0.;
        if (dp.find(u) != dp.end())
            return dp[u];
        double cost1 = dfs(dfs, u / a) + x;
        double cost2 = 0;
        for (int i = 2; i <= 6; i++) {
            cost2 += dfs(dfs, u / i);
        }
        cost2 = cost2 / 5 + y * 6. / 5;
        return dp[u] = min(cost1, cost2);
    };
    dfs(dfs, n);
    cout << fixed << setprecision(10) << dp[n] << '\n';

    return 0;
}



F - Transpose (abc350 F)

題目大意

給定一個括號序列\(s\),長度為\(n\),其中也包括大小寫字母。

依次處理每個匹配的括號裡的字元,將其左右顛倒,並將大小寫字母變換。

問最終的字串。

解題思路

考慮樸素的做法,進行括號匹配,然後處理括號內的字串,容易發現最壞情況下複雜度是\(O(n^2)\),比如((((((((((asjigjiogjwifjwefckfj))))))))))

注意到同一個字元塊執行兩次上述變換後相當於沒變換,從上述的最壞情況下可以啟示我們,我們不需要實際進行變換,僅僅將這一塊字串看作整體,然後打個標記。就跟線段樹的懶標記差不多。

比如(((as)(sf))(ef)),我們先將每一塊字串看作整體,從 \(0\)開始標號,則變為(((0)(1))(2)),然後處理每對匹配的括號,比如變成了((34)(2)),然後處理 (34),這裡我們就不給34重複打標記,因為那樣的複雜度可能會變回原來的\(O(n^2)\),而是將 \(34\)看成一個整體 \(5\),在\(5\)上打標記,最後輸出時再傳遞給 \(34\)。變成 (5(2)),然後是(56),然後是7

就跟線段樹的思想一樣,我們會發現\(34 \to 5, 2 \to 6, 56 \to 7\),它們的關係形成了一棵樹的關係(但可能不是二叉樹),然後打標記都是在父親節點打標記,最後輸出時,從根節點開始遍歷,不斷下放標記,下放到葉子時,再根據標記決定是否倒序輸出即可。

神奇的程式碼
#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);
    string s;
    cin >> s;
    s = "(" + s + ")";
    vector<string> info;
    info.push_back("(");
    info.push_back(")");
    string t;
    vector<int> tr;

    for (auto& i : s) {
        if (i == '(' || i == ')') {
            if (!t.empty()) {
                tr.push_back(info.size());
                info.push_back(t);
            }
            t.clear();
            if (i == '(')
                tr.push_back(0);
            else
                tr.push_back(1);
        } else
            t += i;
    }

    vector<vector<int>> edge(info.size());
    stack<int> st;
    for (auto i : tr) {
        if (i == 0) { // (
            st.push(0);
        } else if (i == 1) { // )
            int fa = info.size();
            info.push_back("");
            edge.push_back(vector<int>());
            while (!st.empty() && st.top() != 0) {
                edge[fa].push_back(st.top());
                st.pop();
            }
            st.pop();
            st.push(fa);
        } else {
            st.push(i);
        }
    }
    auto inverse = [](string& s) {
        reverse(s.begin(), s.end());
        for (auto& i : s) {
            if (islower(i))
                i = toupper(i);
            else
                i = tolower(i);
        }
    };
    auto dfs = [&](auto dfs, int u, int d) -> void {
        if (edge[u].empty()) { // leaf
            if (d)
                inverse(info[u]);
            cout << info[u];
            return;
        }
        if (d)
            reverse(edge[u].begin(), edge[u].end());
        for (auto v : edge[u]) {
            dfs(dfs, v, d ^ 1);
        }
    };
    dfs(dfs, info.size() - 1, 1);

    return 0;
}



G - Mediator (abc350 G)

題目大意

給定\(n\)個點,執行下列 \(q\)次操作,分兩種,強制線上。

  • 1 u v,連無向邊\(u \to v\),連邊之前保證\(u,v\)不連通。
  • 2 u v,詢問是否有一個點\(x\),使得\(u \to x \to v\)

解題思路

<++>

神奇的程式碼