10/23

NPSC 2019 難題選解 + 競賽小技巧

Intro

講者的心聲:今年好難QQ

雖說是2019的題解,但是觀念教學性質還是比較多QQ

這次內容有點多,能講多少算多少吧

今天可能會學到

greedy method / 貪心法

binary search / 二分搜尋

dynamic programming / 動態規劃

standard template library (STL)

prefix sum / 前綴和

coordinate compression / 離散化

程式競賽要注意的小細節

Warm Up

這已經是第\(N\)次放這張圖片了 OAO

Warm Up

這張圖片也放了很多次 QQ

Hello World !

*1 / 萬用起手式

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

*2 / 輸入輸出

int main() {
    int number;
    scanf("%d", &number)
    printf("%d\n", number);
}
int main() {
    int number;
    cin >> number;
    cout << number << '\n';
}

*3 / 長長整數

int main() {
    int x = 2147483647;
    long long y = 9223372036854775807;
    cout << x << ' ' << y << '\n';
}
#include<bits/stdc++.h>
#define int long long
using namespace std;

*4 / 陣列放外面

int main() {
    int a[10000000] = {};
    cout << a[9999999] << endl;
}
int a[10000000];
int main() {
    cout << a[9999999] << endl;
}

*5 / 同餘與取模

int main() {
    int n, mod, ans = 1;
    cin >> n >> m;
    for(int i=0;i<n;i++) ans = (ans * i) % mod;
    cout << ans << endl;
}
(a + b) \mod M = (a \mod M + b \mod M) \mod M \\ (a - b) \mod M = (a \mod M - b \mod M) \mod M \\ (a \times b) \mod M = (a \mod M \times b \mod M) \mod M \\ (a \div b) \mod P = (a \mod P \times b^{P-2} \mod P) \mod P

*6 / 快速冪

int pow(int a, int b, int m) { // a^b % m
    int ans = 1;
    while(b) {
        if(b & 1) ans = (ans * a) % m;
        a = (a * a) % m, b >>= 1;
    }
    return ans;
}

二進位拆分 or 分治

*7 / 時間複雜度

int main() {
    int n, ans = 0;
    cin >> n;
    for(int i=0;i<n;i++) ans += i;
    cout << ans << '\n';
}
int main() {
    int n, ans = 0;
    cin >> n;
    for(int i=0;i<n;i++) {
        for(int j=i;j<n;j++) ans += i*j;
    } cout << ans << '\n';
}
int main() {
    int n, ans = 0;
    cin >> n;
    for(int i=1;i<n;i*=2) ans += i;
    cout << ans << endl;
}
int main() {
    int n, ans = 0;
    cin >> n;
    ans = n * (n + 1) / 2;
    cout << n << '\n';
}

\(10^8\)經驗法則

*\(\infty\) / 沒AC怎麼辦

1. 變數歸零

2. 輸出格式 (要換行)

3. long long

4. 陣列戳到外面(?)

5. 除以零

6. 變數重名 (尤其是迴圈的i, j)

7. \(10^8\)法則

 

0. 演算法錯誤 (沒救QQ)

Semi / 初賽

➣ 貪心演算法

每天都在用

int val[9] = {1, 5, 10, 20, 50, 100, 200, 500, 1000};
int cnt[9];
int main() {
    int p, ans = 0;
    cin >> p;
    for(int i=0;i<9;i++) cin >> cnt[i];
    for(int i=8;i>=0;i--) {
        int take = min(cnt[i], p/val[i]);
        ans += take, p -= val[i] * take;
    }
    cout << ans << '\n';
}

Problem A

你有三疊牌,每疊牌有\(N\)張

有\(N\)個回合,每個回合要在每一疊牌各拿出一張,三張牌中的最小值的就是得分

求總得分的最大值

\((N \leq 10^5)\)

Solution

考慮以下貪心策略:

1. 每次看三堆中最小的牌

2. 拿出最小的那張

3. 剩下兩堆都各拿最大的那張

 ➝ 就是全部牌的前\(N\)小啦

#include <bits/stdc++.h>
using namespace std;
int a[300010];
signed main() {
    int n, ans = 0;
    cin >> n;
    for(int i=0;i<3*n;i++) cin >> a[i];
    sort(a, a + 3*n);
    for(int i=0;i<n;i++) ans += a[i];
    cout << ans << '\n';
}

