[WIP]ABCの高度典型を解いてうはうはしたい

謝罪

常体と敬体がごっちゃです。ごめんなさい。 エンタメとしてお楽しみください。

解法を書くのに、公式解説、公式解説放送を主に参考にしています。それだけで理解できなかったものは、他の方のブログを参考にしています。その際は、参考にしたブログのリンクを貼っています。

ABC212-G Power Pair

問題概要

素数PPが与えられ、xny(modP)x^n \equiv y \pmod{P}を満たすnnが存在する(x,y)(x,y)の組(0x,yP1)(0\leq x,y\leq P-1)の個数を求める問題。

素数PPに対する原始根rrが必ず存在するため、x=ra,y=rbx=r^a,y=r^bとか表現すると、 xny(modP)ranrb(modP)anb(modP1) x^n \equiv y \pmod{P} \Leftrightarrow r^{an} \equiv r^b \pmod{P} \Leftrightarrow an \equiv b \pmod{P-1} となるので(最後の変換はフェルマーの小定理より)、anb(modP1)an \equiv b \pmod{P-1}を満たす(a,b)(a,b)の組(1a,bP1)(1\leq a,b\leq P-1)を数えれば良い。

さて、ここからどう数えるかだが、

  • P1P-1aaの最大公約数が11であるようなaaを見つけてきたとき、n=1,2,P1n=1,2,\cdots P-1のときbbP1P-1通りの値を取る。
  • P1P-1aaの最大公約数が22であるようなaaを見つけてきたとき、n=1,2,P1n=1,2,\cdots P-1のときbb(P1)/2(P-1)/2通りの値を取る。

というように、P1P-1aaの最大公約数がggであるようなaaを見つけてきたとき、n=1,2,P1n=1,2,\cdots P-1のときbb(P1)/g(P-1)/g通りの値を取ることがわかる。

a=1,2,P1a=1,2,\cdots P-1のそれぞれに対してP1P-1との最大公約数を取るのはO(PlogP)O(P\log P)かかってしまうので、GCD(P1,a)GCD(P-1,a)の値がggになるようなaaの個数を数えることにする。これはggが大きい順に数えるとうまくいく(説明がめんどいのでコードを貼ってごまかす)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
mint ans = 1;
map<long,long> mp;
for(auto&i:f){ //fはP-1の約数(降順)
    // GCD(P-1, x) = iとなるxの個数を求める
    long n = P/i;
    for(auto&j:f){
        if(j<=i)continue;
        if(j%i==0){
            n -= mp[j];
        }
    }
    mint a = P/i;
    mint b = n;
    ans += a*b;
    mp[i] = n;
}

提出コード

感想

原始根って便利なんですね

ABC212-H Nim Counting

問題概要

長さKKの数列(A1,A2,,AK)(A_1,A_2,\cdots,A_K)が与えられ(1Ai2161\leq A_i\leq 2^{16})、ここから11個以上NN個以下の数を選び(重複可能)、それらの選んだ数のXORをとったとき、00ならないような選び方の個数を求める問題。

この問題ではxorの畳み込みという技術を使います。xorの演算子を\oplus、xor畳み込みの演算子を*とすると、xor畳み込みというのは、

A=(a1,a2,,an)B=(b1,b2,,bn)ci=xy=iaxby A = (a_1, a_2, \cdots, a_n) \\ B = (b_1, b_2, \cdots, b_n) \\ c_i = \sum_{x\oplus y = i} a_x b_y となるようなベクトルC=AB=(c1,c2,,cn)C = A * B = (c_1, c_2, \cdots, c_n)を求めることで、O(nlogn)O(n\log n)で行えます。すなわち、これでxor畳み込み後に添え字が0$以外の要素の総和を求めることでこの問題は解けそうです。

Hadamard変換

