AtCoder Beginner Contest 291

A - camel Case (abc291 a)

题目大意

给定一个字符串,找到其是大写字母的位置。

解题思路

逐位判断即可。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    string s;
    cin >> s;
    int pos = find_if(s.begin(), s.end(), [](char c){
            return isupper(c);
            }) - s.begin();
    cout << pos + 1 << '\n';

    return 0;
}



B - Trimmed Mean (abc291 b)

题目大意

给定\(5n\)个分数,去掉最高的 \(n\) 个和最低的\(n\)个,然后算剩下数的平均分。

解题思路

排序后选中间\(3n\)个数的平均值即可。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    int n;
    cin >> n;
    vector<int> s(n * 5);
    for(auto &i : s)
        cin >> i;
    sort(s.begin(), s.end());
    int sum = accumulate(s.begin() + n, s.end() - n, 0);
    cout << fixed << setprecision(10) << 1.0 * sum / (3 * n) << '\n';

    return 0;
}



C - LRUD Instructions 2 (abc291 c)

题目大意

当前位置为\((0,0)\),给定一个 LRUD操作序列,问在执行该序列时,所访问的点坐标是否重复。

解题思路

\(set\)记录下访问过的下标,加速查询,模拟即可。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    int x = 0, y = 0;
    int n;
    string s;
    cin >> n >> s;
    auto check = [&](){
        set<pair<int, int>> visit;
        visit.insert({x, y});
        for(auto &i : s){
            if (i == 'L')
                -- x;
            else if (i == 'R')
                ++ x;
            else if (i == 'D')
                -- y;
            else if (i == 'U')
                ++ y;
            else 
                assert(0);
            if (visit.find({x, y}) != visit.end())
                return true;
            visit.insert({x, y});
        }
        return false;
    };
    cout << (check() ? "Yes" : "No") << '\n';

    return 0;
}



D - Flip Cards (abc291 d)

题目大意

给定\(n\)张卡,每张卡正反两面都写了个数。

现在可以将一些卡面正反颠倒,问有多少种策略,使得前一张的反面和后一张卡的正面的数都不同。

解题思路

从搜索状态考虑,为了策略的合法性(前后两张卡反正面数不同)需要的状态为前一张卡是否翻转,其余信息都可以不需要。

因此设\(dp[i][0/1]\)表示前 \(i\)张卡,第 \(i\)张卡是没翻转/翻转,且前面卡面都符合题意要求的方案数。

转移就枚举第 \(i-1\)位的翻转情况,根据是否符合题意要求。

因为每次仅涉及 \(i-1\)的状态,第一维可以滚动数组优化。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

const int mo = 998244353;

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    int n;
    cin >> n;
    vector<pair<int, int>> card(n);
    for(auto &i : card)
        cin >> i.first >> i.second;
    vector<int> dp(2, 1);
    for(int i = 1; i < n; ++ i){
        vector<int> tmp(2, 0);
        tmp[0] = dp[0] * (card[i].first != card[i - 1].first) + dp[1] * (card[i].first != card[i - 1].second);
        if (tmp[0] >= mo)
            tmp[0] -= mo;
        tmp[1] = dp[0] * (card[i].second != card[i - 1].first) + dp[1] * (card[i].second != card[i - 1].second);
        if (tmp[1] >= mo)
            tmp[1] -= mo;
        dp.swap(tmp);
    }
    cout << (dp[0] + dp[1]) % mo << '\n';

    return 0;
}



E - Find Permutation (abc291 e)

题目大意

现在有一个\(1 \sim n\)排列。已知 \(m\)个大小关系,即第 \(a_i\)个数小于第 \(b_i\)个数。

问能否从这些关系中确定唯一的排列,可以则输出排列,不可以则输出 No

解题思路

初看该题时没啥思路,然后想着看看一些特别情况,比如如何确定\(1\)的位置。

一个位置是 \(1\),那就是说该位置小于其他所有位置。而小于关系是有传递性的,于是考虑如果把这个传递性体现出来,来求得一个位置小于所有位置。

