AtCoder Beginner Contest 344

~Lanly~發表於2024-03-10

A - Spoiler (abc344 A)

題目大意

給定一個字串,包含兩個|,將|和兩個|之間的字元消去。

解題思路

按照題意模擬即可。

Python比較簡潔。

神奇的程式碼
s = input().split('|')
s = s[0] + s[2]
print(s)


B - Delimiter (abc344 B)

題目大意

給定\(n\)個數,倒序輸出。

解題思路

儲存這\(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);
    vector<int> a;
    int x;
    while (cin >> x) {
        a.push_back(x);
    }
    reverse(a.begin(), a.end());
    for (auto x : a) {
        cout << x << '\n';
    }

    return 0;
}



C - A+B+C (abc344 C)

題目大意

給定三個陣列\(a,b,c\),回答 \(q\)次詢問。

每次詢問,給定 \(x\),問能否從三個陣列各選一個數,其和為 \(x\)

解題思路

由於每個陣列的個數不超過\(n=100\),可以事先 \(O(n^3)\)預處理其所有可能的和,排個序。

然後對於每組詢問,花\(O(\log n)\)二分找一下\(x\)是否存在即可。

程式碼是\(O(n^2)\)預處理+ \(O(n\log 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, l;
    cin >> n;
    vector<int> a(n);
    for (auto& x : a)
        cin >> x;
    cin >> m;
    vector<int> b(m);
    for (auto& x : b)
        cin >> x;
    cin >> l;
    vector<int> c(l);
    for (auto& x : c)
        cin >> x;
    vector<int> sum;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            sum.push_back(a[i] + b[j]);
        }
    }
    sort(sum.begin(), sum.end());
    int q;
    cin >> q;
    while (q--) {
        int x;
        cin >> x;
        bool ok = false;
        for (int i = 0; i < l; i++) {
            auto it = lower_bound(sum.begin(), sum.end(), x - c[i]);
            if (it != sum.end() && *it == x - c[i]) {
                ok = true;
                break;
            }
        }
        if (ok)
            cout << "Yes" << endl;
        else {
            cout << "No" << endl;
        }
    }

    return 0;
}



D - String Bags (abc344 D)

題目大意

\(n\)個袋子,每個袋子裡有若干個字串。

給定一個目標串 \(t\),要求從每個袋子選出最多 \(1\)個字串,按順序拼接成 \(t\)

問取出來的字串個數的最小值。

解題思路

考慮樸素搜尋的狀態,即:

  • 當前第幾個袋子
  • 當前匹配到了目標串\(t\)的前幾位

透過以上兩個狀態,就可以從當前袋子選 \(1\)個字串,然後看看能否匹配,並轉移到下一個狀態。

即設 \(dp[i][j]\)表示前 \(i\)個袋子拼接目標串 \(t\)的前 \(j\)位的最小字串數。

狀態數是 \(O(100 \times 100)\),轉移代價是 \(O(10 \times 10 \times 10)\),總時間複雜度是 \(O(10^7)\),可過。

神奇的程式碼
#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);
    string s;
    cin >> s;
    vector<int> dp(s.size() + 1, inf);
    dp[0] = 0;
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) {
        vector<int> dp2 = dp;
        int m;
        cin >> m;
        while (m--) {
            string t;
            cin >> t;
            for (int j = 0; j < s.size(); j++) {
                if (j + t.size() <= s.size() && s.substr(j, t.size()) == t) {
                    dp2[j + t.size()] = min(dp2[j + t.size()], dp[j] + 1);
                }
            }
        }
        dp.swap(dp2);
    }
    if (dp.back() == inf) {
        dp.back() = -1;
    }
    cout << dp.back() << '\n';

    return 0;
}



E - Insert or Erase (abc344 E)

題目大意

給定一個陣列\(a\),進行以下 \(q\)次操作,分兩種。

  • 1 x y,在\(x\)後面插入 \(y\)
  • 2 x,將\(x\)刪去。

保證每次操作後,各個數互不相同。

經過 \(q\)次操作後,輸出最後的陣列 \(a\)

解題思路

由於會在\(x\)後面插入 \(y\),也就是原來的元素之間,會突然插進新的數。陣列的維護需要 \(O(n)\)。但用連結串列就可以 \(O(1)\)實現插入和刪除。

但連結串列對於定位數所在位置需要 \(O(n)\),這非常費時。因此我們需要用 map輔助定位,即\(map[x]\)就是指向 \(x\)的項的指標,從而可以快速定位,然後進行插入和刪除即可。

可以學學std::list,寫法更簡潔,不用手動維護前驅後繼,用map儲存\(x\)對應的迭代器即可。

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

const int dis = 2e5 + 8;

struct Node {
    int val;
    Node *l, *r;
};

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    map<int, Node*> pos;
    Node* L = new Node();
    Node* R = new Node();
    L->r = R;
    R->l = L;
    int n;
    cin >> n;
    Node* la = L;
    auto insert = [](Node* p, Node* x) {
        x->l = p;
        x->r = p->r;
        p->r->l = x;
        p->r = x;
    };
    auto remove = [](Node* x) {
        x->l->r = x->r;
        x->r->l = x->l;
    };
    for (int i = 0; i < n; i++) {
        LL x;
        cin >> x;
        Node* X = new Node();
        X->val = x;
        insert(la, X);
        la = X;
        pos[x] = X;
    }
    int q;
    cin >> q;
    while (q--) {
        int op;
        cin >> op;
        if (op == 1) {
            LL x, y;
            cin >> x >> y;
            Node* Y = new Node();
            Y->val = y;
            insert(pos[x], Y);
            pos[y] = Y;
        } else if (op == 2) {
            LL x;
            cin >> x;
            remove(pos[x]);
        }
    }
    for (auto i = L->r; i != R; i = i->r) {
        cout << i->val << ' ';
    }
    cout << '\n';

    return 0;
}



