51nod1486 大大走格子

实际来源:codeforces559C. Gerald and Giant Chess

基准时间限制:1 秒 空间限制:131072 KB 分值: 160 难度:6级算法题
有一个h行w列的棋盘,里面有一些格子是不能走的,只能向下或向右走,现在要求从左上角走到右下角的方案数。
Input
单组测试数据。
第一行有三个整数h, w, n(1 ≤ h, w ≤ 10^5, 1 ≤ n ≤ 2000),表示棋盘的行和列,还有不能走的格子的数目。
接下来n行描述格子,第i行有两个整数ri, ci (1 ≤ ri ≤ h, 1 ≤ ci ≤ w),表示格子所在的行和列。
输入保证起点和终点不会有不能走的格子。
Output
输出答案对1000000007取余的结果。
Input示例
3 4 2
2 2
2 3
Output示例
2

(不妨按照codeforces原题的说法,把这些不能走的格子起名叫黑点吧)
如果没有这些黑点(n=0)的话,这就是一个经典问题,我们可以考虑用DP解决 f[i][j]=f[i1][j]+f[i][j1] ,仔细观察这个转移方程,我们可以发现它与杨辉三角的方程类似,而我们把 f 数组构成的矩阵旋转45度的话可以发现它正是杨辉三角的一部分,实际上对于n行m列的矩阵(这里的n行m列是指有n*m个格点),从左上走到右下的方案数就是Cn1n+m2,为什么呢?n行m列,只能向下或向右走,那么我们一共要向下走n-1次,向右走m-1次,那么我们从左上走到右下的路径就可以表示为由n-1次向下和m-1次向右组成的序列,而这个序列的顺序是无所谓的,只要是n-1次向下和m-1次向右我们一定能且会走到右下角,所以这就是一个组合问题了,也就是 Cn1n+m2=Cm1n+m2
那么现在有了黑点该怎么办呢?考虑补集思想和容斥原理。那么最终答案= way[]way[]+way[]way[]... (其中 way[] 表示方案数)。而N=2000显然是不能枚举集合的,怎么办呢?我们就需要本题容斥的特殊性了:不同对象之间存在依赖性与阶段性。也就是说我们选出至少两个黑点是在先选出一个黑点的基础上,考虑这个黑点还能到达哪些黑点从而得到的,以此类推。既然有了阶段性我们就可以考虑DP了,首先按照x坐标和y坐标升序排序, f[i] 表示从左上角到第 i 个点中间不经过任何黑点的方案数,转移方程f[i]=calc(xi1,yi1)f[j]calc(xixj,yiyj)[j<i](其中 calc(n,m)=Cnn+m=Cmn+m ),我们可以把右下角视为最后一个黑点,那么答案就是 f[n+1] ,这是一个 O(n2) 的DP,时间内可以出解。

为什么这样是正确的呢?证明:
第一种想法:我们每次DP的时候其实是暴力枚举了一下第一次经过的黑点是哪个,这样的话就减去先从左上角走到这个点,然后再从这个点走到当前点的方案数就好了(因为只要经过一个黑点就是不合法的,所以后面随便走就好了);
第二种想法:我们可以把对 f[i] 的转移方程中 f[j] 展开成 j 对应的方程,同样地,我们可以继续展开下去(额,脑补一下吧),我们可以发现它正是++...的容斥的形式,也就是说我们在每个点上(每个阶段)都分别进行了容斥,以后进行容斥的时候充分利用了之前的结果;
第三种想法:我们可以考虑一条中途至少经过 k 个黑点的路径(也就是DP的时候被f数组计算了 k 次的一条路径)(也就是考虑贡献),再加上左上角和右下角一共k+2个点,中间被分为了 k+1 段路径,在DP的时候我们相当于不断的在每条路径分段的 calc 值上乘 1 ,最后到了右下角的时候前面的符号就是 (1)k ,也就正是我们要的容斥的结果(额,自行脑补一下吧。。。)。
Q.E.D.

(为了程序的可读性以及对 longlong 的使用懒得去修改果断牺牲了常数,时间效率,唉。。。)

#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=2100;
const int M=100100;
const ll mod=1000000007;
int h,w,n;ll f[N],jc[M<<1],inv[M<<1];
struct data{int x,y;}map[N];
inline ll power(ll a,int b)
{
    ll ans=1;
    for (;b;b>>=1,a=(a*a)%mod)
    if(b&1)ans=(ans*a)%mod;
    return ans;
}
#define C(n,m) (jc[n]%mod*inv[(n)-(m)]%mod*inv[m]%mod)
#define calc(n,m) (C(n+m,n)%mod)
bool cmp(data a,data b){return(a.x==b.x?a.y<b.y:a.x<b.x);}
#define x1 (map[i].x)
#define y1 (map[i].y)
#define x2 (map[j].x)
#define y2 (map[j].y)
int main()
{
    scanf("%d%d%d",&h,&w,&n);
    for (int i=1;i<=n;++i)
    scanf("%d%d",&map[i].x,&map[i].y);
    map[++n]=(data){h,w};jc[0]=inv[0]=1;
    for (int i=1;i<=h+w+2;++i)
    {
        jc[i]=(jc[i-1]*(ll)i)%mod;
        inv[i]=power(jc[i],mod-2)%mod;
    }
    sort(map+1,map+n+1,cmp);
    for (int i=1;i<=n;++i)
    {
        f[i]=calc(x1-1,y1-1);
        for (int j=1;j<i;++j)
        if(x2<=x1&&y2<=y1)
        f[i]=(f[i]-f[j]*calc(x1-x2,y1-y2)%mod+mod)%mod;
    }
    printf("%I64d\n",f[n]);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值