2023牛客寒假演算法基礎集訓營5 A-L

空白菌發表於2023-02-07

比賽連結

A

題解

知識點:字首和,二分。

找到小於等於 \(x\) 的最後一個物品,往前取 \(k\) 個即可,用字首和查詢。

時間複雜度 \(O(n + q\log n)\)

空間複雜度 \(O(n)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int a[100007];
ll sum[100007];
int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, q;
    cin >> n >> q;
    for (int i = 1;i <= n;i++) cin >> a[i];
    sort(a + 1, a + n + 1);
    for (int i = 1;i <= n;i++) sum[i] = sum[i - 1] + a[i];
    while (q--) {
        int k, x;
        cin >> k >> x;
        int pos = upper_bound(a + 1, a + n + 1, x) - a - 1;
        cout << sum[pos] - sum[max(0, pos - k)] << '\n';
    }
    return 0;
}

B

題解

知識點:博弈論。

顯然,每次取 \(1\) 是最優策略,但先拿的人串長肯定大於等於後拿的人,因此偶數平局,奇數後手勝。

時間複雜度 \(O(1)\)

空間複雜度 \(O(1)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    cin >> n;
    if (n & 1) cout << "Yaya-win!" << '\n';
    else cout << "win-win!" << '\n';
    return 0;
}

C

題解

知識點:數學。

分類討論:

  1. \(a,b\) 串長相等時,只有 \(a = b\) 時結果是 = ,否則找到第一個不同的位置可以任意修改大小關係,結果是 !

  2. \(a,b\) 串長不同時,不妨假設 \(a\)\(b\) 長,那麼首先就一定有 \(a>b\) 的情況,考慮 \(a\) 最小的情況會不會造成 \(a\leq b\)

    \(a\) 超出 \(b\) 的部分不是相同的數,則一定不能全變成前導 \(0\) 此時無論如何一定有 \(a>b\) ,結果為 > ;否則,先 \(a\) 超出部分全部變成前導 \(0\) ,此時 \(a,b\) 長度相同。

    之後,若 \(a = b\) 則結果為 ! 。否則,找到第一對不同的數,如果 \(b\) 的數是 \(a\) 前導數,則 \(a\) 無論如何都有 \(a>b\) ,結果為 > ,否則結果為 !

時間複雜度 \(O(n)\)

空間複雜度 \(O(n)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    string a, b;
    cin >> a >> b;
    if (a.size() == b.size()) {
        if (a == b) cout << "=" << '\n';
        else cout << "!" << '\n';
        return 0;
    }
    char ans = '>';
    if (a.size() < b.size()) {
        swap(a, b);
        ans = '<';
    }
    char zero = a[0];
    for (int i = 0;i < a.size() - b.size();i++) {
        if (a[i] != zero) {
            cout << ans << '\n';
            return 0;
        }
    }
    a.erase(a.begin(), a.begin() + a.size() - b.size());
    if (a == b) {
        cout << "!" << '\n';
        return 0;
    }
    for (int i = 0;i < a.size();i++) {
        if (a[i] != b[i]) {
            if (b[i] == zero) cout << ans << '\n';
            else cout << "!" << '\n';
            return 0;
        }
    }
    return 0;
}

D

題解

知識點:優先佇列,列舉。

維護一個過關順序,按照區間左端點從小到大排序即可,每次看看左端點最小的是不是小於等於自己在位置加 \(1\) ,然後更新右端點。

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

空間複雜度 \(O(n)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

pair<int, int> a[100007], b[100007];
int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    cin >> n;
    for (int i = 1;i <= n;i++) cin >> a[i].first >> a[i].second;
    for (int i = 1;i <= n;i++) cin >> b[i].first >> b[i].second;
    priority_queue <pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq1, pq2;
    int pos1 = 0, pos2 = 0;
    for (int i = 1;i <= n;i++) {
        pq1.push(a[i]);
        pq2.push(b[i]);
        while (pq1.size() && pq1.top().first <= pos1 + 1) {
            pos1 = max(pos1, pq1.top().second);
            pq1.pop();
        }
        while (pq2.size() && pq2.top().first <= pos2 + 1) {
            pos2 = max(pos2, pq2.top().second);
            pq2.pop();
        }
        if (pos1 > pos2) {
            cout << "sa_win!" << '\n';
            cout << pos1 - pos2 << '\n';
        }
        else if (pos1 < pos2) {
            cout << "ya_win!" << '\n';
            cout << pos2 - pos1 << '\n';
        }
        else {
            cout << "win_win!" << '\n';
            cout << 0 << '\n';
        }
    }

    return 0;
}

