【解題報告】Educational Codeforces Round 20

題目鏈接


A. Maximal Binary Matrix(Codeforces 803A)

思路

本題的入手點是,先滿足“字典序最小”的要求,再滿足對稱性(就比較容易了)。
由於題目要求解的“字典序”最小,所以可以先按照行從上到下,再按照列從左到右的順序爲矩形填上 1 (也就是在作文紙上寫字的順序)。然後就只需要滿足對稱性了。假設當前我們已經填到 (i,j) 這個格子了,那麼分以下兩種情況

  1. i=j 時,由於當前填的位置位於對角線上,填上後肯定不會影響對稱性,所以能填就填。什麼是不能填的情況呢?如果填 1 的次數沒有剩餘了的話就不能填了。
  2. i!=j 時,由於當前填的位置不位於對角線上,所以如果要在 (i,j) 網格內填數的話就得在 (j,i) 網格內填數。有如下情況就不能填:剩餘的填 1 次數爲 1(i,j) 網格內已經填過 1 了。

代碼

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

const int maxn = 110;
int n, k, a[maxn][maxn];

// 輸出矩陣
void output() {
    for(int i = 1; i <= n ;i++) {
        for(int j = 1; j <= n; j++) {
            cout << a[i][j] << ' ';
        }
        cout << endl;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> k;
    // k比格子還要多的時候
    if(k > n * n) {
        cout << -1 << endl;
        return 0;
    }
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            if(k == 0) {
                output();
                return 0;
            }
            if(a[i][j] == 1) {
                continue;
            }
            // 填對角線位置
            if(i == j) {
                a[i][j] = 1;
                k--;
            }
            // 填非對角線位置
            else if(k >= 2) {
                a[i][j] = a[j][i] = 1;
                k -= 2;
            }
        }
    }
    if(k > 0) {
        cout << -1 << endl;
    }
    else {
        output();
    }
    return 0;
}

B. Distances to Zero(Codeforces 803B)

思路

本題的入手點在於將問題“找離某位置最近的 0 ”分解成子問題“找在其左側的最近的 0 ”和子問題“找在其右側的最近的 0 ”。
我們只考慮“找在數組中的位置的 i 左側最近 0 ”(因爲“右側”的情況雷同)。從 1n 遍歷數組,同時維護一個變量 last ,其值表示在當前狀態之前,遇見的最後一個 0 的位置,那麼對於任意位置 iilast 就是該位置到左側最近0的距離。

代碼

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

const int maxn = 2e5 + 10, INF = 3e5;
int n, last, a[maxn], b[maxn], c[maxn];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n;
    for(int i = 0; i < n; i++) {
        cin >> a[i];
    }
    // 從左到右掃描數組,尋找離位置i最近的左側的0
    last = -1;
    for(int i = 0; i < n; i++) {
        if(a[i] == 0) {
            b[i] = 0;
            last = i;
        }
        else if(last >= 0) {
            b[i] = i - last;
        }
        else {
            b[i] = INF;
        }
    }
    // 從右到左掃描數組,尋找離位置i最近的右側的0
    last = -1;
    for(int i = n - 1; i >= 0; i--) {
        if(a[i] == 0) {
            c[i] = 0;
            last = i;
        }
        else if(last >= 0) {
            c[i] = last - i;
        }
        else {
            c[i] = INF;
        }
    }
    // 輸出答案
    for(int i = 0; i < n; i++) {
        cout << min(b[i], c[i]) << ' ';
    }
    return 0;
}

C. Maximal GCD(Codefoeces 803C)

思路

本題的入手點爲考慮到數列的 gcd 必然是 n 的約數。
這題要構造一個數列,其需要滿足兩個條件,一個是個數爲 k ,另一個是和爲 n 。我們先考慮第二個條件。第二個條件首先告訴我們的是,數列的 gcd 必然是 n 的約數。那麼我們可以先枚舉 n 的約數以縮小搜尋的範圍。假設當前枚舉到的約數爲 d ,那麼我們要構造出來的序列必然是以 d 爲首項, d 爲公差的等差序列的子序列。那麼我們可以做出猜想:如果該數據有解,我們就可以構造出這樣一組解

d, 2d, 3d, ..., (k1)d, nsum(d,k1)

