AtCoder Beginner Contest 338

A - Capitalized? (abc338 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;
    auto t = s;
    t[0] = toupper(t[0]);
    transform(t.begin() + 1, t.end(), t.begin() + 1, ::tolower);
    if (s == t)
        cout << "Yes" << endl;
    else
        cout << "No" << endl;

    return 0;
}



B - Frequency (abc338 B)

题目大意

给定一个字符串,给出出现次数最多的字母,若出现次数相同,则输出字典序较小的那个。

解题思路

对字符串计数,然后求最大值,然后按字典序找到第一个等于最大值的即可。

神奇的代码
#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, 26> cnt{};
    string s;
    cin >> s;
    for (auto& i : s) {
        cnt[i - 'a']++;
    }
    int maxx = ranges::max(cnt);
    for (int i = 0; i < 26; ++i)
        if (cnt[i] == maxx) {
            cout << char(i + 'a') << '\n';
            break;
        }

    return 0;
}



C - Leftover Recipes (abc338 C)

题目大意

现有\(n\)种素材各 \(q_i\)个。

制作两种物品,物品\(A\)需要素材各 \(a_i\)个,物品 \(B\)需要素材各 \(b_i\)个。

问制作出来的物品数量的最大值。

解题思路

注意到\(q_i \leq 10^6\)

那么一种物品制作出来的最大数量只有 \(10^6\)

因此我们可以直接枚举制作的物品 \(A\)的数量,然后用剩下的素材看看能制作多少个物品 \(B\)

时间复杂度是 \(O(10^6n)\)

神奇的代码
#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<LL> q(n), a(n), b(n);
    for (auto& x : q)
        cin >> x;
    for (auto& x : a)
        cin >> x;
    for (auto& x : b)
        cin >> x;
    LL ans = 0;
    for (int i = 0; i <= 1000000; ++i) {
        vector<LL> left(n);
        for (int j = 0; j < n; ++j) {
            left[j] = q[j] - 1ll * i * a[j];
        }
        if (ranges::min(left) < 0) {
            continue;
        }
        LL r = 1e9;
        for (int j = 0; j < n; ++j) {
            if (b[j])
                r = min(r, left[j] / b[j]);
        }
        ans = max(ans, r + i);
    }
    cout << ans << '\n';

    return 0;
}



D - Island Tour (abc338 D)

题目大意

给定一个\(n\)个点的环,然后依次访问\(m\)个点 \(v_1,v_2,...,v_m\)

现问删去一条边,求依次访问这些点的距离(边数和)的最小值。

解题思路

一个朴素的\(O(n^2)\)做法即为枚举删去的边 \(i \to i+1\),然后依次访问这些点求距离,取最小值。

考虑如何优化,上述做法的时间复杂度,一个花在了枚举删去的边 \(O(n)\),一个花在了计算距离的 \(O(n)\)

按顺序枚举删去的边,看看后者能否快速从上一个的答案得到。

通过画图可以发现可行。如下图所示。

假设一开始删除 \(n \to 1\)的边,求距离,即 \(\sum_{i=1}^{m-1}|v_{i+1} - v_i|\)

然后考虑删除 \(1 \to 2\)的边,发现 \(1 \to 3\)的路径会变化,长度从原来的 \(3-1变成了5-(3-1)\)。而其他的边比如 \(2 \to 4\)的路径没有变化。

而当删除 \(3 \to 4\)的边时,原来的 \(1 \to 3\)的路径再度发生变化,从 \(5-(3-1)\)变回了 \(3-1\)

综上观察可得,对于原来的路径 \(i \to j(i < j)\),当我们删除边 \(i \to i+1\)时,该路径会发生变化,而当删除边 \(j \to j + 1\)时,该路径会再度发生变化。

因此当我们删边从 \(i-1 \to i\)变成 \(i \to i +1\)时,对于路径 \(x \to y(x < y)\)中,只有所有 \(x = i\)\(y = i\)的路径会发生变化,我们直接更新这些路径的距离即可。其他路径则不会发生变化。

