AtCoder Beginner Contest 328

这篇具有很好参考价值的文章主要介绍了AtCoder Beginner Contest 328。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

A - Not Too Hard (abc328 A)

题目大意

给定\(n\)个数字和一个数 \(x\)

问不大于 \(x\)的数的和。

解题思路

按找要求累计符合条件的数的和即可。

神奇的代码
#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, x;
    cin >> n >> x;
    int sum = 0;
    for (int i = 0; i < n; ++i) {
        int a;
        cin >> a;
        sum += a * (a <= x);
    }
    cout << sum << '\n';

    return 0;
}



B - 11/11 (abc328 B)

题目大意

给定一年的月数和一个月的天数。

问有多少对\((i,j)\),表示第 \(i\)个月的第 \(j\)日, \(i,j\)的数位上每个数字都是一样的。

解题思路

范围只有\(O(100^2)\),枚举所有的 \((i,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;
    int ans = 0;
    auto ok = [&](int x, int y) {
        int t = x % 10;
        while (x) {
            if (t != x % 10)
                return false;
            x /= 10;
        }
        while (y) {
            if (t != y % 10)
                return false;
            y /= 10;
        }
        return true;
    };
    for (int i = 1; i <= n; ++i) {
        int x;
        cin >> x;
        for (int j = 1; j <= x; ++j) {
            ans += ok(i, j);
        }
    }
    cout << ans << '\n';

    return 0;
}



C - Consecutive (abc328 C)

题目大意

给定一个字符串\(s\)和若干个询问。

每个询问问 \(s[l..r]\)子串中,有多少对相邻相同字母的下标。

解题思路

\(a[i]=1\)表示\(s[i] == s[i + 1]\)\(a[i] = 0\)表示 \(s[i] \neq s[i + 1]\)

每个询问就是问 \(\sum_{i=l}^{r-1} a[i]\)

预处理数组\(a\)前缀和 即可\(O(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, q;
    string s;
    cin >> n >> q >> s;
    vector<int> sum(n);
    for (int i = 0; i < n - 1; ++i) {
        sum[i] = (s[i] == s[i + 1]);
        if (i)
            sum[i] += sum[i - 1];
    }
    while (q--) {
        int l, r;
        cin >> l >> r;
        --l, --r;
        int ans = 0;
        if (r)
            ans += sum[r - 1];
        if (l)
            ans -= sum[l - 1];
        cout << ans << '\n';
    }

    return 0;
}



D - Take ABC (abc328 D)

题目大意

给定一个仅包含ABC的字符串\(s\),每次将最左边的ABC删除,直至不能删。

问最后的字符串。

解题思路

可以从左到右依次将\(s\)的每个字符加入栈中,一旦栈顶构成ABC就弹栈。

模拟即可。

神奇的代码
#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;
    string st;
    for (auto& i : s) {
        st += i;
        if (st.size() >= 3) {
            int n = st.size();
            if (st.substr(st.size() - 3) == "ABC") {
                st.pop_back();
                st.pop_back();
                st.pop_back();
            }
        }
    }
    cout << st << '\n';

    return 0;
}



E - Modulo MST (abc328 E)

题目大意

给定一张图,问模\(k\)意义下的最小生成树的代价。

解题思路

注意是模\(k\)意义下的最小代价,在求生成树过程中的每一个值都有可能在加入某条边后超过\(k\)而变的最小,成为最后的答案。

注意点数只有\(8\),边数最多也只有 \(28\),因此总的方案数只有 \(2^{28}=2e8\)。暴力可行。即可以保留中间的所有结果。

考虑\(prim\)求生成树做法,从\(1\)号点不断往外拓展,保留当前最小的结果。我们借用这个想法,但保留当前所有的结果:设 \(dp[i]\)表示所有点与\(1\)号点连通性为 \(i\)的情况下,所有生成树的结果(\(i\&1=0\)的所有情况都是不合法的,\(1\)号点肯定与 \(1\)号点连通)。很显然\(dp[1] = [0]\)

然后枚举下一条边连接,更新所有结果即可。由于可能会有重复的值(同一个生成树可以从不同的加边顺序得到),所以用 set

神奇的代码
#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;
    LL k;
    cin >> n >> m >> k;
    vector<vector<pair<int, LL>>> edge(n);
    for (int i = 0; i < m; ++i) {
        int u, v;
        LL w;
        cin >> u >> v >> w;
        --u, --v;
        edge[u].push_back({v, w});
        edge[v].push_back({u, w});
    }
    int up = (1 << n);
    int st = n - 1;
    vector<set<LL>> candi(up);
    candi[1].insert(0);
    for (int i = 0; i < up; ++i) {
        if (~i & 1)
            continue;
        for (int u = 0; u < n; ++u) {
            if ((~i >> u) & 1)
                continue;
            for (auto& [v, w] : edge[u]) {
                if ((i >> v) & 1)
                    continue;
                for (auto& val : candi[i])
                    candi[i | (1 << v)].insert((val + w) % k);
            }
        }
    }
    cout << *ranges::min_element(candi.back()) << '\n';

    return 0;
}



