1. 程式人生 > 實用技巧 >Kubernetes入門(二)——Dashboard 安裝

Kubernetes入門(二)——Dashboard 安裝

Codeforces Round #663 (Div. 2)

A. Suborrays

題目大意

A 題給定一個長度為 \(n\) 排列(permutation),要求這個排列滿足如下性質:

  • \((p_i\; OR \; p_{i+1} \; OR \; \cdots \; OR \; p_{j}) \ge j - i + 1\quad \forall\; i,j \in[1, n], i \leq j\)

1 <= n <= 100

constructive algorithms math *800

思路分析

這題比較簡單,首先由於按位或的性質,我們有:

\[(p_i\; OR \; p_{i+1} \; OR \; \cdots \; OR \; p_{j}) \ge \max\{p_i, p_{i+1}, \cdots, p_j\} \]

又因為排列中不存在重複的元素,因此無論如何構造,當區間長度大於\(j - i + 1\)時,該區間至少存在一個大於\(j - i + 1\)的數字,因此

any permutation work!!

程式碼

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

void solve(){
    int n; cin >> n;
    for (int i = n; i >= 1; -- i)
        cout << i << (i == 1 ? '\n' : ' ');
}

int main(){
    int t; cin >> t;
    while (t--)
        solve();
    return 0;
}

B. Fix You

題目大意

給定一個 \(n \times m\) 的矩陣,除了終點 \((n, m)\) 為字元 \(C\) 外,其餘位置均為字元 \(R\)\(D\),其中 \(R\) 代表往右走,\(D\) 代表往下走。問最少修改多少處字元保證,從任意位置出發都能到達終點 \(C\) 處。

  • 1 <= n <= 100
  • 1 <= m <= 100

greedy *800

思路分析

這題出的很是巧妙,比賽的時候沒有想出來於是寫了個又臭又長的 BFS

實際上從任何位置出發,由於字元\(R,D\)性質決定,最終都會抵達下沿邊或者右沿邊。因此對於應該矩陣形如:

X
X
X
X X X C

我們只需要修改字元為\(X\)的位置,保證下沿邊均往右,右沿邊均往下。

程式碼

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

void solve(){
    int n, m, ans(0); cin >> n >> m;
    vector<string> mat(n);
    for (int i = 0; i < n; ++ i) cin >> mat[i];
    for (int i = 0; i < n - 1; ++ i) ans += mat[i][m - 1] == 'R';
    for (int i = 0; i < m - 1; ++ i) ans += mat[n - 1][i] == 'D';
    cout << ans << '\n';
}

int main(){
    int t; cin >> t;
    while (t--) solve();
    return 0;
}

C. Cyclic Permutations

題目大意

存在排列 \(p\) ,對於 \(p_i\) , \(p_i\)能與左邊,右邊第一個大於他的數建立無向邊。

定義 **有環排列 \(cp\) **為:

  • \(len (cp) = k \ge 3\)
  • 不存在重複元素
  • \(v_i, v_{i + 1}\)之間均存在無向邊,且\(v_{i}, v_{i + k - 1}\)之間也存在邊(成環)

給定一個數字 \(n\) ,尋找所有的有環排列的數量,並把結果 \(\mod 1e9 + 7\) 後輸出。

3 <= n <= 10^6

combinatorics math dp graphs *1500

思路分析

這題有關排列的數量,顯然是和組合數學有關係的。又因為涉及到組合數學不難想到可能需要用到快速冪

組合數學中常用的技巧是正難則反,因此我們可以考慮找到那些不構成環排的總數量。

經過思考可以發現只要出現\(\searrow \; \nearrow\) 的情況就會出現環。比如\([3, 1, 5]\),1 與 3,5 之間存在無向邊,由於 3 右邊第一個大於他的值為 5 ,因此他們之間便存在環。因此推導可知,所有不構成環排的排列均表現為:\(\nearrow \; \searrow\) 也就是山峰狀。

因此問題歸結為求解存在多少個山峰狀的排列,很明顯山頂為\(\max p\),左右兩遍的排列已經固定(升序或者降序)因此只需要考慮組合而不需要排列。山峰由左往右移動答案為:\(C_{n}^{0} + C_{n}^{1} + \cdots + C_{n}^{n} = 2^n\)。排列的總數為\(n!\)。因此最終數量為:

