AtCoder ABC 350题解(A-G)

总述:这场ABC难度没有之前的大,而且后面的G是典,成功完成人生首次ak ABC,特写一篇题解留念

A - Past ABCs

可以用字符串的substr函数截取后三位,再用stoi()把字符串转数字,判断这个数字在不在范围内,以及不等于316即可
代码:

#include <bits/stdc++.h>
// #define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define db double
using namespace std;
const int maxn = 1e5 + 10;
const int mod = 998244353;
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    string str;
    cin >> str;
    string a = str.substr(0, 3);
    string b = str.substr(3);
    int num = stoi(b);
    if (1 <= num && num <= 349 && num != 316) {
        cout << "Yes" << endl;
    } else
        cout << "No" << endl;
    // system("pause");
    return 0;
}

B - Dentist Aoki

模拟,直接用01数组表示是否存在牙齿,每次操作可以用异或1来表示取反,最后统计数组内有几个1
代码:

#include <bits/stdc++.h>
// #define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define db double
using namespace std;
const int maxn = 1e5 + 10;
const int mod = 998244353;
int arr[maxn];
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    int n, q;
    cin >> n >> q;
    for (int i = 1; i <= n; i++)
        arr[i] = 1;
    while (q--) {
        int x;
        cin >> x;
        arr[x] ^= 1;
    }
    int ans = 0;
    for (int i = 1; i <= n; i++)
        ans += arr[i];
    cout << ans << endl;
    // system("pause");
    return 0;
}

C - Sort

置换环,有两种写法

1.

对于错位的元素,可以将应该在该位置的元素调换过来,这种写法就需要两个数组互相映射,比较绕,不推荐写这种。

2.

对于错位的元素,可以选择将该元素替换至它应该在的地方,每次替换都至少能让一个元素归位,所以最多也是 n − 1 n-1 n1 次就可以有序。写起来更方便,只需要一个数组维护此时各个元素的位置,然后每次while当前元素错位,交换即可,代码:

#include <bits/stdc++.h>
// #define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define db double
using namespace std;
const int maxn = 2e5 + 10;
const int mod = 998244353;
int arr[maxn], pos[maxn];
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> arr[i];
    }
    vector<pii> ans;
    for (int i = 1; i <= n; i++) {
        while (arr[i] != i) {
            ans.push_back(pii(i, arr[i]));
            swap(arr[i], arr[arr[i]]);
        }
    }
    cout << ans.size() << endl;
    for (auto [u, v] : ans)
        cout << u << " " << v << endl;
    // system("pause");
    return 0;
}

D - New Friends

并查集,猜结论
一开始我想去在线维护这种过程,后来看样例发现,这种操作可能有连带性,即操作一次新增一条边后,可能又可以根据新增的这条边去操作其他的元素,所以维护肯定维护不了。
然后看样例里面,3个点的时候,最终连线是一个完全图,就可以去猜一下是不是只要连通块,最终都会变成完全图。可以自己画一个4个点的链,发现连着连着确实变成完全图了,所以思路就有了,直接用并查集维护连通块大小。去统计最终状态的总边数,每个连通块设大小为 s i z [ i ] siz[i] siz[i] ,总边数即为 ∑ s i z [ i ] × ( s i z [ i ] − 1 ) / 2 \sum siz[i] \times (siz[i] - 1) / 2 siz[i]×(siz[i]1)/2 ,然后因为每次操作等价于连一条边,总边数减去初始的边数 m m m 就是最多可以操作的次数。
代码:

#include <bits/stdc++.h>
#define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define db double
using namespace std;
const int maxn = 2e5 + 10;
const int mod = 998244353;
int fa[maxn], siz[maxn];
int Find(int x) {
    return fa[x] == x ? fa[x] : fa[x] = Find(fa[x]);
}
signed main() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        fa[i] = i;
        siz[i] = 1;
    }
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        u = Find(u), v = Find(v);
        if (u != v) {
            fa[u] = v;
            siz[v] += siz[u];
        }
    }
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        if (Find(i) == i) {
            ans += siz[i] * (siz[i] - 1) / 2;
        }
    }
    ans -= m;
    cout << ans << endl;
    // system("pause");
    return 0;
}

E - Toward 0

