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

  • 相关阅读:
    垂直同步
    C++ RAII
    C++ RAII
    LCD刷新率和垂直同步的设置
    ping结果中TTL是什么意思
    垂直同步
    stage.frameRate改变帧频
    ping结果中TTL是什么意思
    stage.frameRate改变帧频
    ping 命令的原理,揭开单向“Ping”通的奥秘
  • 原文地址:https://www.cnblogs.com/Last--Whisper/p/13572836.html
Copyright © 2020-2023  润新知