AtCoder Beginner Contest 356

~Lanly~發表於2024-06-01

A - Subsegment Reverse (abc356 A)

題目大意

給定一個 \(1,2,3,...,n\)的排列\(a\),給定兩個數 \(l,r\),左右顛倒\(a[l..r]\)。輸出。

解題思路

按照題意模擬即可。

神奇的程式碼
#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, b;
    cin >> n >> a >> b;
    --a;
    vector<int> ans(n);
    iota(ans.begin(), ans.end(), 1);
    reverse(ans.begin() + a, ans.begin() + b);
    for (auto x : ans)
        cout << x << ' ';
    cout << '\n';

    return 0;
}



B - Nutrients (abc356 B)

題目大意

給定一天\(n\)種營養的攝入目標量。

給定\(m\)種食物的\(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(m);
    for (auto& x : a)
        cin >> x;
    while (n--) {
        for (auto& x : a) {
            int s;
            cin >> s;
            x -= s;
        }
    }
    bool ok = true;
    for (auto x : a) {
        ok &= x <= 0;
    }
    if (ok) {
        cout << "Yes" << '\n';
    } else {
        cout << "No" << '\n';
    }

    return 0;
}



C - Keys (abc356 C)

題目大意

\(n\)把鑰匙,有些真的,有些假的。一個門,可以拿一些鑰匙開啟它,若其中有 \(k\)個真的鑰匙,則門開啟。

給定了 \(m\)條記錄,表示用了哪些鑰匙,門是否開啟。

對於這\(n\)把鑰匙的真假,共 \(2^n\)種情況,問有多少種情況,不違反上述的記錄。

解題思路

\(n\)只有 \(15\),直接 \(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, k;
    cin >> n >> m >> k;
    vector<pair<vector<int>, int>> a(m);
    for (auto& [v, c] : a) {
        int x;
        cin >> x;
        v.resize(x);
        for (auto& x : v) {
            cin >> x;
            --x;
        }
        string s;
        cin >> s;
        c = s[0] == 'o';
    }
    int ans = 0;
    int up = (1 << n);
    for (int i = 0; i < up; ++i) {
        bool ok = true;
        for (auto& [v, c] : a) {
            int cnt = 0;
            for (auto x : v) {
                if (i & (1 << x)) {
                    ++cnt;
                }
            }
            if ((cnt >= k) ^ c) {
                ok = false;
                break;
            }
        }
        ans += ok;
    }
    cout << ans << '\n';

    return 0;
}



D - Masked Popcount (abc356 D)

題目大意

給定\(n,m\),求 \(\sum_{k=0}^{n}popcount(k\&m)\)

\(popcount(x)\)表示 \(x\)二進位制下 \(1\)的個數。

解題思路

\(n\)高達 \(10^{18}\),直接列舉會超時。

考慮貢獻轉換。

考慮到答案來自於二進位制下\(1\)的個數。 由於\(\&\)運算的特性,這些實際都是來自於\(m\)的每一個\(1\)。 我們需要考慮\(m\)二進位制下每一個 \(1\)對答案的貢獻,即有多少個 \(k\),使得 \(k\&m\)後該位是 \(1\)

假設\(m\)的二進位制表示為 \(1...10...0\),考慮第 \(i\)位上的\(1\),思考有多少個 \(k\),使得 \(k\&m\)的第\(i\)位是 \(1\)

考慮如何計算 \(k\)的數量,首先, \(k\)的第 \(i\)位一定是 \(1\),然後就剩下低位高位的情況數。低位就是低於\(i\)位的那些位數的取值,高於 \(i\)位的就是高於 \(i\)位的位數取值。這個計數問題其實和數位\(dp\)差不多。

由於\(k\)有最大值 \(n\)的限制,低位的情況數會依賴於高位 ,為方便表述,設高位是\(up\),當前位是 \(middle\),低位是 \(down\),比如 \(n=110101\),當前第 \(i=2\)位(從 \(0\)開始),則 \(up=110, middle=1, down=01\)。然後情況數其實就分兩種:

  • 如果高位取值和\(n\)高位不一致,則低位取值沒有限制,因此低位的情況數是\(2^i\) ,而高位的情況數是\(up\),若此時 \(m\)的第 \(i\)位是 \(1\),則其貢獻(出現的次數)為 \(2^i \times up\)
  • 如果高位取值和\(n\)高位一致,則當前位低位的都會收到\(n\)的限制。如果此時 \(middle=0\),則說明該位不能取 \(1\),則 \(m\)的第 \(i\)位沒有貢獻。否則 \(middle=1\)低位的情況數就有 \(down+1\)種情況,而高位的 情況數只有\(1\)種,若此時 \(m\)的第 \(i\)位是 \(1\),則其貢獻(出現的次數)為 \(down + 1\)

綜上,考慮第\(i\)位,其中 \(n\)的高位是 \(up\),當前位是 \(middle\),低位是 \(down\),若 \(m\)的第 \(i\)位是 \(1\),則其對答案的貢獻(滿足 \(k\&m\)的第 \(i\)位是 \(1\)\(k\)的個數)為 \(2^i \times up + middle \times (down + 1)\)

神奇的程式碼
#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);
    LL n, m;
    cin >> n >> m;
    LL ans = 0;
    LL up = n >> 1, middle = n & 1, down = 0, cnt = 1;
    for (int i = 0; i < 60; ++i) {
        if ((m >> i) & 1) {
            ans += 1ll * up * cnt % mo + middle * (down + 1);
            ans %= mo;
        }
        down = down | (middle << i);
        middle = up & 1;
        up >>= 1;
        cnt <<= 1;
    }
    cout << ans << '\n';

    return 0;
}



