AtCoder Beginner Contest 378

~Lanly~發表於2024-11-02
省流版
  • A. 判斷奇偶性即可
  • B. 根據餘數計算偏移天數即可
  • C. 用map記錄每個數出現的位置即可
  • D. 列舉起點,列舉每步的方向,樸素搜尋即可
  • E. 考慮字首和的兩數相減代替區間和的情況,減為負數則加回正數,用樹狀陣列維護減為負數的情況數
  • F. 列舉點,作為連邊的倆個點的lca,考慮維護路徑點度數為\(33..32\)的數量,組合即可

A - Pairing (abc378 A)

題目大意

給定\(4\)個數。

問做的運算元,每次選兩個相同的數,然後丟棄。

解題思路

統計每個數的出現次數\(cnt_i\),答案就是 \(\sum \lfloor \frac{cnt_i}{2} \rfloor\)

神奇的程式碼
#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 = 4;
    array<int, 4> cnt{};
    while (n--) {
        int a;
        cin >> a;
        cnt[a - 1]++;
    }
    int ans = 0;
    for (auto& i : cnt) {
        ans += i / 2;
    }
    cout << ans << '\n';

    return 0;
}



B - Garbage Collection (abc378 B)

題目大意

\(n\)種垃圾,第 \(i\)種垃圾會在天數 \(d\)收取,其中 \(d\)滿足 \(d \% p_i = r_i\)

回答 \(q\)個詢問,每個詢問問在第 \(d_i\)天丟的第\(t_i\)種垃圾,會在第幾天被收取。如果當天丟且當天可收取,則會被收取。

解題思路

假設\(j = t_i\),先算\(r = d_i \% p_j\),如果 \(r \leq r_j\),那麼很顯然多過\(r_j - r\)天就會被收取。否則要過一個迴圈,即\(p_j - r + r_j\)天才會被收取。

神奇的程式碼
#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<array<int, 2>> a(n);
    for (auto& x : a)
        cin >> x[0] >> x[1];
    int Q;
    cin >> Q;
    while (Q--) {
        int t, d;
        cin >> t >> d;
        --t;
        auto [q, r] = a[t];
        int ans = (r - d % q + q) % q;
        cout << d + ans << '\n';
    }

    return 0;
}



C - Repeating (abc378 C)

題目大意

給定一個陣列\(a\),構造相同長度的陣列 \(b\),滿足 \(b_i\)\(a_i\)上一次出現的位置,或者 \(-1\)

解題思路

直接用map記錄每個元素\(a_i\)上次出現的位置,然後輸出\(map[a_i]\)即可

神奇的程式碼
#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;
    map<int, int> pos;
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        int ans = pos.count(x) ? pos[x] + 1 : -1;
        cout << ans << " \n"[i == n - 1];
        pos[x] = i;
    }

    return 0;
}



D - Count Simple Paths (abc378 D)

題目大意

給定一張二維平面,有障礙物。

問方案數,從任意點出發,上下左右走,可以走\(k\)步,不經過障礙物,且每個點只訪問一次。

解題思路

由於平面\(10 \times 10\)\(k \leq 11\),直接花\(O(hw)\)列舉點,然後花\((4^k)\)遍歷所有方案。 其時間複雜度為\(O(hw4^k)\),約為 \(1e8\),可過。

神奇的程式碼
#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, k;
    cin >> h >> w >> k;
    vector<string> s(h);
    for (auto& x : s)
        cin >> x;
    int ans = 0;
    array<int, 4> dx = {0, 1, 0, -1};
    array<int, 4> dy = {1, 0, -1, 0};
    auto ok = [&](int x, int y) -> bool {
        return 0 <= x && x < h && 0 <= y && y < w && s[x][y] != '#';
    };
    vector<vector<int>> visit(h, vector<int>(w, 0));
    auto dfs = [&](auto dfs, int x, int y, int cnt) -> void {
        if (cnt == k) {
            ++ans;
            return;
        }
        visit[x][y] = 1;
        for (int i = 0; i < 4; ++i) {
            int nx = x + dx[i];
            int ny = y + dy[i];
            if (ok(nx, ny) && !visit[nx][ny]) {
                dfs(dfs, nx, ny, cnt + 1);
            }
        }
        visit[x][y] = 0;
    };
    for (int i = 0; i < h; ++i) {
        for (int j = 0; j < w; ++j) {
            if (s[i][j] == '#')
                continue;
            dfs(dfs, i, j, 0);
        }
    }
    cout << ans << '\n';

    return 0;
}



E - Mod Sigma Problem (abc378 E)

題目大意

給定陣列\(a\),和模數 \(m\)。求 \(\sum_{1 \leq l \leq r \leq n} ((\sum_{l \leq i \leq r} a_i )\% m)\)

解題思路

預處理字首和\(sum[i] = (\sum_{j \leq i} a_i )\% m\),則區間和 \([l,r]\)可表示為 \(sum[r] - sum[l - 1]\)

