AtCoder Beginner Contest 370

~Lanly~發表於2024-09-07

A - Raise Both Hands (abc370 A)

題目大意

給出Snuke舉的左右手情況,如果只舉左手,輸出Yes,如果只舉右手,輸出No,否則輸出Invalid

解題思路

逐一判斷即可。

神奇的程式碼
#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 l, r;
    cin >> l >> r;
    if (l == 1 && r == 0)
        cout << "Yes" << '\n';
    else if (l == 0 && r == 1)
        cout << "No" << '\n';
    else
        cout << "Invalid" << '\n';

    return 0;
}



B - Binary Alchemy (abc370 B)

題目大意

給定物品合成成分表\(a_{ij}\)表示物品 \(i\)和物品 \(j\)合成物品 \(a_{ij}\)

問物品 \(1\),依次與 \(1,2,3,..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;
    cin >> n;
    vector<vector<int>> a(n);
    for (int i = 0; i < n; i++) {
        a[i].resize(i + 1);
        for (auto& x : a[i]) {
            cin >> x;
            --x;
        }
    }
    int cur = 0;
    for (int i = 0; i < n; ++i) {
        int x = cur, y = i;
        if (x < y)
            swap(x, y);
        cur = a[x][y];
    }
    cout << cur + 1 << '\n';

    return 0;
}



C - Word Ladder (abc370 C)

題目大意

給定兩個字串\(s,t\)

用最小的次數,使得 \(s=t\),並且字串\(x\)的字典序最小。

操作為,選擇 \(s_i = c\),並且將修改後的 \(s\)放入 \(x\)的末尾。

解題思路

如何次數最小呢?

依次考慮\(s\)從左到右的每一位 \(i\),如果 \(s_i \neq t_i\),那我肯定要 \(s_i = t_i\),但這是我們此時要進行的操作嗎?還是先放一放,改後面的字母后,再改當前位?

由於每次會將修改後的\(s\)放入 \(x\)的末尾,因此我們要優先考慮首先進行的操作,應該是:即刻進行,還是緩一緩在進行。

如果\(s_i > t_i\),那就優先更改當前位,這樣改後的 \(s\)的字典序更小。

如果 \(s_i < t_i\),那就先更改後面位的,最後再改當前位,這樣得到的 \(x\)的字典序最小。

這種回溯的感覺,可以用\(DFS\)實現上述操作。

神奇的程式碼
#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;
    vector<string> ans;
    auto dfs = [&](auto dfs, int pos) {
        if (pos == s.size()) {
            return;
        }
        if (s[pos] == t[pos]) {
            dfs(dfs, pos + 1);
        } else if (s[pos] < t[pos]) {
            dfs(dfs, pos + 1);
            s[pos] = t[pos];
            ans.push_back(s);
        } else {
            s[pos] = t[pos];
            ans.push_back(s);
            dfs(dfs, pos + 1);
        }
    };
    dfs(dfs, 0);
    cout << ans.size() << '\n';
    for (auto& i : ans)
        cout << i << '\n';

    return 0;
}



D - Cross Explosion (abc370 D)

題目大意

二維網格,初始每個格子有牆。

依次進行\(q\)次放炸彈的操作,給定每次放炸彈的位置 \((i,j)\),如果該位置有牆,則該牆消失。

否則,炸彈會爆炸,會產生十字衝擊波,該位置上下左右的各第一個牆都會消失。

問最後還存在的牆的數量。

解題思路

對於第一種情況,直接移除該位置的牆即可。

對於第二種情況,需要找到該列上下、該行左右最近的牆。

牆的數量\(hw \leq 4e5\),可以儲存每個牆的座標。

然後對於每行和每列,分別維護\(hset[i]\)表示第\(i\)行還有牆的列座標,是個\(set\)\(wset[i]\)表示第 \(i\)列還有牆的行座標 ,也是個\(set\)

這樣,對於一個炸彈 \((i,j)\),如果該位置沒有牆\((hset[i].find(j) == hset[i].end())\),則需要找到 \(< j\)的最大和 \(> j\) 的最小的數字。同理對於\(wset\)也要找對應的數字,然後 \(erase\)。這樣每次操作的複雜度都是 \(O(\log)\),總的時間複雜度就是 \(O(q\log (h + w))\)

