2023牛客寒假演算法基礎集訓營2 ABCDEFHJKL

空白菌發表於2023-01-19

比賽連結

A

題解

知識點:數學。

\(n\) 減去區間1的端點得到匹配的一個區間,求一下與區間2的交集。

一個小公式,兩區間 \([L_1,R_1]\)\([L_2,R_2]\) 的交集長度為 \(\max(0, \min(R_1, R_2) - \max(L_1, L_2) + 1)\)

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

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

程式碼

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

bool solve() {
    int n;
    cin >> n;
    int l1, r1, l2, r2;
    cin >> l1 >> r1 >> l2 >> r2;
    int y = n - l1, x = n - r1;
    cout << max(0, min(y, r2) - max(x, l2) + 1) << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

B

題解

知識點:數學。

\(n\) 減去區間1的端點得到匹配的一個區間,求一下與區間2的交集。

一個小公式,兩區間 \([L_1,R_1]\)\([L_2,R_2]\) 的交集長度為 \(\max(0, \min(R_1, R_2) - \max(L_1, L_2) + 1)\)

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

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

程式碼

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

bool solve() {
    int n;
    cin >> n;
    int l1, r1, l2, r2;
    cin >> l1 >> r1 >> l2 >> r2;
    int y = n - l1, x = n - r1;
    cout << max(0, min(y, r2) - max(x, l2) + 1) << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

C

題解

知識點:列舉,差分,字首和。

列舉每個區間 \([L,R]\) 的匹配區間 \([n-R,n-L]\) ,匹配區間的每個點被其他區間覆蓋的次數總和。

我們可以預處理出每個點被區間覆蓋的次數 \(d_i\) ,可以用差分再字首和得到。對此,再做一次字首和,就可以快速得到 \([n-R,n-L]\) 每個點被所有區間覆蓋的次數總和, \(d_{n-L} - d_{n-R-1}\)

再減去與 \([L,R]\) 重合部分的一段,可以用公式 \(\max(0, \min(R, n-L) - \max(L, n-R) + 1)\)

要注意先特判 \(n-R>2\times 10^5\)\(n-L<0\) 的無交集情況。

之後,再處理 \(n-L>2 \times 10^5\)\(n-R<1\) 的越界情況。

時間複雜度 \(O(2 \cdot 10^5 \cdot m)\)

空間複雜度 \(O(2 \cdot 10^5 + m)\)

程式碼

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

const int P = 998244353;
int L[400007], R[400007];
ll d[200007];
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 >> L[i] >> R[i];
        d[L[i]]++;
        d[R[i] + 1]--;
    }
    for (int i = 1;i <= 2e5;i++) d[i] += d[i - 1];
    for (int i = 1;i <= 2e5;i++) d[i] += d[i - 1];
    ll ans = 0;
    for (int i = 1;i <= m;i++) {
        int y = n - L[i], x = n - R[i];
        if (y <= 0 || x > 2e5) continue;
        x = max(x, 1);
        y = min(y, 200000);
        ans = ans + d[y] - d[x - 1] - max(0, min(y, R[i]) - max(x, L[i]) + 1);
        ans %= P;
    }
    cout << ans << '\n';
    return 0;
}

D

題解

知識點:貪心。

一個節點的深度就是這個節點的能量能被獲取的次數,顯然深度越大的節點能量應該越大,所以直接求完深度從小到大排序,能量也從小到大排序,乘在一起加起來就行。

因為 \(1 \leq f_i \leq i-1\) ,所以可以直接求出每個點的深度,不需要樹形dp。

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

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

程式碼

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

const int N = 200007;

int a[N];
int dep[N];

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    cin >> n;
    dep[1] = 1;
    for (int i = 2;i <= n;i++) {
        int f;
        cin >> f;
        dep[i] = dep[f] + 1;
    }
    for (int i = 1;i <= n;i++) cin >> a[i];
    sort(a + 1, a + n + 1);
    sort(dep + 1, dep + n + 1);
    ll ans = 0;
    for (int i = 1;i <= n;i++) {
        ans += 1LL * dep[i] * a[i];
    }
    cout << ans << '\n';
    return 0;
}

E

題解

知識點:數學,二分。

透過一些不容易的證明,可以知道全域性最小值一定出現在 \(\left\lfloor \sqrt n \right\rfloor,\left\lceil \sqrt n \right\rceil\) 兩個點。