\[ans = n! - 2^n \]

注意資料型別即可。

程式碼

#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MOD = 1e9 + 7;
LL qpow(LL a, LL n){
    LL ans(1);
    while (n){
        if (n & 1) ans = (ans * a) % MOD;
        a = (a * a) % MOD;
        n >>= 1;
    }
    return ans % MOD;
}

LL fac(int n){
    LL ans(1);
    while (n){
        ans = (ans * n) % MOD;
        -- n;
    }
    return ans % MOD;
}

int main(){
    int n; cin >> n;
    LL f = fac(n);
    LL dlt = qpow(2, n - 1);
    cout << (f - dlt + MOD) % MOD << endl; // 由於存在負數,因此需要注意
    return 0;
}
  • 其中容易出錯的地方為,當可能需要對負數取$ \mod {}$時,需要進行 (x + mod) % mod處理。

D. 505

題目大意

給定一個 \(n \times m\) 的二進位制矩陣 \(a\)(所有元素為 0 or 1),定義好的二進位制矩陣為它所有邊長為 \(2\) 的倍數,且長寬相等的矩陣中 \(1\) 的個數都為奇數。求問對於當前給定矩陣\(a\)最少進行多少次修改能滿足條件,若不能滿足則輸出 \(-1\)

1 <= n <= m <= 10^6

n * m <= 10^6

bitmasks constructive algorithms dp greedy *2000

思路分析

這是一道非常好的題目,我很喜歡。

首先可以發現,定義邊長為 \(2\) 的合法矩陣為 \(m_{\alpha}\) ,定義邊長為 \(4\)的合法矩陣為 \(m_{\beta}\)。若\(n \ge 4\) , 由於 \(m_{\beta}\) 包含 4 個 \(m_{\alpha}\),若 \(m_{\alpha}\)中 1 的個數為奇數,則 \(m_{\beta}\) 中 1 的個數肯定為偶數,與原假設衝突。所以當 \(n\) 大於 4 時,一定無法修改成功。

因此只需要考慮 \(n = 2, 3\)的情況也就是\(2 \times 2\)的矩陣,因為 \(n = 1\) 時,直接不需要進行修改。

解決該問題有兩種思路,本文分別闡述:

思路分析一:奇偶變化,巧用規律

先考慮 \(n = 2\) 的簡單情況,對於每個需要考慮的矩陣,由於 1 的個數為奇數,因為奇數 = 奇數 + 偶數,所以整個序列應該為:

  1. 奇,偶,奇,偶,\(\cdots\)
  2. 偶,奇,偶,奇,\(\cdots\)

兩種情況(在這裡本文以列為單位進行考慮)。因此我們只需要列舉第一列為 奇數,或者為偶數的情況,迭代下去即可,每一列最多需要修改一個位置即可更替奇偶性。

再考慮 \(n=3\) 的情況,將他看成兩個 \(n = 2\)的情況,視為兩行:

  • 當有單獨一行需要修改奇偶性時,只需要修改一處即可。
  • 當兩行同時需要修改奇偶性時,只需要修改兩者交接的地方,也可以一次奇偶性修改。

因此只需要列舉\(2 * 2\),共四種狀態即可。

思路分析二:狀壓dp,解一解萬

同樣,\(n\)非常小,且為二進位制很難不讓人想到 位運算 。既然想到位運算又是一個計數問題狀壓dp也就不能想到了,問題是如何定義 \(dp\)陣列的含義,以及確定狀態轉移方程。

顯然,\(dp\)遍歷時我們需要從左往右,所以我們只需要關係截止上一步的開銷,並往下一步轉移即可

定義 := dp[i][cur],其含義為,截止至第 i 行,且第 icur 的狀態時,最小開銷為多少。

狀態轉移方程為:

\[dp(i, cmask) = \min(dp(i, cmask), dp(i-1,pmask) + bitcount(cmask\; \oplus \; origin)) \]