其中 sum(a,b) 表示以 a 爲首項, b 爲項數的數列的前 b 項和。然後可以證明“如果該組數據有解的話,那麼上述構造方式構造出的一定是一組解”,還可以證明“如果上述方式構造不出解,那麼該組數據一定無解”。因爲我也想不到該怎麼證明,所以這裏就不敘述怎麼證明了(不負責任的解題報告寫手系列)。
最後就是實現的時候,由於輸入的數據太大,需要注意溢出的問題。

代碼

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

typedef unsigned long long ll;
vector <ll> divisor;
ll n, k, ub, a, s, S;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> k;
    // 按照以1首項1爲公差的數列和求出k的上界
    // 以避免下面出現的懲罰溢出
    ub = 2 * (ll)(sqrt(1.0 * n) + 0.5);
    if(k > ub) {
        cout << -1 << endl;
        return 0;
    }
    // 預處理出n的約數
    for(ll i = 1; i * i <= n; i++) {
        if(n % i > 0) {
            continue;
        }
        divisor.push_back(i);
        if(i != n / i) {
            divisor.push_back(n / i);
        }
    }
    // 對n的約數d
    for(ll d : divisor) {
        // 避免溢出,提前判斷
        if(k * (k - 1) / 2 >= n / d) {
            continue;
        }
        // 求出數列的和
        s = k * (k - 1) * d / 2;
        if(n - s <= (k - 1) * d) {
            continue;
        }
        // 更新最大公約數最小的解
        if(d > a) {
            a = d;
            S = s;
        }
        a = max(a, d);
    }
    if(a == 0) {
        cout << -1 << endl;
        return 0;
    }
    // 輸出數列
    for(ll i = 1; i < k; i++) {
        cout << a + (i - 1) * a << ' ';
    }
    cout << n - S << endl;
    return 0;
}

D. Magazine Ad(Codeforces 803D)

思路

本題的入手點是,利用數據的“二分性”,將求最值的問題轉化爲驗證某個值是否可行。
我們可以枚舉最長的行的長度 x ,然後驗證在所有行的長度不超過 x 的情況下,是否能構造出不超過 k 行的文本。這樣的最小的 x 就是答案。如何驗證是否能構造呢?不妨將被’-‘和空格分割出的字符串稱作“單詞”。先考慮第一行,將單詞一個一個加到第一行的末尾,直到第一行的文本的長度即將超過 x (也就是再加入單詞該行的長度就會超過 x 的時刻)。然後再考慮第二行,第三行,……,直到將所有單詞都處理完。這樣構造的文本必然具有所有行長度不超過 x 情況下的最短行數(證明略)。
但是由於 x (最長的行的長度)的可能長度達到了 105 ,所以我們利用 x 的二分性(較小的 x 不能滿足“最多隻有 k 行”的條件,而較大的 x 可以滿足)將枚舉改成二分查找就行了,查找滿足條件的最小的 x 即可。

代碼

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

string s;
vector <int> vec;
int k, last, l, r;

// 判斷對給定的最長行的長度是否有解
bool ok(int x) {
    int cnt = 0, sum = 0;
    // 對於每個“單詞”
    for(int i = 0; i < vec.size(); i++) {
        if(vec[i] > x) {
            return false;
        }
        // 把該單詞算在下行
        if(sum + vec[i] > x) {
            sum = vec[i];
            cnt++;
        }
        // 把該單詞算在該行
        else {
            sum += vec[i];
        }
    }
    if(sum > 0) {
        cnt++;
    }
    return cnt <= k;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> k;
    getline(cin, s);
    getline(cin, s);
    last = 0;
    // 預處理處單詞
    for(int i = 1; i < s.size(); i++) {
        if(s[i] == ' ' || s[i] == '-') {
            vec.push_back(i - last + 1);
            last = i + 1;
        }
    }
    // 有可能還有最後一個單詞
    if(last <= s.size() - 1) {
        vec.push_back(s.size() - 1 - last + 1);
    }
    // 二分查找
    l = -1;
    r = s.size();
    while(r - l > 1) {
        int mid = (l + r) / 2;
        if(ok(mid) == true) {
            r = mid;
        }
        else {
            l = mid;
        }
    }
    cout << r << endl;
    return 0;
}

