AT1202Contest_c binarydigit 题解

1. 题意简述

给定 H,W,MH,W,MH,W,M,对所有 1≤h≤H,  1≤w≤W1\le h\le H,\;1\le w\le W1≤h≤H,1≤w≤W 求:

h×wh\times wh×w 的 010101 矩阵中,满足

  • 所有行按字典序非降排列;
  • 所有列按字典序非降排列;

的矩阵个数,对 MMM 取模。

2. 关键观察

"按字典序排列"在这里是非严格递增,也就是说相邻行、相邻列可以相等。

如果先把完全相同的行、完全相同的列缩并,就会得到一个行、列都严格递增 的"核心矩阵"。

设核心矩阵有 rrr 行、ccc 列,则它对应到原来的 h×wh\times wh×w 矩阵的方案数为:

(h−1r−1)⋅(w−1c−1) \binom{h-1}{r-1}\cdot \binom{w-1}{c-1} (r−1h−1)⋅(c−1w−1)

因为要把 rrr 种不同的行扩展成 hhh 行(每种至少一行),以及把 ccc 种不同的列扩展成 www 列。

由于 H≤21H\le 21H≤21 很小,而 W≤100W\le 100W≤100 较大,代码采用:

  • 不缩并行,直接在行数上做 DP;
  • 缩并列 ,把不同的列看成若干"列等价类",最后再用组合数 (w−1c−1)\binom{w-1}{c-1}(c−1w−1) 展开。

3. DP 状态

cpp 复制代码
ll f[3][22][N][N];

f[s][i][j][k] 表示:

  • iii 行;
  • 列被离散化后,当前占据的列秩区间为 j,kj,kj,k(即 k−j+1k-j+1k−j+1 个不同的列);
  • sss 描述最左边那一列 的形态:
    • s=0:最左列全为 000;
    • s=1:最左列全为 111;
    • s=2:最左列从上到下是 0⋯01⋯10\cdots01\cdots10⋯01⋯1(混合)。

其中 j>1j>1j>1 只在转移过程中作为"占位"出现;真正用于统计答案时取 j=1j=1j=1。

4. 转移解释

4.1 初始化

cpp 复制代码
for(int i=1;i<=n;i++) 
    f[0][i][0][0]=1;

空列区间时,矩阵没有列,视为全 000 的合法情况。

4.2 最左列全 000 / 全 111

cpp 复制代码
Add(f[0][i][j][k],f[1][i][j][k-1]);
Add(f[0][i][j][k],f[2][i][j][k-1]);
for(int o=0;o<3;o++) Add(f[0][i][j][k],f[o][i][j-1][k-1]);

for(int o=0;o<3;o++) Add(f[1][i][j][k],f[o][i][j-1][k-1]);
  • f[0] 可以从 f[1]f[2] 的最右端再补一列全 000 得到;
  • 也可以把列区间整体右移一位(j-1,k-1 → j,k),仍然保持最左列全 000;
  • f[1] 同理,保持最左列全 111。

4.3 最左列混合

cpp 复制代码
for(int t=1;t<i;t++) {
    for(int x=j;x<=k;x++) {
        Add(f[2][i][j][k],f[0][t][j][x]*f[1][i-t][x][k]%MOD); 
    } 
}

最左列是 0⋯01⋯10\cdots01\cdots10⋯01⋯1 时,设前 ttt 行以 000 开头,后 i−ti-ti−t 行以 111 开头。

  • 上面的 ttt 行、列区间 j,xj,xj,x 构成一个最左列全 000 的子问题 f[0][t][j][x]
  • 下面的 i−ti-ti−t 行、列区间 x,kx,kx,k 构成一个最左列全 111 的子问题 f[1][i-t][x][k]
  • 枚举分界 ttt 与列分界 xxx 相乘累加,得到所有混合第一列的情况。

5. 统计答案

cpp 复制代码
ll getval(int n,int m) {
    ll res=0;
    for(int i=1;i<=m;i++) 
        for(int o=0;o<3;o++) 
            (res+=f[o][n][1][i]*C[m-1][i-1])%=MOD;
    return res;
}
  • iii 是核心矩阵中不同列的个数;
  • f[o][n][1][i] 表示行数为 nnn、列秩区间 1,i1,i1,i(即 iii 个不同列)的合法核心矩阵数;
  • (m−1i−1)\binom{m-1}{i-1}(i−1m−1) 是把 iii 个不同的列扩展成 mmm 列的方案数(每种列至少出现一次,在 iii 个列之间插入 m−im-im−i 个重复列)。

因为行已经在 DP 中直接枚举,所以这里不需要再乘 (h−1r−1)\binom{h-1}{r-1}(r−1h−1)。

6. 复杂度分析