其中 \(cmask\) 代表列舉當前的狀態,\(pmask\) 列舉上一行的狀態, \(bitcount(cmask \oplus origin)\)為計算原始資料與枚舉出的當前狀態需要進行修改的次數,用異或實現,\(bitcount\) 為計算二進位制中 1 的個數。

因此,整個演算法的流程為:

  1. 預處理 dp[0][j]為 0
  2. 計算出 origin
  3. 列舉當前行 i
  4. 列舉當前狀態 cmask 和上一個狀態 pmask
  5. 利用狀態轉移方程進行轉移
  6. i != m時跳 3,否則跳 7
  7. 遍歷搜尋 i = m 時的每個狀態,計算結果

程式碼一

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

int n, m;
int ans;
vector<vector<int>> grid;

int get_ans_2(int x){
    int res = 0;
    for (int i = 0; i < m; ++ i){
        int cur = (grid[0][i] + grid[1][i]) & 1; 
        // cur & 1 (0 --> even, 1 --> odd) 
        // x (0 --> even, 1 --> odd)
        // only (0, 1), (1, 0) need modify, so use XOR !! 
        if (cur ^ x) ++ res;
        x = !x;
    }
    return res;
}

int get_ans_3(int x, int y){
    int res= 0;
    for (int i = 0; i < m; ++ i){
        int cur1 = (grid[0][i] + grid[1][i]) & 1;
        int cur2 = (grid[1][i] + grid[2][i]) & 1;

        if ((cur1 ^ x) || (cur2 ^ y)) ++ res;
        x = !x, y = !y;
    }
    return res;
}

int main(){
    cin >> n >> m;
    vector<string> mat(n);
    grid.resize(n, vector<int>(m, 0));
    for (int i = 0; i < n; ++ i) cin >> mat[i];

    if (n >= 4) { cout << "-1\n"; return 0; }
    if (n <= 1) { cout << "0\n"; return 0; }
    ans = 0;

    for (int i = 0; i < n; ++ i){
        for (int j = 0; j < m; ++ j) grid[i][j] = mat[i][j] - '0';
    }

    if (n == 2){
        ans = min(get_ans_2(0), get_ans_2(1));
    }
    if (n == 3){
        ans = min({get_ans_3(0, 0), get_ans_3(0, 1), get_ans_3(1, 0), get_ans_3(1, 1)});
    }

    cout << ans << '\n';

    return 0;
}

需要注意的點:

  • 利用異或處理奇偶性。

程式碼二

#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define bitcnt(x) __builtin_popcountll(x)

int n, m;
int dp[2][1 << 4]; // (i & 1) --> cur or pre, 1 << 4 --> status
vector<vector<int> > grid;
inline bool check(int cur, int pre){
    for (int k = 0; k + 1 < n; ++ k){
        int cnt = 0;
        cnt += (((cur >> k) & 1) + (cur >> (k + 1)) & 1); // 注意運算子的優先順序
        cnt += (((pre >> k) & 1) + (pre >> (k + 1)) & 1);
        if (!(cnt & 1)) return false;
    }
    return true;
}

int main(){
    cin >> n >> m;

    vector<string> mat(n);
    grid.resize(n + 1, vector<int>(m + 1, 0));
    for (int i = 0; i < n; ++ i) cin >> mat[i];
    for (int i = 1; i <= n; ++ i){
        for (int j = 1; j <= m; ++ j) grid[i][j] = mat[i - 1][j - 1] - '0';
    }

    if (n <= 1) { cout << "0\n"; return 0; }
    if (n >= 4) { cout << "-1\n"; return 0; }

    for (int j = 0; j < (1 << n); ++ j) dp[0][j] = 0; // 清空

    for (int i = 1; i <= m; ++ i){
        int raw = 0;
        for (int j = 1; j <= n; ++ j){
            raw <<= 1;
            raw |= grid[j][i];
        }

        for (int cur = 0; cur < (1 << n); ++ cur){
            dp[i & 1][cur] = inf;
            for (int pre = 0; pre < (1 << n); ++ pre){
                if (check(cur, pre)) dp[i & 1][cur] = min(dp[i & 1][cur], dp[(i & 1) ^ 1][pre] + bitcnt(raw ^ cur));
            }
        }
    }

    int ans = inf;
    for (int j = 0; j < (1 << n); ++ j) ans = min(ans, dp[m & 1][j]);
    cout << ans << '\n';

    return 0;
}