程式碼對於沒有牆的邏輯是:

  • \(it = hset[i].lower\_bound(j)\), 由於沒有牆,此時一定 \(*it > y\)(否則是 \(*it == y\)),如果 \(it != hset[i].end()\),那麼它就是下面的第一個牆(這裡認為左上是原點),要毀掉,於是\(it = hset[i].erase(it)\)\(erase\)返回值是移除了該 \(*it\)後的下一個元素。
  • 然後要找上面的第一個牆,此時 \(it\)\(>y\)的第一個位置(無論剛剛是否\(erase\)了),因此如果 \(it != hset[i].begin()\),那麼 \(prev(it)\)就是上面的第一個牆,要毀掉,於是 \(hset[i].erase(prev(it))\)

同理的思路處理 \(wset\)即可。

神奇的程式碼
#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, q;
    cin >> h >> w >> q;
    vector<int> hh(h), ww(w);
    iota(hh.begin(), hh.end(), 0);
    iota(ww.begin(), ww.end(), 0);
    vector<set<int>> hset(h), wset(w);
    for (int i = 0; i < h; i++) {
        hset[i].insert(ww.begin(), ww.end());
    }
    for (int i = 0; i < w; i++) {
        wset[i].insert(hh.begin(), hh.end());
    }
    while (q--) {
        int x, y;
        cin >> x >> y;
        --x, --y;
        auto it = hset[x].lower_bound(y);
        if (it != hset[x].end() && *it == y) {
            hset[x].erase(y);
            wset[y].erase(x);
        } else {
            if (it != hset[x].end()) {
                wset[*it].erase(x);
                it = hset[x].erase(it);
            }
            if (it != hset[x].begin()) {
                it = prev(it);
                wset[*it].erase(x);
                hset[x].erase(it);
            }
            it = wset[y].lower_bound(x);
            if (it != wset[y].end()) {
                hset[*it].erase(y);
                it = wset[y].erase(it);
            }
            if (it != wset[y].begin()) {
                it = prev(it);
                hset[*it].erase(y);
                wset[y].erase(it);
            }
        }
    }
    int cnt = 0;
    for (int i = 0; i < h; i++) {
        cnt += hset[i].size();
    }
    cout << cnt << '\n';

    return 0;
}



E - Avoid K Partition (abc370 E)

題目大意

給定一個陣列\(a\),劃分成若干個子區間,使得沒有子區間的和為 \(k\)

求劃分方案數。

解題思路

樸素\(dp\)就是設 \(dp[i]\)表示前 \(i\)段劃分滿足條件的方案數。

轉移則列舉最後一次的區間,然後 \(dp[i] = \sum_{1 \leq j \leq n, sum[j+1..i] \neq k} dp[j]\)

複雜度顯然是 \(O(n^2)\)的。

棘手在條件 \(sum[j+1..i] \neq k\)上,如果沒有這個條件,這個轉移其實就是一個字首和,用字首和最佳化即為 \(O(n)\)

我們用字首和相減代替區間和,即 \(sum[j+1..i] = sum[i] - sum[j]\),轉移式即為\(dp[i] = \sum_{1 \leq j \leq n, sum[i] - sum[j] = k} dp[j]\)

換句話說,我們要對\(sum[j] \neq sum[i] - k\)\(dp[j]\)求和,這是個非常稀疏的條件,即如果設 \(cnt[i] = \sum_{sum[j] = i} dp[j]\),即字首和為 \(i\)\(dp\)值,那上述轉移式可改寫成\(dp[i] = \sum_{1 \leq j < i} dp[j] - cnt[sum[i] - k]\)

即一個字首和與一個數的差值,這樣轉移就是\(O(1)\)了,因此維護一個\(dp\)字首和 \(\sum_{1 \leq j < i} dp[j]\)以及字首和的\(dp\)\(cnt[i] = \sum_{sum[j] = i} dp[j]\)即可。

時間複雜度就是\(O(n \log n)\)

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

const int mo = 998244353;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    LL k;
    cin >> n >> k;
    vector<int> a(n);
    for (auto& x : a)
        cin >> x;
    map<LL, int> cnt;
    cnt[0] = 1;
    LL presum = 0;
    int precnt = 1;
    int ans = 0;
    for (auto& i : a) {
        presum += i;
        ans = (precnt - cnt[presum - k] + mo) % mo;
        cnt[presum] = (cnt[presum] + ans) % mo;
        precnt = (precnt + ans) % mo;
    };
    cout << ans << '\n';

    return 0;
}



