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 的個數為奇數,因為奇數 = 奇數 + 偶數,所以整個序列應該為:
- 奇,偶,奇,偶,\(\cdots\)
- 偶,奇,偶,奇,\(\cdots\)
兩種情況(在這裡本文以列為單位進行考慮)。因此我們只需要列舉第一列為 奇數,或者為偶數的情況,迭代下去即可,每一列最多需要修改一個位置即可更替奇偶性。
再考慮 \(n=3\) 的情況,將他看成兩個 \(n = 2\)的情況,視為兩行:
- 當有單獨一行需要修改奇偶性時,只需要修改一處即可。
- 當兩行同時需要修改奇偶性時,只需要修改兩者交接的地方,也可以一次奇偶性修改。
因此只需要列舉\(2 * 2\),共四種狀態即可。
思路分析二:狀壓dp,解一解萬
同樣,\(n\)非常小,且為二進位制很難不讓人想到 位運算 。既然想到位運算又是一個計數問題狀壓dp也就不能想到了,問題是如何定義 \(dp\)陣列的含義,以及確定狀態轉移方程。
顯然,\(dp\)遍歷時我們需要從左往右,所以我們只需要關係截止上一步的開銷,並往下一步轉移即可。
定義 := dp[i][cur]
,其含義為,截止至第 i
行,且第 i
為 cur
的狀態時,最小開銷為多少。
狀態轉移方程為:
\[dp(i, cmask) = \min(dp(i, cmask), dp(i-1,pmask) + bitcount(cmask\; \oplus \; origin)) \]
其中 \(cmask\) 代表列舉當前的狀態,\(pmask\) 列舉上一行的狀態, \(bitcount(cmask \oplus origin)\)為計算原始資料與枚舉出的當前狀態需要進行修改的次數,用異或實現,\(bitcount\) 為計算二進位制中 1 的個數。
因此,整個演算法的流程為:
- 預處理
dp[0][j]
為 0 - 計算出
origin
- 列舉當前行
i
- 列舉當前狀態
cmask
和上一個狀態pmask
- 利用狀態轉移方程進行轉移
- 當
i != m
時跳 3,否則跳 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 比我想象的簡單,還是要多做!!!