E. Roma and Poker(Codeforces 803E)

思路

本題的入手點是,由“多階段決策”這個特徵想到用 解決。
在不考慮數據範圍的情況下顯然可以用搜索來解決,但是本題的數據範圍顯然不允許我們這樣做。因此需要另闢蹊徑。不妨令狀態 i 表示當前處理到輸入的字符串的第 i 位,那麼在狀態 i 我們可以根據 s[i] 的值有不同的可能(有點類似於決策)。

s[i] 可能發生的情況
L 此時只能是輸掉第i局
W 此時只能是贏下第i局
D 此時可以打平第i局
? 此時可以輸,可以贏,也可以打平第i局

雖然每次有這麼多種狀態轉移情況,但是在每個狀態 i ,狀態轉移的結果都是有限的。於是我們改寫狀態爲 (i,j) ,表示當前在第 i 局,主人公的淨勝分爲 j 。那麼可以用 d[i][j] 表示 (i,j) 這個狀態是否可達(可達 d[i][j]=1 , 否則爲 0 )。狀態轉移方程如下:

s[i] 狀態轉移方程
L d[i][j] |= d[i-1][j + 1]
W d[i][j] |= d[i - 1][j - 1]
D d[i][j] |= d[i - 1][j]
? (此處應寫上以上的三條方程)

最後如果狀態 (n,k)(n,k) 上的狀態權值爲 1 的話就表明有解。在轉移過程中有兩個需要注意的點:

  1. j 有可能是負數。解決方案是將所有的j都加上一個偏移量,將原來 j 可能落在的區間 [k,k] 映射到 [lb,ub] 上去( lb0,ub0 )。
  2. 由於在比賽的中途比賽不能終止(也就是一定要恰好在第 n 局結束),所以對於任意 i<n ,在狀態轉移中,任意狀態不能轉移到狀態 (i,lb)(i,ub)

轉移結束後還沒有完成最終任務,我們還需要輸出一個解。爲了構造出一個解,我們可以用矩陣 p[i][j] 記錄轉移的過程,若從 (i1,k)(i,j) 有一個狀態轉移的話就令 p[i][j]=k 。最後就可以從 (n,lb)(n,ub) 根據 p 矩陣的值不斷回溯來構造解了(當然,由於是從終狀態開始回溯,所以構造出來的解是反過來的)。

代碼

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

const int maxn = 2010, mid = 1000;
stack <char> ans;
string s;
int n, k, lb, ub, d[maxn][maxn], p[maxn][maxn];

// 輸的情況的狀態轉移
void lose(int i, int j) {
    if(j == ub || !d[i - 1][j + 1]) {
        return;
    }
    d[i][j] = 1;
    p[i][j] = j + 1;
}

// 贏的情況的狀態轉移
void win(int i, int j) {
    if(j == lb || !d[i - 1][j - 1]) {
        return;
    }
    d[i][j] = 1;
    p[i][j] = j - 1;
}

// 平局的情況的狀態轉移
void draw(int i, int j) {
    if(d[i - 1][j] == 0) {
        return;
    }
    d[i][j] = 1;
    p[i][j] = j;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> k >> s;
    ub = mid + k;
    lb = mid - k;
    s = " " + s;
    d[0][mid] = 1;
    for(int i = 1; i <= n; i++) {
        for(int j = lb; j <= ub; j++) {
            // 比賽不能提前結束
            if(i < n && (j == ub || j == lb)) {
                continue;
            }
            // 根據s[i]的值來決定狀態的轉移方式
            if(s[i] == 'L') {
                lose(i, j);
            }
            else if(s[i] == 'D') {
                draw(i, j);
            }
            else if(s[i] == 'W') {
                win(i, j);
            }
            else {
                lose(i, j);
                draw(i, j);
                win(i, j);
            }
        }
    }
    // 無解的情況
    if(!d[n][lb] && !d[n][ub]) {
        cout << "NO" << endl;
        return 0;
    }
    // 回溯構造解
    int i = n;
    int j = d[n][lb] ? lb : ub;
    while(i > 0) {
        if(p[i][j] > j) {
            ans.push('L');
        }
        else if(p[i][j] == j) {
            ans.push('D');
        }
        else {
            ans.push('W');
        }
        j = p[i--][j];
    }
    // 逆序輸出結果(這裏用了一個棧)
    while(!ans.empty()) {
        cout << ans.top();
        ans.pop();
    }
    return 0;
}