具體的,我們考慮 \(g(x) = \dfrac{n}{x} + x - 1\) 容易知道 \(\sqrt n\) 就是最小值點,但對於 \(f(x) = \left\lfloor \dfrac{n}{x} \right\rfloor + x - 1\) ,考慮 \(\sqrt n\) 兩邊的變化率。若 \(x \in \Z^+\)\(\sqrt n\) 右側 \(\dfrac{n}{x}\) 的減量小於 \(x\) 的增量,所以 \(\left\lfloor \dfrac{n}{x} \right\rfloor\) 的減量小於等於 \(x\) 增量;左側 \(\dfrac{n}{x}\) 的增量大於 \(x\) 的減量,所以 \(\left\lfloor \dfrac{n}{x} \right\rfloor\) 的增量大於等於 \(x\) 減量,所以全域性最小值一定出現在 \(\left\lfloor \sqrt n \right\rfloor,\left\lceil \sqrt n \right\rceil\) 兩個點,就可以比較得出最小值所在點了。

設全域性最小值點為 \(x\) ,若 \(L \geq x\) 顯然答案為 \(L\)

否則,考慮區間 \([L,R]\) 的區域性最小值點,因為 \([1,x]\) 遞減,所以區域性最小值點為 \(t = \min(R,x)\) ,我們在 \([1,t]\) 內二分找到最左側的最小值點即可。

注意這道題直接三分不行,考慮三分:

2211222|2222222|2222222
2222222|2222222|2221122

顯然此時我們就無法判斷是向左還是向右收縮。當然可以動用人類智慧,三分找到個區域性點左右列舉 \(1000\) 個數qwq。

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

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

程式碼

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

ll n, L, R;
ll x;
ll f(ll x) {
    return n / x + x - 1;
}

bool check(ll mid) {
    return f(mid) - f(x) > 0;
}

