AtCoder Beginner Contest 373

~Lanly~發表於2024-10-02
省流版
  • A. 暴力即可
  • B. 求出字母位置,絕對值相加即可
  • C. 顯然答案為兩個陣列的最大值的和
  • D. 注意直接BFS的點權範圍不超過題目範圍,直接BFS即可
  • E. 發現單調性,二分票數,用字首和\(O(1)\)判斷可行性即可
  • F. 樸素揹包DP,相同重量的物品一起考慮,用優先佇列求解\(l\)個相同重量物品最大價值的最優取法即可

A - September (abc373 A)

題目大意

給定\(12\)個字串,問第 \(i\)個字串的長度是不是 \(i\)

解題思路

按照題意依次判斷即可,python可以一行。

神奇的程式碼
print(sum(i + 1 == len(input().strip()) for i in range(12)))



B - 1D Keyboard (abc373 B)

題目大意

給定一個鍵盤,從左到右的\(26\)個字母分別是什麼。

現在依次輸入abcdefgh...xyz,初始手指在a處,然後要移動到b處按b,然後移動到c處按c...

問輸入完這\(26\)個字元需要移動的距離數。

解題思路

當前手指在位置\(x\),移動到下一個字母的位置 \(y\) ,距離數是\(|x-y|\),求出 \(pos[i]\)表示字元 \(i\)的位置,答案就是 \(|pos[i] - pos[i-1]|\)的累加。

神奇的程式碼
#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> pos{};
    for (int i = 0; i < 26; ++i)
        pos[s[i] - 'A'] = i;
    int ans = 0;
    for (int i = 1; i < 26; ++i)
        ans += abs(pos[i] - pos[i - 1]);
    cout << ans << '\n';

    return 0;
}



C - Max Ai+Bj (abc373 C)

題目大意

給定兩個陣列\(a,b\),分別從中選一個數,使得和最大。

解題思路

顯然選的兩個數分別為最大的數即可。python可以兩行。

神奇的程式碼
input()
print(max(map(int, input().split())) + max(map(int, input().split())))


D - Hidden Weights (abc373 D)

題目大意

給定一張有向圖,邊有邊權。

確定點權\(x_i\),滿足對於每一條有向邊 \(u_i \to v_i, w_i\),滿足 \(x_{v_i} - x_{u_i} = w_i\)

題目保證有解。

解題思路

視為無向圖,但反過來的邊權是\(-w_i\),然後從未訪問過的點開始\(BFS\),其點權為\(0\),然後按照上述等式得到周圍點的點權即可。

點數\(n \leq 2 \times 10^5\),邊權 \(\leq 10^9\),所以點權不會超過\(2 \times 10^{14}\),滿足題意範圍的 \(10^{18}\)

神奇的程式碼
#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<vector<array<int, 2>>> edge(n);
    for (int i = 0; i < m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        --u, --v;
        edge[u].push_back({v, w});
        edge[v].push_back({u, -w});
    }
    vector<LL> ans(n);
    queue<int> team;
    vector<int> vis(n);
    auto BFS = [&](int st) {
        team.push(st);
        vis[st] = 1;
        while (!team.empty()) {
            int u = team.front();
            team.pop();
            for (auto& [v, w] : edge[u]) {
                if (vis[v])
                    continue;
                ans[v] = ans[u] + w;
                vis[v] = 1;
                team.push(v);
            }
        }
    };
    for (int i = 0; i < n; ++i)
        if (!vis[i])
            BFS(i);
    for (int i = 0; i < n; ++i)
        cout << ans[i] << " \n"[i == n - 1];

    return 0;
}



E - How to Win the Election (abc373 E)

題目大意

\(n\)個候選人, \(k\)張票。最終票數最多的\(m\)個候選人獲勝,同票數的都會獲勝,因此可能獲勝的可能會超過 \(m\)個 。

現已知部分投票情況。 問每一個候選人,需要給他至少多少張票,才能使得他一定會獲勝,即無論剩餘票數如何分配,他始終都是票數前\(m\)多的。

解題思路

