AtCoder Beginner Contest 378

省流版
  • 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\)取模。

解题思路

<++>

神奇的代码



posted @ 2024-11-02 23:31  ~Lanly~  阅读(374)  评论(0编辑  收藏  举报