Codeforces Round #663 (Div. 2)

Codeforces Round #663 (Div. 2)

A. Suborrays

题目大意

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数组

思路分析

这题比较简单,首先因为按位或的性质,咱们有:大数据

\[(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\)的数字,所以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;
}

B. Fix You

题目大意

给定一个 \(n \times m\) 的矩阵,除了终点 \((n, m)\) 为字符 \(C\) 外,其他位置均为字符 \(R\)\(D\),其中 \(R\) 表明往右走,\(D\) 表明往下走。问最少修改多少处字符保证,从任意位置出发都能到达终点 \(C\) 处。code

  • 1 <= n <= 100
  • 1 <= m <= 100

greedy *800内存

思路分析

这题出的非常巧妙,比赛的时候没有想出来因而写了个又臭又长的 BFSci

实际上从任何位置出发,因为字符\(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;
}

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 \; \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!\)。所以最终数量为:

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

  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\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;
}

注意事项:

  • >><< 运算符的优先级比 +-低。
  • 利用内存压缩,只须要当前和上一个两个状态,用异或^实现取反。

E. Pairs of Pairs

题目大意

给定一个包含 \(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;
}

注意事项:

  • 多 case 尽可能不用 memset
  • 大数据用 printf, scanf

总结

这一次比赛大的比较通常,B的话没有想到,强行写了个 bfs上去,实际上 CF 的前两题多想一想数学一点的解法。

这一次的 D 我以为对我有很大的提高,尤为在于位运算的方面,E 比我想象的简单,仍是要多作!!!

相关文章
相关标签/搜索