AtCoder Beginner Contest 399 ABCDEF 题目解析

A - Hamming Distance

题意

给定两个长度均为 \(N\) 的字符串 \(S\)\(T\),求总共有多少个位置不同。

思路

直接输入字符串后逐位判断即可。

代码

int n;
string s, t;
cin >> n >> s >> t;
int ans = 0;
for(int i = 0; i < n; i++)
    ans += (s[i] != t[i]);
cout << ans;

B - Ranking with Ties

题意

已知有 \(N\) 人参加了一场比赛,第 \(i\) 个人的得分是 \(P_i\)

求出每个人的排名,得分越高排名越靠前。

得分相同的排名也相同,但假设得分相同的人数为 \(k\),他们的排名均为 \(r\),那么下一名的排名应当是 \(r + k\)

思路一

按题意直接模拟即可。

代码一

#include<bits/stdc++.h>
using namespace std;

int a[105], rk[105];

int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    
    int cur = 1; // 下一名的排名
    while(1)
    {
        int mx = 0;
        for(int i = 1; i <= n; i++)
            if(rk[i] == 0) // 未确定排名
                mx = max(mx, a[i]);
        if(mx == 0)
            break;
        int cnt = 0; // 相同排名人数
        for(int i = 1; i <= n; i++)
            if(a[i] == mx)
            {
                rk[i] = cur;
                cnt++;
            }
        cur += cnt;
    }
    
    for(int i = 1; i <= n; i++)
        cout << rk[i] << "\n";
    
    return 0;
}

思路二

明显一个人的排名等于“比他分数高的人数 \(+1\)”。

可以把所有人的成绩放一个新数组里,排序。

然后借助循环或者二分等方法求出比当前分数更高的分有多少个。

代码二

#include<bits/stdc++.h>
using namespace std;
int a[105], b[105];
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        b[i] = a[i];
    }
    sort(b + 1, b + n + 1);
    for(int i = 1; i <= n; i++)
    {
        // upper_bound 找比 a[i] 大的最小位置 pos,数量即 n-pos+1
        int cnt = n - (upper_bound(b + 1, b + n + 1, a[i]) - b) + 1;
        cout << cnt + 1 << "\n";
    }
    return 0;
}

C - Make it Forest

题意

给定一张简单无向图,包含 \(N\) 个点与 \(M\) 条边。

问至少删除多少条边,可以使得该图成为森林图。

森林图:当一张简单无向图中不存在任何环时,才会被称作森林图。

思路

我们的目标是让图中的每一个连通块都变成一棵树。

由于树的性质是“点数 \(-1=\) 边数”,我们只需要对每个连通块都保留“点数 \(-1\)”条边即可。

因此只需要求出每个连通块的点数 \(n\) 与边数 \(m\),那么需要删除的边数就是 \(m - (n-1)\)。求和即为答案。

代码一

借助搜索算法找连通块内点数与边数。

#include<bits/stdc++.h>
using namespace std;

int n, m;
vector<int> G[200005];

bool vis[200005]; // 判断该点是否已被访问过

int cnt, edges;
// 分别表示这一次搜索到的“总点数”以及“总度数”

void dfs(int u, int fa)
{
    vis[u] = true;
    cnt++;
    edges += G[u].size(); // 加上该点的度数
    for(int &v : G[u])
    {
        if(v == fa)
            continue;
        if(vis[v])
            continue;
        dfs(v, u);
    }
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    int ans = 0;
    for(int i = 1; i <= n; i++)
        if(!vis[i])
        {
            cnt = edges = 0;
            dfs(i, 0);
            ans += edges / 2 - (cnt - 1); // 无向图的边数 = 总度数 / 2
        }
    cout << ans;
    return 0;
}

代码二

由于整张图总点数 \(N\) 已知,当所有点都在一个连通块内时,我们的目标就是把整张图变成一棵树,即边数 \(=N-1\)

但如果所有点并不在同一个连通块内,每多一个连通块,相当于在最终的森林图中又少了一条边。

如果我们能够求出图中有多少个连通块 \(K\),就相当于我们可以在 \(N-1\) 的基础上再减少 \(K-1\) 条边。

也就是说,我们只需要留下 \((N-1) - (K-1) = N-K\) 条边即可。

答案即 \(M-(N-K)\)

