かっつのメモ帳

主に競プロ 時々日記

IPSC 2010 L. Lovely stamps

問題のリンク

問題概要

$N$ 種類の1ドル切手と $ M $ 種類の2ドル切手が販売されている。丁度 $K$ ドル使うような切手の買い方の総数を素数 $ P $ で割った余りを求めよ。

ただしどの種類の切手も無限に存在するとして良い。

制約

  • $1 \leq N,M \leq 300$
  • $1 \leq K \leq 10^{12}$
  • $3 \leq P \leq 10^{6}$
  • $P$ は素数

前提知識

導入

数列 $ \{a _ {n} \} $ の第 $n$ 項 $ a _ {n} $ を $x ^ {n}$ の係数とする形式的冪級数

$$ F(x) = a _ {0} + a _ {1} x + a _ {2} x^{2} + \cdots + a _ {n} x^{n} + \cdots $$

を $ \{a _ {n} \} $ の(通常型)母関数と呼ぶ。

代表的な母関数の例として、数列 $\{1,1,\ldots , 1 , \ldots \}$ では

$$ 1 + x + x^{2} + \cdots + x ^ {n} + \cdots = \sum_{n=0}^{\infty} x^{n} = \frac{1}{1-x}$$

のように表される。

形式的冪級数は収束性の議論を無視する(詳しく勉強していないので適切な表現ではない可能性があります)ため、ここでは $ x $ は単なる変数に過ぎず、次のように様々な値への置換を考えることができる。

$x \rightarrow a x(a \neq 0)$ と置換することで、等比数列 $\{a^{0},a^{1},\ldots , a^{n} , \ldots \}$ では

$$ \sum_{n=0}^{\infty} (ax)^{n} = \frac{1}{1-ax}$$

同様に $x \rightarrow x^{k}(k \in \mathbb{N})$ への置換では

$$ \sum _ {n=0}^{\infty} x^{k n}=\frac{1}{1-x^{k}} $$

が得られる。

微分

形式的冪級数 $F=\sum _ {n=0}^{\infty} a_{n} x^{n}$ に対して、形式微分 $ F^{\prime} $ を次のように定義する。

$$ F^{\prime}:=\sum _ {n=1}^{\infty} n \cdot a _ {n} x^{n-1} $$

負の二項係数

$$ \sum _ {n=0}^{\infty}\left(\begin{array}{c} m+n-1 \\ n \end{array}\right) x^{n}=\frac{1}{(1-x)^{m}} $$

が成立する。

証明

組み合わせ的解釈でも導出ができるが、ここでは微分を用いた帰納法で示す。

$ m=d $ での成立を仮定し、$ m=d+1 $ について考える。

$$ \sum _ {n=0}^{\infty}\left(\begin{array}{c} d+n-1 \\ n \end{array}\right) x^{n}=\frac{1}{(1-x)^{d}} $$

の両辺を微分すると、

$$ \sum _ {n=1}^{\infty} \left(\begin{array}{c} d+n-1 \\ n \end{array}\right) n x^{n-1} = \sum _ {n=0}^{\infty} \left(\begin{array}{c} d+n \\ n+1 \end{array}\right) (n+1) x^{n} = d \cdot \frac{1}{(1-x)^{d+1}} $$

より、

$$ \begin{eqnarray} \frac{1}{(1-x)^{d+1}} &=& \sum _ {n=0}^{\infty} \left(\begin{array}{c} d+n \\ n+1 \end{array}\right) \frac{n+1}{d} x^{n} \\ &=& \sum _ {n=0}^{\infty} \left(\begin{array}{c} d+n \\ n \end{array}\right) x^{n} \end{eqnarray} $$

となり、成立が確認できる。


この関係は二項係数から導かれる等式

$$ \sum _ {n=0}^{\infty}\left(\begin{array}{c} m \\ n \end{array}\right) (-x)^{n}= (1-x)^{m} $$

に対応させてしばしば負の二項係数と呼ばれる。

また、左辺に登場する二項係数 $\tbinom{m+n-1}{n}$ は、$ m $ 種類のものから重複を許して $ n $ 個選びとる、重複組合せの総数に対応する。

解法

求める切手の買い方の総数は、

$$ \begin{eqnarray} \frac{1}{(1-x)^{n} (1- x^{2})^{m}} &=& \frac{(1+x) ^ {n}}{(1-x)^{n} (1- x^{2})^{m} (1+x) ^ {n}} \\ &=& \frac{(1+x) ^ {n}}{(1- x^{2})^{n+m}} \\ &=& \sum _ {a=0}^{n} \dbinom{n}{a} x^{a} \sum _ {b=0}^{\infty} \dbinom{ n + m + b - 1}{ n + m - 1} x^{2b} \end{eqnarray} $$

