AtCoder Beginner Contest 350

前面两道阅读理解直接跳过。

C - Sort

大意

给定一个1 \to n的排列A,你可以执行最多n-1次以下操作,让序列变得有序:

  • 选择两个元素,交换它们的位置。

输出任意可行的操作次数及其对应的操作步骤。

思路

i1 \to n,考虑把i交换到第i位。操作n-1次后必定有序。

pos_i表示元素A_i的位置,每次交换A_i,A_{pos_i}即可。

注意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

大意

给定一张无向图,若有三个点x,y,z满足:

  • x,y边相连
  • y,z边相连
  • x,z没有边相连

则用一条边连接x,z,问最多能连多少次边。

思路

最终情况下,一个连通块内的任意两点都会连边,即变成一张完全图。

所以bfs得到每个连通块的点数边数

设第i个连通块有cp_i个点,ce_i条边,则这个连通块对答案的贡献\frac{cp_i \times (cp_i+1)}{2}-ce_i

所有连通块的贡献相加即为答案。

代码

#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

大意

给定n,x,y,a,通过两类操作让n变为0

  • n变为\lfloor \frac{n}{a} \rfloor,花费x的代价
  • 掷一个色子,等概率掷出1\to 6中的一个b,将n变为\lfloor \frac{n}{b} \rfloor,花费y的代价

最优情况下,最小期望花费。

思路

考虑dp

根据定义,当前的期望值是所有后继情况的期望值的概率加权

dp_i表示当前数字为i,将其变为0最小期望花费

明显dp_0=0

考虑决策什么,明显是考虑使用操作1还是操作2

两种操作都会产生一个期望值,期望值中最小的,就是最优决策。

需要分别求出操作1的期望花费p_n和操作2的期望花费q_n

  • 执行操作1,后继情况只有dp_{\lfloor \frac{n}{a} \rfloor},因此p_n=dp_{\lfloor \frac{n}{a} \rfloor}
  • 执行操作2,后继情况有六个,分别是dp_{\lfloor \frac{n}{1} \rfloor},dp_{\lfloor \frac{n}{2} \rfloor},dp_{\lfloor \frac{n}{3} \rfloor},dp_{\lfloor \frac{n}{4} \rfloor},dp_{\lfloor \frac{n}{5} \rfloor},dp_{\lfloor \frac{n}{6} \rfloor}
  • 到达每种情况的概率都是\frac{1}{6},可得q_n=\frac{\sum^6_{i=1}dp_{\lfloor \frac{n}{i} \rfloor}}{6} + y
  • 但是上面情况不能构成状态转移方程,因为i=1时会循环求值
  • 我们变一下上面的公式,得到真正的q_n=\frac{\sum^6_{i=2}dp_{\lfloor \frac{n}{i} \rfloor}}{5} + \frac{6}{5}y

得到p_n,q_n后,就可以做决策,dp_n=\min(p_n,q_n)

虽然n \le 10^{18},但是每次都除以i,最多除\log_in次就会变成0,所以总状态数不多,只有O(\log_2n \times \log_3n \times \log_4n \times \log_5n \times \log_6n),不会太大。(大概10^7级别)。

代码

#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

大意

给定一个字符串S,其中包括括号和大小写字母

依次处理每个匹配的括号里的字符,将其左右颠倒,并将大小写字母变换

问最终的字符串。

思路

首先设P_i表示S_i对应括号的另一个端点的编号。使用括号匹配的方法预处理出P

由于题目中的操作是反反得正的(不考虑括号来说),例如\texttt{((abc))} \to \texttt{(CBA)} \to \texttt{abc},所以我们可以预先确定字符的大小写:

  • 括号深度为偶数的字符不会大小写反转。
  • 括号深度为奇数的字符则大小写反转。

根据这一点提前交换字母的大小写,就无需再考虑字母的大小写了。

这样我们就简化了问题,接下来设f(l,r,d)S_{l\cdots r}按要求处理后的答案(d表示正序或反序)。

那么就可以按d表示的顺序输出S_{l\cdots r}中的字符,遇到形如\texttt{(...)}的子串递归处理即可。

注意反序处理时,遇到的是右括号

Q&A

如何找到形如(...)的子串?

在已经预处理P的情况下:

  • 如果是正序处理,那么当 S_i = \texttt{(} 时,就可以找到一个范围为[i,P_i]子串
  • 如果是反序处理,那么当 S_i = \texttt{)} 时,就可以找到一个范围为[P_i,i]子串

这就说明了上文的最后一句话。

其他需要注意的点

注意递归调用时不能带上括号,同时d要取反。

而且遇到(...)子串时,需要处理完子串后才能继续处理下一个字符

例如:

  • 正序:应调用f(i+1,P_i-1,1)
  • 反序:应调用f(P_i+1,i-1,0)

d取反是因为反序的时候,(...)子串内的字符需要维持和外面相反的顺序。正序同理。

代码

#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;
}
  • 20
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值