➣ 動態規劃 (dp)

最常被拿來當例子的爬樓梯:

狀態、轉移、基底

int dp[100000], n;
int main() {
    cin >> n;
    dp[0] = dp[1] = 1;
    for(int i=2;i<=n;i++) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    cout << dp[n] << '\n';
}

Problem F

給一個數列,你可以把數字塗成紅、綠、藍三種顏色

但是相鄰的數字不能同顏色

求 (綠色數字和 - 紅色數字和) 的最大值

\((N \leq 10^6)\)

Solution

我們先只考慮前\(i\)個數字:

R[i] 是第\(i\)個一定要塗紅色的答案

G[i] 是第\(i\)個一定要塗綠色的答案

B[i] 是第\(i\)個一定要塗藍色的答案

 

轉移不難想到(?)

#include<bits/stdc++.h>
using namespace std;
int R[1000010], G[1000010], B[1000010], a[1000010];
int main() {
    int n;
    cin >> n;
    for(int i=1;i<=n;i++) cin >> a[i];
    for(int i=1;i<=n;i++) {
        R[i] = max(G[i-1], B[i-1]) - a[i];
        G[i] = max(R[i-1], B[i-1]) + a[i];
        B[i] = max(R[i-1], G[i-1]);
    }
    cout << max({R[n], G[n], B[n]}) << '\n';
}

➣ 二分搜尋法

猜數字

// find miximum x that ok(x) = true
int main() { 
    int ub = 1000 + 1, lb = 0;
    while(ub - lb > 1) {
        int mid = (ub + lb) / 2;
        if(ok(mid)) lb = mid;
        else ub = mid;
    }
    cout << lb << '\n';
}

猜數字、猜答案、猜各種有單調性的東西

Problem D

一堆黑白牛在一個山坡上,第\(i\)個位置的高度是\(a_i\),

你可以任意交換這些牛的順序,使得相鄰兩黑牛的高度差不超過\(K\)

求最小可能的\(K\)

\((N \leq 2\times 10^5)\)

Solution

對\(K\)二分搜

 

驗解採用 \(O(n)\) dp

W[i] = 前\(i\)隻牛,且第\(i\)隻是白牛的最大可能總黑牛數

B[i] = 前\(i\)隻牛,且第\(i\)隻是黑牛的最大可能總黑牛數

因為黑牛一開始在哪根本沒差,所以只要看數量

#include<bits/stdc++.h>
#define int long long 
using namespace std;
int n, x, black = 0, h[200010], b[200010], w[200010];
bool ok(int mid) {
    for(int i=0;i<n;i++) b[i] = w[i] = 0;
    b[0] = 1;
    for(int i=1;i<n;i++) {
        w[i] = max(b[i-1], w[i-1]);
        if(abs(h[i] - h[i-1]) <= mid) b[i] = max(b[i-1], w[i-1]) + 1;
        else b[i] = w[i-1] + 1;
    }
    return max(b[n-1], w[n-1]) >= black;
}
signed main() {
    cin >> n;
    for(int i=0;i<n;i++) cin >> x >> h[i], black += x;
    int ub = 2e9 + 1, lb = -1;
    while(ub - lb > 1) {
        int mid = (ub + lb) / 2;
        if(ok(mid)) ub = mid;
        else lb = mid;
    }
    cout << ub << '\n';
}

Final / 決賽

➣ std::set

一個集合

set<int> s; // multiset<int>
int main() {
    int n = 8;
    s.insert(n);
    cout << s.count(5) << '\n';
    s.insert(5);
    s.erase(8);
    cout << s.size() << '\n';
    for(auto i: s) cout << i << ' ';
}

Problem B

給定\(N\)個人來的時間順序,

以及每個人對應行李被送到輸送帶上的時間順序

 

若輸送帶只能放\(k\)個行李,且最多只能\(p\)個人在輸送帶旁等待行李,求是否能讓所有人都拿到行李

 

\((N \leq 10^5)\)

Solution

貌似很多細節的模擬

 

先放前\(k\)個行李在輸送帶上

 

之後讓人依序進來,拿不到行李就繼續等待

;若拿到行李,就直接離開並且補上新的行李

 

