AtCoder Beginner Contest 359

~Lanly~發表於2024-06-23

A - Count Takahashi (abc359 A)

題目大意

給定\(n\)個字串,問有多少個字串是Takahashi

解題思路

注意判斷比較即可。

神奇的程式碼
#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;
    int ans = 0;
    while (n--) {
        string s;
        cin >> s;
        ans += s == "Takahashi";
    }
    cout << ans << '\n';

    return 0;
}



B - Couples (abc359 B)

題目大意

給定\(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;
    n *= 2;
    vector<int> a(n);
    for (auto& x : a)
        cin >> x;
    int ans = 0;
    for (int i = 1; i < n - 1; ++i) {
        ans += a[i - 1] == a[i + 1];
    }
    cout << ans << '\n';

    return 0;
}



C - Tile Distance 2 (abc359 C)

題目大意

給定一個座標系,有格子,如下:

格子

給定起點和終點,問從起點到終點,要穿過多少次藍線。

解題思路

觀察上述格子,可以發現在\(y\)軸移動,每移動一次,必定穿過一次藍線。

由於每行格子交錯排列的,每往上走一個,我左右可走的區間都擴大了\(1\)。比如我在\((5,0)\),我可以左邊往上走到\((3,1) \to (5,1)\)的格子,也可以右邊往上走到\((5,1) \to (7,1)\)

這樣,原本我左右走的橫座標區間是\([4,6)\),往上走一格後,橫座標區間擴大為\([3,7)\),往上走\(n\)格,可到達的橫座標區間範圍為\([4-n, 6+n)\),只要我終點的橫座標在這區間,那我就可以只花費\(y\)軸移動的代價就抵達終點了。而如果不在這個區間,那就再左右移動,每移動一次,橫座標區間就變動\(2\)

容易發現這樣移動一定是最優的。\(y\)軸移動的藍線穿過不可避免,然後\(x\)軸的藍線穿過已經儘可能在移動\(y\)軸時避免了。

神奇的程式碼
#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);
    LL sx, sy, tx, ty;
    cin >> sx >> sy >> tx >> ty;
    LL dy = abs(sy - ty);
    LL odd = (sx & 1);
    LL l = sx - odd + (sy & 1) * (odd ? 1 : -1);
    LL r = l + 2;
    l -= dy, r += dy;
    LL ans = dy + max(0ll, l - tx + 1) / 2 + max(0ll, tx - r + 2) / 2;
    cout << ans << '\n';

    return 0;
}



D - Avoid K Palindrome (abc359 D)

題目大意

給定一個包含AB?的字串\(s\),將?變成AB,問有多少種情況,使得\(s\)沒有長度為\(k\)的迴文子串。

解題思路

注意\(k \leq 10\)

從左到右考慮每個字元,如果當前是?,則考慮其變為A,B,是否出現長度為\(k\)的迴文串。

我們需要知道該?\(k-1\)位的情況,加上該字母,就可以判斷出新增的子串是不是迴文串。

即設\(dp[i][j]\)表示考慮前\(i\)位字元,其中?都已經替換成AB後,且後\(9\)位的字元狀態為\(j\)(因為只有AB兩種,可以編碼成01,用二進位制壓縮表示)。

然後考慮當前位的情況,如果取值為A\(0\),則判斷\(j << 1\)狀態是不是迴文串,不是的話則有\(dp[i+1][(j<<1) \& mask] += dp[i][j]\),否則就狀態非法,不轉移。因為\(j<<1\)是後\(10\)個字元的狀態資訊,而\(j\)的含義是後\(9\)位,所以\(\& mask\)是把第\(10\)位去掉。

同理,取值為\(B\)的話,即\(1\),則判斷\((j << 1) | 1\)是不是迴文串,不是的話就轉移,否則不轉移。

可以事先預處理每個狀態是否是迴文串,然後當\(i \geq k\)時再考慮轉移的合法性。

