[noip2018-提高] 填数游戏

GitHub跳转原题关系图返回列表

将复杂条件的路径字典序要求转化为局部状态的制约,通过打表或者递推找出 O(log m) 的通项公式。

OJ: roj

题目 ID: 3662

难度:提高+/省选-

标签:数学动态规划计数

创建: 2026-10-10 09:49

更新: 2026-10-10 09:49

形式化题目

给定一个 n×mn \times m 的矩阵,要在每个格子中填入 0 或 1。 对于任意两条从左上角 (0,0)(0,0) 到右下角 (n−1,m−1)(n-1,m-1) 的只能向下(D)或向右(R)走的路径 P1P_1 和 P2P_2, 如果路径字符串满足字典序 w(P1)>w(P2)w(P_1) > w(P_2),则要求它们经过的格子填的数字组成的字符串满足字典序 s(P1)≤s(P2)s(P_1) \le s(P_2)。 求满足要求的填数方案数,答案对 109+710^9+7 取模。

数据范围:1≤n≤81 \le n \le 8,1≤m≤1061 \le m \le 10^6。

正解

思路

根据题目条件 w(P1)>w(P2)  ⟹  s(P1)≤s(P2)w(P_1) > w(P_2) \implies s(P_1) \le s(P_2),可以推导出对于任意局部 2×22 \times 2 的矩阵,左下角格子的值必定 ≥\ge 右上角格子的值。也就是说,每条从左下到右上的斜线上的元素必须是非降的。

当两条路径在某一个格子开始分叉(例如 P1P_1 向右,P2P_2 向下)时,它们产生的后续子路径受到严格制约。进一步地,如果一条斜线上存在相邻两个格子填的数字相同,就会导致它们右下方的元素呈现出大面积必须完全对称或者完全相等的限制。

基于状态压缩动态规划,如果以列为单位转移,我们可以针对小范围的 nn 跑出准确答案。这题的一个著名结论是:由于行列具有对称性(即交换 nn 和 mm 不影响方案数),我们假设 n≤mn \le m。当 n≤8n \le 8 且 m>nm > n 时,方案数呈现明显的等比数列规律: 对于 n≥1,m>nn \ge 1, m > n,ans(n,m+1)=ans(n,m)×3ans(n, m+1) = ans(n, m) \times 3。

因此,我们只需要通过打表或者公式推导出边界情况 m=nm=n 和 m=n+1m=n+1 时的前几项,剩下的部分直接使用快速幂计算即可。 对于 1≤n≤81 \le n \le 8,我们可以通过打表给出 An=ans(n,n)A_n = ans(n, n) 和 Bn=ans(n,n+1)B_n = ans(n, n+1) 的值:

  • n=1n=1: A1=2A_1 = 2 且对任意 mm 有 ans(1,m)=2mans(1, m) = 2^m。
  • n=2n=2: A2=12,B2=36A_2 = 12, B_2 = 36
  • n=3n=3: A3=112,B3=336A_3 = 112, B_3 = 336
  • n=4n=4: A4=912,B4=2688A_4 = 912, B_4 = 2688
  • n=5n=5: A5=7136,B5=21312A_5 = 7136, B_5 = 21312
  • n=6n=6: A6=56768,B6=170112A_6 = 56768, B_6 = 170112
  • n=7n=7: A7=453504,B7=1360128A_7 = 453504, B_7 = 1360128
  • n=8n=8: A8=3626752,B8=10879488A_8 = 3626752, B_8 = 10879488

对于 m>nm > n 的情况,答案即为 Bn×3m−n−1(mod109+7)B_n \times 3^{m - n - 1} \pmod{10^9+7}。这种方法将复杂的插头 DP 简化为了时间复杂度 O(log⁡m)O(\log m) 的算法。

代码

python
def power(base, exp, mod):
    res = 1
    base %= mod
    while exp > 0:
        if exp % 2 == 1:
            res = (res * base) % mod
        base = (base * base) % mod
        exp //= 2
    return res

def solve():
    import sys
    input_data = sys.stdin.read().split()
    if not input_data:
        return
    n, m = map(int, input_data[:2])
    
    if n > m:
        n, m = m, n
        
    MOD = 1000000007
    
    if n == 1:
        print(power(2, m, MOD))
        return
        
    A = [0, 0, 12, 112, 912, 7136, 56768, 453504, 3626752]
    B = [0, 0, 36, 336, 2688, 21312, 170112, 1360128, 10879488]
    
    if n <= 8:
        if n == m:
            print(A[n])
        else:
            print((B[n] * power(3, m - n - 1, MOD)) % MOD)
    else:
        print(0)

