AtCoder Beginner Contest 358

~Lanly~發表於2024-06-15

A - Welcome to AtCoder Land (abc358 A)

題目大意

給定兩個字串,問是否是AtCoder Land

解題思路

讀取後判斷即可。

神奇的程式碼
#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, t;
    cin >> s >> t;
    if (s == "AtCoder" && t == "Land")
        cout << "Yes" << '\n';
    else
        cout << "No" << '\n';

    return 0;
}



B - Ticket Counter (abc358 B)

題目大意

售票廳,\(n\)個人來買票,每個人買票耗時\(a\)。第 \(i\)個人 \(t_i\)時刻來 ,如果此時沒人買票則可以立刻買票,否則要排隊等買票。

問每個人最終買到票的時間。

解題思路

維護隊伍無人的時間\(time\),當第\(i\)個人來時,

  • \(time \leq t_i\),則其可以立刻買票,買完票時間為 \(t_i + a\),此時隊伍無人的時間變為 \(time = t_i + a\)
  • \(time > t_i\),則其需要等待至隊伍無人時間\(time\),買完票時間為 \(time + a\),此時隊伍無人的時間變為 \(time = time + 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, a;
    cin >> n >> a;
    int time = 0;
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        if (time <= x) {
            time = x + a;
        } else
            time = time + a;
        cout << time << '\n';
    }

    return 0;
}



C - Popcorn (abc358 C)

題目大意

給定\(n\)個小攤售賣的爆米花種類。

問選擇的最少的小攤數量,可以買到所有爆米花種類。

解題思路

由於\(n \leq 10\),直接 \(O(2^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, m;
    cin >> n >> m;
    vector<int> a(n, 0);
    for (auto& x : a) {
        string s;
        cin >> s;
        for (auto c : s) {
            x = x * 2 + (c == 'o');
        }
    }
    int ans = n, up = (1 << n);
    for (int i = 0; i < up; i++) {
        int cnt = 0;
        for (int j = 0; j < n; j++) {
            if (i & (1 << j)) {
                cnt |= a[j];
            }
        }
        if (cnt == (1 << m) - 1) {
            ans = min(ans, __builtin_popcount(i));
        }
    }
    cout << ans << '\n';

    return 0;
}



D - Souvenirs (abc358 D)

題目大意

\(n\)個盒子,第 \(i\)個盒子價格 \(a_i\),有 \(a_i\)個糖果。

\(m\)個盒子給 \(m\)個人,其中第 \(i\)個人的盒子的糖果數至少有 \(b_i\)個。

問花費價格的最少值,或告知不可行。

解題思路

考慮每個人,買哪個盒子給他。

由於盒子的糖果數和價格是相當的,那對於每個人的\(b_i\),肯定是選擇\(\geq b_i\)的最小的 \(a_i\),二分查詢即可。由於每個盒子只能買一次,因此得將其刪去,用 multiset維護即可。

下述程式碼可以解決糖果數與價格不相當的情況,按照\(b_i\)從大到小考慮,那我肯定是貪心地選最小价格的盒子,滿足 \(a_i \geq b_i\)。用優先佇列維護這個最小值即可,而剩下未選擇的盒子都可以滿足後續的 \(b_i\)。而按 \(b_i\)從小到大考慮的話,可能會使得不可行。

神奇的程式碼
#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(n);
    for (auto& x : a)
        cin >> x;
    for (auto& x : b)
        cin >> x;
    sort(a.begin(), a.end(), greater<int>());
    sort(b.begin(), b.end(), greater<int>());
    priority_queue<int, vector<int>, greater<int>> q;
    bool ok = true;
    LL ans = 0;
    for (int i = 0, j = 0; i < m; i++) {
        while (j < n && a[j] >= b[i]) {
            q.push(a[j]);
            j++;
        }
        if (q.empty()) {
            ok = false;
            break;
        }
        ans += q.top();
        q.pop();
    }
    if (!ok)
        ans = -1;
    cout << ans << '\n';

    return 0;
}



E - Alphabet Tiles (abc358 E)

題目大意

給定\(k\)\(26\)\(c_i\),問字串數量,其 長度在\(1 \sim k\)之間,且第 \(i\)個字母的出現次數不超過 \(c_i\)

解題思路

先列舉長度\(len\),然後考慮 \(len\)個字母分別是什麼字母。

考慮每個字母使用的數量,可以發現我們只需知道此時剩餘字母數,就可以作出轉移。

\(dp[i][j]\)表示使用了前 \(i\)類字母,填了\(j\) 個空位的方案數。

列舉當前字母使用的數量\(k\),轉移則為\(dp[i][j + k] += dp[i - 1][j] \times C_{len - j}^{k}\),即要從當前剩餘的\(len - j\)個空位選 \(k\)個作為當前的字母。

