前面两道阅读理解直接跳过。
C - Sort
大意
给定一个的排列,你可以执行最多次以下操作,让序列变得有序:
- 选择两个元素,交换它们的位置。
输出任意可行的操作次数及其对应的操作步骤。
思路
从,考虑把交换到第位。操作次后必定有序。
令表示元素的位置,每次交换即可。
注意pos的对应位置也要更新。
代码
#include <iostream>
#include <vector>
#include <utility>
using namespace std;
typedef pair<int, int> PII;
int main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int n;
cin >> n;
vector<int> a(n), pos(n);
vector<PII> ans;
for(int i = 0, x; i < n; i++){
cin >> x;
x--;
a[i] = x;
pos[x] = i;
}
for(int i = 0; i < n; i++){
if(a[i] == i) continue;
ans.push_back({i, pos[i]});
swap(a[i], a[pos[i]]);
swap(pos[a[i]], pos[a[pos[i]]]);
}
cout << ans.size() << endl;
for(auto &[x, y]: ans) cout << x + 1 << ' ' << y + 1 << endl;
return 0;
}
D - New Friends
大意
给定一张无向图,若有三个点满足:
- 有边相连
- 有边相连
- 没有边相连
则用一条边连接,问最多能连多少次边。
思路
最终情况下,一个连通块内的任意两点都会连边,即变成一张完全图。
所以bfs得到每个连通块的点数和边数。
设第个连通块有个点,条边,则这个连通块对答案的贡献为。
所有连通块的贡献相加即为答案。
代码
#include <iostream>
#include <queue>
#include <utility>
using namespace std;
#define int long long
typedef pair<int, int> PII;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int n, m;
cin >> n >> m;
vector<vector<int>> G(n);
for(int i = 0; i < m; i++){
int a, b;
cin >> a >> b;
a--, b--;
G[a].push_back(b);
G[b].push_back(a);
}
int ans = 0;
vector<int> vis(n, false);
auto bfs = [&](int i){
queue<int> q;
q.push(i);
vis[i] = 1;
int cp = 1, ce = 0;
while (q.size()) {
int u = q.front();
q.pop();
for (auto v: G[u]) {
ce++;
if (vis[v])
continue;
vis[v] = 1;
q.push(v);
cp++;
}
}
return PII(cp, ce);
};
for(int i = 0; i < n; i++){
if(vis[i]) continue;
auto [cp, ce] = bfs(i);
ans += cp * (cp - 1) - ce;
}
ans /= 2;
cout << ans << endl;
return 0;
}
E - Toward 0
大意
给定,通过两类操作让变为:
- 将变为,花费的代价
- 掷一个色子,等概率掷出中的一个,将变为,花费的代价
问最优情况下,最小期望花费。
思路
考虑dp。
根据定义,当前的期望值是所有后继情况的期望值的概率加权。
设表示当前数字为,将其变为的最小期望花费。
明显。
考虑决策什么,明显是考虑使用操作1还是操作2。
两种操作都会产生一个期望值,期望值中最小的,就是最优决策。
需要分别求出操作1的期望花费和操作2的期望花费。
- 执行操作1,后继情况只有,因此。
- 执行操作2,后继情况有六个,分别是。
- 到达每种情况的概率都是,可得。
- 但是上面情况不能构成状态转移方程,因为时会循环求值。
- 我们变一下上面的公式,得到真正的。
得到后,就可以做决策,。
虽然,但是每次都除以,最多除次就会变成,所以总状态数不多,只有,不会太大。(大概级别)。
代码
#include <iostream>
#include <unordered_map>
using namespace std;
#define int long long
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int n, a, x, y;
cin >> n >> a >> x >> y;
unordered_map<int, double> dp;
auto dfs = [&](auto self, int u) -> double{
if(u == 0) return 0.0;
if(dp.count(u)) return dp[u];
double p = self(self, u / a) + x, q = 0.0;
for(int i = 2; i <= 6; i++) q += self(self, u / i);
q = q / 5 + (y * 6.0 / 5.0);
dp[u] = min(p, q);
return dp[u];
};
dfs(dfs, n);
cout.precision(10);
cout << fixed << dp[n] << endl;
return 0;
}
F - Transpose
大意
给定一个字符串,其中包括括号和大小写字母。
依次处理每个匹配的括号里的字符,将其左右颠倒,并将大小写字母变换。
问最终的字符串。
思路
首先设表示对应括号的另一个端点的编号。使用括号匹配的方法预处理出。
由于题目中的操作是反反得正的(不考虑括号来说),例如,所以我们可以预先确定字符的大小写:
- 括号深度为偶数的字符不会大小写反转。
- 括号深度为奇数的字符则大小写反转。
根据这一点提前交换字母的大小写,就无需再考虑字母的大小写了。
这样我们就简化了问题,接下来设为按要求处理后的答案(表示正序或反序)。
那么就可以按表示的顺序输出中的字符,遇到形如的子串递归处理即可。
注意反序处理时,先遇到的是右括号。
Q&A
如何找到形如(...)的子串?
在已经预处理的情况下:
- 如果是正序处理,那么当 时,就可以找到一个范围为的子串。
- 如果是反序处理,那么当 时,就可以找到一个范围为的子串。
这就说明了上文的最后一句话。
其他需要注意的点
注意递归调用时不能带上括号,同时要取反。
而且遇到(...)子串时,需要处理完子串后才能继续处理下一个字符。
例如:
- 正序:应调用
- 反序:应调用
取反是因为反序的时候,(...)子串内的字符需要维持和外面相反的顺序。正序同理。
代码
#include <iostream>
#include <vector>
using namespace std;
int main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
string s;
cin >> s;
int n = s.size();
vector<int> mch(n, -1), stk;
auto flip = [&](char c) -> char{
if(c >= 'A' && c <= 'Z') return tolower(c);
return toupper(c);
};
int dep = 0;
for(int i = 0; i < n; i++){
if(s[i] == '('){
stk.push_back(i);
dep++;
}
else if(s[i] == ')'){
int f = stk.back();
mch[i] = f; mch[f] = i;
stk.pop_back();
dep--;
}else if(dep & 1) s[i] = flip(s[i]);
}
string ans;
auto dfs = [&](auto self, int l, int r, int d) -> void{
if(d == 0){
while(l <= r){
if(s[l] == '('){
self(self, l + 1, mch[l] - 1, 1);
l = mch[l];
}else ans.push_back(s[l]);
l++;
}
}else{
while(l <= r){
if(s[r] == ')'){
self(self, mch[r] + 1, r - 1, 0);
r = mch[r];
}else ans.push_back(s[r]);
r--;
}
}
};
dfs(dfs, 0, n - 1, 0);
cout << ans << endl;
return 0;
}