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 比我想象的简单,仍是要多作!!!