首页 > 技术文章 > Codeforces Round #674 (Div. 3)

Last--Whisper 2020-09-29 17:03 原文

Codeforces Round #674 (Div. 3)

A. Floor Number

题目大意

题目大意为:一个学生公寓中有多个房间,每个房间有门牌号。已知一楼有且仅有两个房间,其门牌号分别为 1 和 2。给定数字 \(x\) ,之后每层楼中有 \(x\) 间房间。问:给你门牌号 \(n\) 和数字 \(x\) 。给出所在楼层。

1 <= n, x <= 1000

*800

解题思路

非常简单,分情况讨论:

  • 如果 $n \leq 2 $ ,则返回 \(1\)
  • 否则,\(n \leftarrow n - 2, ans = \lceil\dfrac{n}{x}\rceil\)

代码

void solve(){
    int n = read(), x = read();
    if (n <= 2) cout << 1 << '\n';
    else cout << (n - 2 + x - 1)  / x + 1 << '\n';
}

B. Symmetric Matrix

题目大意

\(n\)\(2 \times 2\) 的正方形尝试拼接出一个 \(m\times m\) 的关于主对角线对称矩阵。

如果可以输出:YES 否则输出:NO

*900

解题思路

题目看起来有点麻烦,阅读起来也比较长,实际上仔细一想,只要边长能 mod 2,则我们运用贪心的思路考虑问题,只需要一个 \(2\times 2\) 的主对角线对称矩阵即可。

主要的思路方向是大小类比,转大为小。

代码

void solve(){
    int n = read(), m = read();
    int have = 0;
    for (int i = 0; i < n; ++ i){
        int ul = read(), ur = read();
        int dl = read(), dr = read();
        if (dl == ur) have = 1;
    }
    if (m % 2 != 0 || have == 0) wprint("NO");
    else wprint("YES");
}

C. Increase and Copy

题目大意

给你一个数组 \(a\) ,最开始只包含一个数字 \(1\) 。你可以做如下两种操作:

  1. \(a\) 中的某一元素加一
  2. 复制 \(a\) 中某一元素到数组末尾

回答,需要最少操作多少步,能使数组 \(a\) 中的总和大于 \(n\)

1 <= n <= 1e9

*1100

思路分析

经过思考,显然倍增的效率是大于自增的,于是我们可以初步得出结论,我们需要先自增然后再倍增

设我们自增到 \(k\) 再进行倍增。则需要的步骤数为:

\[ans = \lceil \dfrac{n - k}{k}\rceil + k - 1 \]

处理一下变为:

\[ans = \lceil \dfrac{n}{k} \rceil + k - 2 \]

由基本不等式可得:当 \(k = \sqrt{n}\) 时存在最小值,又因为这里皆为整数,通过计算机验证之后得到如下结论:

\[\begin{aligned} 对于: &\left\{ \lceil \dfrac {n}{k} \rceil + k\right\} or \left\{ \lfloor \dfrac {n}{k} \rfloor + k\right\}\qquad &k \in Z \\ \quad \\ &k = \lfloor \sqrt n \rfloor \lor k = \lceil \sqrt n\rceil 时存在最小值 \end{aligned} \]

可以记住如上结论。

代码

void solve(){
    int n = read();
    int ans(0);
    int x = sqrt(n);
    ans = (n + x - 1) / x + x - 2;
    cout << ans << '\n';
}

D. Non-zero Segments

题目大意

给定一个长度为 \(n\) 且不含 \(0\) 的序列 \(a\),可以插入任何数字(可以为 \(\infty\) )。请问最少插入多少个数字才能保证序列中任意连续子序列和不为 \(0\)

  • 2 <= n <= 200000
  • -1e9 <= ai <= 1e9

*1500

思路分析

这题关键点在于 快速确定区间和为 0 的情况

因为做过类似的,所以可以很快的写出来:即用 hashmap

接下来复制我在CF回复别人的部分:

Let us define \(presum_{k}\) as the prefix sum, \(a_k\) as the array.

\[presum_{k} = \sum_{i=1}^{k} a_i \]

Let's define \(i,j\) as indexs(\(i < j\)):

  • if \(presum_{i} == presum_{j}\), we can know that

\[presum_{j} - presum_{i} == 0 \rightarrow sum\{a_{i+1}, \cdots, a_{j}\} == 0 \]

In order to avoid this situation, we insert a \(+\infty\) behind the \(a_j\). Like this: \(a_{i+1}, \cdots, a_{j-1}, \stackrel{\infty}{\downarrow},a_{j}\).Then reconsider from \(a_j\)

代码

void solve(){
    int n = read();
    unordered_map<LL, int> mp; // 0 --> not in, 1 --> in
    mp.clear();
    LL sum(0), ans(0);
    for (int i = 0; i < n; ++ i){
        int f = read();
        sum += f;
        if (sum == 0 || mp.count(sum)) { 
            ++ ans; 
            mp.clear();  // Simulate inserting an infinite number
            sum = f; 
            mp[sum] = 1;
        }
        else mp[sum] = 1;
    }
    cout << ans << '\n';
}

E. Rock, Paper, Scissors

题目大意

Alice和Bob进行剪刀石头布的游戏,总共进行\(n\)局。

Alice出石头\(a_1\)次,出剪刀\(a_2\)次,出布\(a_3\)次。

Bob出石头\(b_1\)次,出剪刀\(b_2\)次,出布\(b_3\)次。

问Alice最多赢多少次,最少赢多少次。

*1800

思路分析

最大值非常简单,我们希望尽可能能赢

实际上这题应该有更加数学的思路,但是考虑到整个问题空间的大小,因此可以通过暴力搜索直接解决。

