AtCoder Beginner Contest 353

~Lanly~發表於2024-05-11

A - Buildings (abc353 A)

題目大意

給定\(n\)個數字,輸出第一個大於第一個數的下標。

解題思路

依次與第一個數比較,大於則輸出。

神奇的程式碼
n = input()
a = list(map(int, input().split()))
b = [i > a[0] for i in a]
if True not in b:
    print(-1)
else:
    print(b.index(True) + 1)



B - AtCoder Amusement Park (abc353 B)

題目大意

\(n\)組,每組若干個人,坐雲霄飛車。

每個飛車只有 \(k\)個座位。依次給這\(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, k;
    cin >> n >> k;
    int ans = 0;
    int cur = k;
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        if (cur < x) {
            ++ans;
            cur = k;
        }
        cur -= x;
    }
    cout << ans + 1 << endl;

    return 0;
}



C - Sigma Problem (abc353 C)

題目大意

給定\(n\)個數\(a_i\),求 \(\sum_{i=1}^{n-1} \sum_{j=i+1}^{n} (a_i + a_j) \mod 10^8\)

解題思路

注意不是總的求和對\(mo = 10^8\)取模。 \(a \% mo + b \% mo \neq (a + b) \% mo\)

樸素做法是\(O(n^2)\),會超時,最佳化方向一般是列舉一個數,考慮\(\sum\)能否快速算出來。

由於是加法,所以\((a+b) \% mo\)的結果只有兩種:

  • \((a+b) \% mo = a+b, a+b < mo\)
  • \((a+b) \% mo = a+b - mo, a+b \geq mo\)

於是我們可以把取模操作拆分成兩種情況的加法:\(a+b\)\(a+(b - mo)\)

因此我們先對這\(n\)個數排序,然後列舉當前數\(a_j\),計算\(\sum_{i=1}^{j-1} (a_i + a_j) \% mo\)

由前面的分析,可以將\(a_i\)就分成兩部分:

  • 第一部分的 \(a_i + a_j < mo\),因此 \(\sum (a_i + a_j) \% mo = \sum_1 a_i + cnt_1 \times a_j\)
  • 第二部分的 \(a_i + a_j \geq mo\),因此 \(\sum (a_i + a_j) \% mo = \sum_2 a_i + cnt_2 \times (a_j - mo)\)

由於\(a_i\)是遞增的,所以可以透過二分找到第一部分第二部分的分界點 \(a_k\)(這裡認為是最後一個的第一部份的),由此 \(i \leq k\)的都是滿足第一部分的 \(a_i\)\(k < i < j\)的是滿足第二部分的 \(a_i\)

因此\(\sum_1 a_i = presum[k], cnt = k\)\(\sum_2 a_i = presum[j - 1] - presum[k], cnt_2 = j - 1 - k\),其中\(presum[i] = \sum_{i=1}^{k} a_i\)是一個預處理的字首和陣列。

兩部分加起來,就是\(\sum_{i=1}^{j-1} (a_i + a_j) \% mo\)。對所有的\(a_j\)求和,即為答案。

上述也可以理解成,對於一個\(a_j\),它與所有的 \(a_i\)相加取模,看要 \(- mo\)的數量。

總的時間複雜度是 \(O(n\log n)\)。將取模換成 絕對值之類的,做法其實是一樣的,分類討論來去掉絕對值後,分別求和。

程式碼裡下標是從\(0\)開始的,分界點是第二部分的第一個,與上述的解釋有點出入。

神奇的程式碼
#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;
    int mo = 1e8;
    vector<LL> a(n);
    for (auto& x : a)
        cin >> x;
    sort(a.begin(), a.end());
    vector<LL> presum(n);
    partial_sum(a.begin(), a.end(), presum.begin());
    LL ans = 0;
    LL sum = 0;
    for (int i = 0; i < n; ++i) {
        LL bu = mo - a[i];
        if (a[i] < bu) {
            ans += a[i] * i + sum;
        } else {
            auto pos = lower_bound(a.begin(), a.begin() + i, bu) - a.begin();
            LL p = pos > 0 ? presum[pos - 1] : 0;
            ans += a[i] * pos + p;
            LL suf = sum - p;
            ans += suf - bu * (i - pos);
        }
        sum += a[i];
    }
    cout << ans << '\n';

    return 0;
}



D - Another Sigma Problem (abc353 D)

題目大意

給定\(n\)個數\(a_i\),求 \(\sum_{i=1}^{n-1} \sum_{j=i+1}^{n} f(a_i, a_j) \mod 998244353\)

其中\(f(a,b) = ab\),即拼接起來(不是相乘)。最後的結果取模。

解題思路

樸素做法是\(O(n^2)\),會超時,最佳化方向一般是列舉一個數,考慮\(\sum\)能否快速算出來。