の $ x^{K} $ の係数に等しい。

$a$ を固定して考えると、$b = \frac{K-a}{2}$ となりこれが整数になるような $a$ に対して、

$$ \sum _ {a=0}^{n} \dbinom{n}{a} \dbinom{ n + m + \frac{K-a}{2} - 1}{ n + m - 1} $$

を求めれば良い。

$ K $ が大きく $ P $ が小さいことを利用して Lucasの定理を用いるか、$ O(N+M) $ のループを回すかのいずれかで二項係数を計算する。

manabitimes.jp

実装

#include <bits/stdc++.h>
using namespace std;
typedef long long int ll;

ll mod_pow(ll x,ll n,ll Mod){
    x%=Mod;
    ll res=1;
    while(n>0){
        if(n&1LL)res=res*x%Mod;
        x=x*x%Mod; n>>=1LL;
    }
    return res;
}

struct perm{
private:
    int sz;
    ll mod;
public:
    vector<ll> p,invp;
    perm(int n,ll Mod){
        sz=n+1;mod=Mod;
        p.resize(sz),invp.resize(sz);
        p[0]=1;
        for(int i=1;i<=sz-1;i++){
            p[i]=p[i-1]*i%mod;
        }
        invp[sz-1]=mod_pow(p[sz-1],mod-2,mod);
        for(int i=sz-2;i>=0;i--){
            invp[i]=invp[i+1]*(i+1)%mod;
        }
    }
    ll comb(ll x,ll y){
        if(x<y||y<0)return 0;
        return (p[x]*invp[x-y]%mod)*invp[y]%mod;
    }
    ll comb(ll x,ll y,ll p){ // Lucasの定理
        ll res=1;
        while(x>0 or y>0){
            (res*=comb(x%p,y%p))%=p;
            x/=p; y/=p;
        }
        return res;
    }
};

int main(){
    int n,m; cin >> n >> m;
    ll k,p; cin >> k >> p;
    perm P(p-1,p);
    ll res=0;
    for(int i=0;i<=n;i++){
        if((k-i)%2==0){
            ll tmp=P.comb(n,i,p)*P.comb(n+m+(k-i)/2-1,n+m-1,p)%p;
            (res+=tmp)%=p;
        }
    }
    cout << res << endl;
}

November Lunchtime 2020 (Div. 1) Fractions

問題のリンク

問題概要

自然数 $N$ が与えられる。次の条件を満たす自然数 $(i,j)$ の組の総数を求めよ。

  • $1 \leq i,j \leq N$
  • $\frac{i}{i+1} \cdot \frac{j+1}{j}$ と $ \frac{m}{m+1} $ が等しくなる自然数 $ m $ が存在する

制約

  • $1 \leq N \leq 10^{6}$

解法

条件を満たすペアについて常に $i \lt j$ が成立する。

$\frac{i}{i+1} \cdot \frac{j+1}{j} = \frac{m}{m+1}$ を $ m $ について解くと、

$$m = \frac{i(j+1)}{j-i}$$

が得られる。$k=j-i$ とおいて $ i $ を消去すると、

$$ \begin{eqnarray} m &=& \frac{(j-k)(j+1)}{k} \\ &=& \frac{j(j+1)}{k} - (j+1) \end{eqnarray} $$

ここで求めるべきは $1 \leq j \leq N$ のそれぞれに対して $ j $ 未満の $j(j+1)$ の約数の総数を足し合わせたもの、と問題を言い換えることができる。

ところで $ n $ の正の約数の個数を $d(n)$ と表すことにすると、$ j $ と $ j+1 $ は互いに素であるから、$d(j(j+1)) = d(j) \cdot d(j+1)$ が成立する。

$j(j+1)$ は平方数でないので、$ j $ 以下の約数の個数は $\frac{d(j(j+1))}{2}$ 個。ここから $j$ の分として 1 引いたものを足し合わせれば良い。

実装

#include <bits/stdc++.h>
using namespace std;
typedef long long int ll;

int main(){
    int n; cin >> n;
    vector<ll> d(n+2);
    for(int i=1;i<=n+1;i++){
        for(int j=i;j<=n+1;j+=i) d[j]++;
    }
    ll res=0;
    for(int j=1;j<=n;j++){
        res+=d[j]*d[j+1]/2-1;
    }
    printf("%lld\n",res);
}

Codeforces Round #371 (Div. 1) C-Sonya and Problem Wihtout a Legend

問題のリンク

問題概要

長さ $N$ の数列 $ A _ {i} $ が与えられる。数列の値を $1$ 変更するのにコスト $1$ がかかる。この数列を狭義単調増加列にするのに必要な最小コストを求めよ。

