【2 月小记】Part 3: CROI-R3 比赛总结

CROI-R3 比赛总结

T1

题意

给你 \(n\) 个集合,其中第 \(i\) 个集合内每个元素均会产生 \(a_i\) 的费用。

\(m\) 个数,现在要将这些数全部放入这 \(n\) 个集合中。规定第 \(i\) 个数所在集合的元素个数不能超过 \(b_i\)

求最小的总费用,无解输出 \(-1\)

考点

贪心

简析

容易发现,如果用尽可能小的 \(a\) 去装尽可能大的 \(b\),一定是不劣的。可以感性理解为,尽量使得较小费用前面的系数更大,较大费用前面的系数更小,才能确保总和最小。有点类似排序不等式的思想。

然后按照刚才的思路模拟即可。当集合不够装了输出 \(-1\)

T2

题意

给你一个中缀表达式,其中运算符只有加法、减法和乘法。

现有 \(q\) 次询问,每次询问将表达式中某一个乘法运算符改为加法或减法运算符。

要求你对于每次询问,输出操作后表达式的值。

考点

模拟、线段树

特别练码力的一道题

简析

可以想象,刚开始这个表达式里面所有的运算符都是乘法,然后要把若干个乘法运算符改为加法或减法。这涉及到贡献的变化,但这个变化是优美的,易于讨论。

不妨将一堆乘法运算符连起来的东西称作一个块。每次将这个块里面的某个乘法运算符修改为加法或减法时,就相当于把这个块断掉,变成两个块。这时贡献会减去原块的贡献,加上或减去左块或右块的贡献。

我想到开俩线段树,一个维护区间乘积,一个维护乘法块。但好像做麻烦了。不过能 AC。

T3

题意

给你一个 \(2 \times n\) 的网格,需要填充以下五种砖块:

  • \(1 \times 1\) 的小砖块
  • 四种旋转的 L 型砖块(每种覆盖 \(3\) 个格子)

\(m\) 个格子已经被预先填充,不能再用砖块覆盖。要求计算填充剩余格子的方案数。结果对 \(10^9+7\) 取模。

考点

状压 DP、矩阵快速幂

简析

由于砖块最多跨越两列,我们可以按列处理,只需要关注当前列和下一列的情况。而且,填充第 \(i\) 列时,只需要知道第 \(i-1\) 列的填充状态和第 \(i\) 列的预先填充情况。

不妨定义 \(dp_{i, st}\) 表示处理到第 \(i\) 列,且第 \(i\) 列的填充状态为 \(st\) 时的方案数。其中 \(st\)\(2\) 位二进制数,第 0 位表示第一行是否已被填充,第 1 位表示第二行是否已被填充。

分类讨论四种状态的转移情况即可。细节请看:

// 我们用 fill[i][0 / 1] 表示第 i 列某行是否被占领。
// 初始化:第0列视为完全填充,作为起始状态。
dp[0][3] = 1;
for (int i = 1; i <= n; i++) {
    // 枚举前一列的所有可能状态
    for (int st0 = 0; st0 < 4; st0++) {
        if (dp[i-1][st0] == 0) continue;  // 无效状态
        // 接下来,计算当前列的填充刚需。need0: 第一行是否需要填充;need1: 第二行是否需要填充。使用这个变量可以很好地处理事先被占据的方格所带来的问题。
        bool need0 = (filled[i][0] == 0) and ((st0 & 1) == 0), need1 = (filled[i][1] == 0) and ((st0 & 2) == 0);
        // 情况1: 当前列不需要填充新格子
        if (not need0 and not need1) {
            // 全被填充
            dp[i][3] = (dp[i][3] + dp[i-1][st0]) % MOD;
        }
        // 情况2: 只需要填充第一行
        else if (need0 and not need1) {
            // 方式1: 用1×1砖块填充第一行
            dp[i][st0 | 1] = (dp[i][st0 | 1] + dp[i-1][st0]) % MOD;
            // 方式2: 用L型砖块覆盖(1,i), (1,i+1), (2,i+1)
            if (i < n and filled[i+1][0] == 0 and filled[i+1][1] == 0) {
                // 这种填充会使当前列完全填充,且影响下一列
                dp[i][st0 | 3] = (dp[i][st0 | 3] + dp[i-1][st0]) % MOD;
            }
        }
        // 情况3: 只需要填充第二行
        else if (not need0 and need1) {
            // 方式1: 用1×1砖块填充第二行
            dp[i][st0 | 2] = (dp[i][st0 | 2] + dp[i-1][st0]) % MOD;
            // 方式2: 用L型砖块覆盖(2,i), (1,i+1), (2,i+1)
            if (i < n and filled[i+1][0] == 0 and filled[i+1][1] == 0) {
                dp[i][st0 | 3] = (dp[i][st0 | 3] + dp[i-1][st0]) % MOD;
            }
        }
        // 情况4: 两行都需要填充
        else {
            // 方式1: 用两个1×1砖块
            dp[i][st0 | 3] = (dp[i][st0 | 3] + dp[i-1][st0]) % MOD;
            // 方式2和3: 用L型砖块,需要下一列可用
            if (i < n) {
                // 类型A: L型覆盖(1,i), (2,i), (1,i+1)
                if (filled[i+1][0] == 0) {
                    dp[i][st0 | 1] = (dp[i][st0 | 1] + dp[i-1][st0]) % MOD;
                }
                // 类型B: L型覆盖(1,i), (2,i), (2,i+1)
                if (filled[i+1][1] == 0) {
                    dp[i][st0 | 2] = (dp[i][st0 | 2] + dp[i-1][st0]) % MOD;
                }
                // 方式3: 1×1 + L型的组合(两种可能),需要下一列完全可用
                if (filled[i+1][0] == 0 and filled[i+1][1] == 0) {
                    dp[i][st0 | 3] = (dp[i][st0 | 3] + 2LL * dp[i-1][st0]) % MOD;
                }
            }
        }
    }
}

这种做法对于 \(n \le 10^5\) 是可行的,但对于 \(n \le 10^{18}\) 完全不可行。

接着我们注意到,在没有预先填充格子的连续区间内,状态转移是齐次的。这意味着:如果连续 \(k\) 列都没有预先填充的格子,那么它们的转移矩阵是相同的。

先设状态向量。容易知道,对于没有预先填充的列,存在一个 4×4 的转移矩阵。

对于一般情况,转移矩阵取决于:当前列的预先填充情况、下一列的预先填充情况、是否是最后一列。

可以定义一个函数 get_T(b0, b1, nb0, nb1, last)

  • b0, b1:当前列第一行、第二行是否已预先填充
  • nb0, nb1:下一列第一行、第二行是否已预先填充
  • last:当前列是否是最后一列

这个函数返回一个 4×4 矩阵,其中 mt[st1][st2] 表示从状态 st2 转移到状态 st1 的方案数。

对于没有任何预先填充的列,其无填充状态的转移矩阵 mt 为:

从 / 到 00 01 10 11
00 1 0 0 1
01 1 0 0 0
10 1 0 0 0
11 1 1 1 2

解释:

  • 从任何状态都可以转移到 00:用两个 1×1 填充当前列
  • 从状态 00 可以转移到 11:用 L 型砖块有两种方式
  • 从状态 00 可以转移到 01:用覆盖当前列和下一列的 L 型
  • 从状态 00 可以转移到 10:用另一种覆盖当前列和下一列的 L 型

所以我们需要分段处理,在处理关键列时,使用特殊的转移矩阵,在处理连续的无填充列时,使用矩阵快速幂加速。

posted @ 2026-02-08 18:00  L-Coding  阅读(2)  评论(0)    收藏  举报