從左到右列舉\(a_i\),考慮 \(\sum_{j=i+1}^{n} f(a_i, a_j)\)的值,容易發現有兩部分:

  • 第一部分顯然是 \(\sum_{j=i+1}^{n} a_j\)。一個字尾和。
  • 第二部分是關於\(a_i\)的,由於 \(f(a,b) = ab\)\(b\)就是第一部分, \(a\) 則相當於\(a \times 10^{g(b)}\) ,其中\(g(b)\)表示 \(b\)的位數。 即 \(a_i \sum_{j=i+1}^{n} 10^{g(a_j)}\),一個關於 \(10^{g(a_j)}\)的字尾和。

從右往左列舉的話,就可以順便維護字尾和。

程式碼裡是考慮每個數對答案的貢獻,對於每個 \(a_k\),分別計算作為 \(a_j\)\(a_i\)的貢獻,即 作為低位時的貢獻和作為高位時的貢獻。

神奇的程式碼
#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<int> a(n);
    for (auto& x : a)
        cin >> x;
    LL sum = 0;
    LL ten = 0;
    auto calc_ten = [&](int x) {
        int cnt = 0;
        while (x) {
            ++cnt;
            x /= 10;
        }
        LL val = 1;
        while (cnt--)
            val *= 10;
        return val;
    };
    for (int i = n - 1; i >= 0; --i) {
        sum += 1ll * a[i] * i % mo;
        sum += 1ll * a[i] * ten % mo;
        sum %= mo;
        LL val = calc_ten(a[i]);
        ten += val;
        ten %= mo;
    }
    cout << sum << '\n';

    return 0;
}



E - Yet Another Sigma Problem (abc353 E)

題目大意

給定\(n\)個字串,求 \(\sum_{i=1}^{n-1} \sum_{j=i+1}^{n} lcp(s_i, s_j)\)

\(lcp(a,b)\)表示串 \(a,b\)的最長公共字首。

解題思路

之前有道abc287e與這個類似,是求\(\max\)。那個有一些特別的性質,但這裡要求和。

字串字首這種東西,一般會和字典樹\(Trie\)相關,因為它正巧把擁有相同字首的字串合併了。

將這\(n\)個字串都插入到字典樹裡,考慮如何計算答案。一個可能的想法是,比如我遍歷到最深的節點,字串個數可能只有一個,然後看其父親,有分叉點,然後有若干個,這個時候可以組合數一下,得到 \(lcp\)是對應深度的個數,然後再考慮父親節點之類的。這樣做是是可以,但得認真考慮組合數的計算,避免算重。

一個簡便的計算方法,則是將 \(lcp\)看成 \(1+1+1+...\),這裡雖然都是數字 \(1\),但是有意義的: \(lcp = 4 = 1 + 1 + 1 + 1 =\)前一位字母相同 + 前二位字母相同 + 前三位字母相同 + 前四位字母相同 。比如兩個字串的\(lcp\)\(4\),則它分別對 前一位字母相同前兩位字母相同前三位字母相同前四位字母相同分別有\(1\)的貢獻。而這四個相同情況的和就是 \(\sum \sum lcp\),我們只要分別求出,前一位字母相同前二位字母相同......有多少貢獻即可,即字串對數,滿足前一位字母相同前二位相同...。注意這裡與\(lcp\)的區別, \(lcp\)是最長的,而這裡只需要前 \(i\)位。

由此,\(\sum \sum lcp = \sum_{lcp=i} i \times\) 數量 \(= \sum_{i}\)\(i\)位字母相同 \(\times\) 對數。我們的視角從求每個 \(lcp\)的數量轉成求前\(i\)位字母相同的對數。

而前 \(i\)位字母相同的字串對數,即為字典樹中深度是 \(i\)的節點下字串數量\(m\)\(C_m^2\)

因此遍歷下所有節點,對\(C_m^2\) 求和即為答案。總的時間複雜度是\(O(n)\)

更進一步觀察上面提到的兩個方法,可以感覺出來,假想在一個全是\(1\)的二維格子,一種是對每一列求和,另一種是求每一行求和。

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

const int SZ = 26;
template <typename T, typename K> struct Trie {
    struct node {
        K value;
        bool is_terminal;
        int vis_count;
        array<int, SZ> children;

        node(K val) : value(val) {
            is_terminal = false;
            children.fill(0);
            vis_count = 0;
        }
    };

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

    vector<node> tree;

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

    void insert(const T& sequence) {
        int cur = 0;
        for (int i = 0; i < (int)sequence.size(); i++) {
            K value = sequence[i];
            if (tree[cur].children[cast(value)] == 0) {
                tree[cur].children[cast(value)] = (int)tree.size();
                tree.emplace_back(value);
            }
            cur = tree[cur].children[cast(value)];
            tree[cur].vis_count += 1;
        }
        tree[cur].is_terminal = true;
    }

    LL dfs(int cur) {
        LL sum = 0;
        for (int i = 0; i < SZ; i++) {
            if (tree[cur].children[i] == 0)
                continue;
            int child_node = tree[cur].children[i];
            sum += dfs(child_node);
        }
        sum += 1ll * tree[cur].vis_count * (tree[cur].vis_count - 1) / 2;
        return sum;
    }
};

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    Trie<string, char> tree('a');
    for (int i = 0; i < n; i++) {
        string s;
        cin >> s;
        tree.insert(s);
    }
    LL ans = tree.dfs(0);
    cout << ans << '\n';

    return 0;
}



