1. 程式人生 > 其它 >CF Round 768 Div2 題解

CF Round 768 Div2 題解

A題 Min Max Swap

\(T\) 組資料。(\(1\leq T \leq 100\)

給定兩個長度為 \(n\) 的數列 \(\{a_n\},\{b_n\}\),你現在可以進行不限量次操作,每次選擇一個 \(p(1\leq p\leq n)\),並交換 \(a_p,b_p\),問怎樣可以使得兩個數列的分別的最大值的乘積最大(不輸出方案,只輸出最大值)?

\(1\leq n\leq 100,1\leq a_i,b_i \leq 10^4\)

有一個顯然的思路:找出最大值所在的陣列,然後讓另外一個數組裡面的值儘可能小即可。

簡化一下,就是:把每個位置上面小的數放在 a 裡面,大的數放在 b 裡面,遍歷一次即可,複雜度 \(O(n)\)

#include<bits/stdc++.h>
using namespace std;
const int N = 110;
int n, a[N], b[N];
int solve() {
    //read
    cin >> n;
    for (int i = 1; i <= n; ++i) cin >> a[i];
    for (int i = 1; i <= n; ++i) cin >> b[i];
    //solve
    for (int i = 1; i <= n; ++i)
        if (a[i] > b[i]) swap(a[i], b[i]);
    int Max1 = 0, Max2 = 0;
    for (int i = 1; i <= n; ++i)
        Max1 = max(Max1, a[i]), Max2 = max(Max2, b[i]);
    return Max1 * Max2;
}
int main()
{
    int T;
    cin >> T;
    while (T--) cout << solve() << endl;
    return 0;
}

B題 Fun with Even Subarrays

\(T\) 組資料。(\(1\leq T \leq 2*10^4\)

給定一個長度為 \(n\) 的陣列 \(\{a_n\}\),我們可以進行如下操作:

選擇一個長度為 \(2k\) 的區間,然後將後 \(k\) 個數字平移到前 \(k\) 個位置上面(後 \(k\) 個位置上面值不改變),詳情可以見原題。

問,至少需要多少次操作,可以使得整個陣列的值變成一樣?

\(\sum n \leq 2*10^5,1\leq a_i\leq n\)

顯然,最後一個位置的值不可能被變成其他值,那麼我們可以將陣列轉化一下,如果原位置的值等於 \(a_n\)

就記為 0,反之記為 1。那麼問題就變成了:怎麼在最快狀態下使得整個陣列變為 0?

我們直接貪心:記一個指標 \(p\),從 \(n\) 開始,不斷右移,當 \([p,n]\) 內部不是全 0 時,給 p 加上 1,然後記 \(L=\max(1, 2p-(n+1))\),然後將區間 \([L,p-1]\) 上面全部置為 0,同時 res 加上 1,再讓 \(p=L-1\),直到 \(p=0\) 為止,此時 res 的值就是最少操作次數。

關於怎麼判斷一個區間是不是全 0,以及怎麼快速修改每個位置的值,我考場上套的樹狀陣列,總複雜度 \(O(n\log n)\),其實直接遍歷一遍就行了,複雜度 \(O(n)\)

//樹狀陣列版本程式碼
#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, a[N];
//
int t[N];
inline int lowbit(int x) { return x & (-x); }
int ask(int x) {
    int res = 0;
    for (; x; x -= lowbit(x)) res += t[x];
    return res;
}
void add(int x, int val) {
    for (; x <= n; x += lowbit(x)) t[x] += val;
}
int query(int l, int r) {
    return ask(r) - ask(l - 1);
}
//

void change(int l, int r) {
    for (int i = l; i <= r; ++i)
        if (query(i, i) == 1) add(i, -1);
}
int solve() {
    //read
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    //solve
    memset(t, 0, sizeof(int) * (n + 1));
    for (int i = 1; i <= n; ++i)
        if (a[i] != a[n]) add(i, 1);

    int res = 0, p = n - 1;
    while (p >= 1) {
        if (query(p, n) > 0) {
            p++;
            int L = max(1, 2 * p - n - 1);
            change(L, p - 1);
            p = L - 1;
            res++;
        }
        else p--;
    }
    return res;
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) printf("%d\n", solve());
    return 0;
}
#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, a[N];
void change(int l, int r) {
    for (int i = l; i <= r; ++i) a[i] = 0;
}
int solve() {
    //read
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    //solve
    for (int i = 1; i <= n; ++i)
        a[i] = a[i] != a[n];

    int res = 0, p = n - 1;
    while (p >= 1) {
        if (a[p] > 0) {
            p++;
            int L = max(1, 2 * p - n - 1);
            change(L, p - 1);
            p = L - 1;
            res++;
        }
        else p--;
    }
    return res;
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) printf("%d\n", solve());
    return 0;
}

