AtCoder Beginner Contest 375

~Lanly~發表於2024-10-12
省流版
  • A. 列舉所有子串判斷即可
  • B. 模擬計算記錄累加即可
  • C. 根據旋轉的週期性對每個點旋轉次數取模後暴力旋轉即可
  • D. 列舉\(k\),考慮 \(i,j\)的對數,寫成數學表示式後維護個數和位置和即可
  • E. 揹包問題,以前兩個陣列和為狀態,考慮每個數移動到何處轉移即可
  • F. 逆向,刪邊變加邊,維護加邊後的距離變化即可
  • G. 最短路徑圖的割邊判斷,根據最短路條數判斷是否必經即可

A - Seats (abc375 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 n;
    string s;
    cin >> n >> s;
    int ans = 0;
    for (int i = 0; i < n; ++i) {
        ans += (s.substr(i, 3) == "#.#");
    }
    cout << ans << '\n';

    return 0;
}



B - Traveling Takahashi Problem (abc375 B)

題目大意

二維平面,給定\(n\)個點,從\((0,0)\)出發,依次經歷這些點,最後回到 \((0,0)\),求其歐幾里德距離和。

解題思路

依次模擬計算距離和,累加即可。

神奇的程式碼
#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>> p(n + 2);
    p[0] = {0, 0};
    for (int i = 1; i < n + 1; i++) {
        cin >> p[i][0] >> p[i][1];
    }
    p[n + 1] = {0, 0};
    double ans = 0;
    auto dist = [](array<int, 2> a, array<int, 2> b) {
        return sqrt(1ll * (a[0] - b[0]) * (a[0] - b[0]) +
                    1ll * (a[1] - b[1]) * (a[1] - b[1]));
    };
    for (int i = 1; i < p.size(); ++i) {
        ans += dist(p[i], p[i - 1]);
    }
    cout << fixed << setprecision(10) << ans << '\n';

    return 0;
}



C - Spiral Rotation (abc375 C)

題目大意

給定一個\(n \times n\)的正方形,對於 \(i \in [1, n]\) ,依次進行如下操作:

  • \((i,i)\)為一角的 \((n - i + 1) \times (n - i - 1)\) 的正方形順時針旋轉\(90\)度。

輸出最終的矩形。

解題思路

注意到旋轉的區間是往中間收縮的,因為旋轉操作具有結合性和週期性(旋轉\(4\)次等於沒旋轉),所以依次遍歷該正方形的每個元素\((i,j)\),可以求出它進行了多少次 順時針旋轉 \(90\)度,然後對 \(4\)取餘後,再進行對應次數的順時針旋轉即可。

其實暴力的複雜度是\((2n^2 \log n)\),雖然\(n \leq 3000\),但貌似還是不能過。

神奇的程式碼
#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<string> s(n);
    for (auto& i : s)
        cin >> i;
    auto tr = [](int x, int y, int n) { return make_pair(y, n - 1 - x); };
    auto tr_cnt = [&](int x, int y, int n, int c) {
        for (int i = 0; i < c; ++i) {
            tie(x, y) = tr(x, y, n);
        }
        return make_pair(x, y);
    };
    vector<string> t = s;
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            int cnt = min({i, j, n - 1 - i, n - 1 - j}) + 1;
            auto [x, y] = tr_cnt(i, j, n, cnt % 4);
            t[x][y] = s[i][j];
        }
    }
    for (auto i : t)
        cout << i << '\n';

    return 0;
}



D - ABA (abc375 D)

題目大意

給定一個字串\(s\),問有多少\(i < j < k\),滿足 \(s_is_js_k\)是迴文串。

解題思路

列舉\(k\),考慮滿足條件的 \(i\),那必然有\(s_i == s_k\),然後固定這個\(i\),考慮 \(j\)的方案數,即為 \(k - i - 1\)

也就是說,對於當期 \(k\),其對答案的貢獻為 \(\sum_{i < k \& s_i==s_k} k - i - 1 = (k-1) cnt_{a_k} - \sum_{i < k \& s_i == s_k} i\)

