AtCoder Beginner Contest 351

~Lanly~發表於2024-05-04

A - The bottom of the ninth (abc351 A)

題目大意

給定\(9\)\(a_i\)\(8\)\(b_i\),問最後一個 \(b_9\)是多少,使得 \(\sum a_i < \sum b_i\)

解題思路

答案就是\(\sum a_i - \sum b_i + 1\)

神奇的程式碼
a = sum(map(int, input().split()))
b = sum(map(int, input().split()))
print(a - b + 1)


B - Spot the Difference (abc351 B)

題目大意

給定兩個二維矩陣,僅一個位置元素不一樣,找出那個位置。

解題思路

二維遍歷一下就找到了。

神奇的程式碼
#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> a(n), b(n);
    for (auto& x : a)
        cin >> x;
    for (auto& x : b)
        cin >> x;
    auto solve = [&]() {
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (a[i][j] == b[i][j])
                    continue;
                return make_pair(i, j);
            }
        }
        return make_pair(-1, -1);
    };
    auto [x, y] = solve();
    cout << x + 1 << ' ' << y + 1 << '\n';

    return 0;
}



C - Merge the balls (abc351 C)

題目大意

給定一個佇列,執行以下\(q\)次操作。

每次操作,隊尾塞一個 大小為 \(2^{a_i}\)的球。然後重複以下操作:

  • 若隊尾兩個球的大小不同,結束該操作
  • 否則,移除隊尾兩個的兩個球,放一個大小 \(2^{a_i+1}\)的球 。回到上述操作。

解題思路

直接模擬即可,因為每個球最多隻會被移除一次,最多隻有\(q\)個球,因此從對球的操作次數考慮時間複雜度的話,其時間複雜度是 \(O(q)\)

神奇的程式碼
#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<int> team;
    while (n--) {
        int x;
        cin >> x;
        team.push_back(x);
        while (team.size() > 1) {
            int a = team.back();
            int b = team[team.size() - 2];
            if (a == b) {
                team.pop_back();
                team.pop_back();
                team.push_back(a + 1);
            } else {
                break;
            }
        }
    }
    cout << team.size() << '\n';

    return 0;
}



D - Grid and Magnet (abc351 D)

題目大意

給定一個二維網格,一些格子上有磁鐵,當走到磁鐵格子上下左右的格子後,就動不了了。

問從哪個格子出發,其可以到達的格子數最多。

注意不是一次出發能到達的格子的最大值,是好格子的數量,存在一條路徑到達好格子。

解題思路

考慮樸素的做法,就是\(O((hw)^2)\),即列舉起點,然後 \(BFS\)得到到達的格子的數量。由於 \(h,w \leq 10^3\),這顯然會超時。

\(O((hw)^2)\)中,列舉起點花了 \(O(hw)\),每次 \(BFS\)花了 \(O(hw)\)

考慮列舉起點能否最佳化。會發現有些列舉起點是無用的。

假設磁鐵周圍的格子是終止格子,如果列舉的一個起點是非終止格子,且其左邊也是個非終止格子,那這兩個格子的答案應該是一樣的。因為它們可以相互到達,沒任何影響,所能到達的點的範圍是一樣的。

由此,對於這個非終止格子,和周圍同樣是非終止格子合併在一起,在這個區域內任選一個格子進行一次 \(BFS\),就能得到該格子的答案, 且這也是這個區域內所有非終止格子的答案。

因此預處理出所有的終止格子非終止格子,然後從所有的未被訪問過的非終止格子進行\(BFS\),求得訪問過的格子數量。注意終止格子在不同的\(BFS\)可以重複訪問,但同個 \(BFS\)只能訪問一次,所以 終止格子的訪問狀態在每次\(BFS\)之前要重置,為避免重置訪問狀態帶來的額外開銷,訪問狀態\(visit[i]\)就改為記錄訪問時間 \(time[i]\),這樣根據訪問時間,也能判斷本次 \(BFS\)是否訪問該格子,也免去了重置 \(visit[i]=0\)的開銷。

由於所有非終止格子只會訪問一次,每個終止格子最多隻會訪問三次(一個方向是磁鐵,然後可能有三個方向到達此格子),因此最後的時間複雜度是\(O(hw)\)