我們列舉\(r\),然後求所有的 \(l \leq r\),其區間和的和時多少。

由於取模的緣故,其結果但可能為負數,此時要\(+ m\),但有多少個\(l\)需要加呢?自然就是\(sum[l - 1] > sum[r]\)的那些 \(l\)

由於 \(sum[i] \leq m\)只有\(1e5\),可以開一個計數的桶 \(tree[i]\)表示數字 \(i\)出現的次數,那麼上述的 \(l\)的數量就是 \(\sum_{i > sum[r]} tree[i]\)。假設其數量為\(k\),那麼當前 \(r\)對答案的貢獻即為 \((\sum_{l \leq r} sum[r] - sum[l - 1]) + km = r \times sum[r] - \sum_{l \leq r} sum_[l - 1] + km\)。中間一項就是字首和的字首,而 \(k\)就是\(\sum_{i > sum[r]} tree[i]\)

關於\(k\)的求法,涉及到區間求和和單點修改,因此可以用權值樹狀陣列或權值線段樹維護這個桶即可。

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

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

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

    void modify(int x, T v) {
        while (x < n) {
            fenw[x] += v;
            x |= (x + 1);
        }
    }

    T get(int x) {
        T v{};
        while (x >= 0) {
            v += fenw[x];
            x = (x & (x + 1)) - 1;
        }
        return v;
    }
};

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, m;
    cin >> n >> m;
    int presum = 0;
    LL ppresum = 0;
    fenwick<int> cnt(m);
    LL ans = 0;
    cnt.modify(0, 1);
    for (int i = 0; i < n; ++i) {
        int a;
        cin >> a;
        a %= m;
        presum = (presum + a) % m;
        int cc = i + 1 - cnt.get(presum);
        ans += 1ll * (i + 1) * presum - ppresum + 1ll * m * cc;
        cnt.modify(presum, 1);
        ppresum += presum;
    }
    cout << ans << '\n';

    return 0;
}


下述想的比較複雜度,同樣是列舉\(r\),然後看所有\([l,r-1] \to [l,r]\)區間和的變化。分兩類,一類是直接\([l,r-1] + a_r = [l, r]\) ,另一類是\([l,r-1] + a_r - m = [l,r]\)

因為區間和的範圍同樣在\([0,m-1]\),所以用權值線段樹維護 \(cnt_i\)表示區間和\([l,r]=i\)的數量,當新增 \(a_r\)時,線段樹裡的資料都是\([l..r-1]\)的區間和個數,考慮計算貢獻,即\(cnt_{0..m - a_i}\)屬於第一類, \(cnt_{m - a_i..m-1}\)屬於第二類。

分別計算貢獻後,考慮\(cnt_i\)怎麼變化,即怎麼變成\([l..r]\)的區間和個數。由於所有數增加了\(a_r\),因此\(cnt_i\)會進行一個整體偏移 ,即\(cnt_{i+a_r} = cnt_i\),但直接這麼做是 \(O(n)\)的,不能這麼做。但考慮到是整體偏移,我們可以記錄此時表示 \(cnt_0\)的位置,即原來在\([l,r-1]\)時,\(cnt_0\) 表示區間和為\(0\)的個數,在增加 \(a_r\)後, \(cnt_{m - a_r}\)就表示區間和為 \(0\)的個數。即我們自定義\(cnt_0\)的位置,這樣就是整體偏移了。

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