E - Max/Min (abc356 E)

題目大意

給定一個陣列\(a\),求 \(\sum_{i=1}^{n}\sum_{j=i+1}^{n} \lfloor \frac{\max(a_i, a_j)}{\min(a_i, a_j)} \rfloor\)

解題思路

作除法始終是最大值除以最小值,因此可以先對\(a\)進行排序再計算,這不會影響答案。

\(a\)從小到大排序後,考慮列舉 \(j\),然後計算 \(i < j\)情況對答案的貢獻,即列舉最大值。

一個比較明顯的觀察是,可能會有若干個 \(a_i\),使得 \(\lfloor \frac{a_j}{a_i} \rfloor\)是同樣的值。那我們可以把這些值合併地來算,即\(\times\)個數

事實上可以透過數論分塊來求解,\(\lfloor \frac{a_j}{a_i} \rfloor\)的值的可能數量和因子個數同一個數量級,即\(O(\sqrt{a_i})\)個,而造成該取整結果的\(a_i\)的取值就是數論分塊裡的兩個邊界 \(l,r\),可以在 \(a\)中二分得到對應位置,因而得到個數。而這複雜度是\(O(n\log n\sqrt{a_i})\),會超時。

數論分塊複雜度是根號級別,在這裡用不了,只能另尋它路。

這次考慮列舉 \(i\),然後計算 \(i < j\)情況對答案的貢獻,即列舉最小值。

同樣考慮貢獻轉換,原本的想法是求\(\lfloor \frac{a_j}{a_i} \rfloor = val\)\(a_j\)數量\(cnt\),然後累計\(val \times cnt\)

我們將其\(val\)看成\(1+1+1...\),然後重組一下,變成求 \(\lfloor \frac{a_j}{a_i} \rfloor \geq val\)\(a_j\)數量\(cnt\)

即原本是求\(\lfloor \frac{a_j}{a_i} \rfloor = 1,2,3\)\(a_j\)數量,現在求\(\lfloor \frac{a_j}{a_i} \rfloor \geq 1,2,3\)\(a_j\)數量。

這裡的貢獻轉換就是將\(\lfloor \frac{a_j}{a_i} \rfloor = 3\)對答案有\(3\)的貢獻,拆成了 \(1+1+1\),即\(\lfloor \frac{a_j}{a_i} \rfloor \geq 1 + \lfloor \frac{a_j}{a_i} \rfloor \geq 2 + \lfloor \frac{a_j}{a_i} \rfloor \geq 3\),然後合併所有的\(\lfloor \frac{a_j}{a_i} \rfloor \geq 1\)(或\(2,3\)),求其個數。

現在我們的視角轉換成求 \(\lfloor \frac{a_j}{a_i} \rfloor \geq v\)\(a_j\)數量,即列舉\(v\),求\(a_j \geq va_i\)\(j\)的數量。很明顯透過在\(a\)陣列二分\(va_i\),就能求得\(j\)的數量了。

由於\(max_a = 10^6\),那麼這裡列舉的\(v\)的數量就是\(O(\sum_{i=1}^{n} \frac{max_a}{a_i})\)。每次列舉都有一個二分的\(O(\log n)\),因此總的時間複雜度是\(O(\sum_{i=1}^{n} \frac{max_a}{a_i} \log n)\)

如果所有的\(a_i\)都很小,比如都是 \(a_i=1\),那時間複雜度就是 \(O(nmax_a \log n)\),又炸了!

但看這個式子,非常像對數求和,如果\(a_i\)唯一,那複雜度就是\(O(\sum_{i=1}^{max_a} \frac{max_a}{i} \log n) = O(max_a \log max_a \log n)\),是可過的。

因此,如果\(a_i\)有重複的,那麼我們就合併重複的數,並記錄\(cnt_k\)表示 \(a_i=k\)的數量,然後考慮怎麼修正一下答案的計算。

合併相同的數,並從小到大排序,然後列舉當前的 \(a_i\),再列舉 \(v\),求得第一個\(a_j > va_i\)\(a_j\),那麼此時\(\lfloor \frac{a_j}{a_i} \rfloor \geq v\)的數量就是\(cnt_{a_i} \times \sum_{k \geq j} cnt_{a_k}\)。而後者\(\sum_{k=j}^{n} cnt_{a_j}\)就是一個關於\(cnt\)陣列的字尾和,預處理一下就能\(O(1)\)得到。

然後對於相同數之間的貢獻,則是\(\sum_{i} \frac{cnt_{a_i} \times (cnt_{a_i} - 1)}{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 n;
    cin >> n;
    vector<int> r(n);
    for (auto& x : r)
        cin >> x;
    map<int, int> s;
    for (auto& i : r)
        s[i]++;
    vector<int> a;
    vector<int> sum;
    for (auto& [x, y] : s) {
        a.push_back(x);
        sum.push_back(y);
    }
    vector<int> suf(sum.size());
    partial_sum(sum.rbegin(), sum.rend(), suf.rbegin(), plus<int>());
    LL ans = 0;
    n = a.size();
    for (int i = 0; i < n; ++i) {
        int x = a[i];
        int pos = i + 1;
        ans += 1ll * sum[i] * (sum[i] - 1) / 2;
        while (pos < n) {
            pos = lower_bound(a.begin() + pos, a.end(), x) - a.begin();
            if (pos < n)
                ans += 1ll * sum[i] * suf[pos];
            x += a[i];
        }
    }
    cout << ans << '\n';

    return 0;
}



F - Distance Component Size Query (abc356 F)

題目大意

<++>

解題思路

<++>

神奇的程式碼



G - Freestyle (abc356 G)

題目大意

<++>

解題思路

<++>

神奇的程式碼