因此我們只需動態維護兩個陣列:

  • \(cnt_i\)表示字元為\(i\)的位置數
  • \(sum_i\)表示字元為\(i\)的位置的和

就可以\(O(1)\)求得固定\(k\)時, \((i,j,k)\)的方案數。

對所有的\(k\)累加即為答案。時間複雜度為 \(O(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);
    string s;
    cin >> s;
    array<int, 26> cnt{};
    array<LL, 26> sum{};
    LL ans = 0;
    for (int i = 0; i < s.size(); ++i) {
        int c = s[i] - 'A';
        ans += 1ll * cnt[c] * (i - 1) - sum[c];
        cnt[c]++;
        sum[c] += i;
    }
    cout << ans << '\n';

    return 0;
}



E - 3 Team Division (abc375 E)

題目大意

三個陣列,操作為,花費\(1\)的代價,將一個數從一個陣列移動到另一個陣列。

問最小的代價,使得三個陣列的和相等,或告知不可行。

陣列和\(\sum b_i \leq 1500\)

解題思路

考慮兩個陣列的簡化版。

考慮樸素搜尋,即依次考慮每個數,應該移動還是不移動。其複雜度顯然是\(O(2^n)\)

因為狀態是包含了每個數的陣列位置,它過於冗餘了,考慮到我們要求的答案,我們可以僅保留一個簡化的狀態即可:

  • 一個陣列的和

知道了一個陣列的和,對於當前數,移動還是不移動,其帶來的影響都可以進行轉移,最後也能直接得到答案。

因為總數不變,所以知道了一個陣列的和,另一個陣列的和其實也知道了,即設 \(dp[i][j]\)表示考慮了前 \(i\)個數,第一個陣列和為 \(j\)的最小移動代價。對於一個數,考慮移動或不移動,對 \(j\)的影響轉移即可。這樣的複雜度即為 \(O(n \sum b_i)\)

而這裡有三個陣列,按照同樣的思路,知道了前兩個陣列的和,第三個陣列的和也確定了,即設\(dp[i][j][k]\)表示考慮了前 \(i\)個數,第一個陣列和為 \(j\),第二個陣列和為\(k\)的最小移動代價,轉移仍然是對於一個數,考慮移動到哪個陣列,或者不移動即可。這樣的時間複雜度為\(O(n (\sum b_i) ^ 2)\)

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

const int inf = 1e9 + 7;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    array<vector<int>, 3> p{};
    int sum = 0;
    for (int i = 0; i < n; i++) {
        int a, b;
        cin >> a >> b;
        --a;
        p[a].push_back(b);
        sum += b;
    }
    if (sum % 3 != 0)
        cout << -1 << '\n';
    else {
        vector<vector<int>> dp(sum + 1, vector<int>(sum + 1, inf));
        dp[accumulate(p[0].begin(), p[0].end(), 0)]
          [accumulate(p[1].begin(), p[1].end(), 0)] = 0;
        for (auto& i : p[0]) {
            auto dp2 = dp;
            for (int j = 0; j <= sum; j++) {
                for (int k = 0; k <= sum; k++) {
                    if (j >= i)
                        dp2[j - i][k] = min(dp2[j - i][k], dp[j][k] + 1);
                    if (j >= i && k + i <= sum)
                        dp2[j - i][k + i] =
                            min(dp2[j - i][k + i], dp[j][k] + 1);
                }
            }
            dp.swap(dp2);
        }
        for (auto& i : p[1]) {
            auto dp2 = dp;
            for (int j = 0; j <= sum; j++) {
                for (int k = 0; k <= sum; k++) {
                    if (k >= i)
                        dp2[j][k - i] = min(dp2[j][k - i], dp[j][k] + 1);
                    if (k >= i && j + i <= sum)
                        dp2[j + i][k - i] =
                            min(dp2[j + i][k - i], dp[j][k] + 1);
                }
            }
            dp.swap(dp2);
        }
        for (auto& i : p[2]) {
            auto dp2 = dp;
            for (int j = 0; j <= sum; j++) {
                for (int k = 0; k <= sum; k++) {
                    if (k + i <= sum)
                        dp2[j][k + i] = min(dp2[j][k + i], dp[j][k] + 1);
                    if (j + i <= sum)
                        dp2[j + i][k] = min(dp2[j + i][k], dp[j][k] + 1);
                }
            }
            dp.swap(dp2);
        }
        int target = sum / 3;
        if (dp[target][target] == inf) {
            dp[target][target] = -1;
        }
        cout << dp[target][target] << '\n';
    }

    return 0;
}