每次補上行李要重新判斷是否有正在等待的人可以拿到行李

#include <bits/stdc++.h>
using namespace std;
int luggage[100010], people[100010];
set<int> online, queued;
signed main() {
    int n, k, p, x, maxcnt = 0;
    cin >> n >> k >> p;
    for(int i=1;i<=n;i++) cin >> x, luggage[x] = i;
    for(int i=1;i<=n;i++) cin >> x, people[x] = i;
    for(int i=1;i<=k;i++) online.insert(luggage[i]);
    int nxt = k + 1;
    for(int i=1;i<=n;i++) {
        if(online.count(people[i])) {
            online.erase(people[i]);
            while(queued.count(luggage[nxt])) {
                queued.erase(luggage[nxt++]);
            }
            online.insert(luggage[nxt++]);
        }
        else queued.insert(people[i]);
        maxcnt = max(maxcnt, (int)queued.size());
    }
    cout << (maxcnt <= p ? "Yes\n" : "No\n");
}

➣ std::string & map

字串 & 映射

map<string, int> s;
int main() {
    string n;
    cin >> n;
    s[n] = 8;
    s["abcd"] = s[n] + 1;
    cout << s.count("gg") << '\n';
    s.insert({"this", 3});
    cout << n.size() << ' ' << s.size() << '\n';
    for(auto i: s) {
        cout << i.first << ' ' << i.second << '\n';
    }
}

Problem C

給\(N\)個字串,若兩字串相同位置有相同字元稱為一個匹配

 

求這\(N\)個字串的總匹配數

 

\(N\leq 10^6,\Sigma L_i \leq 5\times 10^6\)

Solution

數學題

 

可以發現不同位置是互相獨立的

而對於同一個位置,若某字元出現\(x\)次,則會使答案多出\(C^x_2\)

#include<bits/stdc++.h>
using namespace std;
map<char, int> cnt[1000010];
int main() {
    int n, maxlen = 0;
    string s;
    cin >> n;
    while(n--) {
        cin >> s;
        for(int i=0;i<s.size();i++) cnt[i][s[i]]++;
        maxlen = max(maxlen, (int)s.size());
    }
    long long ans = 0;
    for(int i=0;i<maxlen;i++) {
        for(auto c: cnt[i]) {
            long long val = c.second;
            ans += val * (val - 1) / 2;
        }
    }
    cout << ans << '\n';
}

➣ 前綴和

快速計算多個區間和的好幫手

int a[100001], n, q, l, r;
int main() {
    cin >> n >> q;
    for(int i=1;i<=n;i++) cin >> a[i];
    for(int i=1;i<=n;i++) a[i] += a[i-1];
    while(q--) {
        cin >> l >> r;
        cout << a[r] - a[l-1] << '\n';
    }
}

Problem G

座標平面上有\(N\)個金礦跟\(M\)個銀礦,每個礦都有不同價值

現在選定兩個礦,將以兩礦為對角線的長方形中所有礦物挖起來

 

不過你最多只能挖\(K\)個金礦

求最大價值總和

\((N+M \leq 5000)\)

Solution

枚舉每個礦物的pair

 

然後用二維前綴和計算這兩個礦物之間

的金礦數量跟總價值

 

座標範圍很大不好做前綴和

不過實際的坐標是多少其實不重要,只要維持大小順序就好

所以可以先把值域壓縮

➣ 二維前綴和

s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1]
void compress(int a[], int now = 0) {
    map<int,int> g;
    for(int i=0;i<n+m;i++) {
        if(!g[a[i]]) g[a[i]] = ++now;
        a[i] = g[a[i]];
    }
}
void compress(int a[]) {
    for(int i=0;i<n+m;i++) tmp[i] = a[i];
    sort(a, a+n+m);
    int len = unique(a, a+n+m) - a;
    for(int i=0;i<n+m;i++) {
        tmp[i] = lower_bound(a, a+len, tmp[i]) - a + 1;
    }
    for(int i=0;i<n+m;i++) a[i] = tmp[i];
}

➣ 離散化

