Codeforces Round #663 (Div. 2)

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

思路分析

这题比较简单,首先由于按位或的性质,我们有:

[(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 ? '
' : ' ');
}

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!)。因此最终数量为:

[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 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 的个数为奇数,因为奇数 = 奇数 + 偶数,所以整个序列应该为:

  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
"; 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 比我想象的简单,还是要多做!!!

原文地址:https://www.cnblogs.com/Last--Whisper/p/13572836.html