AtCoder Beginner Contest 308

这几天在收拾东西搬家,先附上代码,晚点补上题解补完了
感觉这次FG都写不太明白

A - New Scheme (abc308 A)

题目大意

给定八个数,问是否满足以下要求:

  • 不严格升序
  • 每个数在\(100 \sim 675\)之间
  • 每个数都是 \(25\)的倍数

解题思路

依次对每个数判断是否符合这三个条件即可。

神奇的代码
#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);
    array<int, 8> a;
    for(auto &i : a)
        cin >> i;
    auto ok = [&](){
        for(int i = 0; i < 8; ++ i){
            if (i && a[i] < a[i - 1])
                return false;
            if (a[i] < 100 || a[i] > 675)
                return false;
            if (a[i] % 25)
                return false;
        }
        return true;
    };
    if (ok())
        cout << "Yes" << '\n';
    else 
        cout << "No" << '\n';

    return 0;
}



B - Default Price (abc308 B)

题目大意

高桥吃\(n\)份寿司,寿司分为很多种类,其中告诉了 \(m\)种寿司的价格,以及没提到的其他种类寿司的统一价格,问需要多少钱。

解题思路

\(map\)记录每种寿司的价格,直接累计计算即可。

当然 \(n\)只有 \(100\),花 \(O(m)\)来计算每份寿司的价格也行。

神奇的代码
#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;
    map<string, int> price;
    vector<string> a(n);
    for(auto &i : a)
        cin >> i;
    vector<string> col(m);
    for(auto &i : col)
        cin >> i;
    int ano;
    cin >> ano;
    for(auto &i : col){
        int p;
        cin >> p;
        price[i] = p;
    }
    int ans = 0;
    for(auto &i : a){
        if (price.find(i) == price.end())
            ans += ano;
        else 
            ans += price[i];
    }
    cout << ans << '\n';

    return 0;
}



C - Standings (abc308 C)

题目大意

抛硬币,给定\(n\)个人抛\(n\)次硬币后的正面次数 \(a\)和反面次数 \(b\)

要求给这\(n\)个人降序排序,排序的关键字为 \(\frac{a}{a+b}\)

解题思路

直接排序就好了。

一般最好避免浮点数比较,因为会有误差,在此处我们要比较\(\frac{a_i}{a_i + b_i} - \frac{a_j}{a_j + b_j}\) 的正负性,可以改为比较\(a_i(a_j + b_j) - a_j(a_i + b_i)\)的正负性,这样就避免浮点数了。

神奇的代码
#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<array<int, 2>> p(n);
    for(auto &i : p)
        cin >> i[0] >> i[1];
    vector<int> id(n);
    iota(id.begin(), id.end(), 0);
    sort(id.begin(), id.end(), [&](int a, int b){
        LL l = 1ll * p[a][0] * (p[b][0] + p[b][1]);
        LL r = 1ll * p[b][0] * (p[a][0] + p[a][1]);
        return l != r ? l > r : a < b;
    });
    for(auto &i : id)
        cout << i + 1 << ' ';
    cout << '\n';

    return 0;
}



D - Snuke Maze (abc308 D)

题目大意

给定一个二维矩阵,问能否从左上走到右下,要求途径的每个格子上的字母组成 snuke的不断重复拼接的形式。

解题思路

直接搜索即可。

考虑搜索的状态,一个是所处的位置(行和列),另一个是已经走过的距离(为了定位要满足snuke中的哪个字母),其状态数只有\(O(5nm)\),转移 \(O(1)\),于是直接记忆化即可。

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

array<int, 4> dx{1, -1, 0, 0};
array<int, 4> dy{0, 0, 1, -1};

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    int h, w;
    cin >> h >> w;
    vector<string> tu(h);
    for(auto &i : tu)
        cin >> i;
    string target = "snuke";
    vector<vector<vector<int>>> visit(h, vector<vector<int>>(w, vector<int>(target.size(), 0)));
    function<bool(int, int, int)> dfs = [&](int x, int y, int len){
        visit[x][y][len] = 1;
        if (tu[x][y] != target[len])
            return false;
        if (x == h - 1 && y == w - 1)
            return true;
        for(int i = 0; i < 4; ++ i){
            int nx = x + dx[i], ny = y + dy[i], nlen = (len + 1) % target.size();
            if (nx < 0 || nx >= h || ny < 0 || ny >= w || visit[nx][ny][nlen])
                continue;
            if (dfs(nx, ny, nlen))
                return true;
        }
        return false;
    };
    auto ok = [&](){
        return dfs(0, 0, 0);
    };
    if (ok())
        cout << "Yes" << '\n';
    else 
        cout << "No" << '\n';

    return 0;
}