这就可以考虑一张由小于关系构成的有向图,\(a < b\)则第 \(a\)号点连一条边到第 \(b\)号点。

如果我能一个入度为 \(0\)的点出发,能到达 \(n-1\)个点 ,那就意味着这个点小于其他所有点,此点就是\(1\)。当然如果入度为 \(0\)的点有多个,那么此时 这些点都可能是\(1\),那就不行了。

\(1\)考虑完后,就去掉其影响,考虑 \(2\),发现还是原来的子问题。

然后就发现就是一个拓扑排序,在这过程中,队列里(此时度数为 \(0\)的点)只能有一个(不然有多个点都可以取该值), 依次确定出\(1,2,3\)的位置。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    int n, m;
    cin >> n >> m;
    vector<int> du(n);
    vector<vector<int>> edge(n);
    for(int i = 0; i < m; ++ i){
        int u, v;
        cin >> u >> v;
        -- u, -- v;
        edge[u].push_back(v);
        ++ du[v];
    }
    vector<int> ans(n);
    auto check = [&](){
        queue<int> team;
        for(int i = 0; i < n; ++ i){
            if (du[i] == 0)
                team.push(i);
        }
        int cur = 0;
        while(!team.empty()){
            if (team.size() != 1)
                return false;
            int u = team.front();
            ++ cur;
            ans[u] = cur;
            team.pop();
            for(auto &v : edge[u]){
                du[v] --;
                if (du[v] == 0)
                    team.push(v);
            }
        }
        return (cur == n);
    };
    if (check()){
        cout << "Yes" << '\n';
        for(auto &i : ans)
            cout << i << ' ';
    }else {
        cout << "No" << '\n';
    }

    return 0;
}



F - Teleporter and Closed off (abc291 f)

题目大意

给定\(n\)号点,依次排列,每个点都有一个数组 \(s\),第 \(i\)个点的 \(s_{i,j}\)表示从\(i\)号点能否到达 \(i+j\)号点。

对于每个\(k=2,3,4,...,n-1\),问从 \(1\)号点出发,再不经过 \(k\)号点的情况下,到达第 \(n\)号点的最小距离。

解题思路

注意这题的\(s\)的长度\(m\)只有 \(10\)

如果不考虑\(k\)的话,就设 \(dp[i]\)表示到达 \(i\)号点的最小距离,因为只能往标号大的点走,因此转移就枚举下一个到达的点,取最小值即可。

但有\(k\)的话, \(dp[n]\)就不一定合法了,因为在转移的时候我们没有限制不能走第 \(k\)号点,所以不知最终的 \(dp[n]\)是否经过第 \(k\)号点。

但因为关键决策就是第 \(k\)号点,因此我们揪出转移会涉及到 \(k\)号点的点,只有\(m - 1\)个,即第 \(k - m + 1, k - m + 2,...,k - 1\)号点,对于这些点 \(l\),我们枚举它们下一个到达的,且大于\(k\)的点 \(r\),此时从\(1\)号点到 \(n\)号点分成了三段:

  • \(1 \to l\)
  • \(l \to r\)
  • \(r \to n\)

那此时从 \(1\)号点到 \(l\)号点, \(r\)号点到 \(n\)号点都是没有限制的,而这个可以事先预处理\(dis1[l]\)表示 \(1\)号点到 \(l\)号点的最短距离,以及 \(dis2[r]\)表示从 \(r\)号点到 \(n\)号点的最短距离。而\(l\)号点到 \(r\)号点的距离是 \(1\),且保证了不经过 \(k\)号点,因此总的距离是 \(dis1[l] + dis2[r] + 1\)

预处理 \(dis1[i],dis2[i]\)的时间复杂度是 \(O(nm)\),对于每个 \(k\)的时间复杂度是\(O(m^2)\)(有 \(m\)个枚举点,每个点枚举下一个目的地有 \(m\)个),因此总的时间复杂度是 \(O(nm^2)\)