神奇的程式碼
#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;
    cin >> h >> w;
    vector<string> a(h);
    for (auto& x : a)
        cin >> x;
    bool empty = false;
    vector<vector<int>> stop(h, vector<int>(w, 0));
    array<int, 4> dx = {1, 0, -1, 0};
    array<int, 4> dy = {0, 1, 0, -1};
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            if (a[i][j] == '#') {
                stop[i][j] = 1;
                for (int k = 0; k < 4; k++) {
                    int ni = i + dx[k];
                    int nj = j + dy[k];
                    if (ni >= 0 && ni < h && nj >= 0 && nj < w) {
                        stop[ni][nj] = 1;
                    }
                }
            }
            if (a[i][j] == '.') {
                empty = true;
            }
        }
    }

    vector<vector<int>> visited(h, vector<int>(w, 0));
    int ans = empty;
    int tt = 0;
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            if (visited[i][j] != 0 || stop[i][j]) {
                continue;
            }
            ++tt;
            queue<pair<int, int>> q;
            q.push({i, j});
            visited[i][j] = tt;
            int cnt = 0;
            while (!q.empty()) {
                auto [x, y] = q.front();
                q.pop();
                cnt++;
                if (stop[x][y]) {
                    continue;
                }
                for (int k = 0; k < 4; k++) {
                    int nx = x + dx[k];
                    int ny = y + dy[k];
                    if (nx >= 0 && nx < h && ny >= 0 && ny < w &&
                        visited[nx][ny] < tt) {
                        visited[nx][ny] = tt;
                        q.push({nx, ny});
                    }
                }
            }
            ans = max(ans, cnt);
        }
    }
    cout << ans << '\n';

    return 0;
}



E - Jump Distance Sum (abc351 E)

題目大意

二維網格,\(n\)個點,一次可以往四個角的方向走。定義 \(dist(p_i, p_j)\) 為從\(p_i \to p_j\)需要的最小步數。若不能到達,則假定步數為\(0\)

\(\sum_{i=1}^{n} \sum_{j=i + 1}^{n} dist(p_i, p_j)\)

解題思路

考慮怎樣的情況是不能到達的。容易發現,一個格子不能到達其上下左右相鄰的格子。

更進一步的,對每個點\((x,y)\),當移動時,觀察 \(x+y\)的變化 ,會發現只有三種情況:\(-2, 0, +2\),無論哪種,其 \(x+y\)的奇偶性不變。因此可以得到 \(x+y\)奇偶性不同的點無法相互到達。相同的點或許可以到達(實際上是可以的)。

先對所有點根據 \(x+y\)的奇偶性分類,考慮同類別的\(p_i, p_j\),其 \(dist\)如何計算。

簡單起見,考慮 \((x_i, y_i) \to (x_j, y_j)\) ,且\(x_j > x_i, y_j > y_i\)。每次移動,對於每個座標上的值,都得 \(+1, -1\),假設 \(x_j - x_i > y_j - y_i\),為了最小步數,那我肯定每次選擇的動作,都會讓 \(x_i + 1\)。至於 \(y_i\),如果每次也 \(y_i + 1\),那最終就超過了 \(y_j\),因此中間需要一些 \(y_i - 1\),來避免超過 \(y_j\)。即一開始先 \((+1, +1)\),當 \(y_i == y_j\)後,就 \((+1, +1)\)\((+1, -1)\)交替,以保持 \(y_i == y_j\)不變,同時 \(x_i \to x_j\)

但這樣最後能到達 \((x_j, y_j)\)嗎?就看是否滿足 \((+1, +1)\)\((+1, -1)\)數量相等。當\(y_i == y_j\)時,還需要執行 \(left = x_j - x_i\)個步驟,如果 \(left\)是偶數,則\((+1, +1)\)\((+1, -1)\)數量相等,可行。事實上,因為\(x_i + y_i\)\(x_j + y_j\)奇偶性相同,並且 \(y_i == y_j\),因此 \(x_i,x_j\)的奇偶性也相同,則 \(x_j - x_i\)必定是偶數,因此也一定能到達 \(x_j, y_j)\)

由上述分析可以得知, \(dist(p_i, p_j) = \max (|x_i - x_j|, |y_i - y_j|)\)

樸素求和就是\(O(n^2)\),棘手在\(\max\)上,注意到上述距離實際上是切比雪夫距離,可以

絕對值去掉的方式比較套路,可以對 \(x_i\)排序,然後按順序遍歷 \(x_i\),這樣 \(x_i - x_j\)就始終是一個符號,但是外層還套了一個 \(\max\)比較棘手。