E

題解

知識點:構造。

考慮將排列 \([1,2,\cdots,n] \to [n,n-1,\cdots ,1]\) ,這樣每個數必然和其他數相遇一次。

每次交換全部順序對,一定保證在 \(n\) 輪內完成交換。

時間複雜度 \(O(n^2)\)

空間複雜度 \(O(n^2)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    cin >> n;
    vector<int> v(n + 1);
    iota(v.begin(), v.end(), 0);
    vector<vector<pair<int, int>>> ans;
    while (!is_sorted(v.begin() + 1, v.end(), greater<int>())) {
        vector<pair<int, int>> t;
        for (int i = 1;i < n;i++) {
            if (v[i] < v[i + 1]) {
                swap(v[i], v[i + 1]);
                t.push_back({ v[i],v[i + 1] });
                i++;
            }
        }
        ans.push_back(t);
    }
    cout << ans.size() << '\n';
    for (auto i : ans) {
        cout << i.size() << '\n';
        for (auto [x, y] : i) cout << x << ' ' << y << '\n';
    }
    return 0;
}

F

題解

方法一

知識點:單調棧,貪心。

因為根據字典序,所以我們要儘可能使前面的字母更大。

考慮用單調棧維護字首字典序最大的子序列,同時需要維護 \(k\) ,若 \(k = 0\) 則不能再彈出元素。若到最後 \(k>0\) ,則還可以對得到的子序列從後往前彈出,可能使字典序更大。將彈出的元素計數,最後從大到小一併加到子序列後面。

時間複雜度 \(O(n)\)

空間複雜度 \(O(n)\)

方法二

知識點:列舉,貪心。

預處理 \(nxt_{i,j}\) 表示字串 \([i,n]\) 的位置中字母 \(j\) 第一個出現的位置。隨後只需要從大到小列舉 \(j\) ,找到最大的且能操作到的字母 \(j\) ,將 \(s_i\)\(j\) 的所有元素全部操作一遍,並計數。

處理出來的是字典序最大的子序列,此時若 \(k\) 還有剩下的,那就從後往前依次彈出子序列元素,同樣計數。

最後將跳過的或彈出的元素從大到小安排到子序列後面即可。

時間複雜度 \(O(n)\)

空間複雜度 \(O(n)\)

程式碼

方法一

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, k;
    cin >> n >> k;
    string s;
    cin >> s;
    array<int, 26> cnt{};
    string ans;
    for (auto ch : s) {
        while (k && ans.size() && ans.back() < ch) {
            cnt[ans.back() - 'a']++;
            ans.pop_back();
            k--;
        }
        ans += ch;
    }
    while (k && ans.size()) {
        cnt[ans.back() - 'a']++;
        ans.pop_back();
        k--;
    }
    for (int i = 25;i >= 0;i--) ans += string(cnt[i], i + 'a');
    cout << ans << '\n';
    return 0;
}

方法二

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int nxt[100007][26];
int cnt[26];
int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, k;
    cin >> n >> k;
    string s;
    cin >> s;
    for (int i = 0;i < 26;i++) nxt[n][i] = -1;
    for (int i = n - 1;i >= 0;i--) {
        for (int j = 0;j <= 25;j++) nxt[i][j] = nxt[i + 1][j];
        nxt[i][s[i] - 'a'] = i;
    }
    string ans;
    for (int i = 0, nnxt;i < s.size(); i = nnxt + 1) {
        for (int j = 25;j >= 0;j--) {
            if (~nxt[i][j] && nxt[i][j] - i <= k) {
                nnxt = nxt[i][j];
                break;
            }
        }
        for (int j = i;j < nnxt;j++) cnt[s[j] - 'a']++;
        ans += s[nnxt];
        k -= nnxt - i;
    }
    while (k && ans.size()) {
        cnt[ans.back() - 'a']++;
        ans.pop_back();
        k--;
    }
    for (int i = 25;i >= 0;i--) if (cnt[i]) ans += string(cnt[i], i + 'a');
    cout << ans << '\n';
    return 0;
}

G

題解

知識點:二分圖,圖匹配。

對於每個需要填的位置,由兩側已知的數字可以得到兩個待選的數字,如 \(0,x,5 \to x = 1,4\) ,除了最後一個空位因為只需要和第 \(n-1\) 個數字有一位不同,所以有更多可能。

