题目描述
小明对搭积木非常感兴趣。他的积木都是同样大小的正立方体。
在搭积木时,小明选取 m m m 块积木作为地基,将他们在桌子上一字排开,中间不留空隙,并称其为第 0 0 0 层。
随后,小明可以在上面摆放第 1 1 1 层,第 2 2 2 层,……,最多摆放至第 n n n 层。摆放积木必须遵循三条规则:
规则 1 1 1:每块积木必须紧挨着放置在某一块积木的正上方,与其下一层的积木对齐;
规则 2 2 2:同一层中的积木必须连续摆放,中间不能留有空隙;
规则 3 3 3:小明不喜欢的位置不能放置积木。
其中,小明不喜欢的位置都被标在了图纸上。图纸共有
n
n
n 行,从下至上的每一行分别对应积木的第
1
1
1 层至第
n
n
n 层。每一行都有
m
m
m 个字符,字符可能是 .
或 X
,其中 X
表示这个位置是小明不喜欢的。
现在,小明想要知道,共有多少种放置积木的方案。他找到了参加蓝桥杯的你来帮他计算这个答案。
由于这个答案可能很大,你只需要回答这个答案对 1000000007 ( 1 0 9 + 7 ) 1000000007(10^9+7) 1000000007(109+7) 取模后的结果。
注意:地基上什么都不放,也算作是方案之一种。
n ≤ 100 n \le 100 n≤100, m ≤ 100 m \le 100 m≤100。
题解
很多题解都不是很清晰,所以准备写一篇详细一点的。
一开始没有注意规则二,即同一层之间摆放的格子必须是连续的。这个点很关键。
朴素的 DP 方程
状态表示
记 f [ i ] [ l ] [ r ] f[i][l][r] f[i][l][r] :摆了 i i i 层,最下面一层(第 i i i 层)摆放的区间为 [ l , r ] [l,r] [l,r] 的方案数量。
显然答案为所有的 f [ i ] [ l ] [ r ] f[i][l][r] f[i][l][r] 之和。
状态转移
有一些状态是不合法的,即会和“小明不喜欢的区域”冲突。当前 i i i 行第 l . . . r l...r l...r 列中有小明不喜欢的位置,那么 f [ i ] [ l ] [ r ] f[i][l][r] f[i][l][r] 一定为 0 0 0。可以预处理“不喜欢的区域”的前缀和,实现 O ( 1 ) O(1) O(1) 查询 l . . r l..r l..r 的前 i i i 行有没有“不喜欢的区域”。
对于合法的情况,有 f [ i ] [ l ] [ r ] = ∑ l ≤ x ≤ y ≤ r f [ i − 1 ] [ x ] [ y ] f[i][l][r]=\sum_{l\le x\le y\le r}f[i-1][x][y] f[i][l][r]=∑l≤x≤y≤rf[i−1][x][y]。这个也比较好理解,因为第 i i i 层必须完全“拖住”第 i − 1 i-1 i−1 层。
暴力转移时间复杂度为 O ( n m 4 ) O(nm^4) O(nm4)。
优化
技巧是利用二维前缀和
观察从第 i − 1 i-1 i−1 行向第 i i i 行转移的过程:
对所有的 [ l , r ] [l,r] [l,r],求 ∑ l ≤ x ≤ y ≤ r f [ i − 1 ] [ x ] [ y ] \sum_{l\le x\le y\le r}f[i-1][x][y] ∑l≤x≤y≤rf[i−1][x][y]。
首先当 y < x y<x y<x 的时候, f [ i − 1 ] [ x ] [ y ] f[i-1][x][y] f[i−1][x][y] 显然等于0,所以我们只需要限制 x ≤ r x\le r x≤r 和 y ≥ l y\ge l y≥l 即可。上面那个式子可以写成 ∑ x = l m ∑ y = 1 r f [ i − 1 ] [ x ] [ y ] \sum_{x =l}^m \sum_{ y=1}^r f[i-1][x][y] ∑x=lm∑y=1rf[i−1][x][y]。
如果我们把 ( x , y ) (x,y) (x,y) 看成平面上的一个点,那么所有满足要求的 1 ≤ x ≤ r 1\le x \le r 1≤x≤r 并且 l ≤ y ≤ r l\le y\le r l≤y≤r 的点 ( x , y ) (x,y) (x,y) 其实形成了一个矩形。左下角坐标为 ( 1 , l ) (1,l) (1,l),右上角坐标为 ( r , m ) (r,m) (r,m) 。
因此,从第 i − 1 i-1 i−1 行向第 i i i 行转移的过程,就是对所有满足要求的 ( l , r ) (l,r) (l,r) ,求以 ( 1 , l ) (1,l) (1,l) 为左下角、以 ( r , m ) (r,m) (r,m) 为右上角的矩形内的 f i − 1 f_{i-1} fi−1 值之和。我们可以在求完所有的 f i − 1 f_{i-1} fi−1 后,用 O ( m 2 ) O(m^2) O(m2) 的代价预处理一遍二维前缀和,再 O ( m 2 ) O(m^2) O(m2) 枚举所有的 ( l , r ) (l,r) (l,r) 并用 O ( 1 ) O(1) O(1) 的时间完成每次回答。
总的时间复杂度为 O ( n m 2 ) O(nm^2) O(nm2),空间复杂度可以通过滚动数组降到 O ( m 2 ) O(m^2) O(m2)。
代码就贴一下yxc的)
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 110, mod = 1e9 + 7;
int n, m;
LL f[N][N][N];
LL s[N][N], c[N][N];
// 前缀和
void get_presum(int i)
{
// 正方形的前缀和,都从 1 开始
for (int j = 1; j <= m; ++j) {
for (int k = 1; k <= m; ++k) {
s[j][k] = (s[j - 1][k] + s[j][k - 1] - s[j - 1][k - 1] + f[i][j][k]) % mod;
}
}
}
// 子矩阵
int submatrix(int x1, int y1, int x2, int y2)
{
return (s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]) % mod;
}
int main()
{
cin >> n >> m;
char str[N];
// 自下往上(倒置过来)
for (int i = n; i; --i) {
cin >> str + 1;
for (int j = 1; j <= m; ++j) {
c[i][j] = c[i][j - 1]+ (str[j] == 'X');
}
}
f[0][1][m] = 1;
get_presum(0);
LL ans = 1;
// dp 按行求解方案数
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
for (int k = j; k <= m; ++k) {
if (c[i][k] - c[i][j - 1] == 0) {
// 注意下标变换 (1, k) 和 (j, m)
f[i][j][k] = (f[i][j][k] + submatrix(1, k, j, m)) % mod;
ans = (ans + f[i][j][k]) % mod;
}
}
}
get_presum(i);
}
cout << (ans + mod) % mod << endl;
return 0;
}