F - Good Set Query (abc328 F)

题目大意

给定数字\(n\),依次给定\(q\)个条件 \((a_i,b_i,d_i)\)

对于一个条件集合\(s\),如果存在一个长度为\(n\)数组 \(x\),对于这个集合里的所有条件,都满足\(x_{a_i} - x_{b_i} = d_i\),那么这个集合是好的。

初始集合为空,依次对每个条件,如果加入到集合后,集合是好的,则加入到集合中。

问最后集合的元素。

解题思路

条件相当于是规定了数组\(x\)元素之间的差的关系。

对于一个条件\((a,b,d)\),我们可以连一条 \(a\to b\)的边,边权为 \(d\),反向边的边权为 \(-d\)

考虑一个集合不是好时,此时形成的图是怎样的。

当加入一个条件\((a,b,d)\),集合可能还是好的,但也可能变得不好,

如果还是好的,有两种情况:

  • 一是\(a,b\)原先没有什么联系,即不连通,加了条边之后连通了,仅此而已。
  • 二是\(a,b\)是连通的,加了 \(a\to b\)这条边后会形成一个环,环的边权和为 \(0\),或者说 \(a\to b\)存在两条路径,其边权和相等。

在第二种情况下,这个条件就是多余的,我们可以不管这个条件,即不加这条边。此时图就没有环,即是一棵树(或森林)。

树是一个非常好的图,有着树上路径唯一的性质,因此情况二下, 我们可以很容易求出 \(a\to b\)的路径和,然后与 \(d\)比较,相等则说明加入这个条件后,集合还是好的。

而如果不相等,则说明不能加入这个条件,即有条件冲突了,说明 \(a\to b\)存在两条边权和不一样的路径

所以问题就剩下,如何在动态加边的情况下,求出\(a\to b\)的长度。

如果是一棵静止的树,一个常用的方法就是预处理每个点到根节点的距离\(dis[i]\),那么 \(a \to b\)距离就是 \(dis[a] - dis[b]\),注意到反向边的边权是负值,所以\(lca\)到根的距离恰好抵销了。

而当两棵树合并时,有一棵树的 \(dis\)就要全部更新,如果随便选一棵树更新的话,总的时间复杂度可能会是\(O(n^2)\)。为降低时间复杂度,可以采用启发式合并的策略,即节点树少的树合并到节点树多的树上,这样每次只用更新节点数少的树的 \(dis\)。更新就是从合并点开始\(DFS\),更新\(dis\)数组。

为计算启发式合并的时间复杂度,可以考虑每个节点的\(dis\)的更新次数——每更新一次,其节点所在的连通块大小至少翻倍,那么每个节点最多更新 \(\log n\)次,其所在的连通块就包含了所有的节点,也就不会再更新了,因此启发式合并的复杂度是 \(O(n\log n)\)

用并查集维护连通性,然后树合并时采用启发式合并的策略更新\(dis\)数组 ,时间复杂度是 \(O(q + n\log n)\)

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

class dsu {
  public:
    vector<int> p;
    vector<int> sz;
    vector<LL> dis;
    vector<vector<array<int, 2>>> edge;
    int n;

    dsu(int _n) : n(_n) {
        p.resize(n);
        sz.resize(n);
        dis.resize(n);
        edge.resize(n);
        iota(p.begin(), p.end(), 0);
        fill(sz.begin(), sz.end(), 1);
        fill(dis.begin(), dis.end(), 0);
    }

