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

相關文章
相關標籤/搜索