记忆化搜索,期望DP
看到数据范围 1 0 18 10^{18} 1018, 但是每次操作都是除法,因此最多也就 l o g log log 次就操作完了,这个数据范围启发我们用搜索来求期望。
考虑 d p [ i ] dp[i] dp[i] 表示当前数字为 i i i 时,让它归零的期望代价,初始化 d p [ 0 ] dp[0] dp[0] 已经是0了,所以等0。
那么显然对于操作1,可以这样转移: d p [ i ] = d p [ i / A ] + X dp[i] = dp[i/A] + X dp[i]=dp[i/A]+X, 即花费 X X X 的代价让它变成 i / A i/A i/A 的期望。
对于操作2,因为是投骰子,所以期望就是投六个数的期望均分6份合起来。即 d p [ i ] = 1 6 ∑ j = 1 6 d p [ i / j ] + Y dp[i]= \frac{1}{6}\sum_{j=1}^{6} dp[i/j]+Y dp[i]=61j=16dp[i/j]+Y
由于是最佳操作,所以每次对于这两个操作,选取期望较小的那个,取min即可。
不过这么写完会发现样例跑不出来,调试后发现,对于操作2,当骰子扔到1的时候,又循环调用了递归自己,所以死循环了。对式子分析了一下,发现可以移项求出 d p [ i ] dp[i] dp[i] 的表达式,所以最终操作2的式子是 d p [ i ] = 1 5 ∑ j = 2 6 d p [ i / j ] + 6 5 × Y dp[i] = \frac{1}{5}\sum_{j=2}^{6}dp[i/j] + \frac{6}{5}\times Y dp[i]=51j=26dp[i/j]+56×Y ,dp[i]就对这两个式子取min即可得到,保险起见可以再加一个记忆化,开long double保证一下精度。
代码:

#include <bits/stdc++.h>
#define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define db long double
using namespace std;
const int maxn = 1e5 + 10;
const int mod = 998244353;
int n, a, x, y;
map<int, db> mp;
db dfs(int now) {
    if (now == 0)
        return 0;
    if (mp[now])
        return mp[now];
    db res1 = x + dfs(now / a);
    db res2 = 0;
    for (int i = 2; i <= 6; i++)
        res2 += 1.0 / 5 * dfs(now / i);
    res2 += 6.0 / 5 * y;
    return mp[now] = min(res1, res2);
}
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    cin >> n >> a >> x >> y;
    cout << fixed << setprecision(15) << dfs(n) << endl;
    // system("pause");
    return 0;
}

F - Transpose

递归,括号序列
题意是,给定一个带大小写字母的合法括号序列,每次选择一对括号(之间不能有其他括号),然后翻转该括号内的字符串,并将字母大小写反转,问最终的字符串长什么样。
首先对于括号序列,不难想到利用栈来求出每个括号匹配的另一半的位置,然后如果从最内部的括号开始,按题意去模拟翻转过程,很显然会超时,考虑一个较长的字母串,外面套很多层括号,每次去括号都需要对整个字母串翻转,但是我们又不知道连着的两层括号其实可以抵消。
于是,得考虑一些其他思路。
图1

然后在草稿纸上画一画图,考虑上图的情况。横线是字母串,半圆表示需要进行翻转的区间,考虑这样的字母串应该要如何表示出来。
首先遍历字母串,A前面的部分就正常一个一个字母输出,然后到了A,就会考虑,能不能把整个A当成一个整体,去输出A解析后的字符串。因为A就一个嵌套,所以很容易处理出来A,接着往后到了B。也类似的,去考虑解析B,只要去看B内部的内容,B的内部又相当于是一条横线,两个待翻转区间,这时候就感觉出来这有点子问题的意思了,可以考虑递归解决。
因为处理A的时候,会发现,因为有一层括号,所以A内部的字符串我们应该逆序遍历,并且大小写翻转。而大小写是否改变,是跟括号嵌套的层数有关,所以考虑递归的函数里增加一个参数表示当前嵌套的层数,奇数次就逆序遍历,大小写反转,偶数次就正常顺着遍历。最终每个位置只遍历了一次, O ( n ) O(n) O(n) 的解决了该问题。
还有为了递归括号的内部,需要前面用栈预处理一下每个括号匹配的位置,然后递归的时候边界调整为匹配的括号的内侧一格处,怎么用栈匹配细节见代码:

#include <bits/stdc++.h>
// #define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define db double
using namespace std;
const int maxn = 5e5 + 10;
const int mod = 998244353;
int pos[maxn];
char ch[maxn];
string cal(int l, int r, int num) {
    if (l > r)
        return "";
    string ans;
    if (num & 1) {
        for (int i = r; i >= l; i--) {
            if (ch[i] == ')') {
                ans += cal(pos[i] + 1, i - 1, num + 1);
                i = pos[i];
            } else {
                if (ch[i] <= 'Z')
                    ch[i] -= 'A' - 'a';
                else
                    ch[i] -= 'a' - 'A';
                ans += ch[i];
            }
        }
    } else {
        for (int i = l; i <= r; i++) {
            if (ch[i] == '(') {
                ans += cal(i + 1, pos[i] - 1, num + 1);
                i = pos[i];
            } else {
                ans += ch[i];
            }
        }
    }
    return ans;
}
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    string str;
    cin >> str;
    int n = str.length();
    for (int i = 1; i <= n; i++)
        ch[i] = str[i - 1], pos[i] = i;
    stack<int> stk;
    for (int i = 1; i <= n; i++) {
        if (ch[i] == '(')
            stk.push(i);
        else if (ch[i] == ')')
            pos[stk.top()] = i, pos[i] = stk.top(), stk.pop();
    }
    cout << cal(1, n, 0) << endl;
    // system("pause");
    return 0;
}

G - Mediator

根号分治,强制在线
题意很简单,初始都是单点,两种操作,一是加一条边联通两个连通块,二是询问两个点是否有公共的相邻点。数据范围都是 1 0 5 10^5 105, 并且利用“加密”来强制在线。
考虑操作2的查询,发现如果我们存图,用vector存每个点的相邻点,操作2等价于两个vector求交集。集合求交也是很典的问题了,离线的话就可以用启发式合并,将查询捆绑,每次遍历查询之类的,参考百度之星的这道题 (当然这题也可以用在线的做法做)
但是这题强制在线,不能提前知道查询,提前建树,所以只好采取在线的做法。
首先,如果询问的两个点的度数比较小,那么直接暴力的遍历,复杂度也不会很大。但如果很多次询问都是问两个度数很大的点,每次遍历他们的相邻节点,复杂度就不能接受了。求集合交集,容易考虑使用bitset优化,交集就只要让两个bitset做与运算,与出来的1就代表交集部分。
但是如果给每个节点都开一个bitset,又显然不能接受这样的空间,但是因为度数很大的节点,数量不会多,所以根据 度数 × 点数 = 2 × 边数 度数\times 点数 = 2 \times 边数 度数×点数=2×边数, 即度数大于 2 × n \sqrt{2\times n} 2×n 的点数(记作超级点)不会超过 2 × n \sqrt{2\times n} 2×n 个,我们只需要准备 2 × n \sqrt{2\times n} 2×n 个bitset即可,这样的空间复杂度是允许的。
然后对于查询,如果问的两个点都是超级点,则将它们对应的bitset 与一下,再遍历一遍结果,找是否有1,复杂度是 O ( n / w ) O(n/w) O(n/w)。如果一个是超级点,一个是普通点,则遍历普通点的邻接表,判断超级点对应的bitset的对应位是否为1,复杂度 O ( 2 × n ) O(\sqrt{2\times n}) O(2×n ) 。如果是两个普通点,则直接遍历第一个邻接表,标记元素是否出现,在遍历第二个邻接表时候判断每个元素是否出现过。复杂度 O ( 2 × n ) O(\sqrt{2\times n}) O(2×n )。所以均摊下来复杂度是可以接受的。
然后就是细节方面问题,维护每个点的度数,操作1给两个点度数增加,判断是否超过根号,超过了就新分配一个bitset给该节点,并且将邻接表里对应的位置1.然后注意,如果操作1已经是超级点了,别忘了给对应的位置1(之前wa3发就是忘记这个了)

代码:

#include <bits/stdc++.h>
#define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define db double
using namespace std;
const int maxn = 2e5 + 20;
const int mod = 998244353;
int x[maxn], in[maxn];
int sq = 450;
int lst = 0, cnt;
int flag[maxn];
vector<int> G[maxn];
bitset<100100> bs[460];
int vis[maxn];
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    int n, q;
    cin >> n >> q;
    for (int i = 1; i <= q; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        int op = 1 + (((a * (1 + lst)) % mod)) % 2;
        int u = 1 + (((b * (1 + lst)) % mod)) % n;
        int v = 1 + (((c * (1 + lst)) % mod)) % n;
        if (op == 1) {
            in[u]++;
            in[v]++;
            G[u].push_back(v);
            G[v].push_back(u);
            if (flag[u])
                bs[flag[u]][v] = 1;
            if (flag[v])
                bs[flag[v]][u] = 1;
            if (!flag[u] && in[u] >= sq) {
                flag[u] = ++cnt;
                bs[cnt].reset();
                for (auto it : G[u])
                    bs[cnt][it] = 1;
            }
            if (!flag[v] && in[v] >= sq) {
                flag[v] = ++cnt;
                bs[cnt].reset();
                for (auto it : G[v])
                    bs[cnt][it] = 1;
            }
        } else {
            int ok = 0;
            if (flag[u] && flag[v]) {
                auto it = bs[flag[u]] & bs[flag[v]];
                for (int i = 1; i <= n; i++)
                    if (it[i]) {
                        lst = i;
                        ok = 1;
                        break;
                    }
            } else if ((!flag[u] && flag[v]) || (!flag[v] && flag[u])) {
                if (flag[u] > 0)
                    swap(u, v);
                for (auto it : G[u]) {
                    if (bs[flag[v]][it]) {
                        lst = it;
                        ok = 1;
                        break;
                    }
                }
            } else {
                for (auto it : G[u])
                    vis[it] = 1;
                for (auto it : G[v])
                    if (vis[it]) {
                        lst = it;
                        ok = 1;
                        break;
                    }
                for (auto it : G[u])
                    vis[it] = 0;
            }
            if (!ok)
                lst = 0;
            cout << lst << "\n";
        }
    }
    // system("pause");
    return 0;
}
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
AtCoder Beginner Contest 134 是一场 AtCoder 的入门级比赛,以下是每道题的简要题解: A - Dodecagon 题目描述:已知一个正十二边形的边长,求它的面积。 解题思路:正十二边形的内角为 $150^\circ$,因此可以将正十二边形拆分为 12 个等腰三角形,通过三角形面积公式计算面积即可。 B - Golden Apple 题目描述:有 $N$ 个苹果和 $D$ 个盘子,每个盘子最多可以装下 $2D+1$ 个苹果,求最少需要多少个盘子才能装下所有的苹果。 解题思路:每个盘子最多可以装下 $2D+1$ 个苹果,因此可以将苹果平均分配到每个盘子中,可以得到最少需要 $\lceil \frac{N}{2D+1} \rceil$ 个盘子。 C - Exception Handling 题目描述:给定一个长度为 $N$ 的整数序列 $a$,求除了第 $i$ 个数以外的最大值。 解题思路:可以使用两个变量 $m_1$ 和 $m_2$ 分别记录最大值和次大值。遍历整个序列,当当前数不是第 $i$ 个数时,更新最大值和次大值。因此,最后的结果应该是 $m_1$ 或 $m_2$ 中较小的一个。 D - Preparing Boxes 题目描述:有 $N$ 个盒子和 $M$ 个物品,第 $i$ 个盒子可以放入 $a_i$ 个物品,每个物品只能放在一个盒子中。现在需要将所有的物品放入盒子中,每次操作可以将一个盒子内的物品全部取出并分配到其他盒子中,求最少需要多少次操作才能完成任务。 解题思路:首先可以计算出所有盒子中物品的总数 $S$,然后判断是否存在一个盒子的物品数量大于 $\lceil \frac{S}{2} \rceil$,如果存在,则无法完成任务。否则,可以用贪心的思想,每次从物品数量最多的盒子中取出一个物品,放入物品数量最少的盒子中。因为每次操作都会使得物品数量最多的盒子的物品数量减少,而物品数量最少的盒子的物品数量不变或增加,因此这种贪心策略可以保证最少需要的操作次数最小。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值