對於當前第\(i\)個候選人,他的票數\(v_i\)目前排第 \(rank_i\)名,那他至少還要多少票才能穩贏呢?

最樸素的想法就是,列舉這個票數,即假設他得到了\(x\)票,看看能否穩贏? (如果你沒想到二分,可能是因為這個最樸素的做法沒想到——透過增設條件來解決問題,從這個條件其實是很容易看出來具有單調性,進而可以二分)

那能否穩贏呢?假設他得到了\(x\)票,那此時的票數是 \(v_i + x\),透過二分可以得到該票數的排名,假設是\(r_i\)名。然後還剩下\(left\)張票沒投。

  • 如果 \(r_i > m\),那他必不可能贏了。
  • 如果 \(r_i \leq m\),即此時他前面只有 \(r_i - 1\)個人,剩下的票分配給其他人,只要少於 \(danger = m - r_i + 1\)個人的票數超過他,那他就能贏。

考慮最壞情況,即為票數\(\leq v_i + x\)的最大的\(danger\)個人的票數達到\(v_i + x + 1\)所需要的票數,少於剩餘未投的票數 \(left\),那第 \(i\)個人一定是票數前 \(m\) 大的,即穩贏。而前者的票數計算相當於是\(danger \times (v_i + x + 1) - \sum v_j\) ,事先對\(v\)排序,後者其實就是一個區間和,可以用字首和最佳化,在 \(O(1)\)的時間內得到。

到此,我們可以 \(O(1)\)判斷當前列舉的票數 \(x\)是不是穩贏的。但列舉的票數的範圍有 \(O(k)\),但容易發現該票數是否穩贏之間具有單調性——給越多票,穩贏的可能性越高。

因此我們不必一個一個列舉票數,而是二分該票數,然後 透過字首和,\(O(1)\) 判斷是否可行即可。

