一、题目
二、解法
首先观察到一个条件 \(0\leq x_{i,j}\leq m\),结合 \(x_{i,j}<x_{i,j+1}\),我们可以用缺少的数来表示一行的状态。
再考虑限制 \(x_{i,j}<x_{i-1,j+1}\),设 \(a_i\) 为第 \(i\) 行缺少的数,手玩可以发现这个限制可以转化成 \(a_{i}+1\geq a_{i+1}\),显然可以设计一个 \(dp\) 来规划每一行缺少的数,设 \(dp[i][j]\) 表示第 \(i\) 行缺少的数是 \(j\),转移:
优化可以考虑转移路径,我们把这个 \(dp\) 放到坐标系上:
这个东西不是很好看啊,但是我们可以把行右移使得斜下的移动变成直下,然而第一列那个直下的转移会变成斜下,需要建虚点把这个东西化成直的,所以每一行就有 \(m+2\) 个点了:
然后把这个东西放在我们熟悉的网格图上:
那么问题变成了从 \((0,0)\) 走到点 \(T(n+m+1,n)\),且不碰到直线 \(A:y=x+1\) 和 \(B:y=x-(m+2)\) 的方案数,这么算的原因是你发现走到终点本身就包含了对最后一行状态求和的操作。
乱走的方案数是 \({2n+m+1\choose n}\),考虑去除不合法的方案。我们可以把终点按照 \(A\) 翻折得到 \(T_A'\),那么从起点走到 \(T_A'\) 就表示存在碰到 \(A\) 的路径方案,同理从起点走到 \(T_B'\) 表示存在碰到 \(B\) 的路径方案,我们把他们减掉。
然后发现 \(AB,BA\)(先按 \(A\) 翻折,再按 \(B\) 翻折,表示存在先碰到 \(A\),再碰到 \(B\) 的方案,反之亦然)这种要把他加回来,对于 \(ABA,BAB\) 要把它减回去\(...\)这就变成了一个容斥原理。
\((x,y)\) 延 \(y=x+c\) 翻折之后的坐标是 \((y-c,x+c)\),然后组合数算一算就行了。
三、总结
考虑转移路径时,可以把形式简单的 \(dp\) 放在网格图上转化成网格图计数问题。
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 3000005;
const int MOD = 1e9+7;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,x,y,ans,fac[M],inv[M];
void init(int n)
{
inv[0]=inv[1]=fac[0]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
for(int i=2;i<=n;i++) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
for(int i=2;i<=n;i++) inv[i]=inv[i-1]*inv[i]%MOD;
}
int C(int n,int m)
{
if(m<0 || n<m) return 0;
return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
void flip1(int &x,int &y) {swap(x,y);x--;y++;}
void flip2(int &x,int &y) {swap(x,y);x+=m+2;y-=m+2;}
int cal(int x,int y) {return C(x+y,y);}
signed main()
{
init(3e6);
n=read();m=read();x=n+m+1;y=n;
ans=cal(x,y);
while(x>=0 && y>=0)
{
flip1(x,y);ans-=cal(x,y);
flip2(x,y);ans+=cal(x,y);
ans=(ans%MOD+MOD)%MOD;
}
x=n+m+1;y=n;
while(x>=0 && y>=0)
{
flip2(x,y);ans-=cal(x,y);
flip1(x,y);ans+=cal(x,y);
ans=(ans%MOD+MOD)%MOD;
}
printf("%lld\n",ans);
}