至于如何求连通块数量,并查集即可。

#include<bits/stdc++.h>
using namespace std;

int n, m;
int fa[200005];

int find(int p)
{
    return p == fa[p] ? p : fa[p] = find(fa[p]);
}

void merge(int u, int v)
{
    fa[find(u)] = find(v);
}

int main()
{
    int n, m;
    cin >> n >> m;
    
    for(int i = 1; i <= n; i++)
        fa[i] = i; // 并查集初始化
    
    for(int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;
        merge(u, v);
    }
    
    int k = 0;
    for(int i = 1; i <= n; i++)
        if(i == find(i)) // 这是某个集合的根
            k++;
    
    cout << m - (n - k);
    
    return 0;
}

D - Switch Seats

题意

有一个长度为 \(2N\) 的数组 \(A=(A_1,A_2,\dots, A_{2N})\),其中 \(1, 2, \dots, N\) 的每一个正整数都在数组 \(A\) 中严格出现 \(2\) 次。

问有多少对 \((a, b)\) \((1 \le a \lt b \le N)\) 满足:

  • 一开始,\(A\) 数组中的两个 \(a\) 并不相邻,两个 \(b\)并不相邻
  • 任意进行一次或多次下面的操作,能够使得最终 \(A\) 数组中的两个 \(a\) 和两个 \(b\) 变成相邻。
    • 选择 \(A\) 数组中的某个 \(a\) 和某个 \(b\),让它们交换位置。

多组数据。

思路

首先,很明显对于每一对 \((a, b)\),能够操作的位置只有 \(4\) 个。可以发现题目中的操作只会进行一次。

如果一开始 \(a\)\(b\) 不相邻,最终又希望只通过交换某个 \(a\) 和某个 \(b\) 让它们变成相邻的,那么可以想到在一开始的数组 \(A\) 当中,第一个 \(a\) 和第一个 \(b\) 一定相邻,第二个 \(a\) 和第二个 \(b\) 也一定相邻。

也就是说只会是下面四种情况

...ab...ab...
...ab...ba...
...ba...ab...
...ba...ba...

于是我们便可以枚举任意两个相邻的数字 \((A_{i-1}, A_{i})\) \((2 \le i \le 2N)\),判断它们是否符合题意即可。

注意答案去重。

代码

#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;

int a[400005];

int pos[200005][2], cnt[200005];
// pos[i][0], pos[i][1] 记录 i 这个数字出现的第一个和第二个位置
// cnt[i] 表示此时已经找到了多少个数字 i
bool vis[200005];
// vis[i] 表示 i 这个数字一开始已经相邻,不可能成为答案

void solve()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        vis[i] = false;
        cnt[i] = 0;
    }
    for(int i = 1; i <= 2 * n; i++)
    {
        cin >> a[i];
        pos[a[i]][cnt[a[i]]++] = i; // 将 a[i] 出现的位置记录
        if(a[i] == a[i - 1]) // 已相邻,标记
            vis[a[i]] = true;
    }
    set<pii> st;
    for(int i = 2; i <= 2 * n; i++)
    {
        if(vis[a[i]] || vis[a[i-1]])
            continue;
        // 判断 (a[i-1], a[i]) 是否能成为答案
        int x = a[i], y = a[i-1];
        if(x > y)
            swap(x, y);
        if(abs(pos[x][0] - pos[y][0]) == 1
           && abs(pos[x][1] - pos[y][1]) == 1) // 前两个位置相邻,后两个位置也相邻
            st.insert(pii(x, y)); // 集合辅助去重
    }
    cout << st.size() << "\n";
}

int main()
{
    int T;
    cin >> T;
    while(T--)
        solve();
    return 0;
}

E - Replace

题意

给定两个长度均为 \(N\) 的仅由小写字母组成的字符串 \(S,T\)

判断是否能够对 \(S\) 进行任意次以下操作,将 \(S\) 变为 \(T\)

  • 选择两个不同的英文字母 \(x, y\),将 \(S\) 字符串中所有的 \(x\) 全部改成 \(y\)

思路

对于 \(S\)\(T\) 两个字符串的每一个不相同的位置 \(i\),我们的目标都是要把 \(S_i \rightarrow T_i\)

如果以 \(26\) 英文字母作为结点,我们可以根据上述关系建立一张有向图。