由于每条边只会被更新两次,因此总的时间复杂度是 \(O(n + 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;
    vector<int> x(m);
    for (auto& i : x) {
        cin >> i;
        --i;
    }
    vector<array<int, 3>> edge(m - 1);
    for (int i = 0; i < m - 1; ++i) {
        int u = x[i], v = x[i + 1];
        if (u > v)
            swap(u, v);
        edge[i] = {u, v, v - u};
    }
    LL sum = 0;
    for (auto& e : edge) {
        sum += e[2];
    }
    LL ans = sum;
    vector<vector<int>> l(n), r(n);
    for (int i = 0; i < m - 1; ++i) {
        l[edge[i][0]].push_back(i);
        r[edge[i][1]].push_back(i);
    }
    for (int i = 0; i < n; ++i) {
        for (auto id : l[i]) {
            sum -= edge[id][2];
            edge[id][2] = n - edge[id][2];
            sum += edge[id][2];
        }
        for (auto id : r[i]) {
            sum -= edge[id][2];
            edge[id][2] = n - edge[id][2];
            sum += edge[id][2];
        }

        ans = min(ans, sum);
    }
    cout << ans << '\n';

    return 0;
}



E - Chords (abc338 E)

题目大意

\(n\)个点的圆环,给定\(m\)条线段,端点不重合,问是否有交点。

解题思路

考虑枚举线段\([l,r]\),我们需要判断是否有线段与其相交,就需要:

  • 判断所有左端点在\([l+1, r-1]\)的线段的右端点的最大值是否大于\(r\)
  • 判断所有右端点在\([l+1, r-1]\)的线段的左端点的最小值是否小于\(l\)

对于第一个判断,则对线段的左端点排序,那上述判断就是一个关于右端点的区间最值问题,因为是多次查询而无修改,则用\(ST\)维护即可。这个区间范围直接通过关于左端点对\(l\)\(r\)的二分得到。
对于第二个判断,则对线段的右端点排序,那上述判断就是一个关于左端点的区间最值问题,因为是多次查询而无修改,则用\(ST\)维护即可。这个区间范围直接通过关于左端点对\(l\)\(r\)的二分得到。

时间复杂度为\(O(n\log n)\)

好像有更简洁的做法

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

template <typename T, class F = function<T(const T&, const T&)>>
class SparseTable {
  public:
    int n;
    vector<vector<T>> mat;
    F func;

    SparseTable(const vector<T>& a, const F& f) : func(f) {
        n = static_cast<int>(a.size());
        int max_log = 32 - __builtin_clz(n);
        mat.resize(max_log);
        mat[0] = a;
        for (int j = 1; j < max_log; j++) {
            mat[j].resize(n - (1 << j) + 1);
            for (int i = 0; i <= n - (1 << j); i++) {
                mat[j][i] = func(mat[j - 1][i], mat[j - 1][i + (1 << (j - 1))]);
            }
        }
    }

    T get(int from, int to) const { // [from, to]
        assert(0 <= from && from <= to && to <= n - 1);
        int lg = 32 - __builtin_clz(to - from + 1) - 1;
        return func(mat[lg][from], mat[lg][to - (1 << lg) + 1]);
    }
};

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vector<array<int, 2>> e(n);
    for (auto& i : e) {
        cin >> i[0] >> i[1];
        if (i[0] > i[1])
            swap(i[0], i[1]);
    }
    sort(e.begin(), e.end(),
         [](const array<int, 2>& a, const array<int, 2>& b) {
             return a[0] < b[0];
         });
    vector<int> s1(n), t1(n);
    for (int i = 0; i < n; ++i) {
        s1[i] = e[i][0];
        t1[i] = e[i][1];
    }
    SparseTable<int> rb(t1, [&](int a, int b) { return max(a, b); });
    auto ee = e;
    sort(ee.begin(), ee.end(),
         [](const array<int, 2>& a, const array<int, 2>& b) {
             return a[1] < b[1];
         });
    vector<int> s2(n), t2(n);
    for (int i = 0; i < n; ++i) {
        s2[i] = ee[i][1];
        t2[i] = ee[i][0];
    }
    SparseTable<int> ls(t2, [&](int a, int b) { return min(a, b); });
    bool ok = false;
    auto check = [&](int l, int r, auto& pos, auto& st) {
        auto L = ranges::upper_bound(pos, l) - pos.begin();
        auto R = ranges::lower_bound(pos, r) - pos.begin();
        if (L >= R)
            return false;
        auto v = st.get(L, R - 1);
        return v < l || v > r;
    };
    for (auto& i : e) {
        if (check(i[0], i[1], s1, rb) || check(i[0], i[1], s2, ls)) {
            ok = true;
            break;
        }
    }
    if (ok)
        cout << "Yes" << endl;
    else
        cout << "No" << endl;

    return 0;
}



