Codeforces Round #663 (Div. 2)
A. Suborrays
题目大意
A 题给定一个长度为 (n) 排列(permutation),要求这个排列满足如下性质:
- ((p_i; OR ; p_{i+1} ; OR ; cdots ; OR ; p_{j}) ge j - i + 1quad forall; i,j in[1, n], i leq j)
1 <= n <= 100
constructive algorithms
math
*800
思路分析
这题比较简单,首先由于按位或的性质,我们有:
又因为排列中不存在重复的元素,因此无论如何构造,当区间长度大于(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 ? '
' : ' ');
}
int main(){
int t; cin >> t;
while (t--)
solve();
return 0;
}
B. Fix You
题目大意
给定一个 (n imes 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 << '
';
}
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 ; earrow) 的情况就会出现环。比如([3, 1, 5]),1 与 3,5 之间存在无向边,由于 3 右边第一个大于他的值为 5 ,因此他们之间便存在环。因此推导可知,所有不构成环排的排列均表现为:( earrow ; 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;
}
- 其中容易出错的地方为,当可能需要对负数取$ mod {}$时,需要进行
(x + mod) % mod
处理。
D. 505
题目大意
给定一个 (n imes 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_{eta})。若(n ge 4) , 由于 (m_{eta}) 包含 4 个 (m_{alpha}),若 (m_{alpha})中 1 的个数为奇数,则 (m_{eta}) 中 1 的个数肯定为偶数,与原假设冲突。所以当 (n) 大于 4 时,一定无法修改成功。
因此只需要考虑 (n = 2, 3)的情况也就是(2 imes 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
的状态时,最小开销为多少。
状态转移方程为:
其中 (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
"; return 0; }
if (n <= 1) { cout << "0
"; 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 << '
';
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
"; return 0; }
if (n >= 4) { cout << "-1
"; 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 << '
';
return 0;
}
注意事项:
>>
和<<
运算符的优先级比+
,-
低。- 利用内存压缩,只需要当前和上一个两个状态,用异或
^
实现取反。
E. Pairs of Pairs
题目大意
给定一个包含 (n) 个结点 (m) 个边的无向连通图。
定义合法点对如下:
例如点对 (P = {{a, b}, {c,d}, cdots}) ,对于其中任意两个点对,共四个元素,在无向图中最多只能有 (2) 条边。
需要你:
- 寻找一个至少包含(lceil frac{n}{2} ceil)结点的简单路径。
- 寻找一个至少包含(lceil frac{n}{2} ceil)结点的合法点对。
思路分析
这个题很类似于我之前写过的一到 1364D,同样是存在两种情况,实现一种即可。且保证至少有一种情况一定存在。
这类题,一般只需要找到临界情况,再分别讨论就可以了。
对于本题,首先思考怎么找到一个至少包含(lceil frac{n}{2}
ceil)结点的简单路径? 答案比较清晰,利用 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
";
cout << dep[i] + 1 << "
";
for (int cur = i; cur != 0; cur = father[cur])
printf("%d ", cur);
printf("
");
return;
}
}
int cnt = 0;
cout << "PAIRING
";
cout << ceil(ceil(n / 2.0) / 2.0) << '
';
for (int i = 0; i < n; ++ i){
for (int j = 0; j + 1 < f[i].size(); j += 2){
printf("%d %d
", 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 比我想象的简单,还是要多做!!!