E - MEX (abc308 E)

题目大意

给定一个数组\(a\),以及同样长度,仅包括字符 MEX的字符串\(s\)

求所有满足 \(i < j < k\),且 \(s_is_js_k=\) MEX\(mex(a_i, a_j, a_k)\)的和。

\(mex(x)\)函数是求不在 \(x\)内的最小非负整数。

解题思路

先考虑找两个字符,比如EX,正常来说我们是从左到右遍历,遇到E时,需要统计所有在该E右边的X的信息,然后得到结果。我们可以从右往左统计每个X对应的数组\(a\)的值,这样对于此时的每一个 E,当前统计的信息就是该E右边的X的信息。

考虑要统计什么信息,假设当前的E对应的数组\(a\)的值是 \(e\),那么所有 EX的组成的mex只有三种情况:

  • \(mex(e, 0)\)
  • \(mex(e, 1)\)
  • \(mex(e, 2)\)

此时以该E开始,右边的所有X组成的EX,对答案的贡献就分这三类,因此我们就记录一下右边有多少个是\(0\)X\(1\)X\(2\)X。然后乘以对应的mex值就是对答案的贡献。这就是我们要统计的信息。

解决了EX,对于MEX,其实可以看成MEX,只是这时候的EX\(6\)种情况:

  • mex(M, 0, 0)
  • mex(M, 0, 1)
  • mex(M, 0, 2)
  • mex(M, 1, 1)
  • mex(M, 1, 2)
  • mex(M, 2, 2)

同样我们就记录EX分别是\(00,01,02,11,12,22\)这些情况的数量,再乘以对应的 mex值,就是该M对答案的贡献。而统计EX 的数量就是刚刚的问题。

代码里记录\(00,01\)的情况用的二进制状压的方法。

神奇的代码
#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> a(n);
    for(auto &i : a)
        cin >> i;
    string s;
    cin >> s;
    array<int, 3> x{};
    array<LL, 8> ex{};
    map<int, int> tr{{0, 1}, {1, 2}, {2, 4}};
    map<int, int> mex;
    auto get_mex = [&](int x, int y, int z){
        set<int> candidate{0, 1, 2, 3};
        if (candidate.count(x))
            candidate.erase(x);
        if (candidate.count(y))
            candidate.erase(y);
        if (candidate.count(z))
            candidate.erase(z);
        return *candidate.begin();
    };
    for(int i = 0; i < 3; ++ i)
        for(int j = 0; j < 3; ++ j)
            for(int k = 0; k < 3; ++ k){
                mex[tr[i] | tr[j] | tr[k]] = get_mex(i, j, k);
            }
    LL ans = 0;
    for(int i = n - 1; i >= 0; -- i){
        if (s[i] == 'X'){
            x[a[i]] ++;
        }else if (s[i] == 'E'){
            for(int j = 0; j < 3; ++ j){
                int status = (tr[a[i]] | tr[j]);
                ex[status] += x[j];
            }
        }else{
            for(int j = 0; j < 8; ++ j){
                ans += mex[tr[a[i]] | j] * ex[j];
            }
        }
    }
    cout << ans << '\n';

    return 0;
}



感觉也可以枚举 E,然后统计左边的 M的信息以及右边X的信息(就是对应的数的个数),然后再枚举它们取值的3\times 3 = 9种情况计算贡献。


F - Vouchers (abc308 F)

题目大意

给定\(n\)个商品的价格,以及 \(m\)个优惠券,每个优惠券只能用于一个商品,且每个商品购买时只能用一个优惠券。

\(i\)个优惠券可以优惠\(d_i\)元,但要求对应商品价格至少 \(l_i( l_i \geq d_i)\)元。

问购买这 \(n\)个商品需要的最少价格。

解题思路

有几种思考方向,可以枚举优惠券,也可以枚举商品。枚举优惠券的话,考虑枚举的顺序,比如以\(d_i\)升序, \(l_i\)的降序。枚举商品的话,考虑以商品价格的升序还是降序枚举。就这几种思考方向,分别思考一下或许会发现一些突破口。

可以发现,以商品价格的升序方式考虑的话,会不断有新的优惠券可以使用,且先前旧的未使用的优惠券也可以使用,这是一个很好的性质,因此我们就可以从这些未使用且可使用的优惠券中选优惠力度最大的。