F - Road Blocked (abc375 F)

題目大意

給定一張無向圖,維護以下兩種操作:

  • 1 x,刪去第\(x\)條邊
  • 2 u v,問\(u \to v\)的最短路長度

其中操作 \(1\)最多 \(300\)次。

解題思路

加邊往往比刪邊好做,注意到詢問沒有強制線上,因此可以倒著考慮操作,這樣刪邊操作就變成加邊操作了。

因為點數\(\leq 300\),先用 \(floyd\)\(O(n^3)\)求出任意兩點的最短路,這樣操作 \(2\)就可以 \(O(1)\)回答,然後考慮操作 \(1\)加邊後,怎麼更新這個兩點的最短路。

考慮加的邊 \(u \to v\)對任意兩點 \(x \to y\) 的最短路的影響,其實就兩種情況:

  • \(x \to y\)的最短路不經過 \(u \to v\)
  • \(x \to y\)的最短路經過 \(u \to v\)

在加邊之前,\(dis[x][y]\)的值就屬於第一種情況,現在加邊後, \(dis[x][y]\)就是這兩種情況的最小值,因此\(dis[x][y]\)只要和第二種情況取個最小值,這樣\(dis[x][y]\)就變成 加上邊 \(u \to v\)後的值了。

而第二種情況的距離即為 \(dis[x][u] + u \to v + dis[v][y]\)\(dis[x][v] + v \to u + dis[u][y]\)

加一邊,更新一次的複雜度為 \(O(n^2)\),因為操作 \(1\)不超過 \(300\),因此總的時間複雜度為 \(O(n^3)\)

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

const LL inf = 1e18;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, m, q;
    cin >> n >> m >> q;
    vector<vector<LL>> dis(n, vector<LL>(n, inf));
    vector<array<int, 3>> edges(m);
    for (int i = 0; i < m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        --u, --v;
        dis[u][v] = dis[v][u] = min(dis[u][v], (LL)w);
        edges[i] = {u, v, w};
    }
    for (int i = 0; i < n; ++i)
        dis[i][i] = 0;
    vector<array<int, 3>> query(q);
    for (auto& [op, x, y] : query) {
        cin >> op >> x;
        if (op == 1) {
            --x;
            auto [u, v, w] = edges[x];
            dis[u][v] = dis[v][u] = inf;
        } else {
            cin >> y;
            --x, --y;
        }
    }
    for (int k = 0; k < n; ++k)
        for (int i = 0; i < n; ++i)
            for (int j = 0; j < n; ++j)
                dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
    vector<LL> ans;
    for (int i = q - 1; i >= 0; --i) {
        auto [op, x, y] = query[i];
        if (op == 1) {
            auto [u, v, w] = edges[x];
            dis[u][v] = dis[v][u] = min(dis[u][v], (LL)w);
            for (int i = 0; i < n; ++i)
                for (int j = 0; j < n; ++j) {
                    dis[i][j] =
                        min(dis[i][j], dis[i][u] + dis[u][v] + dis[v][j]);
                    dis[i][j] =
                        min(dis[i][j], dis[i][v] + dis[v][u] + dis[u][j]);
                }
        } else {
            ans.push_back((dis[x][y] == inf ? -1 : dis[x][y]));
        }
    }
    reverse(ans.begin(), ans.end());
    for (auto x : ans)
        cout << x << '\n';

    return 0;
}



G - Road Blocked 2 (abc375 G)

題目大意