F - Cake Division (abc370 F)

題目大意

給定一個環形陣列,劃分為\(k\)段,使得每段和的最小值最大。

在該最大值的各種劃分方案中,求有多少位置,在所有劃分方案中都不被分開。

解題思路

如果我們確定了這個每段和的最小值\(x\),且是一個鏈的情況,我們有個貪心的策略:從第一個數開始往右延伸,直到第一個不小於\(x\)的位置\(y\),有\(sum[1..y] \geq x, sum[1..y-1] < x\),它們就是一段,我們定義\(f(1) = y + 1\),如果不存在可行的話,\(f(1) = n + 1\)\(f(1) = y + 1\)表示分了一段區間\([1,y+1)\)。如果能往復能分成\(k\)段則可行。時間複雜度是 \(O(n)\)\(O(k \log n)\)。後者就是二分來找到每一段的最右端。

容易發現這個 \(x\)與是否可行具有單調性: \(x\)越大,越難可行, \(x\)越小,越容易可行。因此可以透過二分找到這個 \(x\)

然後考慮環的情況,一種處理方法是考慮每個起點,每個起點都做一次上述驗證,如果存在一個起點滿足上述要求則可行。這樣驗證的複雜度是 \(O(nk\log n)\)

如何最佳化呢?

由於環形,我們拆成鏈,然後複製一份。注意到以每個位置\(i\)為起點,計算\(f(i)\)的值,可以透過\(O(n)\)的滑動視窗得到\(f\)。這就是分一段的區間,而如果分兩段就是\(f(f(x))\)。注意到只要起點固定,它會延伸到哪裡也固定了,不同段之間也相互獨立。因此函式可以簡單的複合起來,相比於一次一次分,最佳化方向就是以倍增的形式分段。

即二分了\(x\)後,預處理倍增陣列 \(up[i][j]\)表示從 \(i\)開始,分了 \(2^j\)段後的右邊界 \(y\),即 \([i,y)\)包含了滿足題意的 \(2^j\)段。只要 \(y - i \leq n\),那麼從 \(i\)開始就可行的。透過倍增陣列來分\(k\)段,每個起點的驗證複雜度就降為 \(O(\log k)\)

最後求有多少位置是不會被斷開的,就在列舉起點的時候,如果不可行,那麼該起點與上一個數之間就不能斷開(斷開了就是以該數為起點,經求得是不可行的),因此後者就是不可行的起點數量。

總的時間複雜度就是\(O(n\log nk)\)

神奇的程式碼
#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, k;
    cin >> n >> k;
    vector<int> a(n + n);
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
        a[i + n] = a[i];
    }
    int N = n + n;
    int l = 1, r = 2e9 + 8;
    auto check = [&](int x) {
        vector<array<int, 20>> up(N + 2);
        up[N][0] = N + 1;
        up[N + 1][0] = N + 1;
        queue<int> windows;
        int r = 0;
        int sum = 0;
        for (int i = 0; i < N; ++i) {
            while (r < N && sum < x) {
                windows.push(a[r]);
                sum += a[r];
                ++r;
            }
            if (sum < x)
                up[i][0] = N + 1;
            else
                up[i][0] = r;

            sum -= windows.front();
            windows.pop();
        };
        for (int i = 1; i < 20; ++i)
            for (int j = 0; j < N + 2; ++j) {
                up[j][i] = up[up[j][i - 1]][i - 1];
            }
        int cnt = 0;
        for (int i = 0; i < n; ++i) {
            int pos = i;
            for (int j = 0; j < 20; ++j) {
                if ((k >> j) & 1) {
                    pos = up[pos][j];
                }
            }
            if (pos <= i + n) {
                ++cnt;
            }
        }
        return cnt;
    };
    while (l + 1 < r) {
        int mid = l + (r - l) / 2;
        if (check(mid))
            l = mid;
        else
            r = mid;
    }
    int cnt = check(l);
    cout << l << ' ' << n - cnt << '\n';

    return 0;
}



G - Divisible by 3 (abc370 G)

題目大意

如果一個數是好的,說明它的因子和能被\(3\)整除。

給定 \(n,m\),問一個長度為 \(m\)的陣列 \(a\)的數量,滿足其各數的乘積不超過 \(n\),且是好數。

解題思路

<++>

神奇的程式碼



相關文章