AT1983 [AGC001E] BBQ Hard

這遍題解需要用到逆元,學習這個新知識用了兩三個小時。

非常有意思的一道題,可以幫助自己拓展思維。

首先,我們可以想像組合數的幾何意義。

組合數

從n + m 中選 n 個的組合數就是從(0,0)走到(n,m)座標的方案數。

題目所給的 n 是 ai + bi,m 是 aj + bj。

所以暴力的做法是,首先求得每一個點的組合數,然後求和。

但是對於每一個點的組合數他顯然是要(n)^2 的處理時間,因為 n m 都是和i j 有關的,顯然是過不了的。

那麼我現在考慮把上面的矩形平移到 n 和m 只和 j 有關係。

那為什麼正確性不變呢?

因為矩形的大小沒有變化,從(-ai,-bi)走到(aj,bj)和原本(0,0)出發是一樣的。

那我們是不是可以透過O( (max(a) + max( b) )^2 )的時間得出每個點的組合數?顯然a和b最大是2000,所以可以接受的。利用二維dp數組,然後好像過河卒一樣求出每個點的方案數,這裡要注意的事項就是,我們的下標的位置不可以是負數,所以我們拿a和b的最大數值作為中心點,然後把他當作(0,0)進行操作。就像下面一樣

最後我們透過循環題目要求的點的和就行了。

但是如果你沒有利用平移這個技巧,你會需要循環 i 和 j 來獲得題目要求的點,那這樣的話又要花掉 n^2 的時間。如果有了平移技巧,i 和 j 都會變成一樣,我們可以直接枚舉 i 就行了,那就下降成 n 了。

dp formula:

\\1for\;\;i\;(1\rightarrow 2S):\\\;\;\;\;\; 2\;\;\;\;\;for\;\;j\;(1\rightarrow 2S):\\ 3\;\;\;\;\;\;\;\;\;dp[i][j] = dp[i][j] + dp[i - 1][j] + dp[i][j - 1]

但是由於題目表明 i = j 的情況都不能算是組合數,所以我們要把每一個點減走重複數量,就是

\sum^{n}_{i = 1}(dp[S+a[i]][S+b[i]] - \binom{2a[i]+2b[i]}{2a[i]})

Time complexity: O( (max(a) + max( b) )^2  + n)

代碼

求出逆元的代碼,組合數上涉及除法取模。

const int Mod = 1e9 + 7;
int quickPow(int x, int n){
    long long res = 1;
    while(n){
        if(n&1) (res *= n)%=Mod;
        (n *= n)%=Mod;
        n>>=1;
    }
    return res;
}
int findInv(int a){
    return quickPow(a, Mod-2);
}

組合數求法

\binom{n}{r} %p= \frac{n!}{r!(n-r)!}%p=(fac[n]%p*inverse[r]%p*inverse[n-r]%p)%p

我們預處理出逆元和階乘的數值,之後就可以O(1)求出。

主代碼

#include<iostream>
#include<cstring>
using namespace std;
long long fac[16000], inv[16000], S = 2050, ans = 0, MOD = 1e9 + 7;
long long dp[4500][4500];
long long a[200005], b[200005];
long long quickPow(int x, int n){
    long long res = 1;
    while(n){
        if(n&1)(res =res % MOD *  x % MOD)%=MOD;
        (x = x % MOD * x % MOD)%=MOD;
        n>>=1;
    }
    return res;
}
int findInv(int a){
    return quickPow(a, MOD-2)%MOD;
}
int combin(int n, int r){
    return fac[n] * inv[n - r] % MOD *inv[r] % MOD;
}
void work(){
    int n;
    cin >> n;
    memset(dp, 0, sizeof(dp));
    for(int i = 1; i <= n; i++){
        cin >> a[i] >> b[i];
        dp[S - a[i]][S - b[i]]++;
    }
    inv[0] = findInv(fac[0]);
    fac[1] = 1;
    fac[0] = 1;
    for(long long i = 1; i <= 8000; i++){
        fac[i] = fac[i - 1] * i % MOD;
        inv[i] = findInv(fac[i]);
    }
    for(int i = 1; i <= S*2; i++){
        for(int j = 1; j <= S*2; j++){
            (dp[i][j] = dp[i][j] % MOD + dp[i - 1][j] % MOD + dp[i][j - 1] % MOD)%=MOD;
        }
    }
    for(int i = 1; i <= n; i++){
        (ans = ans % MOD + dp[S + a[i]][S + b[i]] % MOD) %= MOD;
        (ans -= combin(2*a[i] + 2*b[i], 2*a[i])) %= MOD;
        (ans += MOD) %= MOD;
    }
    cout << (ans * inv[2]) % MOD;
}
int main(){
    work();
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值