面对样本空间很小的题目,我们可以优先计算穷举的复杂度,快速解决。

时间复杂度大致为 \(\mathcal{O}(2\cdot 3!)\)

代码

int a[5], b[5];
int mn;
void dfs(int ca, int cb, vector<int> visa, vector<int> visb, int cnt, vector<int> fa, vector<int> fb){
    visa[ca] = visb[cb] = 1;
    
    if (fa[ca] >= fb[cb]){
        if ((ca + 1) % 3 == cb) cnt += fb[cb];
        fa[ca] -= fb[cb];
        int ok =0;
        for (int i = 0; i < 3; ++ i){
            if (visb[i] == 0) { // 谁没有了,就在他可选的中间挑选一个
                dfs(ca, i, visa, visb, cnt, fa, fb);
                ok = 1;
            }
        }
        if (ok == 0){ // 没有什么可以选的了,说明结束了
            mn = min(mn, cnt);
            return;
        }
    }
    else {
        if ((ca + 1) % 3 == cb) cnt += fb[cb];
        fb[cb] -= fa[ca];
        int ok = 0;
        for (int i = 0; i < 3; ++ i){
            if (visa[i] == 0) {
                dfs(i, cb, visa, visb, cnt, fa, fb);
                ok = 1;
            }
        }
        if (ok == 0){
            mn = min(mn, cnt);
            return;
        }
    }
}


void solve(){
    int n = read();
    for (int i = 0; i < 3; ++ i) a[i] = read();
    for (int i = 0; i < 3; ++ i) b[i] = read();

    int mx(0);
    for (int i = 0; i < 3; ++ i) mx += min(a[i], b[(i + 1) % 3]);

    VI visa(3, 0), visb(3,0); // visa --> a是否访问, visb --> b是否访问
    VI fa(3, 0), fb(3, 0); // fa --> a 的各个数个数, fb --> 同 a
    mn = inf;
    for (int i = 0; i < 3; ++ i) fa[i] = a[i], fb[i] = b[i];
    for (int i = 0; i < 3; ++ i){ // 枚举各种情况
        for (int j = 0; j < 3; ++ j) dfs(i, j, visa, visb, 0, fa, fb);
    }
    wprint(mn, mx);
}

F. Number of Subsequences

题目大意

给定一个长为 \(n\) 含有abc?的字符串,?可能是abc中的任意一个,求所有可能的无?字符串中,子序列abc出现的次数.结果需要 mod \(1e9 + 7\)

3 ≤ n ≤ 200000

*2000 dp

思路分析

通过数据大小和题目所问的,我们可以很快得出大致思路:时间复杂度为 \(\mathcal{O}(n)\) 的线性dp

假如是 dp ,我们肯定需要记录一些东西才能保证在 \(\mathcal{O(1)}\) 的时间内进行状态转移,并且需要考虑记录什么和 ? 如何进行处理。

由于我们只需要考虑 abc ,因此我们可以只记录:

  1. a的数量 \(f[0]\)
  2. ab 的数量 \(f[1]\)
  3. abc 的数量 \(f[2]\)

当没有遇到 ? 时转移方程非常简单,这里就不做赘述。

下面考虑当需要疑问号的情况即可:

首先,我们清楚疑问号可以变为 a,b,c 任意一种的,用生物学的知识去想有点像分裂的过程:

当碰到 ? 号时,会在原有的基础上出现 \(3\) 种新情况。

  1. 对于 \(cur_1\) 。其 \(f[0] = f[0] + 1\)
  2. 对于 \(cur_2\)。 其 \(f[1] = f[1] + f[0]\)
  3. 对于 \(cur_3\)。 其 \(f[2] = f[2] + f[1]\)

而他们各自又继承了 cur的所有值。因此:

\[ \begin{aligned} &for\; new\;cur:\\ &f[2] = 3*f[2] + f[1] \\ &f[1] = 3*f[1] + f[0] \\ &f[0] = 3*f[0] + 1 \\ \end{aligned} \]

这对了吗?,实际上这样写代表你没有理解其核心。需要知道的最开始只拥有 \(1\) 种情况,但每遇到一个 ? 会分裂出 \(3\) 种情况,不妨假设 ? 的个数为 \(c_q\) 。则当前存在 \(3^{c_q}\) 种情况,于是你每次增加都需要考虑所有的情况。而上述方程中:a -> abab -> abc 的转移自带鲁棒性已经考虑了这个问题,而 null -> a 的过程则没有考虑。

因此,我们需要记录当前空间中存在情况的总个数 base ,当遇到 ? 时更新 base 的值。

做这种 \(\mathcal{O}(1)\) 的计数 dp ,肯定是需要记录某些数据,若需要计数的为一个具有顺序的序列,不妨前缀考虑每个子段的个数,逐个转移。

代码

const int maxn = 5;
LL f[maxn];
void solve(){
    int n = read();
    mst(f, 0);
    string ss;
    cin >> ss;
    LL base = 1;
    for (int i = 1; i <= n; ++ i){
        char ch = ss[i - 1];
        if (ch == 'a'){
            f[0] = (f[0] + base) % MOD;
        }else if (ch == 'b'){
            f[1] = (f[1] + f[0]) % MOD;
        }else if (ch == 'c'){
            f[2] = (f[2] + f[1]) % MOD;
        }else {
            f[2] = (3 * f[2] % MOD + f[1]) % MOD;
            f[1] = (3 * f[1] % MOD + f[0]) % MOD;
            f[0] = (3 * f[0] % MOD + base) % MOD;
            base = (1LL * base * 3) % MOD;
        }
        // print(f, 3);
    }
    cout << f[2] << '\n';
}

UPD:
小号变大号太真实了:

推荐阅读