神奇的程式碼
#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, k;
    string s;
    cin >> n >> k >> s;
    int up = (1 << k);
    vector<int> p(up);
    for (int i = 0; i < up; i++) {
        vector<int> bit(k);
        int num = i;
        for (int j = 0; j < k; j++) {
            bit[j] = (num & 1);
            num >>= 1;
        }
        auto rev = bit;
        reverse(rev.begin(), rev.end());
        p[i] = rev == bit;
    }

    up = 1 << (k - 1);
    int mask = up - 1;
    vector<int> dp(up, 0);
    dp[0] = 1;
    for (int i = 0; i < n; ++i) {
        int chr = s[i];
        vector<int> dp2(up, 0);
        for (int j = 0; j < up; j++) {
            if (chr == '?') {
                if (i + 1 < k || !p[j << 1]) {
                    int nxt = (j << 1) & mask;
                    dp2[nxt] = (dp2[nxt] + dp[j]) % mo;
                }
                if (i + 1 < k || !p[j << 1 | 1]) {
                    int nxt = (j << 1 | 1) & mask;
                    dp2[nxt] = (dp2[nxt] + dp[j]) % mo;
                }
            } else {
                if (i + 1 < k || !p[j << 1 | (chr - 'A')]) {
                    int nxt = (j << 1 | (chr - 'A')) & mask;
                    dp2[nxt] = (dp2[nxt] + dp[j]) % mo;
                }
            }
        }
        dp.swap(dp2);
    }
    LL ans = 0;
    for (int i = 0; i < up; i++) {
        ans = (ans + dp[i]) % mo;
    }
    cout << ans << '\n';
    return 0;
}



E - Water Tank (abc359 E)

題目大意

給定柱子長度。然後如下如所示。

example

每一時刻,\(0\)位會多一高度的水,如果該水高度高過柱子,且高過\(1\)位的水高度,則該高度的水會跑到\(1\)位,同理繼續判斷\(1\)位,該水是否跑到\(2\)位。

問每一位出現水的最早時刻。

解題思路

  • 考慮\(1\)位,其答案就是第一根柱子高度\(3(a_1)+1\)

  • 考慮\(2\)位,需要\(0\)位水高\(3\)\(1\)位水高\(1\),答案就是\(3+1+1\)

  • 考慮\(3\)位,則需要\(0,1,2\)的水高均為\(4\),答案就是\(4+4+4+1\)

  • 考慮\(4\)位,則需要\(0,1,2\)水高\(4\)\(3\)位水高\(1\),答案就是\(4+4+4+1+1\)

  • 考慮\(5\)位,則需要\(0,1,2,3,4,\)位水高\(5\),答案就是\(5+5+5+5+5+1\)

觀察上述例子的求解過程,如果要求第\(i\)位的答案,則要求第\(i-1\)裝滿,裝滿的意思就是和柱子\(a_i\)高度同高,而同高會連帶著\(i-2,i-3,...\)位同高,但需要多少位呢?觀察上述會發現,假設前面比柱子\(a_i\)還高的柱子是\(a_j\),那麼\(j,j+1,...,i-1\)位的水高都必須是\(a_i\)

因此,求解第\(i\)位的答案,則需要\(i-1,i-2,...,j\)位與\(a_i\)同高,然後\(j-1,j-2,...,k\)\(a_j\)同高,然後\(k-1,k-2,...\)\(a_k\)同高,其中\(a_i \leq a_j \leq a_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;
    cin >> n;
    vector<int> h(n + 1);
    for (int i = 1; i <= n; ++i)
        cin >> h[i];
    h[0] = 1e9 + 8;
    vector<int> hei;
    hei.push_back(0);
    LL ans = 0;
    for (int i = 1; i <= n; ++i) {
        while (!hei.empty() && h[hei.back()] <= h[i]) {
            ans -= h[hei.back()] * (LL)(hei.back() - hei[hei.size() - 2]);
            hei.pop_back();
        }
        ans += h[i] * (LL)(i - hei.back());
        hei.push_back(i);
        cout << ans + 1 << " \n"[i == n];
    }

    return 0;
}



F - Tree Degree Optimization (abc359 F)

題目大意

給定\(n\)個點的點權\(a_i\),構造一棵樹,使得\(\sum_{i=1}^{n} d_i^2a_i\)最小,其中\(d_i\)表示點\(i\)的度。

解題思路

由於是一棵樹,則有\(\sum d_i = 2n-2, 1 \leq d_i \leq n - 1\)

對於任意滿足上述條件的\(d_i\),都可以構造出對應的樹,使得每個點的度數都是\(d_i\)。(構造方法為,每次選擇度數為1和非1的點連邊,然後更新剩餘度數,歸納可證)

那剩下就是如何分配這些度數。

如果給點\(1\)分配一個度,是其\(d_1 = 1 \to 2\),則代價是\(4a_1 - a_1\),而如果是\(d_1 = 2 \to 3\),則代價是\(9a_1 - 4a_1\)