F - Earn to Advance (abc344 F)

題目大意

二維網格,從左上走到右下,只能向右或向下走。

每一步,當在第\((i,j)\)時,往下走的代價(花費錢數)是 \(d_{i,j}\),往右走的代價是 \(r_{i,j}\)。如果不走,則獲得 \(p_{i,j}\)錢。

初始沒錢。

沿途錢不能為負。問最小步數。

解題思路

這題有兩個比較特別的性質:

  • 充值點的\(p\)一定是遞增的
  • 到達一個充值點時,我們的最小步數(充值次數)一定是越小越好。

一個比較樸素的想法是\(dp[i][j][k]\)表示我們處在 \((i,j)\),且錢數為 \(k\),的最小步數。這樣可以轉移。

但錢數的複雜度高達 \(10^{10}\),因此錢數不能在狀態裡,但我們轉移又得知道錢數。怎麼辦呢?

考慮行走過程,我們會在某些位置停下來,充錢,而這些點的 \(p_{i,j}\)一定是遞增的。

至於充錢點之間如何行走,無關緊要,取最小代價行走即可。

因此我們只考慮轉移點之間的轉移,即設\(dp[i][j]\)表示當前在點 \((i,j)\)時的最小步數,當前錢數。一個二元組。

轉移時考慮下一個點 \((k,l)\),其中滿足 \(p_{k,l} \geq p_{i,j}\),兩點間的最小移動代價可以事先透過 \(O(80^4)\)預處理得到。 然後在\((i,j)\)充夠錢後到達 \((k,l)\)

因此,到達點 \((k,l)\)的方式有好多種,不同的方式有不同的 最小步數,當前錢數。要保留哪個呢?哪個是最優的呢?

可以觀察到,我們保留步數最小的,如果步數相同,則保留錢數最多的,這樣轉移一定最優的。

因為,對於一種到達方式\((i_1,j_1) \to (k, l)\),其最小步數和錢數為\((3,100)\) ,另一種到達方式\((i_2, j_2) \to (k,l)\),為\((2,10)\)。我們可以證明後者是更優的。

從轉移式子和轉移時\(p_{i,j}\)遞增的性質,可以得知\(100 \leq p_{i_1,j_1} \leq p_{k,l}, 10 \leq p_{i_2, j_2} \leq p_{k,l}\),則 \(100 - 10 \leq p_{k,l}\)。即雖然後者步數少,錢少,但如果讓步數相同,即在 \((k,l)\)充值到與前者相同的步數,此時我的錢數一定是更多的,也就是實際上更利於後面的移動。

轉移的問題解決了,所以就一個\(dp\)就沒了。

狀態數是 \(O(n^2)\),轉移是 \(O(n^2)\),總的時間複雜度是\(O(n^4)\)

神奇的程式碼
#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;
    cin >> n;
    vector<vector<int>> p(n, vector<int>(n));
    for (auto& i : p) {
        for (auto& j : i) {
            cin >> j;
        }
    }
    vector<vector<int>> r(n, vector<int>(n - 1));
    for (auto& i : r) {
        for (auto& j : i) {
            cin >> j;
        }
    }
    vector<vector<int>> d(n - 1, vector<int>(n));
    for (auto& i : d) {
        for (auto& j : i) {
            cin >> j;
        }
    }
    vector<vector<array<LL, 2>>> dp(n, vector<array<LL, 2>>(n, {inf, 0}));
    dp[0][0] = {0, 0};
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {

            vector<vector<LL>> dis(n, vector<LL>(n, inf));
            dis[i][j] = 0;
            for (int k = i; k < n; k++) {
                for (int l = j; l < n; l++) {
                    if (k > 0) {
                        dis[k][l] = min(dis[k][l], dis[k - 1][l] + d[k - 1][l]);
                    }
                    if (l > 0) {
                        dis[k][l] = min(dis[k][l], dis[k][l - 1] + r[k][l - 1]);
                    }
                }
            }

            for (int k = i; k < n; k++)
                for (int l = j; l < n; l++) {
                    if (k != n - 1 && l != n - 1) {
                        if (p[k][l] < p[i][j]) {
                            continue;
                        }
                    }
                    auto [ori_stay, ori_money] = dp[i][j];
                    LL cost = max(0ll, (dis[k][l] - ori_money + p[i][j] - 1) /
                                           p[i][j]);
                    LL money = ori_money + cost * p[i][j] - dis[k][l];
                    LL stay = ori_stay + cost + k - i + l - j;
                    if (stay < dp[k][l][0] ||
                        (stay == dp[k][l][0] && money > dp[k][l][1]))
                        dp[k][l] = {stay, money};
                }
        }
    }

    cout << dp[n - 1][n - 1][0] << '\n';

    return 0;
}



G - Points and Comparison (abc344 G)

題目大意

<++>

解題思路

<++>

神奇的程式碼



相關文章