對所有人都這麼求一遍,總的時間複雜度就是\(O(n \log 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;
    LL k;
    cin >> n >> m >> k;
    vector<LL> a(n);
    for (auto& i : a)
        cin >> i;
    vector<int> id(n);
    iota(id.begin(), id.end(), 0);
    sort(id.begin(), id.end(), [&](int i, int j) { return a[i] > a[j]; });
    vector<LL> sum(n);
    sum[0] = a[id[0]];
    for (int i = 1; i < n; ++i)
        sum[i] = sum[i - 1] + a[id[i]];
    vector<LL> ans(n);
    LL left = k - accumulate(a.begin(), a.end(), 0ll);
    auto get_sum = [&](int l, int r, int ex) {
        if (l > r)
            return 0ll;
        LL s = 0;
        if (l <= ex && ex <= r) {
            r += 1;
            s -= a[id[ex]];
        }
        s += sum[r] - (l ? sum[l - 1] : 0);
        return s;
    };
    auto solve = [&](int pos, int rank) {
        auto check = [&](LL votes) {
            LL now_votes = a[pos] + votes;
            int now_rank = lower_bound(id.begin(), id.end(), now_votes,
                                       [&](int x, LL v) { return a[x] > v; }) -
                           id.begin();
            int left_candi = m - now_rank;
            LL demand = left_candi * (now_votes + 1) -
                        get_sum(now_rank, now_rank + left_candi - 1, rank);
            LL ano = left - votes;
            return ano < demand;
        };

        LL l = -1, r = left;
        while (l + 1 < r) { //(l, r]
            LL mid = (l + r) >> 1;
            if (check(mid))
                r = mid;
            else
                l = mid;
        }
        return check(r) ? r : -1;
    };
    for (int i = 0; i < n; ++i) {
        ans[id[i]] = solve(id[i], i);
    }
    for (int i = 0; i < n; ++i)
        cout << ans[i] << " \n"[i == n - 1];

    return 0;
}



F - Knapsack with Diminishing Values (abc373 F)

題目大意

\(n\)種物品,重量 \(w_i\),價值\(v_i\),無限個。

揹包容量 \(W\),現在選物品放入揹包,不超揹包容量,價值最大。

當第 \(i\)種物品放 \(k\)個時,其價值為 \(kv_i - k^2\)

解題思路

考慮最樸素的揹包做法,即\(dp[i][j]\)表示考慮前 \(i\)個物品,放的容量是 \(j\)的最大價值。轉移即考慮當前物品放多少個,其時間複雜度為 \(O(nw^2)\)。此處 \(n \leq 3000\),無法透過。

究其複雜度,主要在考慮每種物品,其重量是\(w_i\),我們需要考慮該物品的數量為 \(1,2,...,\lfloor \frac{W}{w_i} \rfloor\) 。如果每個物品的重量都挺小的,那麼物品數量的數量級就是\(O(W)\)。轉移的複雜度就是 \(O(W)\)

怎麼辦呢?對於重量都小的,我們能否一起考慮呢?

即我們不依次考慮每種物品,而是依次考慮重量為\(i\)的所有物品,即設 \(dp[i][j]\)表示考慮重量\(\leq i\)的物品,放的容量是 \(j\)的最大價值。這樣考慮的物品數量就是\(O(\frac{W}{i})\),對所有的\(i\)的轉移複雜度累加,其複雜度就是 \(O(w^2 \log w)\)

複雜度對了,考慮如何轉移呢,即\(dp[i][j]\),然後列舉選重量為 \(i\)的物品的數量個數 \(l\),假設其物品的價值分別是 \(v_1,v_2,v_3,...\),現在要取 \(l\)個,怎麼取最優?

由於每種物品的價值,隨著取的個數的增加,其價值會減少,最樸素的想法就是一個一個取。

考慮每種物品是一行,從左到右的物品的價值依次減少,即設\(f_i(k) = kv_i - k^2\),第一行第一個物品的價值是\(f_1(1)\),第二個物品的價值就是\(f_1(2) - f_1(1)\),第三個物品的價值就是 \(f_1(3) - f_1(2)\),而第二行的第一個物品的價值就是\(f_2(1)\)

上述怎麼取的問題就變成,每行取一個字首的物品,一共 \(l\)個,價值最大。

我們就考慮每行的第一個未取的物品,每次就取這些行裡,價值最大的。如何快速找到這些的最大值,用優先佇列維護即可,佇列裡的元素最多也就\(O(n)\)

由於取物品數量\(l\)是從 \(1\)開始列舉的,因此求最優的取法就這樣依次迭代,從優先佇列裡取價值最大的物品即可。

總的時間複雜度是 \(O(w^2 \log w \log n)\)

神奇的程式碼
#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, w;
    cin >> n >> w;
    vector<vector<int>> a(w + 1);
    for (int i = 0; i < n; i++) {
        int w, v;
        cin >> w >> v;
        a[w].push_back(v);
    }
    vector<LL> dp(w + 1, -inf);
    dp[0] = 0;
    auto f = [&](int k, int v) { return 1ll * k * v - k * k; };
    auto get_val = [&](int k, int v) { return f(k, v) - f(k - 1, v); };
    for (int i = 0; i <= w; i++) {
        if (!a[i].empty()) {
            vector<LL> dp2 = dp;
            for (int j = 0; j <= w; j++) {
                priority_queue<pair<LL, pair<int, int>>> q;
                for (auto x : a[i]) {
                    q.push({get_val(1, x), {1, x}});
                }
                LL sum = 0;
                for (int l = i; j + l <= w; l += i) {
                    auto [val, p] = q.top();
                    auto [k, v] = p;
                    q.pop();
                    sum += val;
                    dp2[j + l] = max(dp2[j + l], dp[j] + sum);
                    q.push({get_val(k + 1, v), {k + 1, v}});
                }
            }
            dp.swap(dp2);
        }
    }
    LL ans = *max_element(dp.begin(), dp.end());
    cout << ans << '\n';

    return 0;
}



G - No Cross Matching (abc373 G)

題目大意

二維平面,給定兩組點\(p,q\)\(n\)個,打亂 \(q\)的順序,使得\(n\)個線段 \(p_i \to q_i\)互不相交。

給定一種 \(q\)的順序或告知不可能。

解題思路

<++>

神奇的程式碼