首先考虑最简单的情况,如果一开始两字符串完全相等,答案为 \(0\);除此之外,如果根据上述关系,我们发现所有 \(26\) 个英文字母都成为了其它字母的转换目标(即所有字母形成了一个环),根据题目给定的第四个样例可以得知,我们没法找出第 \(27\) 个字母来完成一个暂存的操作,所以此时答案为 \(-1\)

比较特殊的是,如果某个字母出现了多个转换目标,很明显是无解的,输出 \(-1\) 即可。

接下来分类讨论,这张有向图会有很多个连通块,我们只需要一个个连通块单独处理即可:

  • 如果一个连通块是一张有向无环图,明显我们就按照拓扑序一个个处理字母的变换即可,此时的操作数就是这张图的边数。
  • 如果一个连通块包含一个环,那么会出现以下两种情况:
    • 如果这个连通块只是单纯的一个环(也就是所有点都在环上),根据样例四,我们需要先把环中的某个字母变成一个没有用的字母,再依次处理环上每条关系,此时的操作数就是这个环的点数(或者边数)\(+1\)
    • 如果这个连通块中存在某些点不在环上,此时一定会出现环外的点指向环的情况,也就是说环上的某个点一定会出现入度 \(\gt 1\)的情况,此时我们只需要把环上的另外一个字母先改为环外这个字母,之后一起变成目标即可。在这种情况下的操作数还是等于连通块中的总边数。具体可以见下图:

总结,只有单个连通块所有点都在环上的情况需要特殊处理,操作次数需要额外 \(+1\)。其它情况的操作数均等于连通块内的总边数。

代码实现上,连通块的处理可以借助并查集,统计每个点的入度是否均为 \(1\) 即可。

注意处理自环的情况,自环不计入答案。

代码

#include<bits/stdc++.h>
using namespace std;

int to[26];
// to[i] 记录 i 字母要变成的目标字母
bool vis[26];
// vis[i] 标记 i 是否被当作目标字母

int ind[26];
// 记录入度

int fa[26];
int find(int p)
{
    return p == fa[p] ? p : fa[p] = find(fa[p]);
}
void merge(int u, int v)
{
    fa[find(u)] = find(v);
}

int main()
{
    memset(to, -1, sizeof to);
    
    int n;
    string a, b;
    cin >> n >> a >> b;
    
    if(a == b)
    {
        cout << 0;
        return 0;
    }
    
    for(int i = 0; i < n; i++)
    {
        int x = a[i] - 'a', y = b[i] - 'a';
        if(to[x] != -1 && to[x] != y)
        {
            cout << -1;
            return 0;
        }
        to[x] = y; // 处理出所有转换关系
        vis[y] = true; // 标记 y 成为了其他字母的转换目标
    }
    
    bool haveTemp = false;
    for(int i = 0; i < 26; i++)
        if(vis[i] == false) // 只要有一个字母没有被成为目标,那么图中的所有环就都可以被解决
        {
            haveTemp = true;
            break;
        }
    if(haveTemp == false)
    {
        // 此时所有字母都是其他字母的目标,不存在任何可以调整的字母
        cout << -1;
        return 0;
    }
    
    int ans = 0;
    for(int i = 0; i < 26; i++) // 并查集初始化
        fa[i] = i;
    
    for(int i = 0; i < 26; i++)
    {
        if(to[i] != -1)
        {
            ind[to[i]]++;
            if(to[i] != i) // 不是自环,答案 +1
            {
                ans++;
                merge(i, to[i]);
            }
        }
    }

    for(int i = 0; i < 26; i++)
    {
        if(find(i) == i) // i 是某个集合的根
        {
            bool flag = true; // 判断这个集合是否是单纯的环
            int cnt = 0; // 求集合内点的数量
            for(int j = 0; j < 26; j++)
                if(find(j) == i)
                {
                    cnt++;
                    if(ind[j] != 1)
                        flag = false;
                }
            if(cnt > 1 && flag == true) // 是一个点数大于 1 的环
                ans++; // 多交换一次
        }
    }
    
    cout << ans;
    
    return 0;
}

F - Range Power Sum

题意

给定一个长度为 \(N\) 的数组 \(A = (A_1, A_2, \dots, A_N)\) 以及一个正整数 \(K\)