F - Negative Traveling Salesman (abc338 F)

题目大意

给定一张图,边有边权,问访问所有点的最小边权和。边权有负,但无负环。多次访问算多次边权。

解题思路

一个朴素的做法就是状压\(DP\),设 \(dp[i][j]\)表示当前访问的点的状态为\(i\),当前在点\(j\)的最小距离。

因为会有边权为负,直接\(DP\)的话(即两个\(for\)循环,\(i\)\(j\)),在转移时\(i\)不变的情况下,可能会得到更优的 \(dp[i][k](k < j,即更新已经\)for\(过的状态)\),导致 原来\(dp[i][k]\)往后更新的状态不是最优的。

这跟用\(BFS\)求最短距离遇到的问题一样,需仿照\(dijkstra\)那样,用一个优先队列来维护 \(DP\)顺序,从\(dp[i][j]\)最小的状态 \((i,j)\)往后转移,或者像\(SPFA\)那样,通过多次进队解决,由于\(n\)只有 \(20\),跑一次 \(floyd\)也行。

考虑其复杂度,状态数已经是\(O(2^n n)\)\(1e7\)的级别了,再加上 优先队列的 \(log\)复杂度,第一次尝试时不出意料超时了。

考虑优化,注意到原来的两层循环状态\((i,j)\),可能转移到 \((i|(1<<k),k)\)\((i,k)\) ,其中第二维大小关系不是固定的,但第一维要么更大,要么不变,而我们使用 \(dijkstra\)式的方式维护更新时,主要是防止\(i\)不变的转移造成的重复更新。

原来是直接维护状态\((i,j)\)的转移顺序,状态数达到 \(O(2^n n)\),但转移的第一维有明显的方向性,因此我们可以对第一维一层一层的求解,在每一层的求解使用优先队列维护转移。这样优先队列里的状态数降为\(O(n)\)\(n\)只有 \(20\)就基本没什么耗时,最后的复杂度就是\(O(2^n n \log n)\)

神奇的代码
#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<vector<array<int, 2>>> edge(n);
    for (int i = 0; i < m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        --u, --v;
        edge[u].push_back({v, w});
    }
    int up = (1 << n);
    vector<vector<int>> dp(up, vector<int>(n, INT_MAX));
    for (int i = 0; i < n; ++i) {
        dp[1 << i][i] = 0;
    }
    for (int i = 0; i < up; ++i) {
        priority_queue<array<int, 3>, vector<array<int, 3>>,
                       greater<array<int, 3>>>
            pq;
        for (int j = 0; j < n; ++j) {
            if (dp[i][j] != INT_MAX)
                pq.push({dp[i][j], i, j});
        }

        while (!pq.empty()) {
            auto [d, s, u] = pq.top();
            pq.pop();
            if (dp[s][u] < d)
                continue;
            for (auto [v, w] : edge[u]) {
                if (dp[s | (1 << v)][v] > dp[s][u] + w) {
                    dp[s | (1 << v)][v] = dp[s][u] + w;
                    if ((s | (1 << v)) == s)
                        pq.push({dp[s | (1 << v)][v], s | (1 << v), v});
                }
            }
        }
    }
    int ans = INT_MAX;
    for (int i = 0; i < n; ++i) {
        ans = min(ans, dp[(1 << n) - 1][i]);
    }
    if (ans == INT_MAX) {
        cout << "No" << '\n';
    } else
        cout << ans << '\n';

    return 0;
}



G - evall (abc338 G)

题目大意

给定一个字符串,包含数字和\(+*\)。问所有的子串的表达式的值。若表达式不合法则其值为\(0\)

解题思路

<++>

神奇的代码



posted @ 2024-01-27 22:32  ~Lanly~  阅读(184)  评论(5编辑  收藏  举报