F - Tile Distance (abc353 F)

題目大意

給定一個\(k\),其定義了一種網格。

比如\(k=3\)定義瞭如下網格:k3

感性理解下,詳細定義可去原問題。

從格子走到另一個格子代價是 \(1\)

現在給定起點終點,問從起點到終點的最小代價。

解題思路

感覺就是一個大力分類討論

神奇的程式碼



G - Merchant Takahashi (abc353 G)

題目大意

\(n\)個城市,給定 \(c\),定義了從城市 \(i \to j\)的花費為 \(c|i-j|\)

\(m\)個活動依次舉行,第 \(i\)個活動 在第\(t_i\)個城市舉行,參加則獲得 \(p_i\)收益。

初始在第 \(1\)個城市,問如何選擇參加活動,使得收益最大化。

解題思路

我選擇參加活動,則需要計算出城市間轉移的代價,而該代價取決於我之前在哪個城市,因此狀態資訊裡只需要保留該資訊就可以轉移了。

一個樸素的\(dp\)即為 \(dp[i][j]\)表示考慮前 \(m\)個活動後,我最終在城市 \(j\)的最大收益。轉移則為\(dp[i][t_i] = \max_{1 \leq j \leq n}(dp[i - 1][j] - c|t_i - j|) + p_i\),每次轉移其實只有\(dp[.][t_i]\)這一個值會發生變化,複用其他的值,時間複雜度是\(O(nm)\)。(複用的意思即為\(dp[t_i] = \max_{1 \leq j \leq n}(dp[j] - c|t_i - j|) + p_i\)

考慮最佳化轉移,轉移即為一個區間取最大值,但棘手在絕對值,解決辦法就是透過分類討論,將絕對值去掉。

\(dp[t_i] = \max( \max_{1 \leq j \leq t_i}(dp[j] + cj) - ct_i, \max_{t_i \leq j \leq n}(dp[j] - cj) + ct_i)\)

將絕對值拆掉後,轉移式就變成了兩個關於\(dp[j] + cj\)\(dp[j] - cj\)的區間最大值問題,用兩個線段樹維護這兩類值即可。

時間複雜度是 \(O(m \log n)\)

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

const int N = 2e5 + 8;
const LL inf = 1e18;

class segment {
#define lson (root << 1)
#define rson (root << 1 | 1)
  public:
    LL maxx[N << 2];

    void build(int root, int l, int r, vector<LL>& a) {
        if (l == r) {
            maxx[root] = a[l - 1];
            return;
        }
        int mid = (l + r) >> 1;
        build(lson, l, mid, a);
        build(rson, mid + 1, r, a);
        maxx[root] = max(maxx[lson], maxx[rson]);
    }

    void update(int root, int l, int r, int pos, LL val) {
        if (l == r) {
            maxx[root] = max(maxx[root], val);
            return;
        }
        int mid = (l + r) >> 1;
        if (pos <= mid)
            update(lson, l, mid, pos, val);
        else
            update(rson, mid + 1, r, pos, val);
        maxx[root] = max(maxx[lson], maxx[rson]);
    }

    LL query(int root, int l, int r, int L, int R) {
        if (L <= l && r <= R) {
            return maxx[root];
        }
        int mid = (l + r) >> 1;
        LL resl = -inf, resr = -inf;
        if (L <= mid)
            resl = query(lson, l, mid, L, R);
        if (R > mid)
            resr = query(rson, mid + 1, r, L, R);
        return max(resl, resr);
    }
} lsg, rsg;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, c;
    cin >> n >> c;
    vector<LL> a(n);
    for (int i = 0; i < n; ++i)
        a[i] = -inf;
    a[0] = 0;
    rsg.build(1, 1, n, a);
    lsg.build(1, 1, n, a);
    int m;
    cin >> m;
    LL ans = 0;
    for (int i = 0; i < m; ++i) {
        int t;
        LL p;
        cin >> t >> p;
        int id = t;
        --t;
        LL dp1 = lsg.query(1, 1, n, 1, id) - 1ll * c * t;
        LL dp2 = rsg.query(1, 1, n, id, n) + 1ll * c * t;
        LL dp = max(dp1, dp2) + p;
        ans = max(ans, dp);
        lsg.update(1, 1, n, id, dp + 1ll * c * t);
        rsg.update(1, 1, n, id, dp - 1ll * c * t);
    }
    cout << ans << '\n';

    return 0;
}