這可以把問題抽象成每個點起始度數為\(1\),然後把剩下的\(n-2\)個度分配給每個點,使得代價最小,每次僅分配\(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);
    int n;
    cin >> n;
    vector<int> a(n);
    for (auto& x : a)
        cin >> x;
    priority_queue<pair<LL, int>, vector<pair<LL, int>>, greater<pair<LL, int>>>
        q;
    LL ans = accumulate(a.begin(), a.end(), 0ll);
    for (int i = 0; i < n; i++) {
        q.push({a[i] * 3ll, 2});
    }
    for (int i = 0; i < n - 2; i++) {
        auto [x, y] = q.top();
        q.pop();
        ans += x;
        if (y < n - 1) {
            LL ori = x / (2 * y - 1);
            LL nxt = ori * (2 * y + 1);
            q.push({nxt, y + 1});
        }
    }
    cout << ans << '\n';

    return 0;
}



G - Sum of Tree Distance (abc359 G)

題目大意

給定一棵樹,點有點權\(a_i\)。求\(\sum_i \sum_j f(i,j)\),其中\(a_i == a_j\)\(f(i,j)\)表示點\(i \to j\)的距離,邊權為\(1\)

解題思路

距離的最終來源是邊數,考慮每條邊被算入了多少次,即對答案貢獻的次數。

\(\sum_i \sum_j f(i,j) = \sum_e sum_e\),其中\(sum_e\)表示邊\(e\)對答案貢獻的次數,考慮該次數怎麼算。

考慮邊\((u,v)\),將該樹分成了兩個連通塊,如果這兩個連通塊各有一點\(i,j\),其\(a_i == a_j\),那麼從點\(i \to j\)必定經過該邊,因此需要統計每個點權,在兩個連通塊的出現次數,其乘積的和則是該邊的貢獻。

問題就變成了統計一個子樹裡,各個點權的出現次數\(cc_i\),事先預處理每個點權的出現次數\(cnt_i\),對點權求和,即\(\sum cc_i \times (cnt_i - cc_i)\)就是該邊對答案的貢獻。

由於點權是稀疏的,用map來維護出現次數,合併兒子之間的map,採用啟發式合併,即用數量少的合併到數量大的,這樣每次合併最壞的複雜度是\(O(\frac{n}{2})\),而最壞的情況最多隻有\(O(\log n)\)次(每一次最壞情況,合併後的點數會翻倍,最多翻倍\(O(\log n)\)次。

合併的時候,計算邊貢獻的式子,\(\sum cc_i \times (cnt_i - cc_i)\)只有一項發生變化,可以動態\(O(1)\)維護出更新後的貢獻\(sum\)

最終的時間複雜度就是\(O(n \log^2 n)\),一個\(\log\)是啟發式合併,另一個\(\log\)map

程式碼裡的\(sum\)是考慮父親邊\(u \to fa\)對答案的貢獻。由於返回型別是\(pair\),用\(map\)構造\(pair\)會複製構造造成巨大的效能損失,用\(move\)函式進行移動構造。或者返回值僅為\(map\),編譯器也會最佳化成移動構造。

神奇的程式碼
#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>> edge(n);
    for (int i = 0; i < n - 1; i++) {
        int u, v;
        cin >> u >> v;
        u--;
        v--;
        edge[u].push_back(v);
        edge[v].push_back(u);
    }
    vector<int> a(n);
    vector<int> cnt(n);
    for (auto& x : a) {
        cin >> x;
        --x;
        cnt[x]++;
    }
    LL ans = 0;
    auto dfs = [&](auto& dfs, int u, int fa) -> pair<map<int, int>, LL> {
        map<int, int> cc;
        LL sum = 0;
        for (auto v : edge[u]) {
            if (v == fa)
                continue;
            auto&& [son_ret, son_sum] = dfs(dfs, v, u);
            if (son_ret.size() > cc.size()) {
                swap(son_ret, cc);
                swap(son_sum, sum);
            }
            for (auto& [k, v] : son_ret) {
                sum -= 1ll * cc[k] * (cnt[k] - cc[k]);
                cc[k] += v;
                sum += 1ll * cc[k] * (cnt[k] - cc[k]);
            }
        }
        sum -= 1ll * cc[a[u]] * (cnt[a[u]] - cc[a[u]]);
        cc[a[u]]++;
        sum += 1ll * cc[a[u]] * (cnt[a[u]] - cc[a[u]]);
        ans += sum;
        return {move(cc), sum};
    };
    dfs(dfs, 0, 0);
    cout << ans << '\n';

    return 0;
}