考虑是否存在反例,因为每个商品只能用一个优惠券,因此每次都只会从未使用中选最好的一个。当考虑一个新商品,同时还有更多的优惠券变得可使用时,这些优惠券不能代替先前的优惠券适用(不满足最低价格),因此也替换不了。

感觉这个贪心貌似就对了。

如果考虑商品价格降序枚举的话,会不断有优惠券变得不可使用,考虑使用门槛低优惠力度大的优惠券,会发现这样的做法不太行。

神奇的代码
#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> p(n);
    for(auto &i : p)
        cin >> i;
    vector<array<int, 2>> coupon(m);
    for(auto &i : coupon)
        cin >> i[1];
    for(auto &i : coupon)
        cin >> i[0];
    priority_queue<array<int, 2>> best{};
    sort(p.begin(), p.end());
    sort(coupon.begin(), coupon.end(), [](const auto& a, const auto& b){
            return a[1] < b[1];
            });
    int pos = 0;
    LL ans = 0;
    for(auto &i : p){
        while(pos < m && coupon[pos][1] <= i){
            best.push(coupon[pos]);
            ++ pos;
        }
        int discout = 0;
        if (!best.empty()){
            discout = best.top()[0];
            best.pop();
        }
        ans += i - discout;
    }
    cout << ans << '\n';

    return 0;
}



G - Minimum Xor Pair Query (abc308 G)

题目大意

初始空数组,维护三种操作:

  • 1 x 往数组里加入一个数\(x\)
  • 2 x 往数组里删除一个数\(x\)
  • 3 从数组选两个数,使得异或值最小,求这个最小值

解题思路

不考虑操作1,2,仅考虑如何从数组里选两个异或值最小。很显然选择的两个数,在二进制下第一个不同的位置尽可能的在低位。为此我们可以对这\(n\)个数建立一棵 trie树,发现该问题可以递归的解决。

考虑一个节点\(u\),左儿子(二进制下为\(0\))\(lson\),右儿子(二进制下为\(1\))\(rson\)

  • 如果\(u\)下只有两个数,一个数在 \(lson\)子树内,一个在 \(rson\)内,那以 \(u\)节点为根的子树的最小异或值 \(ans_u = lson \oplus rson\)。一般我们要遍历到叶子才知道这个\(lson,rson\)是多少,但为了减少时间复杂度,可以把这个信息往上提到\(lson,rson\)处,这样就不用遍历到叶子。
  • 如果\(lson\)下有至少两个数,那我们选择的两个数要么都在\(lson\)里,要么都不在 \(lson\)里。那么以\(u\)节点为根的子树的最小异或值 \(ans_u = min(ans_u, ans_{lson})\),而\(ans_{lson}\)是个子问题。
  • 如果\(rson\)下有至少两个数,同样我们选择的两个数要么都在\(rson\)里,要么都不在 \(rson\)里。那么以\(u\)节点的子树的最小异或值 \(ans_u = min(ans_u, ans_{rson})\)\(ans_{rson}\)同样是个子问题。
  • 其余情况\(ans_u\)都是无穷大。

对于叶子节点,如果该叶子对应两个数,那么 \(ans_{leaf} = 0\),否则为无穷大。

考虑修改的话,我们发现上述产生答案的信息(以\(u\)节点为根的子树中数的个数 以及 只有一个数时其数是多少,其实就是叶子信息)是可以实时维护的:插入就是个数\(+1\),再判断数的个数是不是 \(1\),是的话同步一下儿子的信息。于是问题就解决了。

两个信息的维护其实都是一个很简单的\(dp\)

  • \(cnt_u\)表示以 \(u\)节点为根的子树的数的个数,则 \(cnt_u = cnt_{lson} + cnt_{rson}\)
  • \(value_u\)表示以 \(u\)节点为根的子树中,如果数的个数为\(1\),其值是多少,则 \(value_u = value_{lson} [cnt_{lson} == 1] + value_{rson} [cnt_{rson} == 1]\)

\(ans_u\)就根据上述的情况就可以得到了。

考虑到信息的维护,trie树插入得写成递归形式,因为插入后要合并信息,更新答案。

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

const int inf = (1 << 30) + 114514;
const int SZ = 2;
template<typename T, typename K>
struct Trie {
    struct node {
        K value;
        int ans;
        int vis_count;
        array<int, SZ> children;

        node(K val) : value(val) {
            vis_count = 0;
            fill(children.begin(), children.end(), 0);
            ans = inf;
        }
    };

    int cast(K val) {
        int ret = val;  
        assert(ret < SZ and ret >= 0);
        return ret;
    }

    vector<node> tree;

    Trie(K val) {
        tree.push_back(node(val));
    }