    inline int get(int x) { return (x == p[x] ? x : (p[x] = get(p[x]))); }

    inline void dfs(int u, int fa) {
        for (auto& [v, w] : edge[u]) {
            if (v == fa)
                continue;
            dis[v] = dis[u] - w;
            dfs(v, u);
        }
    }

    inline bool unite(int x, int y, int w) {
        int fx = get(x);
        int fy = get(y);
        if (fx != fy) {
            if (sz[fx] > sz[fy]) {
                swap(x, y);
                swap(fx, fy);
                w = -w;
            }

            edge[x].push_back({y, w});
            edge[y].push_back({x, -w});
            dis[x] = dis[y] + w;
            dfs(x, y);

            p[fx] = fy;
            sz[fy] += sz[fx];
            return true;
        } else {
            return dis[x] == dis[y] + w;
        }
    }
};

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, q;
    cin >> n >> q;
    dsu ji(n);
    for (int i = 1; i <= q; ++i) {
        int a, b, d;
        cin >> a >> b >> d;
        --a, --b;
        if (ji.unite(a, b, d))
            cout << i << ' ';
    }
    cout << '\n';

    return 0;
}



好像是带权并查集裸题怪不得这么多人过得这么快

还是借用上面计算\(a \to b\)的思想,由于反向边边权取反,因此对于任意一条闭合回路,其边权和一定是 \(0\)

由此每个点只需记录到根的距离,而不必关心树的形态,分别考虑在路径压缩和合并时距离的更新即可。

压缩的时候,\(dis[x \to root] = dis[x \to p[x]] + dis[p[x]]\),这里的 \(p[x]\)是压缩前的父亲, 但\(dis[p[x]]\)是更新后的值。
合并的时候, \(dis[x] = x \to fx, dis[y] = y \to fy\),令 \(p[fx] = fy\),则 \(dis[fx] = fx \to x \to y \to fy = -dis[x] + w + dis[y]\)。而 \(dis[x]\)会在路径压缩的时候更新。

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

class dsu {
  public:
    vector<int> p;
    vector<int> sz;
    vector<LL> dis;
    int n;

    dsu(int _n) : n(_n) {
        p.resize(n);
        sz.resize(n);
        dis.resize(n);
        iota(p.begin(), p.end(), 0);
        fill(sz.begin(), sz.end(), 1);
        fill(dis.begin(), dis.end(), 0);
    }

    inline int get(int x) {
        if (x != p[x]) {
            int t = p[x];
            p[x] = get(p[x]);
            dis[x] += dis[t];
        }
        return p[x];
    }

    inline bool unite(int x, int y, int w) {
        int fx = get(x);
        int fy = get(y);
        if (fx != fy) {

            p[fx] = fy;
            dis[fx] = -dis[x] + dis[y] + w;
            sz[fy] += sz[fx];
            return true;
        } else {
            return dis[x] == dis[y] + w;
        }
    }
};

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, q;
    cin >> n >> q;
    dsu ji(n);
    for (int i = 1; i <= q; ++i) {
        int a, b, d;
        cin >> a >> b >> d;
        --a, --b;
        if (ji.unite(a, b, d))
            cout << i << ' ';
    }
    cout << '\n';

    return 0;
}


G - Cut and Reorder (abc328 G)

题目大意

给定两个数组\(A,B\),可以进行以下两种操作任意次:

  • 选择一个数\(x\),将数组 \(A\)分成\(x+1\)个部分,然后重新排序,组成一个新数组。代价为\(xC\)
  • \(A_i=A_i + k\),代价为\(|k|\)

问最小的代价,使得\(A\)变成 \(B\)

解题思路

首先注意到一点,所有的结果都可以转换成一次操作一+若干次操作二。

由此我们只需考虑如何操作一,因为进行完操作一后。操作二的代价是固定的。

考虑朴素做法,即枚举分界点,有\(O(2^n)\)种情况,然后对每一个部分进行排序,有 \(O(n!)\)种。其复杂度过大了,期间存在重复计算的情况,考虑如何压缩,设计合适的\(dp\)状态。

