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+1int 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]\) 对答案的贡献。至于如何快速求出这个贡献,我们考虑递推。
在此之前,根据二项式定理,我们要记住以下式子:
然后我们考虑所有以 \(i\) 作为右端点的区间对答案的贡献,记作 \(T_{i, k}\),其中 \(k\) 表示当前的次方数。我们可以得到:
于是我们便能获得 \(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;
}