这其实跟一张图,必定经过某条边的最短路是一样的想法。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    int n, m;
    cin >> n >> m;
    vector<string> s(n);
    for(auto &i : s)
        cin >> i;
    vector<int> dis1(n, n + 5), dis2(n, n + 5);
    dis1[0] = 0;
    for(int i = 0; i < n - 1; ++ i){
        for(int j = 0; j < m && i + j + 1 < n; ++ j){
            if (s[i][j] == '1')
                dis1[i + j + 1] = min(dis1[i + j + 1], dis1[i] + 1);
        }
    }
    dis2[n - 1] = 0;
    for(int i = n - 2; i >= 0; -- i){
        for(int j = 0;j < m && i + j + 1 < n; ++ j){
            if (s[i][j] == '1')
                dis2[i] = min(dis2[i], dis2[i + j + 1] + 1);
        }
    }
    for(int i = 1; i < n - 1; ++ i){
        int ans = n + 5;
        for(int j = max(0, i - m + 1); j < i; ++ j){
            for(int k = i - j; k < m; ++ k){
                if (s[j][k] == '1')
                    ans = min(ans, dis1[j] + 1 + dis2[j + k + 1]);
            }
        }
        if (ans == n + 5)
            ans = -1;
        cout << ans << " \n"[i == n - 2];
    }

    return 0;
}



G - OR Sum (abc291 g)

题目大意

两个长度为\(n\)的数组 \(A\)\(B\)

现在可以对 数组\(A\)进行左循环移位操作,即将 \(A\)的第一个元素放到最后。

\(\sum_{i=0}^{n - 1} A_i | B_i\)的最大值。

解题思路

朴素的方法复杂度是\(O(n^2)\),即有 \(O(n)\)种操作情况,每种情况的答案计算复杂度为 \(O(n)\)。考虑优化计算。

因为位运算各个数位独立,我们依次考虑每个数位\(1\)的数量,再乘以该数位的基(就是\(2^i\))就可以得到结果。

\(\sum_{i = 0}^{\log m}2^i \sum_{j=0}^{n - 1}A^{i}_{(j + k) \% n} | B^{i}_{j}\)

其中\(k\)就是进行了 \(k\)次操作。\(A^i_j\)就是 \(A_j\)在二进制下的第 \(i\)位的值,\(0\)或者 \(1\)

单看第二个求和式子感觉是个卷积式,只要把\(B\)颠倒一下,就是\(\sum_{j=0}^{n - 1}A^{i}_{(j + k) \% n} | B^{i}_{n - j - 1}\)

而因为 \(A | B = A + B - A \& B\),因此\(\sum_{j=0}^{n - 1}A^{i}_{(j + k) \% n} + B^{i}_{n - j - 1} - A^{i}_{(j + k) \% n} \& B^{i}_{n - j - 1}\)

前两项是定值,而后一项,因为其取值只有\(0\)\(1\)与运算乘法运算是一样的结果,因此其可以看成是个卷积。

\(a = (A_0, A_1, ... , A_{n-1}, A_0, A_1, ..., A_{n - 1}), b = (B_{n-1}, B_{n-2}, ..., B_{1}, B_{0}), c = a * b\)
此时\(c_{n + k - 1} = \sum_{i = 0}^{n - 1} b_{i} \times a_{n + k - 1 - i}\),和上面的\(\&\)卷积式是一样的。

因此通过一次卷积,就能得到每个数位的移动\(k\)次后的 或运算的结果,每种操作的每个数位累计求和,求个最大值即可。卷积复杂度是\(O(n \log n)\),总的时间复杂度是 \(O(n \log n \log m)\)

神奇的代码



Ex - Balanced Tree (abc291 h)

题目大意

<++>

解题思路

<++>

神奇的代码



posted @ 2023-02-27 15:05  ~Lanly~  阅读(234)  评论(0编辑  收藏  举报