\(A\) 数组的每一段区间 \([l, r]\) \((1 \le l \le r \le N)\) 内的数字总和的 \(K\) 次方之和,输出对 \(998\,244\,353\) 取模。

思路

我们考虑数字是从前往后一个一个加入到 \(A\) 数组里的。

也就是说,对于每个数字 \(A_i\),我们只考虑以 \(i\) 作为右端点的所有区间 \([1, i], [2, i], \dots, [i,i]\) 对答案的贡献。至于如何快速求出这个贡献,我们考虑递推。

在此之前,根据二项式定理,我们要记住以下式子:

\[\begin{aligned} (a + b)^k &= \sum_{i = 0}^k C_k^i \cdot a^i \cdot b^{k-i} \\ &=C_k^0 \cdot a^0 \cdot b^k + C_k^1 \cdot a^1 \cdot b^{k-1} + \dots + C_k^k \cdot a^k \cdot b^0 \end{aligned} \]

然后我们考虑所有以 \(i\) 作为右端点的区间对答案的贡献,记作 \(T_{i, k}\),其中 \(k\) 表示当前的次方数。我们可以得到:

\[\begin{aligned} T_{i, k} &= (A_i)^k + (A_i + A_{i-1})^k + (A_i + A_{i-1} + A_{i-2})^k + \dots \\ &= (A_i)^k + [A_i + (A_{i-1})]^k + [A_i + (A_{i-1} + A_{i-2})]^k + \dots \\ &= (A_i)^k + [C_k^0 A_i^k (A_{i-1})^0 + C_k^1A_i^{k-1}(A_{i-1})^1 + \dots + C_k^kA_i^0(A_{i-1})^k] + \dots \\ &= (A_i)^k + C_k^0 A_i^k \cdot [(A_{i-1})^0 + (A_{i-1} + A_{i-2})^0 + \dots] + C_k^1 A_i^{k-1} \cdot [(A_{i-1})^1 + (A_{i-1} + A_{i-2})^1 + \dots] + \dots \\ &= (A_i)^k + C_k^0 A_i^k \cdot T_{i-1,0} + C_k^1 A_i^{k-1} \cdot T_{i-1,1} + C_k^2 A_i^{k-2} \cdot T_{i-1,2} + \dots \\ &= (A_i)^k + \sum_{j=0}^k C_k^j A_i^{k-j} T_{i-1,j} \end{aligned} \]

于是我们便能获得 \(T_{i, k}\) 的递推式。

根据题意,我们需要把每个右端点的 \(k\) 次方总和全部加起来,因此答案就是 \(T_{1, K} + T_{2, K} + \dots + T_{N,K}\)

时间复杂度 \(O(N\cdot K^2)\)

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 998244353;

ll A[15];
// A[i] 表示当前正在处理的右端点这个数字的 i 次方
ll T[200005][15];
// T[i][k] 表示以 i 作为右端点时,所有区间总和的 k 次方之和
ll C[15][15];
// C[i][j] 表示组合数

int main()
{
    int n, k;
    cin >> n >> k;
    
    for(int i = 0; i <= k; i++) // 杨辉三角求组合数
    {
        C[i][0] = C[i][i] = 1;
        for(int j = 1; j < i; j++)
            C[i][j] = (C[i-1][j] + C[i-1][j-1]) % mod;
    }
    
    A[0] = 1; // 单独处理 0 次方
    for(int i = 1; i <= n; i++)
    {
        cin >> A[1]; // 输入当前这个数字,放在 1 次方的位置上
        for(int j = 2; j <= k; j++)
            A[j] = A[j - 1] * A[1] % mod; // 递推求出 2 到 k 次方
        
        for(int j = 0; j <= k; j++) // 求出每一个 T[i][j]
        {
            T[i][j] = A[j]; // 先把当前数字的 j 次方加进来
            for(int u = 0; u <= j; u++)
                T[i][j] = (T[i][j] + C[j][u] * A[j-u] % mod * T[i-1][u]) % mod;
        }
    }
    
    ll ans = 0;
    for(int i = 1; i <= n; i++)
        ans = (ans + T[i][k]) % mod;
    cout << ans;
    
    return 0;
}
posted @ 2025-03-29 23:41  StelaYuri  阅读(100)  评论(0)    收藏  举报