F. Coprime Subsequences(Codeforces 803F)

思路

本題的入手點是,運用分類的思想,正難則反的思想和容斥的思想來解決計數問題。

在開始描述解法之前先做一些鋪墊。令 cnt(a,f) 表示序列 a 中能被 f 整除的數的個數。 num(a,f) 表示序列 a 中滿足性質“所有數都能被 f 整除”的子序列的個數(子序列指從原序列中拿出一些數構成的順序不變的新序列)。顯然,根據集合的計數原理也好,根據組合公式相加的公式也好,我們有

num(a,f)=2cnt(a,f)1

回到問題中。我們要求的是

a序列的滿足性質“所有的數兩兩互質”的子序列的個數

“兩兩互質”聽上去不那麼清晰,可以將上述描述改成

a序列中滿足性質“所有的數的最大公約數gcd爲1”的子序列的個數

就算描述變成這樣也還是不容易有思路,那麼根據“正難則反”的思想,我們可以把上述描述進一步變形:

a序列中不滿足性質“所有數的gcd爲2或3或…或max{a_i, i <= n}”的子序列的個數

注意是“不滿足”那個性質的子序列的個數。那麼上述表述所描述的值就可以用容斥原理轉化成求

num(a,1)num(a,2)num(a,3)num(a,5)...+num(a,2×3)+num(a,2×5)+num(a,3×5)...num(a,2×3×5)...

其中 cnt(a,f) 可以預處理出來, num(a,f) 可以通過快速冪算法求出來,上述式子中的 num(a,f) 的係數跟 f 有關,實際這個係數就是 μ(f) ,其中 μ() 就是著名的莫比烏斯函數(各大百科都能找到定義),用線性篩算法可以預處理出來。至於爲什麼係數是莫比烏斯函數,我覺得是以因爲莫比烏斯函數正好對應了素因子容斥問題中的容斥原理公式的項的係數(如果不明白,那麼請研究莫比烏斯函數,或者百度、谷歌其資料)。

代碼

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

typedef long long ll;
const int maxn = 1e5 + 5, mod = 1e9 + 7;
bool vis[maxn];
int n, a, f[maxn], mu[maxn], prime[maxn];
ll ans, cnt[maxn];

// 篩法預處理出莫比烏斯函數
// 存在mu[]中
void init(int N) {
    memset(vis, 0, sizeof(vis));
    mu[1] = 1;
    int cnt = 0;
    for(int i = 2; i < N; i++) {
        if(!vis[i]) {
            prime[cnt++] = i;
            mu[i] = -1;
        }
        for(int j = 0; j < cnt && i * prime[j] < N; j++) {
            vis[i * prime[j]] = 1;
            if(i % prime[j]) {
                mu[i * prime[j]] = -mu[i];
            }
            else {
                mu[i * prime[j]] = 0;
                break;
            }
        }
    }
}

// 枚舉統計a數組中的各個數的約數的出現次數
void divisor(int n) {
    for(int i = 1; i * i <= n; i++) {
        if(n % i == 0) {
            cnt[i]++;
            if(i != n / i) {
                cnt[n / i]++;
            }
        }
    }
}

// 計算快速冪的函數
ll modPow(ll a, ll n, ll mm) {
    ll ans = 1;
    for(; n > 0; n >>= 1) {
        if(n & 1) {
            ans = (ans * a) % mm;
        }
        a = (a * a) % mm;
    }
    return ans;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    init(maxn);
    cin >> n;
    for(int i = 1; i <= n; i++) {
        cin >> a;
        divisor(a);
    }
    // 計算上面提到的num()函數
    for(int i = 1; i < maxn; i++) {
        f[i] = modPow(2, cnt[i], mod) - 1;
        f[i] = (f[i] + mod) % mod;
    }
    // 容斥原理 
    for(int i = 1; i < maxn; i++) {
        ans = (ans + mu[i] * f[i] + mod) % mod;
    }
    cout << ans << endl;
    return 0;
}

其它題目略

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章