狀態數是 \(O(26n)\),轉移是 \(O(n)\),加上我們一開始列舉的長度複雜度 \(O(n)\),總的時間複雜度是 \(O(26n^3)\),由於 \((n \leq 10^3\),會超時。

考慮最佳化,容易發現列舉不同的長度計算每個\(dp[i][j]\),會有很多重複的計算。但是轉移代價會依賴總長度\(len\)

考慮我們先計算 \(len = k\)\(dp[i][j]\),即長度為 \(k\)的符合條件的字串數量,看看能否得到其餘 \(len\)的數量。

很顯然\(dp[26][k]\)是長度為 \(k\)的字串數量,而 \(dp[26][k-1],dp[26][k-2],...\)同樣是長度為 \(k\)的字串,但有 \(1,2,...\)個空位上的字母是未確定的,其中空位的位置也有很多種情況。

\(dp[26][k-1]\)為例,它表示考慮了前 \(26\)個字母,填了 \(k-1\)個空位的方案數,此時還有一個空位。它相當於是,原先\(k-1\)個字母的方案數,再插入一個空位。 而空位的位置數量有 \(C_{k}^{1}\)種, 考慮 \(\frac{dp[26][k-1]}{C_{k}^{1}}\),即將空位的情況數去掉,其值就變成了長度為 \(k-1\)的符合條件的字串數量。

其餘情況同理。這就把列舉長度的複雜度最佳化掉了。

因此答案就是 \(\sum_{i=1}{k} \frac{dp[26][k - i]}{C_{n}^{i}}\),總的時間複雜度是 \(O(26n^2)\)

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

const int mo = 998244353;

long long qpower(long long a, long long b) {
    long long qwq = 1;
    while (b) {
        if (b & 1)
            qwq = qwq * a % mo;
        a = a * a % mo;
        b >>= 1;
    }
    return qwq;
}

long long inv(long long x) { return qpower(x, mo - 2); }

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    array<int, 26> cnt;
    for (auto& x : cnt)
        cin >> x;
    vector<int> dp(n + 1);
    dp[0] = 1;
    vector<int> fac(n + 1, 1), ifac(n + 1, 1);
    for (int i = 1; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % mo;
    }
    ifac[n] = inv(fac[n]);
    for (int i = n - 1; i >= 0; --i) {
        ifac[i] = 1ll * ifac[i + 1] * (i + 1) % mo;
    }
    auto C = [&](int n, int m) -> int {
        if (n < m)
            return 0;
        return 1ll * fac[n] * ifac[m] % mo * ifac[n - m] % mo;
    };
    for (int i = 0; i < 26; ++i) {
        vector<int> dp2(n + 1, 0);
        for (int j = 0; j <= n; ++j) {
            for (int k = 0; k <= cnt[i] && j + k <= n; ++k) {
                dp2[j + k] = (dp2[j + k] + 1ll * dp[j] * C(n - j, k) % mo) % mo;
            }
        }
        dp.swap(dp2);
    }
    int ans = 0;
    for (int i = 1; i <= n; ++i) {
        ans = (ans + 1ll * dp[i] * inv(C(n, n - i)) % mo) % mo;
    }
    cout << ans << '\n';

    return 0;
}



F - Easiest Maze (abc358 F)

題目大意

給定\(n,m,k\),構造一個 \(n \times m\)的迷宮,從右上走到右下,路徑唯一,長度為 \(k\)

解題思路

<++>

神奇的程式碼



G - AtCoder Tour (abc358 G)

題目大意

給定一個二維網格,格子上有數。

給定起點,重複以下操作\(k\)次。

  • 每次操作,要麼不動,要麼上下左右四個方向選一個移動一格。操作完後,獲得收益,收益為格子上的數。

問最優操作下的收益和的最大值。

解題思路

觀察最優情況的特點,一定是從起點出發,到達某一個點,然後一直停留直到操作次數到達\(k\)

因此首先列舉終點\(a_{ij}\),然後計算從起點到終點,怎樣走收益最大。

我從起點到達終點時,路徑有很多,不同路徑下,最終收益不一樣,而最終收益關係到兩個狀態:已經獲得的收益值\(presum\),還剩下的操作次數\(cnt\)

收益最大,則要求\(presum + cnt \times a_{ij}\)最大。

觀察上述式子,它並不意味著我越短路徑到達\(a_{ij}\)是最優的。

考慮一極端情況, \(x \to y\)耗時三步,收益為 \(1,1\)\(a_y=10^6\),而另一個方案,耗時五步,收益為\(10^6-1, 10^6-1, 10^6 - 1,10^6-1\),兩種方案,後面的 \(k-4\)操作的收益相同,而前 \(4\)次的收益,顯然是後者高:雖然後者耗時5步,但損失很少:只有\(4\),而前者雖然耗時 \(3\)步,但每步的損失高達 \(10^6\)。這啟示我們要以最小損失代價到達終點。

