A 題給定一個長度爲 \(n\) 排列(permutation),要求這個排列知足以下性質:c++
- \((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
數組
這題比較簡單,首先因爲按位或的性質,咱們有:大數據
又由於排列中不存在重複的元素,所以不管如何構造,當區間長度大於\(j - i + 1\)時,該區間至少存在一個大於\(j - i + 1\)的數字,所以ui
any permutation work!!spa
#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; }
給定一個 \(n \times m\) 的矩陣,除了終點 \((n, m)\) 爲字符 \(C\) 外,其他位置均爲字符 \(R\) 或 \(D\),其中 \(R\) 表明往右走,\(D\) 表明往下走。問最少修改多少處字符保證,從任意位置出發都能到達終點 \(C\) 處。code
1 <= n <= 100
1 <= m <= 100
greedy
*800
內存
這題出的非常巧妙,比賽的時候沒有想出來因而寫了個又臭又長的 BFS。ci
實際上從任何位置出發,因爲字符\(R,D\)性質決定,最終都會抵達下沿邊或者右沿邊。所以對於應該矩陣形如:get
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; }
存在排列 \(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!\)。所以最終數量爲:
注意數據類型便可。
#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; }
(x + mod) % mod
處理。給定一個 \(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 的個數爲奇數,由於奇數 = 奇數 + 偶數,因此整個序列應該爲:
兩種狀況(在這裏本文以列爲單位進行考慮)。所以咱們只須要枚舉第一列爲 奇數,或者爲偶數的狀況,迭代下去便可,每一列最多須要修改一個位置便可更替奇偶性。
再考慮 \(n=3\) 的狀況,將他當作兩個 \(n = 2\)的狀況,視爲兩行:
所以只須要枚舉\(2 * 2\),共四種狀態便可。
一樣,\(n\)很是小,且爲二進制很難不讓人想到 位運算 。既然想到位運算又是一個計數問題狀壓dp也就不能想到了,問題是如何定義 \(dp\)數組的含義,以及肯定狀態轉移方程。
顯然,\(dp\)遍歷時咱們須要從左往右,因此咱們只須要關係截止上一步的開銷,並往下一步轉移便可。
定義 := dp[i][cur]
,其含義爲,截止至第 i
行,且第 i
爲 cur
的狀態時,最小開銷爲多少。
狀態轉移方程爲:
其中 \(cmask\) 表明枚舉當前的狀態,\(pmask\) 枚舉上一行的狀態, \(bitcount(cmask \oplus origin)\)爲計算原始數據與枚舉出的當前狀態須要進行修改的次數,用異或實現,\(bitcount\) 爲計算二進制中 1 的個數。
所以,整個算法的流程爲:
dp[0][j]
爲 0origin
i
cmask
和上一個狀態 pmask
i != m
時跳 3,不然跳 7i = 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; }
注意事項:
>>
和 <<
運算符的優先級比 +
,-
低。^
實現取反。給定一個包含 \(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; }
注意事項:
memset
printf, scanf
這一次比賽大的比較通常,B的話沒有想到,強行寫了個 bfs
上去,實際上 CF 的前兩題多想一想數學一點的解法。
這一次的 D 我以爲對我有很大的提高,尤爲在於位運算的方面,E 比我想象的簡單,仍是要多作!!!