參考了:
[1].乘法逆元詳解【費馬小定理+擴展歐幾里得算法】
[2].逆元的三種求法 (費馬小定理,擴展歐幾里得,遞推求階乘逆元)
[3].【Codeforces Round #313 (Div. 1) C】 CodeForces 559C Gerald and Giant Chess
不得不說,這道題很難。
要掌握的知識點:
第一部分:得到dp狀態轉移方程
- 1.排列組合,從一個開始到達結束,一共C(m+n,n)種方法。
- 2.計數類dp。
- 3.用總方案數減去經過黑格子的方案個數,得到的就是答案。
- 4.但是經過黑格子的路徑可能經過很多個黑格子,導致重複計算。
- 5.於是可以設dp[i]表示從起點到黑點i的合法路徑個數,合法路徑是說這之前不經過其他黑點。
- 6.那麼dp[i]=(起點走到點i的路徑總數)-∑(dp[j]*(點j到點i的路徑總數),點j在點i的左上方)
第二部分:排序問題 - 1.保證計算點i時,所有在點i左上方的點都計算過。可以通過座標排序解決。
第三部分:如何設計C(m+n,n) - 0.乘法逆元
- 1.費馬小定理。
- 2.擴展歐幾里得算法。
- 3.階乘的乘法逆元遞推公式。
- ps:每個我都有實現,其中計算N!的逆元我用的是費馬小定理。擴展歐幾里得算法閒置了。
/**
* @author week
* @Title: Giant_Chess
* @ProjectName 19-0316
* @Description:
* @date 2019/6/811:09
*/
import java.util.*;
public class Main {
static int mod=1000000007;
//緩存n!的值
static long f[]=new long[200010];
//緩存n!階乘的乘法逆元
static long inv[]=new long[200010];
//dp表,含義是到達第i個黑點,有幾種方法
static long dp[]=new long[2010];
//保存黑節點
static class Node{
public int x;
public int y;
public Node(int x, int y) {
this.x = x;
this.y = y;
}
}
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
init(200009);
while (sc.hasNext()){
int h=sc.nextInt();
int w=sc.nextInt();
int n=sc.nextInt();
Node list[]=new Node[n+1];
for(int i=0;i<n;i++){
int x=sc.nextInt();
int y=sc.nextInt();
list[i]=new Node(x,y);
}
list[n]=new Node(h,w);
//按x軸優先排序
Arrays.sort(list, new Comparator<Node>() {
@Override
public int compare(Node o1, Node o2) {
if(o1.x!=o2.x) return o1.x-o2.x;
return o1.y-o2.y;
}
});
for (int i=0;i<=n;i++)
{
int x1=list[i].x-1;
int y1=list[i].y-1;
dp[i] = C(x1,y1) ;
for(int j = 0 ; j < i ; j++) {
if( list[j].x <= list[i].x && list[j].y <= list[i].y ) {
int x2 = x1 - list[j].x+1 ; int y2 = y1 - list[j].y+1 ;
dp[i] = (dp[i]-C(x2,y2)*dp[j]%mod)%mod ;
if( dp[i] <= 0 ) dp[i] = (dp[i]+mod)%mod;
}
}
}
System.out.println(dp[n]);
}
}
/**
* C(m+n,n)=(m+n)!/(m!*n!)
* @param m
* @param n
* @return
*/
public static long C(int m,int n){
if(n<0||m<0) return 0;
long ans=f[n+m]*inv[n]%mod*inv[m]%mod;
return ans;
}
/**
* 遞推公式:https://blog.csdn.net/qq_40861916/article/details/82928080
* 因爲是從後往前的,所以要先用費馬小定理算出來最大的
*/
public static void init(int N){
f[0]=1;
for(int i=1;i<=N;i++)
f[i]=f[i-1]*i%mod;
inv[0]=1;
inv[N]=Fermat(f[N],mod-2);
for(int i=N-1;i>0;i--)
inv[i]=inv[i+1]*(i+1)%mod;
}
/**
* 費馬小定理:當有兩數a,pa,p滿足gcd(a,p)=1gcd(a,p)=1時,則有ap≡a(mod p)ap≡a(mod p)。
* 變一下形:a⋅ap−2≡1(mod p)a⋅ap−2≡1(mod p)。是不是和上面的乘法逆元的定義是相似的?
* 所以,我們可以使用快速冪求出ap−2ap−2,即求出aa的逆元。
* @param a 求解的數
* @param b p-2
* @return 乘法逆元
*/
public static long Fermat(long a,int b){
long ret=1;
while (b>0){
if((b&1)==1){
ret=ret*a%mod;
}
a=a*a%mod;
b=b>>1;
}
return ret;
}
static class returnType{
public int ans;
public int x;
public int y;
public returnType(int ans, int x, int y) {
this.ans = ans;
this.x = x;
this.y = y;
}
}
/**
*擴展歐幾里得算法
* @param a
* @param b
* @return ans爲最小公約數,x%mod爲其一個逆元,y爲另外一個解,可以不關心
*公式遞推:https://blog.csdn.net/alxbaj/article/details/81121447
*/
public static returnType extendedEuclidean(int a,int b){
if(b==0){
return new returnType(a,1,0);
}
returnType type=extendedEuclidean(b,a%b);
return new returnType(type.ans,type.y,type.x-a/b*type.y);
}
}