考虑重复计算的状态:设想对数组 \(A\)切分成出一段 \(a_i\),然后把它放到最前面,其他段任意分。关于 \(a_i\)的这个子状态会被重复考虑多次——这个状态是不必要的。

即当我们考虑数组 \(A\)中的某一段时,我们只关心两个信息。

  • 能不能选择这一段,即这一段中有没有和之前选择的段重叠了。
  • 能不能放到数组 \(B\)的某一段,即 某一段有没有和之前放置的部分重叠了。

由此可以设\(dp[i][j]\)表示将 \(A\)中的 \(i\)部分( \(0/1\)表示)放到 \(B\)中的 \(j\)部分的最小代价。但这个状态数高达 \(O(2^{2n})\),且可能会包含很多非法状态(可能\(A\)中某连续部分放不到\(B\)某连续部分上),得转换一下状态。

为保持上面的连续性,可以规定将\(A\)中的 \(i\)部分放到 \(B\)中的最前面,即前 \(popcount(i)\)(二进制下\(1\)的个数)位。即设 \(dp[i]\)表示将 \(A\)中的 \(i\)部分放到 \(B\)中的最前面的最小代价 ,记\(l=popcount(i)\),即放到 \(B\)中的前 \(l\)位。

考虑往后转移,即选择\(i\)中一段连续的\(0\),然后放到 \(B[l+1,...]\)

当然也可以考虑从前转移,但计算代价部分需要\(O(n^3)\)预处理一下操作二代价,否则转移会多出一个\(O(n)\) 复杂度。而往后转移的代价可以在迭代更新维护。

初始情况即\(dp[0] = 0\),其余无限大。注意从 \(0\)往后转移时没有代价 \(C\)。其余的则有。

