AtCoder Beginner Contest 357

~Lanly~發表於2024-06-09

A - Sanitize Hands (abc357 A)

題目大意

給定\(m\)個物品。

依次來 \(n\)個人,每個人拿\(a_i\)個物品。

問有幾個人可以拿走所需物品。

解題思路

求一遍字首和然後upper_bound一下,或者直接累計求和。

神奇的程式碼
#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(n);
    for (auto& x : a)
        cin >> x;
    partial_sum(a.begin(), a.end(), a.begin());
    auto ans = upper_bound(a.begin(), a.end(), m) - a.begin();
    cout << ans << '\n';

    return 0;
}



B - Uppercase and Lowercase (abc357 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);
    string s;
    cin >> s;
    int lower = count_if(s.begin(), s.end(), [](char c) { return islower(c); });
    int upper = s.size() - lower;
    if (lower >= upper) {
        transform(s.begin(), s.end(), s.begin(), ::tolower);
    } else {
        transform(s.begin(), s.end(), s.begin(), ::toupper);
    }
    cout << s << '\n';

    return 0;
}



C - Sierpinski carpet (abc357 C)

題目大意

給定\(n\),輸出一個 \(3^{n} \times 3^{n}\)的圖形 \(g_n\),其中有 \(9 \times 9\)格,中間格子是全 .,其餘\(8\)格的圖案是 \(g_{n-1}\)

\(g_0\)的圖形是 #

解題思路

按照題意遞迴構造。

列舉\(3^{n} \times 3^{n}\)的每個位置\((i,j)\),設\(sz=3^{n-1}\)。然後透過\((\frac{i}{sz}, \frac{j}{sz})\)判斷其屬於 \(9\)個方格中的哪個,如果是中間,則就是 .,否則就進入子問題\((i \% sz, j % sz, n - 1)\),即 \(g_{n-1}\)的圖案,位置 \((i \% sz, j \% sz)\)的圖形。

神奇的程式碼
#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;
    auto ns = [&](int i) {
        int sz = 1;
        for (int j = 0; j < i; j++) {
            sz *= 3;
        }
        return sz;
    };
    int sz = ns(n);
    auto solve = [&](auto solve, int i, int j, int n) -> char {
        if (n == 0)
            return '#';
        int small = ns(n - 1);
        int x = i / small, y = j / small;
        if (x == 1 && y == 1) {
            return '.';
        } else
            return solve(solve, i % small, j % small, n - 1);
    };
    for (int i = 0; i < sz; i++) {
        for (int j = 0; j < sz; j++) {
            cout << solve(solve, i, j, n);
        }
        cout << '\n';
    }

    return 0;
}



D - 88888888 (abc357 D)

題目大意

給定\(n\),定義 \(f(n)\)表示 \(n\)\(n\)的拼接得到的數字。

\(f(n) \% 998244353\)

解題思路

\(a_1\)表示 \(1\)\(n\)\(a_2\)表示\(2\)\(n\)

容易得到\(a_1 = n, a_2 = xa_1 + n = xn + n = n(x + 1), a_3 = xa_2 + n = n(x^2 + x + 1)\),其中 \(x=10^k\)\(k\)\(n\)的位數。

展開得到通項公式\(a_n = xa_{n-1} + n = n(x^{n-1} + x^{n-2} + \cdots + x + 1) = n \frac{x^n - 1}{x - 1}\)

透過費馬小定理知 \(\frac{1}{x-1} \equiv (x-1)^{mo-2} \mod mo,mo=998244353\)

因此最終答案就是\(n (x^n - 1)(x - 1)^{mo-2}\) ,用快速冪計算即可。時間複雜度是\(O(\log n)\)

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

const int mo = 998244353;

long long qpower(long long a, long long b) {
    long long qwq = 1;
    while (b) {
        if (b & 1)
            qwq = qwq * a % mo;
        a = a * a % mo;
        b >>= 1;
    }
    return qwq;
}

long long inv(long long x) { return qpower(x, mo - 2); }

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    LL n;
    cin >> n;
    __int128 ten = 1;
    while (ten <= n) {
        ten *= 10;
    }
    ten %= mo;
    LL up = ((qpower(ten, n) - 1) + mo) % mo;
    LL down = inv(ten - 1);
    LL ans = n % mo * up % mo * down % mo;
    cout << ans << '\n';

    return 0;
}



E - Reachability in Functional Graph (abc357 E)

題目大意

給定一張基環內向森林(有向圖,每個點出度為\(1\)),問點對數量 \((i,j)\),滿足從點 \(i\)出發可以到達點 \(j\)

解題思路