注意到上述距離實際上是切比雪夫距離,可以將其轉換成曼哈頓距離。得到新點\((\frac{x_i + y_i}{2}, \frac{x_j + y_j}{2})\),然後計算兩點的曼哈頓距離,即兩維度的差的絕對值的和。其中 \(x\)維度和 \(y\)維度是可以分開算的,因此就按照上述絕對值去掉的方式,維護字首和,計算求和即可。

時間複雜度是\(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);
    int n;
    cin >> n;
    array<vector<pair<int, int>>, 2> p{};
    for (int i = 0; i < n; i++) {
        int x, y;
        cin >> x >> y;
        p[(x + y) & 1].push_back({x + y, x - y});
    }
    auto solve_one = [&](vector<int>& x) -> LL {
        sort(x.begin(), x.end());
        LL ans = 0, sum = 0;
        for (int i = 0; i < x.size(); i++) {
            ans += 1LL * x[i] * i - sum;
            sum += x[i];
        }
        return ans;
    };
    auto solve = [&](vector<pair<int, int>>& p) -> LL {
        vector<int> x, y;
        for (auto& [a, b] : p) {
            x.push_back(a);
            y.push_back(b);
        }
        return solve_one(x) + solve_one(y);
    };
    LL ans = solve(p[0]) + solve(p[1]);
    ans /= 2;
    cout << ans << '\n';

    return 0;
}



F - Double Sum (abc351 F)

題目大意

給定\(n\)個數 \(a_i\),計算 \(\sum_{i=1}^{n}\sum_{j=i+1}^{n}\max(a_j-a_i, 0)\)

解題思路

將求和式變一下,即為\(\sum_{j=1}^{n}\sum_{i<j, a_i < a_j} a_j - a_i = \sum_{j=1}^{n} \sum_{i < j, a_i < a_j} a_j - \sum_{i < j, a_i < a_j} a_i\)

即兩個二維偏序問題,一個關於\(i < j, a_i < a_j\)的計數問題,一個關於\(i < j, a_i < a_j\)\(a_i\)求和問題。

二維偏序的解法,假想在一個二維座標系內,就是問一個矩形的和。

可透過迴圈滿足一個不等式關係(即滿足一維),再用一個資料結構維護另一個不等式關係(維護另一維)求和。

以第一個偏序問題為例,即列舉下標\(j\),把小於 \(j\)的加入到資料結構中(滿足了 \(i < j\)),然後在滿足小於\(a_j\)範圍求和,此為一個區間查詢操作。故可以建一棵樹狀陣列或線段樹維護此查詢操作。其下標的含義是\(a_j\)而不是 \(j\)。由於這裡的 \(a_j\)高達 \(10^8\),但數量不超過 \(10^5\),因此需要事先離散化。

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

// starting from 1
template <typename T> class fenwick {
  public:
    vector<T> fenw;
    int n;

    fenwick(int _n) : n(_n) { fenw.resize(n); }

    inline int lowbit(int x) { return x & -x; }

    void modify(int x, T v) {
        for (int i = x; i < n; i += lowbit(i)) {
            fenw[i] += v;
        }
    }

    T get(int x) {
        T v{};
        for (int i = x; i > 0; i -= lowbit(i)) {
            v += fenw[i];
        }
        return v;
    }
};

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vector<int> a(n);
    set<int> candidate;
    for (auto& x : a) {
        cin >> x;
        candidate.insert(x);
    }
    map<int, int> rank;
    for (auto x : candidate) {
        rank[x] = rank.size() + 1;
    }

    fenwick<LL> presum(rank.size() + 1), cnt(rank.size() + 1);
    LL ans = 0;
    for (int i = 0; i < n; ++i) {
        int pos = rank[a[i]];
        ans += 1ll * a[i] * cnt.get(pos) - presum.get(pos);
        cnt.modify(pos, 1);
        presum.modify(pos, a[i]);
    }
    cout << ans << '\n';

    return 0;
}



G - Hash on Tree (abc351 G)

題目大意

給定一棵有根樹,根是\(1\)。點有權值\(a_i\),定義樹的權值為\(f(1)\)

\(f\)的計算方式為:

  • \(i\)是葉子,則 \(f(i) = a_i\)
  • 否則, \(f(i) = a(i) + \prod_{j \in son(i)} f(j)\)

執行\(q\)次操作,每次操作修改一個點的權值,回答修改後的樹的權值,即\(f(1)\)

解題思路

<++>

神奇的程式碼