制約

  • $1 \leq N \leq 3000$
  • $1 \leq A _ {i} \leq 10^{9}$

解法

CFブログで ko_osaga 氏が言及している方針

界隈で Slope Trick と呼ばれるテクニックらしい。

始めに、数列 $ A _ {i} $ の値から予め $i$ を引いておくことで、狭義単調増加から広義単調増加を扱う問題に言い換えておく。

(余談だが、この言い換えを挟むと $ A _ {i} $ の取りうる値は $A _ {0}, \ldots ,A _ {N-1}$ に限られることを利用して O(N2) のDPで解くこともできる。実装例)


結論から述べると、この問題は次のようにして解決可能。

  • 前から数列を見てコスト計算をしていく。
    • 空集合、もしくは $A _ {i}$ が集合内の最大値以上の時はコスト変化無し。
    • それ以外の場合は、集合内の最大値を $X$ としてコストに $X- A _ {i}$ を加算し、$X$ を集合から取り除き、代わりに $A _ {i}$ を集合に加える。
  • $A _ {i}$ を集合に加える。

c++ ならば std::priority_queue 等を用いて $O(NlogN)$ で実装できる。

証明

クリックで展開

以下数列の添字は 1-indexed として扱う。

$f _ {i} (x)$ を $A _ {i} \leq x$ を満たしつつ $A[1 \ldots i]$ の範囲で広義単調増加列になるような最小コストとして定義する。これは次の数式で計算できる。

  • $f _ {i} (x) = \min _ {y \leq x} (| A _ {i} - y|)$ , if i=1
  • $f _ {i} (x) = \min _ {y \leq x} ( f _ {i-1} (y) + | A _ {i} - y|)$ , otherwise

式の形から明らかに、$x$ に対して $f _ {i} (x)$ は非増加列となる。

ここで、$f _ {i} (x)$ の傾きが変化する点、つまり $g _ {i} (x)= f _ {i} (x+1) - f _ {i}(x)$ として $g _ {i} (x) \neq g _ {i} (x+1)$ となるような $x$ の集合を考える。

$f _ {i} (x)$ が最小となるような最小の $x$ を $Opt(i)$ とおくと、この集合の最大値に一致し、最終的に求めるべき答えは $ f _ {N} (Opt(N))$ となる。

次に $f _ {i-1} (x)$ までの結果が求められていると仮定して、新たに $A _ {i}$ を加えた時の状態を考える。

$f _ {i} (x)$ は、絶対値関数 $h(x)=|A _ {i} -x|$ を $f _ {i-1} (x)$ に足し合わせて累積min を取った関数と見なすことができる。

ここで $A _ {i}$ の大きさに関して場合分けをする。


(1) $Opt(i-1) \leq A _ {i}$ の場合

$f _ {i} (x)$ において傾きが変化する点の集合は、$f _ {i-1} (x)$ における集合に $A _ {i}$ を加えたものになる。

傾きが変化する点の集合の最大値を考えると、$Opt(i)= A _ {i}$

従って、$f _ {i} (Opt(i))= f _ {i-1} (A _ {i}) + h(A _ {i}) = f _ {i-1} (Opt(i-1))$

(2) $Opt(i-1) \gt A _ {i}$ の場合

この場合、傾きの変化の前後に関わらず $Opt(i-1)$ において最小値を取り続ける。なぜなら、$Opt(i-1)$ は元々傾きが $-1,0$ の境界の点であるため。

よって $f _ {i} (x)$ の最小値の更新は簡単に可能で、具体的には $h(x)$ を加えたことによる差分だけを見れば良い。

つまり、$f _ {i} (Opt(i))= f _ {i-1} (Opt(i-1)) + (Opt(i-1) - A _ {i})$

続いて傾きの変化する点の集合について考える。$A _ {i}$ より左側の傾きは $-1$、右側の傾きは $+1$ され、傾きの最大値(=0)は不変であることを考慮すると、集合から $Opt(i-1)$ を1つ取り除き、$A _ {i}$ を2つ加える(前後で傾きが2変化するため)ことで辻褄が合う。


以上のことをまとめると上記で示した方針が得られる。

実装

#include <bits/stdc++.h>
using namespace std;
typedef long long int ll;

struct SlopeTrick{
    ll res;
    priority_queue<ll> pq;
    SlopeTrick():res(0),pq(){}
    void add(const ll x){
        if(pq.size() and pq.top()>x){
            res+=pq.top()-x;
            pq.pop(); pq.push(x);
        }
        pq.push(x);
    }
};

int main(){
    int n; cin >> n;
    SlopeTrick slope;
    for(int i=0;i<n;i++){
        int a; cin >> a;
        a-=i;
        slope.add(a);
    }
    printf("%lld\n",slope.res);
}

類題