关于其时间复杂度,初看可能认为是\(O(n^2 2^n)\),但由于转移时枚举连续的段,其复杂度可能会比这小。分别考虑总状态数和总转移数的话,其实是 \(O(n2^n)\)文章来源地址https://www.toymoban.com/news/detail-746179.html

  • 总状态数是\(O(2^n)\)
  • 总转移数是\(O(n2^n)\)。注意到每次转移都是由一个连续的段产生的,考虑这些连续的段的转移次数。一个长度为 \(k\)的连续的段有\((n-k+1)个\),能选择该段的状态数有 \(2^{n-k}\),由此 总的转移次数为\(\sum_{i=1}^{n}(n-k+1)2^{n-k}=2^n(n-3)+2=O(n2^n)\)
    由此总的时间复杂度就是总状态数+总转移数,即\(O(2^n + n2^n) = O(n2^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;
    LL c;
    cin >> n >> c;
    vector<LL> a(n), b(n);
    for (auto& i : a)
        cin >> i;
    for (auto& i : b)
        cin >> i;
    size_t up = (1 << n);
    vector<LL> dp(up, numeric_limits<LL>::max());
    dp[0] = 0;
    for (size_t i = 0; i < up; ++i) {
        int cnt = popcount(i);
        for (int j = 0; j < n; ++j) {
            if ((i >> j) & 1)
                continue;
            LL sum = c * (i != 0);
            LL nxt = 0;
            for (int k = j; k < n; ++k) {
                if ((i >> k) & 1)
                    break;
                nxt |= (1 << k);
                sum += abs(a[k] - b[cnt + k - j]);
                dp[i | nxt] = min(dp[i | nxt], dp[i] + sum);
            }
        }
    }
    cout << dp.back() << '\n';

    return 0;
}


到了这里,关于AtCoder Beginner Contest 328的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • AtCoder Beginner Contest 336

    给定一个数 (n) ,将 long 中的 o 重复 (n) 次后输出。 模拟即可。 神奇的代码 给定一个数 (n) ,问 (n) 的二进制表示下的末尾零的数量。 即找到最小的 (i) 使得 (n (1 i)) 不为零的位置。枚举即可。 或者直接用内置函数 __builtin_ctz 。(count tail zero? 神奇的代码 定义一个数

    2024年01月20日
    浏览(32)
  • AtCoder Beginner Contest 326

    100楼层,一次可以上最多两层,或下最多三层。 给定两楼层,问能否一次到达。 比较大小,然后判断其差是是不是在 (2) 或 (3) 内即可。 神奇的代码 给定一个 (n) ,问不小于 (n) 的形如 (326) 的数字是多少。 形如 (326) 的数字,即数位有 (3) ,且百位 (times) 十位

    2024年02月08日
    浏览(23)
  • AtCoder Beginner Contest 349

    (n) 个人游戏,每局有一人 (+1) 分,有一人 (-1) 分。 给定最后前 (n-1) 个人的分数,问第 (n) 个人的分数。 零和游戏,所有人总分是 (0) ,因此最后一个人的分数就是前 (n-1) 个人的分数和的相反数。 神奇的代码 对于一个字符串,如果对于所有 (i geq 1) ,都有恰好

    2024年04月13日
    浏览(47)
  • AtCoder Beginner Contest 341

    给定 (n) ,输出 (n) 个 (0) 和 (n+1) 个 (1) 交替的字符串。 (101010...) 循环输出即可。 神奇的代码 货币兑换。 (A) 国货币每 (x_a) 钱可兑换 (B) 国货币 (y_a) 钱。 (B) 国货币每 (x_b) 钱可兑换 (C) 国货币 (y_b) 钱。 ... 给定你拥有的每国货币钱数和兑换规则,依次兑换

    2024年02月19日
    浏览(20)
  • AtCoder Beginner Contest 314

    怎么好多陌生单词 审核怎么这么逆天,半小时还没审完 给定 (pi) 的值以及数 (n) ,要求保留 (n) 位小数输出,不四舍五入。 字符串形式储存然后截取末尾即可。 神奇的代码 (n) 个人玩轮盘赌游戏,简单说就是一个转盘有 (37) 个数字以及一个箭头,箭头会等概率停在某

    2024年02月13日
    浏览(29)
  • AtCoder Beginner Contest 309

    感觉F写了个乱搞做法 给定一个 (3 times 3) 的网格,以及两个数字。 问这两个数字是否 水平相邻 。 求出两个数字的横纵坐标,看是否横坐标相同,纵坐标差一即可。 读题不仔细,开题就WA了。 神奇的代码 给定一个矩形。将最外围的数字顺时针旋转一格。 可以模拟一个指针

    2024年02月13日
    浏览(25)
  • AtCoder Beginner Contest 313

    貌似这次很难,还好去吃烧烤了 发现原来G就是个类欧几里德算法,参考abc283 ex,更了个G 给定 (n) 个数 (a_i) ,问第一个数要成为唯一的最大的数,应该加多少。 找到后面的最大的数 (m) ,答案就是 (max(0, m + 1 - a_0)) 。 神奇的代码 给定 (m) 条关于 (n) 个数的大小关系,

    2024年02月14日
    浏览(29)
  • AtCoder Beginner Contest 310

    感觉F又双叒叕写复杂了 点杯咖啡,要 (p) 元,但可以用一个优惠券,使得咖啡只要 (q) 元,但你需要额外购买 (n) 个商品中(价格为 (a_i) )的一个。 问点杯咖啡的最小价格。 考虑直接买还是使用优惠券,使用优惠券的话就选 (n) 个商品中价格最小的。 两种情况取最小

    2024年02月16日
    浏览(20)
  • AtCoder Beginner Contest 327

    2023.11.08 update: 更新了G 给定一个字符串 (s) ,问是否包含 ab 或 ba 。 遍历判断即可。 神奇的代码 给定 (b) ,问是否存在 (a) 使得 (a^a = b) 。 由于指数爆炸的缘故, (a) 的范围不会很大,枚举 (a) ,看 (b % a) 是否为 (0) ,然后用 (b) 不断除以 (a) ,看最后除的次数是

    2024年02月06日
    浏览(34)
  • AtCoder Beginner Contest 320

    给定 (a,b) ,输出 (a^b + b^a) 。 因为数不超过 (10) ,可以直接用 pow 计算然后转成 (int) 。不会有精度损失。 神奇的代码 给定一个字符串 (s) ,求长度最长的回文串。 因为长度只有 (100) ,可以直接枚举子串,然后暴力判断是不是回文串(翻转后是否和自身相同),复杂

    2024年02月08日
    浏览(27)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包