状态数:3×H×W×W≈6.3×1053\times H\times W\times W\approx 6.3\times 10^53×H×W×W≈6.3×105。

转移主要耗时在

cpp 复制代码
for(t=1;t<i;t++)
    for(x=j;x<=k;x++)

这部分约为 O(H2W3)O(H^2W^3)O(H2W3),在 H≤21,  W≤100H\le 21,\;W\le 100H≤21,W≤100 时约 2×1082\times 10^82×108 次简单运算,配合较小的常数可以通过。

空间复杂度:O(HW2)O(HW^2)O(HW2),约几 MB。

7. 完整代码(带注释)

cpp 复制代码
#include<bits/stdc++.h>
#define fir first
#define se second
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;

template <typename ty>
inline void read(ty &x) {
    x=0; bool f=0; char c=getchar();
    while(c<'0'||c>'9') {
        if(c=='-') f=1;
        c=getchar();
    }
    while(c>='0'&&c<='9') {
        x=(x<<3)+(x<<1)+(c^48);
        c=getchar();
    }
    if(f) x=-x;
}
template <typename ty,typename ...T>
inline void read(ty &x,T &...te) {
    read(x); read(te...);
}

const int N=105;
ll f[3][22][N][N];   // f[s][i][j][k] 见上文状态定义
ll C[N][N];          // 组合数
int n,m,MOD;

inline void Add(ll &x,ll y) {
    x+=y;
    if(x>=MOD) x-=MOD;
}

// 求 h=n, w=m 时的答案
ll getval(int n,int m) {
    ll res=0;
    for(int i=1;i<=m;i++) 
        for(int o=0;o<3;o++) 
            (res+=f[o][n][1][i]*C[m-1][i-1])%=MOD;
    return res;
}

int main() {
    read(n,m,MOD);
    
    const int maxn=100;
    // 预处理组合数
    for(int i=0;i<=maxn;i++) {
        C[i][0]=1;
        for(int j=1;j<=i;j++) 
            C[i][j]=(C[i-1][j-1]+C[i-1][j])%MOD;
    }
    
    // 空列初始化
    for(int i=1;i<=n;i++) 
        f[0][i][0][0]=1;
    
    // DP
    for(int i=1;i<=n;i++) 
        for(int j=1;j<=m;j++) 
            for(int k=j;k<=m;k++) {
                
                // 最左列全 0
                Add(f[0][i][j][k],f[1][i][j][k-1]);
                Add(f[0][i][j][k],f[2][i][j][k-1]);
                for(int o=0;o<3;o++) Add(f[0][i][j][k],f[o][i][j-1][k-1]);
                
                // 最左列全 1
                for(int o=0;o<3;o++) Add(f[1][i][j][k],f[o][i][j-1][k-1]);
                
                // 最左列混合:枚举 0/1 行分界 t 与列分界 x
                for(int t=1;t<i;t++) {
                    for(int x=j;x<=k;x++) {
                        Add(f[2][i][j][k],f[0][t][j][x]*f[1][i-t][x][k]%MOD); 
                    } 
                }
                
            }
            
    // 输出
    for(int nn=1;nn<=n;nn++,puts("")) 
        for(int mm=1;mm<=m;mm++) 
            printf("%lld ",getval(nn,mm));
    
    return 0;
}

8. 总结

本题的关键在于:

  1. 把"行/列按字典序排列"理解成非严格递增
  2. 通过缩并相同的行、列,把原问题拆成"严格递增核心矩阵计数 + 组合数展开";
  3. 用三态 DP 描述最左列的形态(全 000 / 全 111 / 混合),混合情况按行、列分界分治转移。

这样就能在 O(H2W3)O(H^2W^3)O(H2W3) 的时间复杂度内求出所有 (h,w)(h,w)(h,w) 的答案。

相关推荐
会周易的程序员17 小时前
Libnodave S7 通信库:架构设计与实现解析
linux·c++·物联网·架构·c·s7·工业协议
jinyishu_17 小时前
C++ 多态完全指南:从基础语法到底层原理
开发语言·c++·程序人生·面试
atunet18 小时前
从算法优化到系统加速的多层级思考7
算法
Navigator_Z18 小时前
LeetCode //C - 1156. Swap For Longest Repeated Character Substring
c语言·算法·leetcode
Reart18 小时前
Leetcode 1143.最长公共子序列(720)
后端·算法
无相求码19 小时前
const vs #define:C语言常量定义的差异
c语言·算法
Android洋芋19 小时前
AI辅助C盘清理
c语言·开发语言·人工智能·ai辅助c盘清理
ScilogyHunter19 小时前
GCC/Clang 原始字符串详解
c语言·原始字符串
先吃饱再说19 小时前
LeetCode 226. 翻转二叉树
算法