給定一張無向圖,對於每一條邊,回答以下詢問:

  • 刪去該邊後,\(1 \to n\)的最短路長度是否發生變化。

解題思路

題意就是問一條邊是不是最短路的必經之邊,換句話說就是一條邊是否被所有最短路徑經過。

由於\(1 \to n\)可能有多條最短路,我們先求出每條邊,是否被一條最短路經過。

如果一條邊 \(u \to v\)滿足: \(1 \to n = 1 \to u \to v \to n\)或者 \(1 \to n = 1 \to v \to u \to n\),這說明該邊被一條最短路徑經過。

但怎麼說該邊被所有的最短路徑經過呢?

如果\(1 \to n\)的最短路徑有 \(z\)條,而 \(1 \to u\)的最短路徑有 \(x\)條, \(v \to n\)的最短路徑有 \(y\)條,由乘法原理,\(1 \to n\)且經過 \(u \to v\)的路徑就有 \(xy\)條,而如果有\(z = xy\),那這就說明該邊是必經邊。

因此分別從點\(1\)和點 \(n\)進行 \(dijkstra\),求出 \(dis_1[i],dis_n[i]\)\(cnt_1[i],cnt_n[i]\) 表示\(1 \to i\)的最短路徑長度和數量, \(n \to i\)的最短路徑長度和數量,對於任意一條邊\(u \to v, w\),如果滿足二者任意其一:

  • \(dis_1[u] + w + dis_n[v] == dis_1[n]\)\(cnt_1[u] \times cnt_n[v] == cnt_1[n]\)
  • \(dis_1[v] + w + dis_n[u] == dis_1[n]\)\(cnt_1[v] \times cnt_n[u] == cnt_1[n]\)

則該邊是最短路必經邊。

其實根據一條邊是否被最短路經過,建立一張最短路圖,這些必經邊就是所謂的割邊,即刪去該邊後, \(1 \to n\)不連通。

注意 \(cnt\)可能會非常大,甚至會超過 \(int128\), 而此處我們只需判斷乘積是否相等,可以取模。

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

const LL inf = 1e18;
const int mo = 1e9 + 7;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, m;
    cin >> n >> m;
    vector<vector<array<int, 2>>> edge(n);
    vector<array<int, 3>> e(m);
    for (int i = 0; i < m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        --u, --v;
        edge[u].push_back({v, w});
        edge[v].push_back({u, w});
        e[i] = {u, v, w};
    }
    auto dijkstra = [&](int s) {
        vector<LL> dis(n, inf);
        vector<LL> cnt(n, 0);
        dis[s] = 0;
        cnt[s] = 1;
        priority_queue<array<LL, 2>, vector<array<LL, 2>>,
                       greater<array<LL, 2>>>
            team;
        team.push({0, s});
        while (!team.empty()) {
            auto [d, u] = team.top();
            team.pop();
            if (dis[u] < d)
                continue;
            for (auto [v, w] : edge[u]) {
                if (dis[v] > dis[u] + w) {
                    dis[v] = dis[u] + w;
                    team.push({dis[v], v});
                    cnt[v] = cnt[u];
                } else if (dis[v] == dis[u] + w) {
                    cnt[v] += cnt[u];
                    if (cnt[v] >= mo)
                        cnt[v] -= mo;
                }
            }
        }
        return make_pair(dis, cnt);
    };
    auto [dis1, cnt1] = dijkstra(0);
    auto [dis2, cnt2] = dijkstra(n - 1);
    vector<int> ans(m, 0);
    for (int i = 0; i < m; ++i) {
        auto& [u, v, w] = e[i];
        if (dis1[u] + w + dis2[v] == dis1[n - 1] &&
                cnt1[u] * cnt2[v] % mo == cnt1[n - 1] ||
            dis1[v] + w + dis2[u] == dis1[n - 1] &&
                cnt1[v] * cnt2[u] % mo == cnt1[n - 1]) {
            cout << "Yes" << '\n';
        } else {
            cout << "No" << '\n';
        }
    }

    return 0;
}