AtCoder Beginner Contest 345

~Lanly~發表於2024-03-21

A - Leftrightarrow (abc345 A)

題目大意

給定一個字串,問是不是形如<======...====>的字串。

解題思路

根據長度構造出期望的字串,再判斷是否相等即可。

神奇的程式碼
s = input()
print("Yes" if s == "<" + "=" * (len(s) - 2) + ">" else "No")


B - Integer Division Returns (abc345 B)

題目大意

給定\(a\),輸出 \(\lceil \frac{a}{10} \rceil\)

解題思路

上下取整的轉換,\(\lceil \frac{a}{10} \rceil = \lfloor \frac{a + 9}{10} \rfloor\)。用下取整即可。

Python//是下取證,C++/是向\(0\)取整,即正數時是下取整,負數時是上取整

神奇的程式碼
a = int(input())
print((a + 9) // 10)


C - One Time Swap (abc345 C)

題目大意

給定一個字串\(s\),長度為\(n\),問交換任意兩個字元,可以得到的不同字串個數。

解題思路

注意到交換的兩個字元\(s_i \neq s_j\)不相同的話,得到的一定是個新的字串,並且沒有其他交換方式得到這個字串。

\(s_i == s_j\)時,則字串不變。

因此, 可以 總情況數-字串不變數總情況數即為\(\frac{n(n-1)}{2}\)字串不變數則是\((i,j)\)滿足 \(s_i==s_j, i < j\)的個數,這是一個經典計數問題,維護\(s_i\)的出現次數即可\(O(n)\)求得。

最後特別注意一下原字串\(s\)是否會出現,即有兩個字母相同就會出現。

當然也可以正向統計。

神奇的程式碼
#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 = {0};
    LL n = 1ll * s.size() * (s.size() - 1) / 2;
    for (auto c : s) {
        n -= cnt[c - 'a'];
        cnt[c - 'a']++;
    }
    if (ranges::max(cnt) > 1)
        ++n;
    cout << n << '\n';

    return 0;
}



D - Tiling (abc345 D)

題目大意

給定一個網格,有\(k\)塊矩形板,問是否能選若干塊板,恰好鋪滿網格,板可旋轉。

解題思路

只有\(7\)塊板,網格大小隻有 \(10 \times 10\),範圍比較小,可以直接搜尋。

如何搜尋呢?起初考慮每塊板放在何處,這個複雜度比較大。

從左上考慮,沒被覆蓋的第一個格子一定是某塊板的左上角,因此從左到右,從上到下,找到沒被覆蓋的第一個格子,列舉其被哪塊板覆蓋即可。

複雜度感性理解下不會很大

神奇的程式碼
#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, h, w;
    cin >> n >> h >> w;
    vector<array<int, 2>> a(n);
    for (auto& i : a) {
        cin >> i[0] >> i[1];
    }
    vector<vector<int>> full(h, vector<int>(w, 0));
    auto set_full = [&](int i, int j, int k, int v) {
        for (int x = 0; x < a[k][0]; x++) {
            for (int y = 0; y < a[k][1]; y++) {
                full[i + x][j + y] = v;
            }
        }
    };
    auto ok = [&](int i, int j, int k) -> bool {
        if (i + a[k][0] > h || j + a[k][1] > w)
            return false;
        for (int x = 0; x < a[k][0]; x++) {
            for (int y = 0; y < a[k][1]; y++) {
                if (full[i + x][j + y])
                    return false;
            }
        }
        return true;
    };
    auto find_unfull = [&]() -> pair<int, int> {
        for (int i = 0; i < h; ++i) {
            for (int j = 0; j < w; ++j) {
                if (!full[i][j])
                    return {i, j};
            }
        }
        return {-1, -1};
    };
    vector<int> used(n, 0);
    auto dfs = [&](auto self, int x, int y) -> bool {
        if (x == -1 && y == -1)
            return true;
        for (int i = 0; i < n; ++i) {
            if (used[i])
                continue;
            if (ok(x, y, i)) {
                set_full(x, y, i, 1);
                auto [nx, ny] = find_unfull();
                used[i] = 1;
                if (self(self, nx, ny))
                    return true;
                used[i] = 0;
                set_full(x, y, i, 0);
            }
            swap(a[i][0], a[i][1]);
            if (ok(x, y, i)) {
                set_full(x, y, i, 1);
                auto [nx, ny] = find_unfull();
                used[i] = 1;
                if (self(self, nx, ny))
                    return true;
                used[i] = 0;
                set_full(x, y, i, 0);
            }
            swap(a[i][0], a[i][1]);
        }
        return false;
    };
    if (dfs(dfs, 0, 0))
        cout << "Yes" << endl;
    else
        cout << "No" << endl;

    return 0;
}