換句話說,對上述式子變形,注意到 \(presum = \sum_{k - cnt} a_x\),即 \(k-cnt\)\(a_x\)的和,我們將式子改寫成 \(k \times a_{ij} - (k - cnt) a_{ij} + \sum_{k - cnt} a_x\)。注意到後兩項的項數相同,合併一下,得到

\[k \times a_{ij} - \sum_{k - cnt}(a_{ij} - a_x) \]

前一項是一個定值,而後一項可以假象在一個新的網格圖上走,格子數變為\(b_x = a_{ij} - a_x\)(上述的損失代價),問 從起點到終點的最短路徑(損失代價最小)。 這麼操作其實就相當於把步數損失放到每一步的計算裡,從而消去了棘手的\(cnt\)(這個平均值的處理技巧差不多)

但有個問題是,最短路徑裡,邊權有負(\(x \to y\),邊權是 \(b_y\) ),則求起點到終點的最短路,不能用\(dijkstra\),但這是網格圖, \(SPFA\)會被卡死了。怎麼辦呢。

可以先考慮終點取 \(a_{ij}\)最大的,那麼 \(b_x\)都是正的,此時可以用 \(dijkstra\)求最短路。然後考慮次大的 \(a_{ij}\),則原本 最大的格子的 \(b_x\)變成負的,此時求最短路怎麼辦呢?細想會發現,如果最短路會經過這個負格子,那我可以就停留在這個格子上(這樣我的損失代價會不斷減小),這樣最終收益比到達終點更大。

因此會發現這個最短路徑中,它不會經過 \(b_x\)是負數的格子。由此實際上就是一個正權的 \(dijkstra\)最短路問題。

由此求出最短路徑,即最小損失代價,從而就知道此時終點的最優收益。對所有終點的最優收益取個最大值即為答案。

列舉終點的複雜度是\(O(hw)\)\(dijkstra\)的複雜度是 \(O(hw \log hw)\),因此總的時間複雜度為\(O((hw)^2 \log hw)\)


有點傻傻了,好像一個樸素的\(DP\)就解決了。

首先還是注意到最優情況下,一定是從起點出發,到達某一個點,然後一直停留直到操作次數到達\(k\)

而到達某個格子時,並不一定是最短距離到達最好,因為最後的收益包含兩部分,\(presum\)\(cnt\)(和上述同意義):\(presum + cnt \times a_{ij}\)最大,而每個\(cnt\)都可能作為最終的答案。

\(cnt\)的範圍最大就是 \(O(hw)\),因此我們可以保留這個狀態,即設 \(dp[c][i][j]\)表示走 \(c\)步到達 \(a_{ij}\)的最大收益,即\(presum\) 。那最終的答案就是\(\max_{i, j, c} dp[c][i][j] + (k - c) \times a_{ij}\)

狀態數是\(O((hw)^2)\),轉移是 \(O(1)\),因此總的時間複雜度為 \(O((hw)^2)\)

神奇的程式碼
#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 h, w, k, sx, sy;
    cin >> h >> w >> k >> sx >> sy;
    --sx, --sy;
    vector<vector<int>> a(h, vector<int>(w));
    for (auto& i : a)
        for (auto& j : i)
            cin >> j;
    LL ans = 0;
    array<int, 4> dx = {0, 0, 1, -1}, dy = {1, -1, 0, 0};
    auto solve = [&](int ex, int ey) {
        LL sum = 1ll * a[ex][ey] * k;
        vector<vector<int>> cost(h, vector<int>(w));
        for (int i = 0; i < h; ++i)
            for (int j = 0; j < w; ++j)
                cost[i][j] = a[ex][ey] - a[i][j];
        priority_queue<pair<LL, pair<int, int>>> team;
        vector<vector<LL>> dis(h, vector<LL>(w, numeric_limits<LL>::max()));
        team.push({0, {sx, sy}});
        dis[sx][sy] = 0;
        while (!team.empty()) {
            auto [d, p] = team.top();
            team.pop();
            auto [x, y] = p;
            if (dis[x][y] < -d)
                continue;
            for (int i = 0; i < 4; ++i) {
                int nx = x + dx[i], ny = y + dy[i];
                if (nx < 0 || nx >= h || ny < 0 || ny >= w)
                    continue;
                if (cost[nx][ny] < 0)
                    continue;
                if (dis[nx][ny] > -d + cost[nx][ny]) {
                    dis[nx][ny] = -d + cost[nx][ny];
                    team.push({-dis[nx][ny], {nx, ny}});
                }
            }
        }
        return sum - dis[ex][ey];
    };
    for (int i = 0; i < h; ++i) {
        for (int j = 0; j < w; ++j) {
            if (a[i][j] >= a[sx][sy]) {
                LL ret = solve(i, j);
                ans = max(ans, ret);
            }
        }
    }
    cout << ans << '\n';

    return 0;
}