考慮樸素做法,顯而易見的\(O(n^2)\):從每個點出發,進行 \(DFS\)

分析樸素做法,會發現操作重複的地方:假想一條鏈 \(1 \to 2 \to 3 \to 4 \cdots\),從 \(1\)出發 \(DFS\),從 \(2\)出發 \(DFS\),會發現非常冗餘,當從 \(1\)出發時,它能到達的點, \(2\)也能到達,也就是說可以利用 \(2\)節點的資訊,以避免重複搜尋。

我們想透過後繼節點的答案來得到當前點的答案,那得先計算後繼節點答案,然後再考慮當前節點。這其實非常像拓撲排序:把邊方向,然後我們處理入度為 \(0\)的點(這意味著原圖中的後繼節點已經計算完畢了)。

然而拓撲排序適用於 \(DAG\)(有向無環圖),這裡有環,因此得事先找到環。環上的節點是處處到達的。

如何找到環呢?考慮這是一張基環內向森林,它有個性質,即從任何點出發,最終一定會走到環內。

注意到答案分兩類,一類是環上的點的貢獻,其貢獻值即為環大小。另一類是非環上的點的貢獻,由於從它出發最終會走到環內,因此其貢獻值為環大小\(+\)沿途的點數,我們從環上的點出發,就能一路更新沿途的點數及貢獻了。

首先從任意點進行\(DFS\),直到重複訪問到某個點時,這個點就是環上的點,標記一下。

然後從標記的點進行\(DFS\),遍歷環上的點,統計環大小和標記環上的點。同時統計環上的點對答案的貢獻(即環大小)。

最後從環上的點往非環上的點進行 \(DFS\),統計非環點對答案的貢獻。

\(3\)\(DFS\)即可,時間複雜度是 \(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;
    vector<vector<int>> edge(n), inv(n);
    for (int i = 0; i < n; ++i) {
        int p;
        cin >> p;
        --p;
        edge[i].push_back(p);
        inv[p].push_back(i);
    }
    vector<int> cnt(n), cir(n), visit(n);
    int tt = 0;
    auto dfs1 = [&](auto dfs1, int u) -> void {
        visit[u] = tt;
        for (auto v : edge[u]) {
            if (!visit[v]) {
                dfs1(dfs1, v);
            } else if (visit[v] == tt) {
                cir[v] = 1;
            }
        }
    };
    for (int i = 0; i < n; ++i) {
        if (!visit[i]) {
            ++tt;
            dfs1(dfs1, i);
        }
    }
    fill(visit.begin(), visit.end(), 0);
    int cur = 0;
    auto dfs2 = [&](auto dfs2, int u) -> void {
        visit[u] = 1;
        cir[u] = 1;
        ++cur;
        for (auto v : edge[u]) {
            if (!visit[v]) {
                dfs2(dfs2, v);
            }
        }
        cnt[u] = cur;
    };
    for (int i = 0; i < n; ++i) {
        if (cir[i] == 1 && !visit[i]) {
            cur = 0;
            dfs2(dfs2, i);
        }
    }
    LL ans = 0;
    for (int i = 0; i < n; ++i) {
        if (cir[i])
            ans += cnt[i];
    }
    auto dfs3 = [&](auto dfs3, int u, int cc) -> void {
        for (auto& v : inv[u]) {
            if (!cir[v]) {
                ans += cc + 1;
                dfs3(dfs3, v, cc + 1);
            }
        }
    };
    for (int i = 0; i < n; ++i) {
        if (cir[i]) {
            dfs3(dfs3, i, cnt[i]);
        }
    }
    cout << ans << '\n';

    return 0;
}



F - Two Sequence Queries (abc357 F)

題目大意

給定兩個陣列\(a,b\), 維護一下三種操作:

  • 1 l r x,給\(a_l, a_{l+1}, \cdots, a_r\)都加上 \(x\)
  • 2 l r x,給\(b_l, b_{l+1}, \cdots, b_r\)都加上 \(x\)
  • 3 l r,求\(\sum_{i=l}^{r} a_i \times b_i \mod 998244353\)

解題思路

區間操作,考慮線段樹,維護的資訊是什麼。

首先肯定有\(\sum a_i b_i\),考慮對它修改後,要額外維護什麼資訊,才能得到新的 \(\sum a_i b_i\)

對它修改,即變為 \(\sum (a_i + x)(b_i + y) = \sum a_i b_i + y\sum a_i + x\sum b_i + \sum xy\)

因此我們還需維護\(\sum a_i\)\(\sum b_i\),而它們的更新則需要自己的資訊即可。