const int N = 2e5 + 8;

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

    void pushup(int root) {
        cnt[root] = cnt[lson] + cnt[rson];
        sum[root] = sum[lson] + sum[rson];
    }

    void build(int root, int l, int r) {
        if (l == r) {
            cnt[root] = 0;
            sum[root] = 0;
            return;
        }
        int mid = (l + r) >> 1;
        build(lson, l, mid);
        build(rson, mid + 1, r);
        pushup(root);
    }

    void pushdown(int root, int l, int mid, int r) {
        if (lazy[root]) {
            sum[lson] += lazy[root] * cnt[lson];
            sum[rson] += lazy[root] * cnt[rson];
            lazy[lson] += lazy[root];
            lazy[rson] += lazy[root];
            lazy[root] = 0;
        }
    }

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

    void insert(int root, int l, int r, int pos, LL val) {
        if (l == r) {
            cnt[root] += 1;
            sum[root] += val;
            return;
        }
        int mid = (l + r) >> 1;
        pushdown(root, l, mid, r);
        if (pos <= mid)
            insert(lson, l, mid, pos, val);
        else
            insert(rson, mid + 1, r, pos, val);
        pushup(root);
    }

    pair<int, LL> query(int root, int l, int r, int L, int R) {
        if (L <= l && r <= R) {
            return {cnt[root], sum[root]};
        }
        int mid = (l + r) >> 1;
        pushdown(root, l, mid, r);
        pair<int, LL> ans = {0, 0};
        if (L <= mid) {
            auto tmp = query(lson, l, mid, L, R);
            ans.first += tmp.first;
            ans.second += tmp.second;
        }
        if (R > mid) {
            auto tmp = query(rson, mid + 1, r, L, R);
            ans.first += tmp.first;
            ans.second += tmp.second;
        }
        return ans;
    }

    pair<int, LL> query_from(int root, int l, int r, int L, int R) {
        if (L > R)
            return {0, 0};
        L = (L % n + n) % n + 1;
        R = (R % n + n) % n + 1;
        debug(L, R);
        if (L <= R)
            return query(root, l, r, L, R);
        pair<int, LL> ans = {0, 0};
        auto tmp = query(root, l, r, L, r);
        ans.first += tmp.first;
        ans.second += tmp.second;
        tmp = query(root, l, r, 1, R);
        ans.first += tmp.first;
        ans.second += tmp.second;
        return ans;
    }

    void update_from(int root, int l, int r, int L, int R, LL val) {
        if (L > R)
            return;
        L = (L % n + n) % n + 1;
        R = (R % n + n) % n + 1;
        if (L <= R)
            update(root, l, r, L, R, val);
        else {
            update(root, l, r, L, r, val);
            update(root, l, r, 1, R, val);
        }
    }

} sg;

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;
        x %= m;
    }
    sg.build(1, 1, m);
    sg.n = m;
    int l = 0;
    LL ans = 0;
    for (int i = 0; i < n; i++) {
        int r = l + m - a[i];
        auto [cnt, sum] = sg.query_from(1, 1, m, l, r - 1);
        ans += 1ll * cnt * a[i] + sum;
        auto [cnt2, sum2] = sg.query_from(1, 1, m, r, l + m - 1);
        ans += 1ll * cnt2 * (a[i] - m) + sum2;

        sg.update_from(1, 1, m, l, r - 1, a[i]);
        sg.update_from(1, 1, m, r, l + m - 1, a[i] - m);
        l = r % m;
        sg.insert(1, 1, m, (l + a[i]) % m + 1, a[i]);
        ans += a[i];
    }
    cout << ans << '\n';

    return 0;
}



F - Add One Edge 2 (abc378 F)

題目大意

給定一棵樹,求加一條邊的方案數,使得沒有重邊,且環上的所有點的度數為\(3\)

解題思路

加一條邊\(u \to v\),首先這兩個點的度數為 \(2\),然後假設 \(u \to v\)路徑上的所有點的度數為 \(3\)

假設 \(u,v\)的最近公共祖先是 \(lca\),即 \(u \to lca\)\(v \to lca\)的所有點的度數為 \(3\)

注意到這是一個向父親方向的,要求路徑上所有點為 \(3\)的資訊,可以透過預處理 \(up[i]\)表示從 \(i\)往父親走,其點度為 \(3\)的最淺深度之類的資訊。然後我們只需列舉 \(u,v\),看 \(up[u],up[v]\)\(lca\)的深度關係,即可知道加的這條邊 \(u \to v\)是否符合要求。

但上述時間複雜度為 \(O(n^2)\),我們考慮列舉 \(lca\),然後看其子樹有多少對符合條件的 \(u,v\)

\(lca\)的角度,我們需要什麼資訊?即從該 \(lca\)往兒子方向走,其一路點度數為 \(3\),最後一個點度數為 \(2\),這樣的路徑條數。不同子樹之間的這類點就可以連邊(當然 \(lca\)的度數也要是 \(3\))。

注意重邊的情況,即 \(lca\)度數為 \(2\),其一個兒子的度數也為 \(2\)

上述過程可能就是樹形\(dp\)(?\(dp[i]\)表示 \(i\)子樹內,一路往兒子方向,其點度數為 \(3\),最後一個點度數為 \(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<vector<int>> edge(n);
    vector<int> du(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);
        du[u]++;
        du[v]++;
    }
    LL ans = 0;
    auto dfs = [&](auto dfs, int u, int fa) -> int {
        int ret = 0;
        for (int v : edge[u]) {
            if (v == fa)
                continue;
            int nxt = dfs(dfs, v, u);
            if (du[u] == 2)
                ans += nxt;
            else if (du[u] == 3)
                ans += 1ll * nxt * ret;
            ret += nxt;
        }
        if (du[u] == 2)
            return 1;
        else if (du[u] == 3)
            return ret;
        else
            return 0;
    };
    dfs(dfs, 0, 0);
    int extra = 0;
    for (int u = 0; u < n; u++) {
        for (auto v : edge[u]) {
            if (du[u] == 2 && du[v] == 2)
                extra++;
        }
    }
    ans -= extra / 2;
    cout << ans << '\n';

    return 0;
}



G - Everlasting LIDS (abc378 G)

題目大意

給定\(a,b,m\),求 \(1 \sim ab\)的全排列數量,滿足以下條件:

  • 最長上升子序列長度為\(a\)
  • 最長下降子序列長度為\(b\)
  • 存在 \(n\)使得在末尾增加一個數 \(n+0.5\),其上述兩個長度不改變。

輸出數量對 \(m\)取模。

解題思路

<++>

神奇的程式碼