注意事項:

  • >><< 運算子的優先順序比 +-低。
  • 利用記憶體壓縮,只需要當前和上一個兩個狀態,用異或^實現取反。

E. Pairs of Pairs

題目大意

給定一個包含 \(n\) 個結點 \(m\) 個邊的無向連通圖。

定義合法點對如下:

例如點對 \(P = \{\{a, b\}, \{c,d\}, \cdots\}\) ,對於其中任意兩個點對,共四個元素,在無向圖中最多隻能有 \(2\) 條邊。

需要你:

  • 尋找一個至少包含\(\lceil \frac{n}{2}\rceil\)結點的簡單路徑。
  • 尋找一個至少包含\(\lceil \frac{n}{2}\rceil\)結點的合法點對。

思路分析

這個題很類似於我之前寫過的一到 1364D,同樣是存在兩種情況,實現一種即可。且保證至少有一種情況一定存在

這類題,一般只需要找到臨界情況,再分別討論就可以了。

對於本題,首先思考怎麼找到一個至少包含\(\lceil \frac{n}{2}\rceil\)結點的簡單路徑? 答案比較清晰,利用 dfs 即可,若深度滿足條件即可輸出。

所以,我們首先對於無向連通圖建立一顆 dfs樹,下面補充幾個重要知識點

對於無向圖建立 dfs樹 :存在樹邊,返祖邊;不存在橫叉邊和前向邊

對於無向圖建立 bfs樹: 存在樹邊,橫叉邊;不存在返祖邊和前向邊

因此我們首先搜尋是否存在簡單路徑,若不存在簡單路徑即在每一層選取兩個元素組成點對。由於同層結點一定不存在橫叉邊。又因為最多存在兩條返祖邊,因此可以保證一定合法。

程式碼

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

// dfs 圖論問題
const int maxn = 5e5 + 50;
vector<int> E[maxn], f[maxn];
int dep[maxn], father[maxn];
bool vis[maxn];

// 多case 不要直接memset

void dfs(int cur = 1, int fa = 0, int d = 0){
    dep[cur] = d;
    father[cur] = fa;
    vis[cur] = true;
    for (auto &go: E[cur]){
        if (go == fa || vis[go]) continue;
        dfs(go, cur, d + 1);
    }
}

void solve(){
    int n, m; cin >> n >> m;
    for (int i = 0; i <= n; ++ i) E[i].clear(), f[i].clear(), dep[i] = father[i] = 0, vis[i] = false;

    for (int i = 0; i < m; ++ i){
        int u, v;
        scanf("%d %d", &u, &v);
        E[u].push_back(v);
        E[v].push_back(u);
    }

    dfs();
    for (int i = 1; i <= n; ++ i){
        f[dep[i]].push_back(i); // 假如到層中
        if (dep[i] >= (n + 1) >> 1){
            cout << "PATH\n";
            cout << dep[i] + 1 << "\n";
            for (int cur = i; cur != 0; cur = father[cur])
                printf("%d ", cur);
            printf("\n");
            return;
        }
    }

    int cnt = 0;
    cout << "PAIRING\n";
    cout << ceil(ceil(n / 2.0) / 2.0) << '\n';
    for (int i = 0; i < n; ++ i){
        for (int j = 0; j + 1 < f[i].size(); j += 2){
            printf("%d %d\n", f[i][j], f[i][j + 1]);
            cnt += 2;
            if (cnt >= (n + 1) >> 1) return;
        }
    }

}

int main(){
    int t; scanf("%d", &t);
    while (t--) solve();
    return 0;
}

注意事項:

  • 多 case 儘量不用 memset
  • 大資料用 printf, scanf

總結

這一次比賽大的比較一般,B的話沒有想到,強行寫了個 bfs上去,實際上 CF 的前兩題多想想數學一點的解法。

這一次的 D 我覺得對我有很大的提升,尤其在於位運算的方面,E 比我想象的簡單,還是要多做!!!