bool solve() {
    cin >> n >> L >> R;
    x = sqrt(n);
    x = f(x) <= f(x + 1) ? x : x + 1;//全域性最小值點
    if (L >= x) cout << L << '\n';
    else {
        x = min(R, x);//[L,R]的最小值點
        ll l = L, r = x;
        while (l <= r) {
            ll mid = l + r >> 1;
            if (check(mid)) l = mid + 1;
            else r = mid - 1;
        }
        cout << l << '\n';
    }
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

F

題解

知識點:BFS。

找到同時能到起點和終點的點即可,於是從起點和終點分別搜尋。

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

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

程式碼

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

int n, k;
bool dt[500007][10];

struct node {
    int x, y;
};
queue<node> q;
void bfs(node st, vector<vector<int>> &vis, vector<vector<int>> &dir) {
    vis[st.x][st.y] = 1;
    q.push(st);
    while (!q.empty()) {
        node cur = q.front();
        q.pop();
        for (int i = 0;i < 2;i++) {
            int xx = cur.x + dir[i][0];
            int yy = cur.y + dir[i][1];
            if (xx <= 0 || xx > n || yy <= 0 || yy > 3 || vis[xx][yy] || dt[xx][yy]) continue;
            vis[xx][yy] = 1;
            q.push({ xx,yy });
        }
    }
}


bool solve() {
    cin >> n >> k;
    for (int i = 1;i <= n;i++) dt[i][1] = dt[i][2] = dt[i][3] = 0;
    for (int i = 1;i <= k;i++) {
        int x, y;
        cin >> x >> y;
        dt[x][y] ^= 1;
    }
    vector<vector<int>> vis1(n + 1, vector(4, 0));
    vector<vector<int>> vis2(n + 1, vector(4, 0));
    vector<vector<int>> dir1 = { {1,0},{0,1} };
    vector<vector<int>> dir2 = { {-1,0},{0,-1} };
    bfs({ 1,1 }, vis1, dir1);
    bfs({ n,3 }, vis2, dir2);
    if (!vis1[n][3]) cout << 0 << '\n';
    else {
        int ans = 0;
        for (int i = 1;i <= n;i++) {
            for (int j = 1;j <= 3;j++) {
                if (vis1[i][j] && vis2[i][j]) ans++;
            }
        }
        cout << ans - 1 << '\n';
    }
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

H

題解

方法一

知識點:列舉,差分,貪心。

因為分成子序列,因此可以隨意分配,我們優先把一個數字分在一個序列裡,剩下的分在同一個。

因此,我們可以先求出每個數字的出現次數,再考慮出現次數對答案的貢獻:

k/貢獻/出現次數 1 2 3 4 5
1 1 0 0 0 0
2 1 2 1 1 1
3 1 2 3 2 2
4 1 2 3 4 3
5 1 2 3 4 5

我們發現規律 \(k<cnt\) 時為 \(k-1\) ,否則為 \(cnt\)

我們對此進行兩次差分得到表格:

k/貢獻二次差分/出現次數 1 2 3 4 5
1 1 0 0 0 0
2 -1 2 1 1 1
3 0 -2 1 0 0
4 0 0 -2 1 0
5 0 0 0 -2 1

我們在 \(ans_2 ,ans_{cnt}\) 處加 \(1\)\(ans_{cnt+1}\) 處減一即可。

最後字首和兩次,就是答案了。

時間複雜度 \(O(n + 10^5)\)

空間複雜度 \(O(n+10^5)\)

方法二

知識點:列舉,貪心,字首和,二分。

利用上面發現的規律:

\(k<cnt\) 時為 \(k-1\) ,否則為 \(cnt\)

我們先求出每個數字出現的次數,然後按照次數從小到大排序,隨後列舉 \(k\)

我們用二分找到 \(cnt > k\) 的位置,於是 \(cnt\leq k\) 的部分為 \(cnt\) 用字首和得到總和,否則就是個數乘 \(k-1\)

時間複雜度 \(O(n \log 10^5)\)

空間複雜度 \(O(n + 10^5)\)

程式碼

方法一

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

int cnt[100007];
int ans[100007];
bool solve() {
    int n;
    cin >> n;
    for (int i = 1;i <= 1e5;i++) ans[i] = cnt[i] = 0;
    for (int i = 1;i <= n;i++) {
        int x;
        cin >> x;
        cnt[x]++;
    }
    for (int i = 1;i <= 1e5;i++) {
        if (!cnt[i]) continue;
        ans[2]++;
        ans[cnt[i]]++;
        ans[cnt[i] + 1] -= 2;
    }
    for (int i = 1;i <= n;i++) ans[i] += ans[i - 1];
    for (int i = 1;i <= n;i++) ans[i] += ans[i - 1];
    for (int i = 1;i <= n;i++) cout << ans[i] << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

方法二

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

int cnt[100007];
int sum[100007];
bool solve() {
    int n;
    cin >> n;
    for (int i = 1;i <= 1e5;i++) cnt[i] = 0;
    for (int i = 1;i <= n;i++) {
        int x;
        cin >> x;
        cnt[x]++;
    }
    sort(cnt + 1, cnt + 100000 + 1);
    for (int i = 1;i <= 1e5;i++) sum[i] = sum[i - 1] + cnt[i];
    for (int i = 1;i <= n;i++) {
        int idx = upper_bound(cnt + 1, cnt + 100000 + 1, i) - cnt;
        cout << sum[idx - 1] + (100000 - idx + 1) * (i - 1) << '\n';
    }
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

J

題解

知識點:數學。

分類討論:

\[\begin{aligned} a_i<0,a_j<0 &\Rightarrow \max(|a_i-a_j|,|a_i+a_j|) = a_i+a_j\\ a_i<0,a_j \geq 0 &\Rightarrow \max(|a_i-a_j|,|a_i+a_j|) = (-a_i)+a_j\\ a_i\geq 0,a_j<0 &\Rightarrow \max(|a_i-a_j|,|a_i+a_j|) = a_i+(-a_j)\\ a_i\geq 0,a_j \geq 0 &\Rightarrow \max(|a_i-a_j|,|a_i+a_j|) = (-a_i)+(-a_j) \end{aligned} \]

綜上 \(\max(|a_i-a_j|,|a_i+a_j|) = |a_i|+|a_j|\)

所以

\[\begin{aligned} \sum_{i=1}^{n}\sum_{j=1}^{n} \max(|a_i-a_j|,|a_i+a_j|) &= \sum_{i=1}^{n}\sum_{j=1}^{n} (|a_i|+|a_j|)\\ &=\sum_{i=1}^{n}\sum_{j=1}^{n} |a_i| + \sum_{i=1}^{n}\sum_{j=1}^{n} |a_j|\\ &=2n\sum_{i=1}^{n} |a_i| \end{aligned} \]

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

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

程式碼

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

bool solve() {
    int n;
    cin >> n;
    ll ans = 0;
    for (int i = 1;i <= n;i++) {
        int x;
        cin >> x;
        ans += abs(x);
    }
    ans *= 2 * n;
    cout << ans << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

K

題解

知識點:圖論建模,列舉。

題目可以轉化為 \(n\) 個點 \(m\) 條邊的一張無向圖。 \(q\) 次詢問,每次選出 \(k\) 個點,求這些點構成的最大子圖的邊數。

直接列舉的最壞複雜度是 \(O(m\sum k)\) ,顯然不可行,問題出在有些點的邊可能很多。

此時我們利用平衡思想。因為邊的方向不重要,所以可以給邊定向,減少某些點的邊數。我們考慮一條邊的兩個點 \(x,y\) 的度數 \(d_x,d_y\) ,當其滿足 \(d_x \leq d_y\) 時,建 \(x \to y\) 的邊;否則,建 \(y \to x\) 的邊。這種操作能將邊數平衡到 \(O(\sqrt m)\) 的複雜度。

證明:

\(x\) 的出邊個數為 \(cnt_x\) ,則有 \(cnt_x ^2 \leq cnt_x d_x\leq \sum d_y \leq 2m\) ,因此可以證明 \(cnt_x \leq \sqrt{2m}\)

時間複雜度 \(O(\sqrt m \sum k)\)

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

程式碼

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

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

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

    void add(int u, int v) {
        e[++idx] = edge{ v,h[u] };
        h[u] = idx;
    }
};
const int N = 2e5 + 7, M = 2e5 + 7;
Graph g(N, M);

int feat[N];
bool vis[N];
int deg[N];

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, m, q;
    cin >> n >> m >> q;
    vector<pair<int, int>> edge(m + 1);
    for (int i = 1;i <= m;i++) {
        int u, v;
        cin >> u >> v;
        edge[i] = { u,v };
        deg[u]++;
        deg[v]++;
    }
    for (int i = 1;i <= m;i++) {
        auto [u, v] = edge[i];
        if (deg[u] < deg[v]) g.add(u, v);
        else g.add(v, u);
    }
    while (q--) {
        int k;
        cin >> k;
        for (int i = 1;i <= k;i++) cin >> feat[i], vis[feat[i]] = 1;
        int ans = 0;
        for (int i = 1;i <= k;i++) {
            for (int j = g.h[feat[i]];j;j = g.e[j].nxt) {
                int v = g.e[j].v;
                if (!vis[v]) continue;
                ans++;
            }
        }
        cout << ans << '\n';
        for (int i = 1;i <= k;i++) vis[feat[i]] = 0;
    }
    return 0;
}

L

題解

知識點:數論,列舉。

考慮先將 \(a_i\cdot a_j \bmod p\)\(a_k \bmod p\) 的值的個數統計到 \(cnta\)\(cntb\) 中。

隨後 \(\displaystyle ans_x = \sum_{(i+j) \mod p = x} cnta_i \cdot cntb_j\) ,但此時我們沒考慮 \(i = k\)\(j = k\) 的情況,我們只要單獨把 \((a_i\cdot a_j + a_i) \bmod p\) 的答案減去 \(2\) 即可,代表減掉 \((i,j,i),(j,i,i)\) 兩組。

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

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

程式碼

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

int a[5007];
int cnta[5007], cntb[5007];
ll ans[5007];
int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, p;
    cin >> n >> p;
    for (int i = 1;i <= n;i++) cin >> a[i];
    for (int i = 1;i <= n;i++) cnta[a[i] % p]++;

    for (int i = 1;i <= n;i++)
        for (int j = 1;j <= n;j++)
            if (i != j) cntb[1LL * a[i] * a[j] % p]++;

    for (int i = 0;i < p;i++)
        for (int j = 0;j < p;j++)
            ans[(i + j) % p] += 1LL * cnta[i] * cntb[j];
    for (int i = 1;i <= n;i++)
        for (int j = 1;j <= n;j++)
            if (i != j) ans[(1LL * a[i] * a[j] + a[i]) % p] -= 2;
    for (int i = 0;i < p;i++) cout << ans[i] << " \n"[i == p - 1];
    return 0;
}

相關文章