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. 总结
本题的关键在于:
- 把"行/列按字典序排列"理解成非严格递增;
- 通过缩并相同的行、列,把原问题拆成"严格递增核心矩阵计数 + 组合数展开";
- 用三态 DP 描述最左列的形态(全 000 / 全 111 / 混合),混合情况按行、列分界分治转移。
这样就能在 O(H2W3)O(H^2W^3)O(H2W3) 的时间复杂度内求出所有 (h,w)(h,w)(h,w) 的答案。