注意到每個數字只能選一次,換句話說,空位和數字是一一配對關係。考慮畫成二分圖,題目保證一定有解,所以只要匹配一次即可。可以使用匈牙利演算法或者網路流求解二分圖匹配,這裡採用匈牙利演算法。

直接使用複雜度是跑滿 \(O(n^2)\) 的(但是交了也能過?),根本原因是每次跑完一個點的增廣路,需要把訪問標記全清空,複雜度是 \(O(n)\) 的。但實際上沒有必要,考慮對於某次增廣失敗的所有右部點,他們的左部配對沒有任何變化,無論你用哪個其他的左部點去配對這些增廣失敗的右部點,這些右部點都會走原來的路徑,而原來的路徑依舊是失敗的,根本沒必要再走一次。因此,我們每次配對的時候,如果某個右部點增廣失敗了,就保留訪問標記,不需要清空,而只清空成功的增廣路上的所有訪問標記,這個只需要在dfs最後加上清空即可,能讓演算法跑的巨快。

特別地在這道題,每個點最多被訪問四次就會被永久標記,複雜度變成線性。

時間複雜度 \(O(n)\)

空間複雜度 \(O(n)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

struct Graph {
    struct edge {
        int v, nxt;
    };
    int idx;
    vector<int> h;
    vector<edge> e;

    Graph(int n = 0, int m = 0) { init(n, m); }

    void init(int n, int m) {
        idx = 0;
        h.assign(n + 1, 0);
        e.assign(m + 1, {});
    }

    void add(int u, int v) {
        e[++idx] = { v,h[u] };
        h[u] = idx;
    }
};

const int N = (1 << 16) + 7, M = N;
Graph g;
int n;
int a[N];

int p[M], vis[M];
bool dfs(int u) {
    for (int i = g.h[u];i;i = g.e[i].nxt) {
        int v = g.e[i].v - n - 1;
        if (vis[v]) continue;
        vis[v] = 1;
        if (p[v] && !dfs(p[v])) continue;
        p[v] = u;
        vis[v] = 0;
        return true;
    }
    return false;
}
int Hungary() {
    int cnt = 0;
    for (int i = 2;i <= n;i += 2) cnt += dfs(i);
    return cnt;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin >> n;
    g.init(2 * n, 2 * n + n);
    for (int i = 1;i <= n;i += 2) cin >> a[i], vis[a[i]] = 1;
    for (int i = 2;i < n;i += 2) {
        int t = a[i - 1] ^ a[i + 1];
        int x1 = a[i - 1] ^ (t & -t);
        t -= t & -t;
        int x2 = a[i - 1] ^ (t & -t);
        if (!vis[x1]) g.add(i, x1 + n + 1);
        if (!vis[x2]) g.add(i, x2 + n + 1);
    }
    for (int i = 0;i < 16 && (1 << i) <= a[n - 1];i++) {
        int x = a[n - 1] ^ (1 << i);
        if (!vis[x]) g.add(n, x + n + 1);
    }
    Hungary();
    for (int i = 0;i < n;i++) if (p[i]) a[p[i]] = i;
    for (int i = 1;i <= n;i++) cout << a[i] << ' ';
    cout << '\n';
    return 0;
}

H

題解

知識點:模擬。

直接按照要求模擬即可。

時間複雜度 \(O(n)\)

空間複雜度 \(O(1)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int x, y, k, n;
    ll t;
    cin >> x >> y >> k >> n >> t;
    ll sum = 0, cnt = 0, nowc = x;
    for (int i = 1;i <= n;i++) {
        sum += nowc * (n - i + 1);
        cnt += n - i + 1;
        nowc = x + cnt / k * y;
        if (sum >= t) {
            cout << i << '\n';
            return 0;
        }
    }
    cout << -1 << '\n';
    return 0;
}

I

題解

知識點:貪心。

為了保證一定不會輸,我們從後往前考慮。

若最後一局才一贏,但贏回本至少需要留 \(\left\lceil \dfrac{m}{2} \right\rceil\) ,但我們希望在前面贏的多,因此我們留最少的 \(\left\lceil \dfrac{m}{2} \right\rceil\) ,則剩下 \(\left\lfloor \dfrac{m}{2} \right\rfloor\) ;若在 \(n-1\) 局才贏,那麼至少需要留除去第 \(n\) 局剩下部分的一半取上整,同樣的我們留最少的就行,以此類推,最後剩下的所有給第一局,若中途不夠分,則無解。

時間複雜度 \(O(n)\)

空間複雜度 \(O(n)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll a[100007];
int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    ll m;
    cin >> n >> m;
    for (int i = n;i >= 1;i--) {
        if (!m) {
            cout << -1 << '\n';
            return 0;
        }
        if (i == 1) a[i] = m;
        else {
            a[i] = (m + 1) / 2;
            m -= (m + 1) / 2;
        }
    }
    for (int i = 1;i <= n;i++) cout << a[i] << " \n"[i == n];
    return 0;
}

J

題解

知識點:構造

屬於靈光一現題qwq。

我們需要儘可能構造一條單向通道,這樣才能保證儘可能多的格子都會被踩到。

可以考慮螺旋形,先考慮偶數情況,例如 \(n=6\)\((1,1)\) 出發向右:

\[\begin{pmatrix} 0 & 0 & 0 & |1 & 1 & 1\\ 2 & 2 & 2 & |0 & 0 & 1\\ 2 & 1 & 1 & |\underline 2 & \underline0 & \underline1\\ 2 & 1 & 0 & 0 & 1 & 2\\ 2 & 1 & 1 & 1 & 1 & 2\\ 2 & 2 & 2 & 2 & 2 & 2\\ \end{pmatrix} \]

注意到每次經過下劃線處會加 \(1\)

奇數情況取偶數情況左下角矩形即可,出發點在 \((n,n)\)

具體實現可以看程式碼。

時間複雜度 \(O(n^2)\)

空間複雜度 \(O(n^2)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int a[1007][1007];
int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    cin >> n;
    bool odd = n & 1;
    if (odd) cout << n << ' ' << n << '\n', n++;
    else cout << 1 << ' ' << 1 << '\n';
    int x = 1, y = 1, cur = 0;
    for (int i = 1;i <= n / 2;i++) {
        for (int j = 1;j <= n - 2 * (i - 1) - 1;j++) {
            a[x][y] = cur;
            if (y == n / 2) (cur += 1) %= 3;
            y++;
        }
        for (int j = 1;j <= n - 2 * (i - 1) - 1;j++) {
            a[x][y] = cur;
            if (x == n / 2) (cur += 1) %= 3;
            x++;
        }
        for (int j = 1;j <= n - 2 * (i - 1) - 1;j++) {
            a[x][y] = cur;
            y--;
        }
        for (int j = 1;j <= n - 2 * (i - 1) - 1;j++) {
            a[x][y] = cur;
            x--;
        }
        x++, y++;
    }
    if (odd) n--;
    for (int i = 1 + odd;i <= n + odd;i++)
        for (int j = 1;j <= n;j++)
            cout << a[i][j] << " \n"[j == n];
    cout << n * n - 1 << '\n';
    return 0;
}

K

題解

知識點:貪心。

每次最多可以淘汰 \(\left\lfloor \dfrac{x}{2} \right\rfloor\) 個人,只要取 \(\left\lceil \dfrac{x}{2} \right\rceil\) 即可。

特別地,若 \(x = 1,2\) ,則一定無法繼續淘汰人。

時間複雜度 \(O(\log x)\)

空間複雜度 \(O(1)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    ll x;
    cin >> x;
    int cnt = 0;
    while (x > 2) {
        x = x / 2 + 1;
        cnt++;
    }
    cout << cnt << '\n';
    return 0;
}

L

題解

知識點:線性dp。

\(f_i\) 為剩餘 \(i\) 個人的最小花費。初始狀態為 \(f_n = 0\) 其他無窮大,轉移方程見程式碼。

最後從小到大遍歷一次就能獲得人數最小的最小花費。

時間複雜度 \(O(nm)\)

空間複雜度 \(O(n)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int b[507], x[507];
ll f[100007];
int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, m;
    cin >> n >> m;
    for (int i = 1;i <= m;i++) cin >> b[i] >> x[i];
    for (int i = 1;i <= n;i++) f[i] = 1e18;
    f[n] = 0;
    for (int i = n;i >= 3;i--) {
        if (f[i] >= 1e18) continue;
        for (int j = 1;j <= m;j++) {
            if (i >= x[j]) f[i / x[j] * x[j]] = min(f[i / x[j] * x[j]], f[i] + b[j]);
        }
    }
    for (int i = 1;i <= n;i++) {
        if (f[i] < 1e18) {
            cout << f[i] << '\n';
            break;
        }
    }
    return 0;
}

相關文章