if __name__ == '__main__':
    solve()
cpp
#include <iostream>

using namespace std;

typedef long long ll;

const ll MOD = 1000000007;

ll power(ll base, ll exp) {
    ll res = 1;
    base %= MOD;
    while (exp > 0) {
        if (exp % 2 == 1) res = (res * base) % MOD;
        base = (base * base) % MOD;
        exp /= 2;
    }
    return res;
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    ll n, m;
    if (!(cin >> n >> m)) return 0;

    if (n > m) swap(n, m);

    if (n == 1) {
        cout << power(2, m) << "\n";
        return 0;
    }

    ll A[] = {0, 0, 12, 112, 912, 7136, 56768, 453504, 3626752};
    ll B[] = {0, 0, 36, 336, 2688, 21312, 170112, 1360128, 10879488};

    if (n <= 8) {
        if (n == m) {
            cout << A[n] << "\n";
        } else {
            cout << (B[n] * power(3, m - n - 1)) % MOD << "\n";
        }
    } else {
        cout << 0 << "\n";
    }

    return 0;
}

复杂度

  • 时间复杂度: O(log⁡max⁡(n,m))O(\log \max(n, m))。算法的瓶颈仅仅在于一次底数为 2 或 3 的快速幂,非常高效。
  • 空间复杂度: O(1)O(1)。仅需要常数级的变量与预处理数组空间。

总结

这是一道非常经典的寻找性质和规律的题目。在考场上,即使不能用复杂的插头 DP(轮廓线 DP)解决 10610^6 级别的数据,依然可以通过编写暴力 DFS 打表观察 ans(n,n)ans(n, n) 和 ans(n,n+1)ans(n, n+1),以及 ans(n,m)ans(n, m) 在 m>nm > n 时每次乘以 3 的规律。利用这种规律性可以直接拿到满分,是数论/找规律算法中经常遇见的“缩减状态机”的一个经典体现。

★ 残余不确定性(T14 结论分层 + T28)

现象

本解依赖两张硬编码表:

cpp
ll A[] = {0, 0, 12, 112, 912, 7136, 56768, 453504, 3626752};    // 下标 0..8
ll B[] = {0, 0, 36, 336, 2688, 21312, 170112, 1360128, 10879488};
cpp
if (m <= n) cout << A[n] << "\n";
else        cout << (B[n] * power(3, m - n - 1)) % MOD << "\n";

★ expert 自己声明:「n >= 4 时 A 数组与 B 数组的具体常数值可能存在记忆偏差, 需要 worker 验证」—— 即这两张表是凭记忆写出的,不是推导出来的。

机械核实(父会话)

项 结果
数据覆盖的 n game1.in 首行 2 2 · game10.in 2 994052 · game20.in 8 951872 ⇒ n 覆盖 2…8
即表的下标 2..8 被实际用到 ✅
闸门结果 ✅ main.cpp PASS=20/20 · main.py 20/20

⇒ 表在覆盖范围内是正确的(否则不可能 20/20)。

★★ 为什么仍然要标注风险(T28 的用法)

  1. 20/20 只证明「在这些 n 上正确」 —— 不证明「表本身可推导」
  2. 若某天出现 n = 9(或更大的 n)的测试点,会数组越界(表只到下标 8)
  3. ★ 更根本的:「凭记忆打表」不是可复现的解法 —— 正确做法应是写一个暴力/搜素程序把表打出来,让表可复现、可验证 (先例 T69:任务里不要放「非必需的重活」;但这里「打表」正是必需的重活)

建议(留给后续 R3)

★ 用独立暴力(n ≤ 8 时直接枚举/搜索)重新生成 A/B 两张表, 与硬编码值逐项比对;再把生成过程写进 index.md。 这样表就从「记忆」变成「可复现的推导结果」。

本次交付的诚实声明

text
✅ 真实数据 20/20 逐字节通过(main.cpp 与 main.py 双通过)
⚠ A[n] / B[n] 两张表是【凭记忆写出】的,未做独立推导验证
⚠ 表只覆盖下标 0..8;n > 8 会越界