E - Colorful Subsequence (abc345 E)

題目大意

\(n\)個球排成一排,球有顏色\(c_i\),有價值\(v_i\)

現需恰好移除 \(l\)個球,使得倆倆球顏色不同,且剩下的球的價值和最大。問最大值。

解題思路

按順序考慮每個球,我們的決策就是是否移除這個球。考慮我們需要哪些狀態。

首先肯定是前\(i\)個球這個基本狀態。由於要恰好移除 \(l\)個球,因此當前已經移除的球數也必須知道。

除此之外,對於當前球移除或不移除,還取決於上一個球的顏色,因為不能有相鄰兩個相同的顏色的球。

據此可以有兩種 \(dp\)方式,一種樸素的是 \(dp[i][j][k]\)表示前 \(i\)個球,已經移除了 \(j\)個,且最後一個球的顏色是 \(k\)時的剩餘球最大價值和。這樣對於當前球移除與否都能轉移。初見這個\(dp\)感覺時間複雜度是\(O(n^3k)\),但細想轉移的話可見是 \(O(n^2k)\)

還有一種方式為 \(dp[i][j]\) 表示前\(i\)個球,且我保留第 \(i\)個球,並且已經移除了 \(j\)個球的最大價值和,轉移就列舉上一個保留的球的下標,其時間複雜度是 \(O(nk^2)\)

由於 \(n\)\(10^5\), \(k\)\(500\),兩者的時間複雜度都太高了。思考如何最佳化,事實上最後這兩種方式都可以最佳化到 \(O(nk)\)

先考慮第一種 \(dp\)方式的轉移式子:

\[dp[i][j][k] = \max ( dp[i - 1][j][k_1] + v_i, dp[i - 1][j - 1][k]) \]

其中\(1 \leq k_1, k \leq n\)表示顏色,且 \(k_1 \neq k\)。雖然這裡是\(O(n^3k)\),但考慮到,如果保留球,實際上只有一個\(dp[i][j][k]\)會改動,需要遍歷\(dp[i-1][j][k_1]\),如果不保留球,則直接 \(dp[i][j][k] = dp[i - 1][j - 1][k]\) ,轉移是\(O(1)\),只有一個狀態轉移是 \(O(n)\)。所以總的是\(O(n^2k)\)

兩大項的最大值分別對應著保留球移除球這兩種決策。其中保留球的決策中,需額外條件\(k_1 \neq k\),即上一個球顏色不與當前球顏色相同。而 移除球的決策中,就一個值而已。

考慮如何最佳化轉移,初看其實感覺很難最佳化,因為狀態數就已經是\(O(n^2k)\),最佳化轉移的同時剛好可以把狀態數最佳化到\(O(nk)\)

對於後者轉移,其就一項,轉移就\(O(1)\),無需過多考慮。

對於前者轉移,如果沒有\(k \neq k_1\)的條件,我維護\(dp[i-1][j][...]\)的最值,那轉移就不需要遍歷 \(k_1\),透過維護的最值可以\(O(1)\)轉移,就是在求 \(dp[i-1][j][...]\)的每一項時,就可以維護 \(mx[i-1][j] = \max(dp[i-1][j][...])\)

棘手的就是\(k_1 \neq k\)這個條件,就是要求\(dp[i-1][j][...]\)\(dp[i-1][j][k]\)這一項的最值。

如何處理這個條件呢?事實上我們除了維護最大值,再維護一個次大值,這兩個的顏色一定是不同的,那轉移時,一定就是從這兩個選一個出來轉移。

即維護\(mx[i-1][j] = [[MAX1, color], [MAX2, color]]\),這樣,我每個\(dp[i][j][k]\)從可以從 \(mx[i-1][j]\)\(O(1)\)找到原轉移式裡的 \(\max(dp[i-1][j][k_1])\)這一項 ,得以轉移。

轉移式變為\(dp[i][j][k] = \max(mx[i - 1][j][0] + v_i, mx[i - 1][j][1] + v_i, dp[i - 1][j - 1][k])\) ,其中\(mx\)那部分要判斷顏色是否不同\((c_i \neq color)\)。最後的答案即為\(\max(dp[n][l][...])\),或者說是 \(mx[n][l][0]\)

但至此,只是將一個轉移從\(O(n)\)降到了 \(O(1)\),但狀態數還是\(O(n^2k)\),降不下來 ,怎麼辦呢?