#include<bits/stdc++.h>
using namespace std;
int n, m, k, x[5010], y[5010], v[5010], tmp[5010], gold[5010][5010];
long long tot[5010][5010];
void compress(int a[], int now = 0) { /* .. */ };
int main() {
    long long ans = 0;
    cin >> n >> m >> k;
    for(int i=0;i<n+m;i++) cin >> x[i] >> y[i] >> v[i];
    compress(x), compress(y);
    for(int i=0;i<n+m;i++) tot[x[i]][y[i]] = v[i];
    for(int i=0;i<n;i++) gold[x[i]][y[i]] = 1;
    for(int i=1;i<=n+m;i++) for(int j=1;j<=n+m;j++) {
        tot[i][j] += tot[i][j-1] + tot[i-1][j] - tot[i-1][j-1];
        gold[i][j] += gold[i][j-1] + gold[i-1][j] - gold[i-1][j-1];
    }
    for(int i=0;i<n+m;i++) for(int j=i;j<n+m;j++) {
        int x1 = min(x[i], x[j]), x2 = max(x[i], x[j]);
        int y1 = min(y[i], y[j]), y2 = max(y[i], y[j]);
        long long val = tot[x2][y2] - tot[x1-1][y2] - tot[x2][y1-1] + tot[x1-1][y1-1];
        int weight = gold[x2][y2] - gold[x1-1][y2] - gold[x2][y1-1] + gold[x1-1][y1-1];
        if(weight <= k) ans = max(ans, val);
    }
    cout << ans << '\n';
}

➣ 0/1背包

大家應該都會(?)

int v[1000], w[1000], dp[100000], n, W;
int main() {
    cin >> n >> W;
    for(int i=0;i<n;i++) cin >> v[i] >> w[i];
    for(int i=0;i<n;i++) {
        for(int j=W;j>=w[i];j--) {
            dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
        }
    }
    cout << dp[W] << '\n';
}

Problem F

⼀組密碼是⼀個只有 0 與 1 組成的字串

⼀組密碼的複雜度是「有⾄少⼀個 1 的⼦區間數量」

 

有 \(Q\) 個詢問, 每個詢問包含⾮負整數 \(K_i\)

對於每個詢問輸出任意一種複雜度 \(K_i\) 、長度為\(N\)的密碼

若不存在滿⾜條件的密碼,輸出⼀⾏ "No" 

\((N \leq 300)\)

性質觀察1

「有⾄少⼀個 1 的⼦區間數量」= \(\frac{n(n+1)}{2}\) - 「全是0的連續區間數」

 

所以複雜度\(K\)的密碼就有\((\frac{n(n+1)}{2} -  K)\)個「全是0的連續區間」

性質觀察2

若存在長度\(N - 1\)、複雜度\(K\)的密碼,

存在長度\(N\)、複雜度\(K\)的密碼

 

當然更長的也都會有,只要一直把 1 串在後面就行

所以我們只要找到最短的解

Solution

Note:複雜度\(K\)的密碼有\((\frac{n(n+1)}{2} -  K)\)個「全是0的連續區間」

長度為\(L\)的連續0可以提供\(\frac{L(L+1)}{2}\)個「全是0的連續區間」

 

-> 有無限多個價值\(\frac{L(L+1)}{2}\)的東西,而每個東西的重量是\(L\),目標是求最小重量且價值總和 = \((\frac{n(n+1)}{2} -  K)\)

#include<bits/stdc++.h>
using namespace std;
int dp[100000], tr[100000], dif[100000];
int main() {
    int n, q, x;
    cin >> n >> q;
    for(int i=1;i<=n*(n+1)/2;i++) dp[i] = 1e9;
    for(int i=1;i<=n;i++) {
        for(int j=i*(i+1)/2;j<=n*(n+1)/2;j++) {
            if(dp[j-i*(i+1)/2] + i < dp[j]) {
                dp[j] = dp[j-i*(i+1)/2] + i;
                tr[j] = j-i*(i+1)/2, dif[j] = i;
            }
        }
    }
    while(q--) {
        cin >> x;
        x = n*(n+1)/2 - x;
        int cnt = 0;
        string ans;
        while(x) {
            for(int i=0;i<dif[x];i++) ans += '0', cnt++;
            if(x = tr[x]) ans += '1', cnt++;
        }
        while(ans.size() < n) ans += '1';
        if(ans.size() == n) cout << ans << '\n';
        else cout << "No\n";
    }
}

期待各位拿下好成績

Thanks for your listening

Made with Slides.com