Address
Solution
- 非常有意思,思路非常棒的题目
- 发现一个很优秀的条件:对于 1≤i≤n , 1≤j≤m 满足 0≤xi,j≤m ,并且对于任意的 1≤i≤n , 1≤j<m 满足 xi,j<xi,j+1
- 这个条件告诉我们:这个数组每行都是 [0,m] 内的数扔掉其中一个之后再按顺序排列的
- 如 m=3 时 xi,j 一行内的合法方案只有 4 种( 1,2,3 , 0,2,3 , 0,1,3 , 0,1,2 )
- 于是我们把问题转化成了一个序列 ai∈[0,m](1≤i≤n) 的计数(即 ai 在第 i 行没有出现过)
- 而对于 xi,j<xi−1,j+1 的限制,分析一下可以发现,这等价于对于每个 1<i≤n 都有 ai≥ai−1−1
- 可以假设 a0=0 ,那么上面的条件对于 1≤i≤n 都满足
- 我们有了一个 DP 状态: f[i][j] 表示确定到前 i 个数,第 i 个数不超过 j 的方案数( j 的上界是 m+1 ,原因下面会解释)
- f[0][0]=1
- f[i][j]=f[i][j−1]+f[i−1][j+1]
- 意义是第 i 个数为 j 时,第 i−1 个数必须 ≤j+1 ,这个方案再加上第 i 个数不超过 j−1 的方案数
- 注意到如果 j 的上界为 m ,那么这个转移在 j<m 和 j=m 时会有区别,不便于接下来的讨论
- 答案为 f[n][m+1]
- 我们有了 O(nm) 的 DP 之后,我们考虑从 f[0][0] 转移到 f[n][m+1] 的过程
- 假设现在在 f[i][j]
- 执行第一种转移之后, j 会加一
- 执行第二种转移之后, i 加一, j 减一
- 显然这个过程中, j 不能小于 0 或大于 m+1
- 把从 f[0][0] 到 f[n][m+1] 的转移过程描述成一个 −1 和 1 构成的序列, −1 表示第一种转移, 1 表示第二种转移
- 那么经过前 k 次转移之后 j 的值,就等于这个序列的长度为 k 的前缀和
- 于是这个序列需要满足任意的前缀和都不能小于 0 或大于 m+1
- 把问题抽象到二维平面上,答案即为从 (0,0) 出发,只能往右或往上走,在 y=x 和 y=x+m+1 两条直线的夹缝之间(相当于不能触碰直线 y=x−1 和 y=x+m+2 )到达 (n,n+m+1) 的方案数
- 如果不考虑限制,那么方案数显然为 (n2n+m+1)
- 答案可以看成
- (n2n+m+1)−先越过下边界的方案数−先越过上边界的方案数
- 考虑先越过下边界的方案数。考虑一条 (0,0) 到 (n,n+m+1) 的,第一个越过的边界为下边界的路径,如果找到第一个越过下边界的点,将这个点之后的路径(下图中蓝色)关于直线 y=x−1 翻折(下图中紫色)
- 发现这其实就是 (0,0) 到 (n+m+2,n−1) ,第一个越过的边界为下边界的方案数
- 同理,先越过上边界到达 (n,n+m+1) 的方案数,就是先越过上边界到达 (n−1,n+m+2) 的方案数
- 定义两个函数
-
f(x,y) :从 (0,0) 开始第一个越过的边界为下边界,到达 (x,y) 的方案数
-
g(x,y) :从 (0,0) 开始第一个越过的边界为上边界,到达 (x,y) 的方案数
- 注意上面的 (x,y) 均满足 y−x<0 或 y−x>m+1
- 考虑 f(x,y) :由于 y−x<0 或 y−x>m+1 ,故 (0,0) 到 (x,y) 的路径必然越过边界
- 所以
- f(x,y)=(xx+y)−先越过上边界到达(x,y)的方案数
- 而对于一条先越过上边界到达 (x,y) 的路径,将这条路径在第一个越过 y=x+m+1 的位置之后的部分做一个关于直线 y=x+m+2 的翻折,就得到一条从 (0,0) 开始,第一个越过的边界为上边界,到达 (y−m−2,x+m+2) 的路径
- 于是
- f(x,y)=(xx+y)−g(y−m−2,x+m+2)
- 同理
- g(x,y)=(xx+y)−f(y+1,x−1)
- 如要计算一个 f(x,y) 或 g(x,y) ,则可以按照上面的两个式子进行递归求解
- 边界:当 x<0 或 y<0 时, f(x,y)=g(x,y)=0
- 可以证明,递归最多 O(n+m) 次可以到达边界
- 答案为
- (n2n+m+1)−f(n+m+2,n−1)−g(n−1,n+m+2)
- 复杂度 O(n+m)
Code
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
int f(int, int);
int g(int, int);
const int N = 3e6 + 5, ZZQ = 1e9 + 7;
int n, m, fac[N], inv[N];
int C(int n, int m)
{
return 1ll * fac[n] * inv[m] % ZZQ * inv[n - m] % ZZQ;
}
int f(int x, int y)
{
if (x < 0 || y < 0) return 0;
return (C(x + y, x) - g(y - m - 2, x + m + 2) + ZZQ) % ZZQ;
}
int g(int x, int y)
{
if (x < 0 || y < 0) return 0;
return (C(x + y, x) - f(y + 1, x - 1) + ZZQ) % ZZQ;
}
int main()
{
std::cin >> n >> m;
fac[0] = inv[0] = inv[1] = 1;
for (int i = 1; i <= (n << 1) + m + 1; i++)
fac[i] = 1ll * fac[i - 1] * i % ZZQ;
for (int i = 2; i <= (n << 1) + m + 1; i++)
inv[i] = 1ll * (ZZQ - ZZQ / i) * inv[ZZQ % i] % ZZQ;
for (int i = 2; i <= (n << 1) + m + 1; i++)
inv[i] = 1ll * inv[i] * inv[i - 1] % ZZQ;
std::cout << ((C((n << 1) + m + 1, n) - f(n + m + 2, n - 1)
+ ZZQ) % ZZQ - g(n - 1, n + m + 2) + ZZQ) % ZZQ;
return 0;
}