天下り的ですが、 H0=1Hk=(Ek1Ek1Ek1Ek1)(Hk0 0Hk) \begin{aligned} H_0 &= 1 \\ H_k &= \begin{pmatrix} E_{k-1} & E_{k-1} \\ E_{k-1} & -E_{k-1} \end{pmatrix} \begin{pmatrix} H_k & 0 \\\ 0 & H_k \end{pmatrix} \end{aligned} と行列HiH_iを定義します(EkE_kkk次の単位行列)。このとき、HkH_k2k2^k次の行列です。 HkHk=2kE2k H_k H_k = 2^k E_{2^k} が成り立ちますので、Hk1=2kHkH_k^{-1} = 2^{-k} H_kとなります。また、 長さ2k2^kのベクトルAABBに対して次が成り立ちます。 (HkA)(HkB)=Hk(AB) (H_k A) (H_k B) = H_k (A * B) 左辺はベクトルの各要素ごとの積を表しています。これを変形すると、 AB=2kHk((HkA)(HkB)) A * B = 2^{-k} H_k ((H_k A) (H_k B)) が成り立ちます。しかし、これは長さ2k2^kのベクトルと2k2^k次の正方行列の積の計算をする必要がありますので、n=2kn=2^kとして、結局O(n2)O(n^2)かかってしまいます。 しかし、この行列HkH_kとベクトルの積はうまいことに高速化ができます。

高速Walsh-Hadamard変換

HkH_kを変形します。 Hk=(Ek1Ek1Ek1Ek1)(Hk0 0Hk)=(Ek1Ek1Ek1Ek1)(Ek2Ek200Ek2Ek20000Ek2Ek200Ek2Ek2)(Hk20000Hk20000Hk20000Hk2)= \begin{aligned} H_k &= \begin{pmatrix} E_{k-1} & E_{k-1} \\ E_{k-1} & -E_{k-1} \end{pmatrix} \begin{pmatrix} H_k & 0 \\\ 0 & H_k \end{pmatrix} \\ &= \begin{pmatrix} E_{k-1} & E_{k-1} \\ E_{k-1} & -E_{k-1} \end{pmatrix} \begin{pmatrix} E_{k-2} & E_{k-2} & 0 & 0 \\ E_{k-2} & -E_{k-2} & 0 & 0 \\ 0 & 0 & E_{k-2} & E_{k-2} \\ 0 & 0 & E_{k-2} & -E_{k-2} \end{pmatrix} \begin{pmatrix} H_{k-2} & 0 & 0 & 0 \\ 0 & H_{k-2} & 0 & 0 \\ 0 & 0 & H_{k-2} & 0 \\ 0 & 0 & 0 & H_{k-2} \end{pmatrix} \\ &= \cdots \end{aligned} この行列の積への分解は高々O(logn)O(\log n)回で終わります(上記の変形をし続けると最後にH0H_0が対角上に並んだ行列、すなわち単位行列が現れます)。 そして、各分解された行列をよく見ると、各行で00ではない要素は必ず22つです。すなわち、この分解された行列単体とベクトルの積は、O(n)O(n)で行うことができ、行列はO(logn)O(\log n)個しかありませんので、結局O(nlogn)O(n\log n)で行うことができます。

ピンと来ないという私のために、n=8n=8のときのH3H_3を分解してみます。 H3=(E2E2E2E2)(E1E100E1E10000E1E100E1E1)(E0E0000000E0E000000000E0E0000000E0E000000000E0E0000000E0E000000000E0E0000000E0E0)(H000000000H000000000H000000000H000000000H000000000H000000000H000000000H0)=(1000100001000100001000100001000110001000010001000010001000010001)(1010000001010000101000000101000000001010000001010000101000000101)(1100000011000000001100000011000000001100000011000000001100000011) \begin{aligned} H_3 &= \begin{pmatrix} E_2 & E_2 \\ E_2 & -E_2 \end{pmatrix} \begin{pmatrix} E_1 & E_1 & 0 & 0 \\ E_1 & -E_1 & 0 & 0 \\ 0 & 0 & E_1 & E_1 \\ 0 & 0 & E_1 & -E_1 \end{pmatrix} \\& \begin{pmatrix} E_0 & E_0 & 0 & 0 & 0 & 0 & 0 & 0 \\ E_0 & -E_0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & E_0 & E_0 & 0 & 0 & 0 & 0 \\ 0 & 0 & E_0 & -E_0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & E_0 & E_0 & 0 & 0 \\ 0 & 0 & 0 & 0 & E_0 & -E_0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & E_0 & E_0 \\ 0 & 0 & 0 & 0 & 0 & 0 & E_0 & -E_0 \end{pmatrix} \begin{pmatrix} H_0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & H_0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & H_0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & H_0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & H_0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & H_0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & H_0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & H_0 \end{pmatrix} \\ &= \begin{pmatrix} 1 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 1 \\ 1 & 0 & 0 & 0 & -1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & -1 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & -1 \end{pmatrix} \begin{pmatrix} 1 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 1 & 0 & 0 & 0 & 0 \\ 1 & 0 & -1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & -1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 1 \\ 0 & 0 & 0 & 0 & 1 & 0 & -1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & -1 \end{pmatrix} \\& \begin{pmatrix} 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & -1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & -1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & -1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 1 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & -1 \end{pmatrix} \end{aligned} これを実装するのにも結構頭を使います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
template<class T> vector<T> Hadamard(const vector<T>&A,const int k){
    // 2^k次行列
    if((int)A.size()!=(1<<k)){
        cout<<"A size should be 2^k"<<endl;
        return vector<T>();
    }
    vector<T> res = A;
    int h = 1;
    for(int i=0;i<k;i++){
        vector<T> tmp(1<<k,0);
        for(int j=0;j<(1<<k);j+=h*2){
            for(int l=j;l<j+h;l++){
                tmp[l] = res[l] + res[l+h];
                tmp[l+h] = res[l] - res[l+h];
            }            
        }
        h <<= 1;
        res = tmp;
    }
    return res;
}