C題 And Matching

\(T\) 組資料。(\(1\leq T \leq 400\)

給定 \(n\) 個數 \([0,n)\),保證 \(n\) 是 2 的冪,問能否給這 \(n\) 個數找到一種配對方式,使得 \(\sum\limits_{i=1}^{n/2}a_i\&b_i=k\),並輸出該方案?

\(4\leq n \leq 2^{16},0\leq k < n\)

經過長時間觀察,我發現了一種 \(k=0\) 時的構造方案:\((0,n-1),(1,n-2),\cdots,(\frac{n}{2}-1,\frac{n}{2})\) (這種方案是純觀察到的,但是存在性不容置疑:對每個數,直接尋找按位反差的那個值來配對即可)

如果 \(0 < k < n-1\),我們發現找出下面兩組: \((0,n-1),(k,n-k-1)\),然後交換一下,變為 \((0,n-k-1),(k,n-1)\)即可,別的值不變,這兩組的值之和增加了 \(k\) ,剛好滿足要求。

\(k=n-1\) 的時候,我一開始以為無解,然後 WA 了幾發之後老老實實去找方案:我們選擇 \((0,n-1),(1,n-2)\) 兩組變換為 \((0,1),(n-2,n-1)\),這樣可以增加 \(n-2\);隨後,我們選擇 \((2,n-3),(3,n-4)\) 兩組,變化為 \((2,n-4),(3,n-3)\)。從二進位制上面不難發現,僅有最後一位出現了變化,其他位置保持不變,所以答案增加 1,剛好湊成 \(n-1\),成功。(\(n=4\) 的時候無解,因為此時僅有兩組,不夠分)

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int vis[N];
void solve(int n, int k) {
    if (k == n - 1) {
        if (n == 4) puts("-1");
        else {
            printf("%d %d\n%d %d\n%d %d\n%d %d\n", 0, 1, n - 2, n - 1, 2, n - 4, 3, n - 3);
            for (int i = 4; i < n / 2; ++i)
                printf("%d %d\n", i, n - 1 - i);
        }
        return;
    }
    memset(vis, 0, sizeof(int) * (n + 1));
    if (k) {
        vis[0] = vis[n - 1] = vis[k] = vis[n - k - 1] = 1;
        printf("%d %d\n%d %d\n", 0, n - k - 1, k, n - 1);
    }
    for (int i = 0; i < n / 2; ++i)
        if (!vis[i]) printf("%d %d\n", i, n - i - 1);
    return;
}
int main()
{
    int T;
    cin >> T;
    while (T--) {
        int n, k;
        cin >> n >> k;
        solve(n, k);
    }
    return 0;
}

D題 Range and Partition

\(T\) 組資料。(\(1\leq T\leq 3*10^4\)

給定一個長度為 \(n\) 的數列 \(\{a_n\}\) 和正整數 \(k\),問能否找到一個區間 \([x,y]\),使得我們能將數列分成恰好 \(k\) 段,每段裡面,在該區間內部的數要比不在該區間的數要多?若存在,請輸出 \(y-x\) 最小值。

\(1\leq k\leq n\leq 2*10^5,1\leq a_i\leq n\)

存在性毋庸置疑:讓這個區間為 \([1,n]\) 即可,然後想怎麼分。

我們先來看看,在確定了區間之後,如何判斷該區間是否合法:我們構建一個新陣列 \(\{b_n\}\),原陣列中在該區間內部的數標記為 1,反之標記為 0,那麼問題就變成了:給定一個 01 陣列,問能否恰好分成 \(k\) 組,每組內部 1 的數量都大於 0?

今年牛客冬令營的第一場 F 題有一個類似的題目,問能否分成若干段,使得每段中位數都大於某個值。那場的題解和標準證明都在這:題解,我們直接搬運一下結論:當區間內 1 的個數減去 0 的個數大於等於 \(k\) 時存在分組方案,直接掃一遍,複雜度 \(O(n)\)。(本題還需要輸出方案,道理也差不多

如果我們暴力列舉 \(x,y\),那麼總複雜度就是 \((n^3)\),如果列舉 \(x\) 並二分 \(y\),那麼複雜度就是 \(O(n^2\log n)\)

上面因為搬運結論,剛給忘了一個東西:我們並不需要掃一遍整個陣列,只需要先給排個序,然後每次統計數量的時候直接兩次二分即可,這樣複雜度就降到了 \(O(n \log^2 n)\),應該可以通過了。

#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, k, a[N], b[N];
bool check(int x, int y) {
    int l = lower_bound(a + 1, a + n + 1, x) - a;
    int r = upper_bound(a + 1, a + n + 1, y) - a - 1;
    return (r - l + 1) - (n - (r - l + 1)) >= k;
}
void output(int x, int y) {
    printf("%d %d\n", x, y);
    int L = 1;
    for (int i = 1; i < k; ++i) {
        int cnt = 0;
        for (int R = L; R <= n; ++R) {
            x <= b[R] && b[R] <= y ? cnt++ : cnt--;
            if (cnt > 0) {
                printf("%d %d\n", L, R);
                L = R + 1;
                break;
            }
        }
    }
    printf("%d %d\n", L, n);
}
void solve()
{   //read & init
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]), b[i] = a[i];
    sort(a + 1, a + n + 1);
    //solve
    int res = 1e9 + 10, ansx = 0, ansy = 0;
    for (int x = 1; x <= n; ++x) {
        int l = x, r = n + 1;
        while (l < r) {
            int mid = (l + r) >> 1;
            if (check(x, mid)) r = mid;
            else l = mid + 1;
        }
        if (r != n + 1 && res > r - x) {
            res = r - x, ansx = x, ansy = r;
        }
    }
    //output
    output(ansx, ansy);
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) solve();
    return 0;
}

