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  阅读(206)  评论(0)    收藏  举报