由於是區間操作, \(lazy\)資訊就維護 \(x,y\)

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

const int N = 2e5 + 8;
const int mo = 998244353;

class segment {
#define lson (root << 1)
#define rson (root << 1 | 1)
  public:
    LL sab[N << 2];
    LL sum[N << 2][2];
    LL lazy[N << 2][2];

    void pushup(int root) {
        sab[root] = (sab[lson] + sab[rson]) % mo;
        sum[root][0] = (sum[lson][0] + sum[rson][0]) % mo;
        sum[root][1] = (sum[lson][1] + sum[rson][1]) % mo;
    }

    void build(int root, int l, int r, vector<int>& a, vector<int>& b) {
        if (l == r) {
            sab[root] = 1ll * a[l - 1] * b[l - 1] % mo;
            sum[root][0] = a[l - 1];
            sum[root][1] = b[l - 1];
            return;
        }
        int mid = (l + r) >> 1;
        build(lson, l, mid, a, b);
        build(rson, mid + 1, r, a, b);
        pushup(root);
    }

    void pushdown(int root, int l, int mid, int r) {
        if (lazy[root][0] || lazy[root][1]) {
            sab[lson] =
                (sab[lson] + lazy[root][0] * sum[lson][1] % mo +
                 lazy[root][1] * sum[lson][0] % mo +
                 lazy[root][0] * lazy[root][1] % mo * (mid - l + 1) % mo) %
                mo;
            sum[lson][0] =
                (sum[lson][0] + lazy[root][0] * (mid - l + 1) % mo) % mo;
            sum[lson][1] =
                (sum[lson][1] + lazy[root][1] * (mid - l + 1) % mo) % mo;

            sab[rson] = (sab[rson] + lazy[root][0] * sum[rson][1] % mo +
                         lazy[root][1] * sum[rson][0] % mo +
                         lazy[root][0] * lazy[root][1] % mo * (r - mid) % mo) %
                        mo;
            sum[rson][0] = (sum[rson][0] + lazy[root][0] * (r - mid) % mo) % mo;
            sum[rson][1] = (sum[rson][1] + lazy[root][1] * (r - mid) % mo) % mo;

            lazy[lson][0] = (lazy[lson][0] + lazy[root][0]) % mo;
            lazy[lson][1] = (lazy[lson][1] + lazy[root][1]) % mo;
            lazy[rson][0] = (lazy[rson][0] + lazy[root][0]) % mo;
            lazy[rson][1] = (lazy[rson][1] + lazy[root][1]) % mo;

            lazy[root][0] = lazy[root][1] = 0;
        }
    }

    void update(int root, int l, int r, int L, int R, LL val, int op) {
        if (L <= l && r <= R) {
            sab[root] = (sab[root] + val * sum[root][op ^ 1] % mo) % mo;
            sum[root][op] = (sum[root][op] + val * (r - l + 1) % mo) % mo;
            lazy[root][op] = (lazy[root][op] + val) % mo;
            return;
        }
        int mid = (l + r) >> 1;
        pushdown(root, l, mid, r);
        if (L <= mid)
            update(lson, l, mid, L, R, val, op);
        if (R > mid)
            update(rson, mid + 1, r, L, R, val, op);
        pushup(root);
    }

    LL query(int root, int l, int r, int L, int R) {
        if (L <= l && r <= R) {
            return sab[root];
        }
        int mid = (l + r) >> 1;
        pushdown(root, l, mid, r);
        LL ans = 0;
        if (L <= mid)
            ans += query(lson, l, mid, L, R);
        if (R > mid)
            ans += query(rson, mid + 1, r, L, R);
        ans %= mo;
        return ans;
    }

} sg;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, q;
    cin >> n >> q;
    vector<int> a(n), b(n);
    for (auto& i : a)
        cin >> i;
    for (auto& i : b)
        cin >> i;
    sg.build(1, 1, n, a, b);
    while (q--) {
        int op;
        cin >> op;
        if (op == 3) {
            int l, r;
            cin >> l >> r;
            int ans = sg.query(1, 1, n, l, r);
            cout << ans << '\n';
        } else {
            int l, r, x;
            cin >> l >> r >> x;
            sg.update(1, 1, n, l, r, x, op - 1);
        }
    }

    return 0;
}



G - Stair-like Grid (abc357 G)

題目大意

給定\(n\),定義一個網格,前兩行有 \(4\)個格子,接著兩行有 \(6\)個格子,接著兩行有 \(8\)個格子 \(...\)

其中有 \(m\)個格子不可走。

從左上到右下,只能往下走和往右走。問方案數。

解題思路

<++>

神奇的程式碼