不過我們還發現:\(x,y\) 均符合單調性質,所以我們還可以雙指標,將列舉 \(x,y\) 的複雜度降到 \(O(n)\),這樣就可以在 \(O(n\log n)\) 的複雜度內解決該問題了。

其實我們甚至不用直接列舉 \(x,y\),而是直接列舉對應的下標:在排序好的陣列上列舉 \(i,j\),保證 \(i,j\) 之間的數要多於不在該範圍的數,然後不斷更新 \(x=a_i,y=a_j\) 即可。這個 \(i,j\) 甚至不需要雙指標,直接預先算好那個合適的距離 \(len=\lceil \frac{n+k}{2}\rceil\) 即可。

#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, k, a[N], b[N];
void output(int x, int y) {
    printf("%d %d\n", x, y);
    int L = 1;
    for (int i = 1; i < k; ++i) {
        int cnt = 0;
        for (int R = L; R <= n; ++R) {
            x <= b[R] && b[R] <= y ? cnt++ : cnt--;
            if (cnt > 0) {
                printf("%d %d\n", L, R);
                L = R + 1;
                break;
            }
        }
    }
    printf("%d %d\n", L, n);
}
void solve()
{   //read & init
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]), b[i] = a[i];
    //solve
    sort(a + 1, a + n + 1);
    int res = 1e9 + 10, x = 0, y = 0;
    int len = (n + k + 1) / 2;
    for (int L = 1; L + len - 1 <= n; ++L) {
        int R = L + len - 1;
        if (res > a[R] - a[L])
            res = a[R] - a[L], x = a[L], y = a[R];
    }
    //output
    output(x, y);
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) solve();
    return 0;
}