    void insert(int v, int pos, int cur, const T &sequence, bool remove = false) {

        if (remove) {
            tree[cur].vis_count -= 1;
        } else {
            tree[cur].vis_count += 1;
        }

        if (pos < sequence.size()){

            K value = sequence[pos];
            if (tree[cur].children[cast(value)] == 0) {
                tree[cur].children[cast(value)] = (int) tree.size();
                tree.emplace_back(v);
            }

            insert(v, pos + 1, tree[cur].children[cast(value)], sequence, remove);

            if (tree[cur].vis_count == 1){
                int lson = tree[cur].children[cast(value)];
                int rson = tree[cur].children[cast(value) ^ 1];
                if (lson != 0 && tree[lson].vis_count != 0)
                    tree[cur].value = tree[lson].value;
                else 
                    tree[cur].value = tree[rson].value;
            }

            tree[cur].ans = inf;
            int lson = tree[cur].children[0], rson = tree[cur].children[1];
            if (lson != 0 && tree[lson].vis_count > 1)
                tree[cur].ans = min(tree[cur].ans, tree[lson].ans);
            if (rson != 0 && tree[rson].vis_count > 1)
                tree[cur].ans = min(tree[cur].ans, tree[rson].ans);
            if (lson != 0 && rson != 0 && tree[lson].vis_count == 1 && tree[rson].vis_count == 1)
                tree[cur].ans = tree[lson].value ^ tree[rson].value;
        }else{
            tree[cur].ans = inf;
            if (tree[cur].vis_count > 1)
                tree[cur].ans = 0;
        }
    }

    void remove(int v, const T& sequence) {
        insert(v, 0, 0, sequence, true);
    }

    int get_min(){
        return tree.front().ans;
    }

};

int main(void) {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
    Trie<array<int, 30>, int> tree(0);
    auto tr2bin = [](int x){
        array<int, 30> bin{};
        for(int i = 0; i < 30; ++ i){
            bin[i] = ((x >> (29 - i)) & 1);
        }
        return bin;
    };

    int q;
    cin >> q;
    while(q--){
        int op;
        cin >> op;
        if (op == 1){
            int x;
            cin >> x;
            tree.insert(x, 0, 0, tr2bin(x));
        }else if (op == 2){
            int x;
            cin >> x;
            tree.remove(x, tr2bin(x));
        }else{
            cout << tree.get_min() << '\n';
        }
    }

    return 0;
}



看了看题解发现异或最小有个神奇的性质:其最小值一定相邻两个数之间,这里数是升序排序的。证明的话题解也有,通俗点讲就是考虑证明\(min(x \oplus y, y \oplus z) < x \oplus z\)。考虑\(x\)\(z\) 二进制下从高到低位第一个不同的地方 ,其\(x\)\(0\)\(z\)\(1\)(因为 \(x < z\)),而 \(y\)再此处的值要么是 \(0\)要么是 \(1\),肯定与 \(x\)\(z\)相同,因此其异或值在该位是 \(0\),因此可以更小。

有了这个性质之后,我们可以用一个multiset维护这个序列,用于插入操作、以及删除操作的查找和删除,再用另一个multiset维护相邻两数的异或值。

神奇的代码
#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);

    multiset<int> a, ans;
    int q;
    cin >> q;
    while(q--){
        int op;
        cin >> op;
        if (op == 1){
            int x;
            cin >> x;
            if (!a.empty()){
                auto nxt = a.upper_bound(x), pos = (nxt == a.begin() ? nxt : prev(nxt));
                if (nxt != a.begin()){
                    ans.extract(*pos ^ *nxt);
                    ans.insert(*pos ^ x);
                }
                if (nxt != a.end()){
                    ans.insert(x ^ *nxt);
                }
            }
            a.insert(x);
        }else if (op == 2){
            int x;
            cin >> x;
            auto pos = a.lower_bound(x), prv = (pos == a.begin() ? pos : prev(pos)), nxt = next(pos);
            if (pos != a.begin()){
                ans.extract(*prv ^ *pos);
            }
            if (nxt != a.end())
                ans.extract(*pos ^ *nxt);
            if (pos != a.begin() && nxt != a.end())
                ans.insert(*prv ^ *nxt);
            a.extract(x);
        }else{
            cout << *ans.begin() << '\n';
        }
    }

    return 0;
}



Ex - Make Q (abc308 Ex)

题目大意

<++>

解题思路

<++>

神奇的代码



posted @ 2023-07-02 01:28  ~Lanly~  阅读(479)  评论(2编辑  收藏  举报