template<class T> vector<T> xor_convolution(const vector<T>&A,const vector<T>&B,const int k){
    // xor convolution
    // A,Bのサイズは2^k
    vector<T> a = Hadamard(A,k);
    vector<T> b = Hadamard(B,k);
    vector<T> res(1<<k);
    for(int i=0;i<(1<<k);i++){
        res[i] = a[i] * b[i];
    }
    res = Hadamard(res,k);
    return res; //これを2^kで割る必要がある
}

このコードは最後に得た値の各要素を2k2^kで割り算してやる必要があるので注意です。

問題に戻る

問題で求めたいのは、C=(c1,c2,,c216)C=(c_1,c_2,\cdots,c_{2^{16}})(cic_iii個の石がある山の個数)があって、 H(H(C))+H((H(C))2)+H((H(C))3)++H((H(C))N) H(H(C)) + H((H(C))^2) + H((H(C))^3) + \cdots + H((H(C))^N) です(CiC^iは各要素のii乗を取ることを意味します)。Hadamard変換は線形変換なので、次のように変形できます。D=H(C)D=H(C)とすると、 H(D+D2+D3++DN) H( D + D^2 + D^3 + \cdots + D^N) かなりスッキリしました。あとは、等比数列の和の公式を使って、括弧の中身を計算することができます。

等比数列の和の公式は di(diN1)di1 \frac{d_i(d_i^N-1)}{d_i-1} です。di=0,1d_i=0,1の場合はこのまま計算すると値がおかしくなるのでそこだけ別処理することに注意します。

提出コード

参考サイト

感想

これのAND版やOR版もあるらしい。考えた人天才。

ABC213-G Connectivity 2

問題概要

NN頂点MM辺の単純無向グラフGGで、00本以上の辺を取り除いて新しいグラフHHを作る。各k=2,3,,Nk=2,3,\cdots,Nに対して、頂点11kkHHで連結になるような辺の取り除き方の個数を求める問題。

言い換え