注意到狀態裡的\(k\),當初之所以定義\(k\)這個狀態,是因為轉移時要避免出現相鄰球顏色相同(即能夠轉移),事實上這個狀態非常冗餘,它的用途只是為了轉移,最後求解答案時不需要該狀態(即對該狀態的所有取值取個最值,而不關心具體是什麼),觀察上述的轉移式,會發現這個\(k\)已經沒有用了, 保留球的話轉移從\(mx\)來就可以了,移除球的話,最終能成為\(mx[i][j]\)裡的情況,也必定是 \(mx[i-1][j-1]\)裡的最大值或次大值。

換句話說,這個 \(k\)的狀態其實可以砍掉(就變成了 \(mx\)了),即 \(mx[i][j]\)表示前 \(i\)個球,移除 \(j\)個球后的兩個兩元組,分別表示(最大價值和,最後一個球的顏色)(次大價值和,最後一個球的顏色)。轉移時同樣考慮當前球保留或移除,保留的話,則從\(mx[i-1][j][0] + v \to mx[i][j]\)\(mx[i-1][j][1] + v \to mx[i][j]\) (看哪個顏色不同),移除的話就\(mx[i-1][j-1][0] \to mx[i][j]\)\(mx[i-1][j-1][1] \to mx[i][j]\),這樣就維護出 \(mx[i][j]\)的最大值和次大值,可以轉移了。

由於每個\(mx[i][j]\)的轉移都是依賴上一個\(mx[i-1]\),這裡也可以滾動陣列最佳化下。

最後的時間複雜度是 \(O(nk)\)

神奇的程式碼
#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, k;
    cin >> n >> k;
    vector<array<pair<LL, int>, 2>> dp(k + 1, {{{-inf, -1}, {-inf, -1}}});
    dp[0][0] = {0, 0};
    auto update = [&](array<pair<LL, int>, 2>& a, pair<LL, int> b) {
        if (b.first > a[0].first) {
            swap(a[0], b);
        }
        if (b.second != a[0].second && b.first > a[1].first) {
            swap(a[1], b);
        }
    };
    for (int i = 0; i < n; ++i) {
        int c, v;
        cin >> c >> v;
        vector<array<pair<LL, int>, 2>> dp2(k + 1, {{{-inf, -1}, {-inf, -1}}});
        for (int j = 0; j <= k; ++j) {
            if (dp[j][0].second != c) {
                update(dp2[j], {dp[j][0].first + v, c});
            }
            if (dp[j][1].second != c) {
                update(dp2[j], {dp[j][1].first + v, c});
            }
            if (j > 0) {
                update(dp2[j], dp[j - 1][0]);
                update(dp2[j], dp[j - 1][1]);
            }
        }
        dp.swap(dp2);
    }
    cout << max(-1ll, dp[k][0].first) << '\n';

    return 0;
}



還有另一種方式為 \(dp[i][j]\) 表示前\(i\)個球,且我保留第 \(i\)個球,並且已經移除了 \(j\)個球的最大價值和,轉移就列舉上一個保留的球的下標,期間的球就被移除掉。其時間複雜度是 \(O(nk^2)\),其中狀態數\(O(nk)\),轉移耗時 \(O(k)\)

即轉移式\(dp[i][j] = \max(dp[i - k - 1][j - k]) + v_i, c_i \neq c_{i - k - 1}\)

考慮如何最佳化轉移。觀察轉移式,轉移列舉的是\(0 \leq k \leq l\),將 \(dp[i][j]\)看作是一個二元函式的話,那一系列 \((i-k-1, j - k)\)點構成的是一條斜線,注意到這條斜線的特徵是\(i-k-1-(j-k)=i-j-1\)是個定值。也就是說轉移實際上就是在一條斜線取最值,但仍有 \(c_i \neq c_{i-k-1}\)這一棘手的條件。

對於這一棘手條件,處理的方法同上述一樣,我們維護一條斜線的最大值和次大值。即\(mx[o]\)表示當前狀態下, 滿足\(i-j-1=o\)的這條斜線的\(dp[i][j]\)的最大值和次大值及其顏色,每個 \(dp[i][j]\)都從 \(mx[i-j-1]\)中得到顏色不相同的最值即可。這樣轉移就變為 \(O(1)\)了。

