题目描述
给定一个 H*W 的棋盘,棋盘上只有 N 个格子是黑色的,其他格子都是白色的。
在棋盘左上角有一个卒,每一步可以向右或向下移动一格,并且不能移动到黑色格子中。
求这个卒从左上角移动到右下角,一共有多少种路线。
输入格式
第一行包含三个整数H,W,N。
接下来N行,每行包含两个整数x,y,描述一个黑色格子位于x行y列。
数据保证左上角和右下角的格子都是白色的。
输出格式
输出一个整数表示结果对109+7取模后的值。
数据范围
1≤H,W≤105
1≤N≤2000
输入样例1:
3 4 2
2 2
2 3
输出样例1:
2
输入样例2:
100 100 3
15 16
16 15
99 88
输出样例2:
545732279
解题报告
题意理解
一个H×W的方格,现在你要从(1,1)走到(n,m),每一次可以往下走,或者往右边走,同时有些格子是禁忌格子,也就是你不可以走到那里去,现在要你统计,有多少个合法方案.
Hint:合法方案数要对109+7取模
算法选择
一道棋盘问题,不是搜索,就是动态规划,一般来说这种棋盘,往往就是这个套路.而这道题目显然根据合法方案数,和那个超大取模数,就可以让我们确定这道题目是计数类动态规划.
思路详解
计数类型的动态规划,不同于其他问题,它具有一个非常显著的特性,就是不能重叠,不能遗漏.这也决定了,计数类型的动态规划算法,与其他动态规划算法的一个区别,就是它和组合数学紧密相连.
一个题目的答案目标,决定着题目的性质.
最值问题:单调性or重叠性(不漏可重)
方案数问题:组合数学性or完美匹配集合性质(不重不漏).
(以上性质,请形象理解,毕竟都不是专业术语,而是笔者自己刷题的感悟)
这道题目既然最终的目标让我们求解方案数,那么显然我们就要明确一点,我们必须不重不漏.
而且这道题目,既然是和组合数学有关,那么我们不得不想到,这道题目的前世,也就是著名的一大棋盘模型.
从(1,1)走到(n,n)的方案数,为Cn2×n.
因为我们从(1,1)走到(n,n),那么显然我肯定是要走n步向右走,n步向左走,那么此时我们就可以认为,一组合法的方案就是,由一组01串构成,其中0表示向右走,1表示向下走,而且0和1的个数都是n个.
而我们本题和上面的经典模型相比,只有一点不同,那就是我们这道题目,它存在禁忌格子,我们不可以抵达那个格子.
综上所述,我们发现按照原题的合法方案数,其实就是所有的方案数-不合法的方案数.
即合法方案数=所有的方案数-不合法的方案数.
看到这一点,我们立即就会想到,这道题目是不是要用到容斥原理.
因此这道题目的算法就是.组合数求法(费马小定理+逆元)+容斥原理+动态规划=本题计数型动态规划.
具体算法都想到了,那么接下来就是最为精妙,也最为重要的一步,状态设计,决策转移.
因为对于一道动态规划题目而言,状态设计,既是第一步,也是仅次于决策转移的重要一步.
计算合法方案非常困难,那么我们不如计算不合法的方案数.这就是容斥原理最为核心的一个点,那就是反向思维,抛去难点,算容易点.
我们会发现禁忌(黑色)格子的数量最多也就2000个,相比于白色格子最多的1010,我们不用说,状态的设计,肯定得和黑色禁忌格子有关联
一个方案里面只要有黑色禁忌格子,那么绝对就是不合法的方案,所以说我们不妨,通过总方案减去不合法方案,来达到计算我们最终的合法方案数.
首先呢,我们可以将所有的黑色格子排序,排序的准则,就是行列坐标递增.而且我们可以认为,左上角是第0个黑色格子,右下角是N+1个黑色格子.
那么我们就可以设第i个黑色格子的横纵坐标为(Xi,Yi),那么F[i]表示为从左上角走到第i个黑色格子,而且途中不经过其他黑色格子的方案数
因此我们可以找出公式
F[i]=Cxi−1xi−1+yi−1−i−1∑j=0F[j]∗Cxi−xjxi−xj+yi−yj
其中xi≤xj,yi≤yj
一个数学公式,总是让人崩溃,那么我们不如将其转换成为中文理解.
F[i]=(1,1)到(xi,xj)的总方案数−前i−1黑格方案数∗黑格到(xi,yi)的方案数
然后这道题目的最终答案,显然就是我们的F[n+1]
然后一个重点就是,我们要预先预处理关于109+7的乘法逆元来计算.
代码解析
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=200010;
const int Mod=1e9+7;
int n,m,k;
long long f[N],jc[N],jcinv[N];
pair<int,int> date[N];
long long ksm(long long a,long long b,long long c)
{
long long ans=1%c;
while(b)
{
if (b&1)
ans=ans*a%c;
a=a*a%c;
b>>=1;
}
return ans;
}
long long C(int n,int m)
{
return (long long)jc[n]*jcinv[m]%Mod*jcinv[n-m]%Mod;
}
void init()
{
ios::sync_with_stdio(false);
cin>>n>>m>>k;
for(int i=1;i<=k;i++)
cin>>date[i].first>>date[i].second;
jc[0]=jcinv[0]=1;
for(int i=1;i<N;i++)
{
jc[i]=jc[i-1]*(long long)i%Mod;
jcinv[i]=jcinv[i-1]*(long long)ksm(i,Mod-2,Mod)%Mod;
}
sort(date+1,date+1+k);
date[k+1]=make_pair(n,m);
f[0]=1;
return ;
}
void work()
{
for(int i=1;i<=k+1;i++)
{
int x=date[i].first,y=date[i].second;
f[i]=C(x+y-2,x-1);
for(int j=1;j<i;j++)
{
int a=date[j].first,b=date[j].second;
if (a<=x && b<=y)
f[i]=(f[i]-f[j]*(long long)C(x-a+y-b,x-a))%Mod;
}
}
}
void out()
{
cout<<(f[k+1]+Mod)%Mod<<endl;
return ;
}
signed main()
{
init();
work();
out();
return 0;
}
tql
F[i]=(1,1) 到 (xi,xj) 的总方案数 − 前 i−1 黑格方案数 ∗ 黑格到 (xi,yi) 的方案数
这里写错了吧,应该是:
F[i]=(1,1) 到 (xi,yi) 的总方案数 − 前 i−1 黑格方案数 ∗ 黑格到 (xi,yi) 的方案数
其中 xi≤xj,yi≤yj
以及这里,应该是:
其中 xj≤xi,yj≤yi
10^{10} -> 1010
讲解十分清晰,把自己刷题的心得体会也含在了其中,谢谢