問題で求められている「頂点11kkが連結(ry」の個数をC(k)C(k)とします。また、次のようにf(S),g(S)f(S),g(S)を定義します。

  • f(S):=f(S) := SSを頂点集合とするGGの連結部分グラフの個数
  • g(S):=g(S) := SSを頂点集合とするHHの部分グラフの個数

すると、次のようにC(k)C(k)を表現できます。ちなみにVVGGの頂点集合です。 C(k)={1,k}SVf(S)g(VS) C(k) = \sum_{\lbrace 1,k \rbrace \subset S \subset V} f(S) g(V\setminus S) 全てのf(S),g(S)f(S),g(S)が既知ならば、これはO(N2N)O(N2^N)で計算できます。

g(S)の計算

これは簡単です。 辺の両端が頂点集合SSに含まれるような辺の個数をE(S)E(S)とすれば、2E(S)2^{E(S)}g(S)g(S)になります。 各頂点集合についてMM個の辺がそれぞれ含まれているかどうかを判定するのでO(M2N)O(M2^N)で計算できます。

f(S)の計算

問題はこちらです。f(S)=g(S)f(S)=g(S)-(頂点集合がSSであるような非連結なグラフの個数)です。g(S)g(S)は既知なので、後者を計算する必要があります。 連結でないグラフというのは、連結成分が22個以上あるグラフのことです。「この22個以上の連結成分」というのを、「11つの連結成分があって、残りの使ってない頂点たちを好きなようにしてもらう」という数え方をします。

SSに含まれる頂点を11つ取ってきて、これをvvとするとf(S)f(S)は次のようになります(このvvがないとカウントが重複してしまいます)。 f(S)=g(S)vTSf(T)g(ST) f(S) = g(S) - \sum_{ v \in T \subsetneq S} f(T) g(S\setminus T)

これをSSについていい感じの順番で計算していくことで、O(3N)O(3^N)で計算できます。

部分集合の部分集合の列挙

部分集合のiiの部分集合jjの列挙はO(3N)O(3^N)でできます。

1
2
3
4
5
6
for (int i = 0; i < (1 << N); i++) {
    for (int j = i; j >= 0; j--) {
        j &= i;
        // (i, j) は条件を満たす
    }
}

提出コード

感想

f(S),g(S)f(S),g(S)に分けてg(S)g(S)の計算方法考えるのむずくね??

ABC213-H Stroll

問題概要

問題

DPで解けそうな問題設定だが、時間が間に合わない。

DP解

ds,td_{s,t}ttキロメートル歩いて地点ssにいる通り数とすると次のように計算できる。 ds,t=(s,i,x)ds,tx×pi,tx d_{s,t} = \sum _{(s^\prime,i,x)} d_{s^\prime,t-x} \times p_{i,t-x}

ただし、シグマ記号は「ss^\primeからssに向かうii番目の長さxxの道」を全てのペアについて足し合わせることを意味する。 これをttの小さい方から計算すればO(MT2)O(MT^2)で計算できるがこれだと時間がかかりすぎる。

シンプルにN=2N=2のときを考えると、 d1,t=ud2,u×ptu d_{1,t} = \sum_u d_{2,u} \times p_{t-u} という式になって、畳み込みっぽい見た目をしている。実際にはd2,ud_{2,u}定数ならば畳み込みで計算できる。

分割統治FFT

定数ならば畳み込みができるということで、半分を事前に計算して定数にし、いい感じに再帰的に計算するとなんとO(Tlog2T)O(T\log^2T)で計算できてしまうのだ。言葉での説明が難しいので図を用意した。 図は単純に11次元のDPを分割統治FFTで解いたものである。

分割統治FFT

ただ、計算に使用する数値は全て確定していなければならないので、以下のような順番で計算する必要がある。

分割統治FFT(計算順序)

今回は、頂点ごとにDPの値を持つが、全ての辺について上記の図のような遷移があるので、各遷移を全ての辺ごとに行う。計算量はO(MTlog2T)O(MT\log^2T)となる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void DCFFT(int l,int r,vector<edge>&E,vector<vector<mint>>&p, vector<vector<mint>>&dp){
    int m = (l+r)/2;
    if(l+1==r)return ;
    DCFFT(l,m,E,p,dp);
    for(int i=0;i<(int)E.size();i++){
        auto [u,v] = E[i];
        // [m,r)を更新
        /*
        本来だったら
        for(int i=m;i<r;i++){
            dp[v][i] += sum_x (dp[u][i-x]*p[v][x]);
        }
        をやるが、FFTを使う
        */
        auto dp2u = vector<mint>( dp[u].begin()+l, dp[u].begin()+m );
        auto p2 = vector<mint>( p[i].begin(), p[i].begin()+r-l );
        auto dp3u = convolution(dp2u,p2);
        for(int j=m;j<r;j++){
            dp[v][j] += dp3u[j-l];
        }
    }
    DCFFT(m,r,E,p,dp);
}

提出コード

感想

添字をミスりまくってめっちゃ時間かかった。 地味に赤diff初AC。

ABC214-G Three Permutations

包除原理

条件が扱いづらいので、包除原理を使います。

  • f(x)f(x)を、xx個の相異なる添字を選んだときに、その添字でpi=rip_i=r_iまたはqi=riq_i=r_iとなるようなrir_iの割り当て方の個数とする。

すると、包除原理により、求める答えは N!+x=1N(1)xf(x)(Nx)! N! + \sum_{x=1}^N (-1)^x f(x) (N-x)! となります。(Nx)!(N-x)!は残りのNxN-x個の添字に対する割り当て方の個数です。

この数え方にすぐに納得しない私は具体例を考えてみました。

  • N=3,p=(1,2,3),q=(2,3,1)N=3, p=(1,2,3), q=(2,3,1)のとき
    • r=(3,1,2)r=(3,1,2)11通りだけであるが、これを上記の計算式で求める。
  • f(1)f(1)について
    • i=1i=1を被らせるとき、r1=1,2r_1=1,2のどちらかで22通り
      • (1,3,2),(1,2,3),(2,1,3),(2,3,1)(1,3,2),(1,2,3),(2,1,3),(2,3,1)44通りがある
    • i=2i=2を被らせるとき、r2=2,3r_2=2,3のどちらかで22通り
      • (2,1,3),(2,3,1),(3,1,2),(3,2,1)(2,1,3),(2,3,1),(3,1,2),(3,2,1)44通りがある
    • i=3i=3を被らせるとき、r3=3,1r_3=3,1のどちらかで22通り
      • (3,1,2),(3,2,1),(1,2,3),(1,3,2)(3,1,2),(3,2,1),(1,2,3),(1,3,2)44通りがある
    • f(1)=2+2+2f(1) = 2+2+2
    • ここで注目したいのが、例えば(1,2,3)(1,2,3)という順列はi=1i=1i=2i=2のパターンの両方に含まれているということである。ここで22回取り除かれている順列は、f(2)f(2)以降で調整されていくのが包除原理である。
  • f(2)f(2)について
    • i=1,2i=1,2を被らせるとき(1,2,3),(1,3,2),(2,3,1)(1,2,3),(1,3,2),(2,3,1)33通りがある
    • i=2,3i=2,3を被らせるとき(1,2,3),(3,2,1),(2,3,1)(1,2,3),(3,2,1),(2,3,1)33通りがある
    • i=3,1i=3,1を被らせるとき(1,2,3),(2,1,3),(2,3,1)(1,2,3),(2,1,3),(2,3,1)33通りがある
    • f(2)=9f(2) = 9
    • ここで、(1,2,3),(2,3,1)(1,2,3),(2,3,1)のパターンは逆に足しすぎていので、f(3)f(3)で調整する。
  • f(3)f(3)について
    • i=1,2,3i=1,2,3を被らせるとき(1,2,3),(2,3,1)(1,2,3),(2,3,1)22通りがある
    • f(3)=2f(3) = 2
  • よって、3!6×2!+9×1!2×0!=612+92=13! - 6\times 2! + 9\times 1! - 2\times 0! = 6 - 12 + 9 - 2 = 1となり、正しく計算できていることがわかる。

グラフに変換

グラフを使います。頂点をNN個用意して、頂点pip_iqiq_iを繋ぐ辺を張ります。

辺を張る

ここで注意するべきは、連結成分は必ずサイクルになります。なぜなら、p,qp,qはどちらも順列であるので、同じ数がちょうど22回出現する\Leftrightarrow頂点の次数が22であるからです。

「各辺は、その両端の頂点番号を使ってはいけない」ことを示します。 次の画像は被らせる添字とグラフの対応を表したものです(画像には「サイクル」とありますが、「連結成分」と書くべきでした)。

被らせる添字を選ぶ

元のグラフの連結成分ごとに、kk個の辺を選ぶ(=kk個の数を被らせる)方法の場合の数をそれぞれ計算すれば、DPによってf(x)f(x)を計算できます。 次の節ではそれぞれの連結成分で、kk個の辺を選ぶ方法の場合の数を計算する方法を説明します。

DPで計算

サイクル状の頂点と辺を、v1,e1,v2,e2,,vy,ey,v1v_1,e_1,v_2,e_2,\cdots,v_y,e_y,v_1と割り振ります。

  • DP[ii][jj] :=:= ii番目までの辺を使って、jj個の辺を選んだときの場合の数

しかし、これだと情報が不十分です。辺e1e_1を選ぶとv1,v2v_1,v_2のどちらかを選べます。e1e_1v2v_2を選んだ場合、e2e_2v3v_3を選ぶしかありません。逆に、e1e_1v1v_1を選んだか、e1e_1を選んでいない場合は、e2e_2v2,v3v_2,v_322つの選択肢から選ぶことができます。よって、この情報を追加します。

さらに、円環なので、一番初めの頂点v1v_1が使われたという情報によって、eye_yv1v_1が選べるかどうかが決まります。よって、この情報も追加します。

  • DP[ii][jj][kk][ll] :=:= ii番目までの辺を使って、jj個の辺を選んだとき、ii番目の辺がvi+1v_{i+1}を選んだ/選んでいない(k=0/1k=0/1)、v1v_1を使った/使っていない(l=0/1l=0/1)の場合の数

こちらのDPですが、遷移が多いので注意深く実装します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
mint dp[3001][3001][2][2];

vector<mint> f(int n){
    if(n==1){
        return vector<mint>{mint(1), mint(1)};
    }
    rep(i,3001)rep(j,3001)rep(k,2)rep(l,2)dp[i][j][k][l] = mint(0);
    // nはサイクルの長さ
    // dp[i][j][k][l] :=
    // i番目までの辺を見て、j個使ったとき、
    // k=0: 直前を使っていない k=1: 直前を使った
    // l=0: 0を使っていない l=1: 0を使った
    dp[0][0][0][0] = 1;
    for(int i=1;i<=n;i++){
        for(int j=0;j<=n;j++){
            if(i==1){
                // 使わない
                dp[i][j][0][0] += dp[i-1][j][0][0];
                if(j>0){
                    // 前を使う
                    dp[i][j][0][1] += dp[i-1][j-1][0][0];
                    // 後ろを使う
                    dp[i][j][1][0] += dp[i-1][j-1][0][0];
                }
            }else if(i==n){
                // 使わない
                dp[i][j][0][0] += dp[i-1][j][0][0];
                dp[i][j][0][0] += dp[i-1][j][1][0];
                dp[i][j][0][1] += dp[i-1][j][0][1];
                dp[i][j][0][1] += dp[i-1][j][1][1];
                if(j>0){
                    // 前を使う
                    dp[i][j][0][0] += dp[i-1][j-1][0][0];
                    dp[i][j][0][1] += dp[i-1][j-1][0][1];
                    // 後ろを使う
                    dp[i][j][1][0] += dp[i-1][j-1][0][0];
                    dp[i][j][1][0] += dp[i-1][j-1][1][0];
                }
            }else{
                rep(l,2){
                    // 使わない
                    dp[i][j][0][l] += dp[i-1][j][0][l];
                    dp[i][j][0][l] += dp[i-1][j][1][l];
                    if(j>0){
                        // 前を使う
                        dp[i][j][0][l] += dp[i-1][j-1][0][l];
                        // 後ろを使う
                        dp[i][j][1][l] += dp[i-1][j-1][0][l];
                        dp[i][j][1][l] += dp[i-1][j-1][1][l];
                    }
                }
            }
        }
    }
    vector<mint> res(n+1, mint(0));
    rep(j,n+1){
        rep(k,2){
            rep(l,2){
                res[j] += dp[n][j][k][l];
            }
        }
    }
    return res;
}

提出コード

ABC214-H Collecting

問題概要

問題リンク

KK人で分担して有向グラフ上の落とし物をできるだけ多く拾う問題。

最小費用流問題

強連結成分にある落とし物はすべて取るのが最適なので、強連結成分一つをまとめて1つの頂点として考えた新しいグラフを作ります。そのグラフはDAGになります。(このとき、頂点11から辿ることができない頂点とその頂点から出てる辺は無視します)。

サンプル11の場合、次のようなグラフを作りSSからTTK=2K=2だけ流せば最小コストの符号を取ったものが答えとなる。

最小費用流

しかし、負辺があるので、(私が知っている)アルゴリズムが適用できない。

負辺を取り除いたものがこちらです。

負辺を取り除く

SからTまでグラフにそって適当なルートを通ったときに、通った辺のコストが「全体の落とし物のうち拾えなかった個数」を示しています。

このようなグラフは、強連結成分分解後にMM頂点にしてトポロジカル順に頂点番号をつけたグラフについて、次のような操作によって機械的に構築できます。

  • 各頂点uuごとにiui_uouo_uを用意して、iui_uからouo_u(1,0)(1,0)(,Xu)(\infty,X_u)の辺を張る。XuX_u
  • (u,v)(u,v)に対して、ouo_uからivi_v(,Xu+1+Xu+2++Xv1)(\infty, X_{u+1}+X_{u+2}+\cdots+X_{v-1})の辺を張る。
  • SSからi1i_1(K,0)(K,0)の辺を張る。
  • 各頂点uuごとに、ouo_uからTT(,Xu+1+Xu+2++XM)(\infty,X_{u+1}+X_{u+2}+\cdots +X_M)の辺を張る。

各頂点について容量が11の辺があるのは、一人だけ「XuX_uを拾えなかった」ことを回避できるからです。 よって、求まったコストをKuXuK\sum_u X_uから引けば答えになります。

提出コード

参考サイト

感想

フローのこういうテクニックは覚えていきたい。

ABC215-G Colorful Candies 2

NN個のキャンディがあり、ii番目のキャンディの色はCiC_iである。 K=1,2,,NK=1,2,\cdots,Nについて、NN個のうちKK個のキャンディを選ぶ(同様に確からしい)とき、キャンディの種類数の期待値を求める問題。

式を作る

とりあえずKKを固定して考える。確率変数XiX_iを種類iiのキャンディを11以上選ぶとき11、選ばないとき00とすると、答えは次の式で求められる。 E[iXi]=iE[Xi] E\left[\sum_i X_i\right] = \sum_i E\left[X_i\right] 線形性で右辺に変形できる。その種類のキャンディがmm個あった場合に11つ以上選ぶ組み合わせはNCKNmCK_N \text{C} _K - _{N-m} \text{C} _K通りと二項係数で計算できる。

この計算方法だと、各KKについて最大NN種類の計算をすることになるので、O(N2)O(N^2)かかりそうだが、ちょうどxx個ある種類のキャンディをまとめて数え上げればO(N(N))O(N\sqrt(N))になる(詳しくはコード、計算量の節を参照)。

メインの実装自体はかなり簡潔。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(void){
    std::cin.tie(0)->sync_with_stdio(0);
    int N;cin>>N;
    Combination<mint> comb(N+1);
    map<int,int>mp;
    rep(i,N){
        int c;cin>>c;
        mp[c]++;
    }
    map<int,int> mp2;
    for(auto&[k,v]:mp){
        mp2[v]++; //v個あるキャンディの種類数
    }
    for(int k=1;k<=N;k++){
        mint ans = 0;
        for(auto&[m,a]:mp2){
            mint A = a;
            ans += (comb.com(N,k) - comb.com(N-m,k)) / comb.com(N,k) * A; 
        }
        cout << ans << endl;
    }
}

mapを使っているため定数倍が重め。

提出コード

計算量について

aia_iを種類iiの個数だとすると、種類数(キャンディが11個以上ある)がMM個、キャンディの総数がNN個のとき、次の条件たちを満たす。

  • a1+a2++aM=Na_1+a_2+\cdots+a_M=N
  • 1aiN1 \leq a_i \leq N

計算量は、aia_iの種類数をDDとすると、O(DN)O(DN)である。DDは高々O(N)O(\sqrt N)にしかならないというのがミソである。

なぜなら、DDをできるだけ大きくしようとすると、aia_iはできるだけ小さくするべきなので、 a1=1,a2=2,a_1 = 1, a_2 = 2, \cdotsと設定していくべきであり、1+2+3++D=O(D2)=O(N)1+2+3+\cdots+D = O(D^2) = O(N)となる。

感想

今回の記事の中ではかなり簡単に見えるが、XiX_iの定義を思いつくところは経験値が必要だと思った。

Licensed under CC BY-NC-SA 4.0
Built with Hugo
テーマ StackJimmy によって設計されています。