程式碼裡是將\(mx[o]\)壓成一個數,即我們的列舉順序不是樸素的\(dp[i][0],dp[i][1],dp[i][2]...\),而是一條條斜線,即 \(dp[i][0],dp[i+1][1],dp[i+2][2],...,dp[i+1][0],dp[i+2][1]...\),當求完一條斜線的所有值時,再求下一條斜線。這樣的話就不用保留其他斜線的最值資訊,當需要的時候才算。樸素列舉順序實際上是依次求每條斜線的每個點,所以需要保留歷史資訊,而當依次求完每條斜線時,就不需要保留其他斜線的資訊了。

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

const LL inf = 1e18 + 7;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, k;
    cin >> n >> k;
    vector<int> c(n + 2), v(n + 2);
    for (int i = 1; i <= n; ++i) {
        cin >> c[i] >> v[i];
    }
    c[n + 1] = n + 1;
    n += 2;
    vector<vector<LL>> dp(n, vector<LL>(k + 1, -1));
    dp[0][0] = 0;
    for (int i = 1; i < n - k; i++) {
        array<pair<LL, int>, 2> mx{{{-inf, -1}, {-inf, -1}}};
        for (int j = 0; j <= k; ++j) {
            pair<LL, int> la = {dp[i + j - 1][j], c[i + j - 1]};
            if (la.first > mx[0].first)
                swap(la, mx[0]);
            if (la.second != mx[0].second && la.first > mx[1].first)
                swap(la, mx[1]);

            dp[i + j][j] = mx[c[i + j] == mx[0].second].first + v[i + j];
        }
    }
    cout << max(-1ll, dp.back().back()) << '\n';
    return 0;
}


F - Many Lamps (abc345 F)

題目大意

給定一張圖,點上有燈,初始燈滅。

選擇一條邊,邊上的兩點的燈的狀態會反轉。

給出一種邊的選擇方案,使得恰好有\(k\)盞燈亮。

解題思路

是個構造題。初看這張圖感覺無從下手,因為構造題一般要按照某種順序執行,但圖基本沒有什麼順序可言。

可以在更簡化的圖考慮,比如圖的\(DFS\)樹,或者是生成樹。

考慮在圖的一棵生成樹上,會發現有一種構造方法,可以使得根之外,其他點上的燈都能控制亮或滅。

從下往上,葉子到根考慮每個點,如果該點燈滅,則可以選擇其父親邊,使其燈亮,或者燈亮變燈滅。

即每個點,都可以透過其父親邊使得它亮或滅,除了根節點之外,這個燈能亮能滅,全憑其點的度數是奇是偶。即一個連通塊,除了個點之外,我能保證讓其餘點都燈亮。

注意到每選擇一條邊,要麼兩燈亮,要麼兩燈滅,要麼一亮一滅,即亮燈數量始終是偶數。因此\(k\)是奇數的情況無解。

否則,注意原圖不一定連通,則對於每個連通塊,透過\(DFS\)從葉子考慮,儘量開滿所有的燈,直到達到\(k\)盞燈為止,多開了就滅。

神奇的程式碼
#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, k;
    cin >> n >> m >> k;
    vector<vector<array<int, 2>>> edge(n);
    vector<int> du(n, 0);
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        --u, --v;
        edge[u].push_back({v, i + 1});
        edge[v].push_back({u, i + 1});
    }
    if (k & 1) {
        cout << "No" << endl;
        return 0;
    }

    int up = 0;
    vector<int> vis(n, 0);
    vector<int> ans;
    function<void(int, int, int)> dfs = [&](int u, int fa, int fa_id) {
        debug(u, fa, fa_id);
        int cnt = 0;
        vis[u] = 1;
        for (auto& [v, id] : edge[u]) {
            if (vis[v] == 0) {
                dfs(v, u, id);
            }
        }
        if (du[u] & 1) {
            --k;
        }
        if (u != fa) {
            if (k > 0 && (~du[u] & 1)) {
                ans.push_back(fa_id);
                --k;
                du[u]++;
                du[fa]++;
            } else if (k < 0 && (du[u] & 1)) {
                ans.push_back(fa_id);
                ++k;
                du[u]++;
                du[fa]++;
            }
        }
    };
    for (int i = 0; i < n; i++) {
        if (k != 0 && vis[i] == 0) {
            dfs(i, i, -1);
        }
    }
    if (k != 0)
        cout << "No" << '\n';
    else {
        cout << "Yes" << '\n';
        cout << ans.size() << '\n';
        for (auto& i : ans)
            cout << i << ' ';
        cout << '\n';
    }

    return 0;
}



G - Sugoroku 5 (abc345 G)

題目大意

扔骰子,\([1,k]\)等機率出現。

對於每個 \(i \in [1,n]\),問扔\(i\)次骰子後,其和 \(\geq n\)的機率。

解題思路

<++>

神奇的程式碼