デラノイ数に関するメモ
デラノイ数って?
- (以降「縦」と表現)
- (以降「横」と表現)
- (以降「斜め」と表現)
漸化式
表現方法その1
を「斜めを使う回数」とする。
を固定すると,縦回,横回,斜め回を並び替えるので通り数は 。
これをすべての について足し合わせて
となる。
表現方法その2
「斜め」のことはおいておいて,個の「縦」と個の「横」を並び替えることを考える。
を「『縦横』と並ぶ回数」とする。
を固定すると,まず個の「縦」と個の「横」を並び替えたときにちょうど回「縦横」という連続部分列が現れる通り数は となる*1。
また,個の「縦横」それぞれについてそのまま進むか「斜め」に進むかを選ぶとすると選び方は 通りある。
これをすべての について足し合わせて
となる。
この2つ,本当にイコール?
としても一般性を失わない。このとき となる。
のをに置き換えてを適用すると
と書き換えられる*2。あとは
であることを示せればよい。ここで
なので
となる。最後に
なので結果的に
とかける。よって
で,つまり
となる。
なんでこんな記事書いたの?
Wikipedia にあまりにもサラッと書いてあって頭悩ませたのでメモです。
Floor Sum を理解したい
おことわり
この記事は厳密性を保証しません。
Floor Sum とは?
を高速に求めるやつ(ただしで,)。
どういうアルゴリズムを使えばいいのか
まず,, として , の状態を作り上げます。すると
となります。 と は簡単に求められるので, の部分をうまく求めればいいです。
のときは当然 となる*1ので,以下では の場合について考えます。
ここで, となるような の個数をごとに数え上げることを考えます。
は整数なので, と は同値であり,
よって となるような の個数は 個あります。
これを からまで足し合わせて
となります。ここで, という記号を導入すると
となります。
つまり,結局のところ
となります。ここで,
の部分は元の問題と同じ構造をしているので再帰的に解けば問題が解けます。
再帰関数を作る
とおきます。
するとさっきの式は
と書き直せます。
計算量
雑に見積もります。
は を呼び出すので,だいたいユークリッドの互除法と同じくらいの計算量になることが分かります。よって,計算量は で抑えられそうだということが分かります。
いかがでしたか?
間違っている部分などがあれば教えて下さい。
*1:の定義からです。
ABC231: G - Balls in Boxes
問題概要
から までの番号がついた 個の箱があり,箱 に入っているボールの数は である。 個の数から等確率で つ選んでボールを 足すという操作を 回 繰り返す。箱 に操作後に入っているボールの数を として の期待値はいくらか。
(問題文は https://atcoder.jp/contests/abc231/tasks/abc231_g に記載されている。)
制約
アイディア
あとで答えを で割ることにして,すべての操作における の和を求めることを考える。
まず重要なのは,各操作における の総和を求めることは,各操作を終えた後に 個の箱それぞれからボールを つずつ取り出す方法を数えるのと同じである。そこで,操作前に箱に入っていたボールを赤色に,操作で入れるボールを白色に塗ることにする。このとき,最後に選んだボールのうち赤色に塗られたものを取り出した箱の添字の集合を,白色に塗られたものを取り出した箱の添字の集合をとする。このときかつである。また,箱に入っている白色のボールの個数をとする。に関して以下が成り立つ。
この状況に限定した場合における「操作後に 個の箱それぞれからボールを つずつ取り出す方法」は である。また,このような状況になる場合の数は である。ところで,これらをかけ合わせると
となる。ここで,を固定した場合のすべての適切なの組み合わせについての総和をとする。が *1にのみ依存することを示す。
今, を
とおく。すると,はと書き直せる。また,であり,である。つまり,は,を満たすすべてのの組み合わせについてを求めその総和をとったものにをかけたものとなる。逆に,,を満たすすべてのの組み合わせについての総和を取るととなる。ところで,
より
となるが,のの係数がちょうどとなっている *2。のの係数を求めるにはを回微分してからを代入してで割ればよく,実際にやるととなる。よってなのでとなり,にのみ依存することが示された。
これによって
は
と書き換えられる。結局求めたいものはすべてのについてのこの総和である。ここではが同じものをまとめて計算することを考えると,が同じであるすべてのについての総和が求まればよく,これは簡単なのDPで可能である。
実装
まずを「個目までの数から個選ぶ際の選んだ数の積」の和としてこれを求める。初期状態はで,遷移は (ただしとする)となる。
また,も求めておく。, である。
そして を求め,最後にで割る。あるいは最初から を求めてもよいかもしれない。
コード
#include "bits/stdc++.h" using namespace std; const long long int MOD = 998244353LL; long long int dp[1001][1001],calc[1001],A[1000]; long long int pow_binary(long long int x, long long int y){ long long int ret = 1LL; while(y){ if(y%2){ ret *= x; ret %= MOD; } x *= x; x %= MOD; y /= 2LL; } return ret; } int main(void){ int N,i,j; long long int K,perK,V=1LL,ANS=0LL; cin >> N >> K; perK = pow_binary(N,MOD-2); for(i=0; i<N; ++i){ cin >> A[i]; A[i] %= MOD; } dp[0][0] = 1LL; calc[0] = 1; for(i=0; i<N; ++i){ calc[i+1] = (calc[i]*(K-i))%MOD; for(j=0; j<=i; ++j){ dp[i+1][j] += dp[i][j]; dp[i+1][j] %= MOD; dp[i+1][j+1] += (dp[i][j]*A[i])%MOD; dp[i+1][j+1] %= MOD; } } for(i=0; i<=N; ++i){ calc[i] *= dp[N][N-i]; calc[i] %= MOD; calc[i] *= V; calc[i] %= MOD; ANS += calc[i]; ANS %= MOD; V *= perK; V %= MOD; } cout << ANS << endl; return 0; }
感想
結構悩んで面白かった。分かる人からしたら基礎中の基礎なんだろうけど。
DDCC2020予選: E - Majority of Balls
問題概要
個のボールがあり,個は赤色で個は青色である。
個 のボールのうちの個を指定してその個のボールのうちどちらの色の方が多いか,という質問を 回まですることができる。
このとき, 個のボールの色をすべて当てよ。
(問題文は https://atcoder.jp/contests/ddcc2020-qual/tasks/ddcc2020_qual_e に記載されている。)
制約
- は奇数
- 回まで質問ができる
アイディア
とする。
ここで とすると,ボールの個のボールでも色のボールが多いことになってしまうので矛盾する。よって である。
このことから,となるがに存在する(もしに対してであるとするととなりと矛盾)。
このとき,なので,とするとに含まれる色のボールの個数は以上以下であり,よってに含まれる色のボールの個数は以上以下となる(ボールが色ならば色のボールの個数はつ増え,それ以外の場合は変わらないため)。
次に,とすると(ただしはではない方の色)とすると,に含まれる色のボールの個数は以上以下となるので,に含まれる色のボールの個数は以上以下となる(ボールが色ならば色のボールの個数はつ増え,それ以外の場合は変わらないため)。
以上から,に含まれる色のボールの個数は以上以下となり,つまりに含まれるのボールの個数は個,よって区間]のボールは赤色と青色のボールを同数含む。
区間]は両色のボールを同数ずつ含むので,この区間内のボールに関しては自身を除いた個のボールをそれぞれ質問で投げればよい。
さらに,とのボールの色は違うので(同じであればとなるので矛盾),] ]についても両色のボールを同数ずつ含むといえるので,この区間内のボールに関しても自身を除いた個のボールをそれぞれ質問で投げればよい。
これが基本方針である。
高速化
となる を探すところで二分法を使えばこの部分での質問は回程度におさまり,全体的な質問回数が回程度となる。
実装
素直に実装するだけ。
コード
#include "bits/stdc++.h" using namespace std; int N,RB='R'+'B'; string reply; char getreply(){ cin >> reply; return reply[0]; } void sendquestion(int s){ int i; cout << "?"; for(i=0; i<N; ++i){ cout << " " << s+i; } cout << endl; cout.flush(); } char c,ans[225]; int main(void){ int i,j,p,l,r,m; cin >> N; l = 1; r = N+1; sendquestion(1); c = getreply(); while(r-l>1){ m = (l+r)/2; sendquestion(m); if(c == getreply()){ l = m; }else{ r = m; } } p = l; for(i=p; i<=p+N; ++i){ cout << "?"; for(j=p; j<=p+N; ++j){ if(i==j){ continue; } cout << " " << j; } cout << endl; cout.flush(); ans[i-1] = RB-getreply(); } for(i=1; i<=N*2; ++i){ if(p<=i && i<=p+N){ continue; } cout << "?"; for(j=1; j<=p; ++j){ if(i==j){ continue; } cout << " " << j; } for(j=p+N; j<=N*2; ++j){ if(i==j){ continue; } cout << " " << j; } cout << endl; cout.flush(); ans[i-1] = RB-getreply(); } cout << "! "; for(i=0; i<N*2; ++i){ cout << ans[i]; } cout << endl; cout.flush(); return 0; }
感想
橙パフォうれしい
CODE FESTIVAL 2016 Grand Final: J - 123 Pairs
問題概要
から までの 個の数を つずつ 個のペアにするとき,差が のペアが 個,差が のペアが 個,差が のペアが 個あるようなペアの作り方は何通りあるか。
(問題文は https://atcoder.jp/contests/cf16-exhibition-final/tasks/cf16_exhibition_final_j に記載されている。)
制約
アイディア
まず,差が のペアと差が のペアは共に偶奇の異なる数同士がペアになるため,偶奇の一致する数同士がペアになることになる差が のペアは偶数個である必要がある。つまり の場合答えは となる。
以下 の場合のみを考え, とする。
実は,このようなペアの作り方は以下の 種類のペアの組み合わせの並び替えで構成される。
- 差が のペア つが組み合わさっている。 (例: 1 - 4, 2 - 5, 3 - 6 )
- 差が のペア つの間に差が のペアが つ挟まっている。(例: 1 - 4, 2 - 3 )
- 差が のペア つの間に差が のペアが 個以上挟まっている。(例: 1 - 3, 2 - 5, 4 - 7, 6 - 8 )
- 差が のペア単独 。(例: 1 - 2 )
差が であるような 個のペアのうち,タイプ とタイプ の構成に関わるペアの個数をそれぞれ とする。 このとき,タイプ ,タイプ,タイプ ,タイプのペアの組み合わせはそれぞれである。よってペアの組み合わせの並び替え方は であり, 個のペアのタイプ への割り振り方は, 個の石と 個の仕切りを並べることを考えて であるので で求められる。 あとは考えられる をすべて試せばよい。
実装
二項係数をメモ化再帰で実装すると楽かもしれない。
「 個の石と 個の仕切りを並べることを考えて」の部分で地味にバグらせやすいので注意。
コード
#include "bits/stdc++.h" using namespace std; typedef long long ll; const ll MOD = (ll)(1e9+7); ll memo[5050][5050]; bool done[5050][5050]; ll comb(int n, int k){ if(n < k){ return 0ll; } if(done[n][k]){ return memo[n][k]; } done[n][k] = true; if(k==0 || k==n){ return memo[n][k] = 1ll; } memo[n][k] = (comb(n-1,k-1)+comb(n-1,k))%MOD; return memo[n][k]; } int main(void){ int N,A,B,C,b,x,y,z,m; ll ans=0ll,tmp; cin >> N >> A >> B >> C; if(B&1){ cout << 0 << endl; return 0; } b = B>>1; for(x=0; x*3<=C; ++x){ m = min(A,C-x*3); for(y=0; y<=m; ++y){ z = C - x*3 - y; tmp = comb(A+b+x,x); tmp *= comb(A+b,b); tmp %= MOD; tmp *= comb(A,y); tmp %= MOD; if(b){ tmp *= comb(z+b-1,z); tmp %= MOD; }else if(z){ tmp = 0ll; } ans += tmp; ans %= MOD; } } cout << ans << endl; return 0; }
感想
急に解法が降ってきて嬉しかったので共有しておきます。
ABC138: F - Coincidence
問題概要
とが与えられる。このとき かつ を満たす整数の組の個数を求めよ。
(問題文は https://atcoder.jp/contests/abc138/tasks/abc138_f に記載されている。)
制約
アイディア
の条件下で が成り立つことが証明できる。よって, が成り立つときには必ず が成り立つ。また,このときをを割ったときの商はとなる。逆に,をを割ったときの商はであるときが成り立つため,条件「」は条件「かつをを割ったときの商が」と言い換えられ,さらにであるため条件「をを割ったときの商が」は と言い換えられ,最終的に「かつ」となる。よって,条件をまとめると以下の5つである。
そして,実はをあとは,上4つに関するフラグを持ちながらを保つように遷移を行いつつ地道にDPを行えばよい。
2019/08/19追記: が成り立つとき も成り立つため, に関するフラグは不必要です。コード例では に関するフラグを持たずに実装しています。
実装
を上から 桁目まで見てかつyの最後の桁がであるときの組み合わせの個数とする。ただし,はそれぞれ
- : が確定しているかどうか
- : が確定しているかどうか
- : が確定しているかどうか
- : が確定しているかどうか
を示すフラグである。
今見ている桁が上から 桁目であるとし, の上から 桁目をそれぞれ とし, に付け加える桁を とすると,
- が false でかつ のとき ではないことが確定するので NG
- が false でかつ のとき ではないことが確定するので NG
- が false でかつ のとき ではないことが確定するので NG
- が false でかつ のとき ではないことが確定するので NG
- かつ のとき ではないことが確定するので NG
これらのケースを除外したあとで
- : が true または ならば true
- : が true または ならば true
- : が true または ならば true
- : が true または ならば true
として, に を足せばよい。
計算量はであるが,定数が比較的重い。
2019/08/19追記: 先程のアイディアで追記したとおり,フラグ は不必要であるため下の実装例では持っていない。また,下の実装例ではフラグ をbitによって管理している。
コード
#include "bits/stdc++.h" using namespace std; typedef long long ll; const ll MOD = (ll)(1e9+7); ll dp[63][8][2]; int main(void){ int i,j,k,l,r,p,s,t; ll L,R,ans=0ll; dp[0][0][0] = 1ll; cin >> L >> R; for(i=0; i<60; ++i){ l = (!!(L&(1ll<<(59-i)))); r = (!!(R&(1ll<<(59-i)))); for(s=0; s<2; ++s){ for(t=0; t<2; ++t){ if(s==1 && t==0){ continue; } for(j=0; j<8; ++j){ for(p=0; p<2; ++p){ if((!(j&1)) && (l > s)){ continue; } if((!(j&2)) && (t > r)){ continue; } if((!(j&4)) && (p > s)){ continue; } k = 0; if((j&1) || (l < s)){ k += 1; } if((j&2) || (t < r)){ k += 2; } if((j&4) || (p < s)){ k += 4; } dp[i+1][k][t] += dp[i][j][p]; dp[i+1][k][t] %= MOD; } } } } } for(j=0; j<8; ++j){ for(p=0; p<2; ++p){ if(!(j&4)){ continue; } ans += dp[60][j][p]; ans %= MOD; } } cout << ans << endl; return 0; }
感想
コンテスト中に通せなくて少しショックだったけど自分で思いつけてよかった...
AGC033: C - Removing Coins
問題概要
頂点からなる木が与えられる。 番目の辺は と を結んでいる。
最初それぞれの頂点にコインが枚おいてある。高橋くんと青木くんはそれぞれコインが置いてある頂点 をつ選びその頂点に置いてあるコインをすべて取り,そのあとすべてのコインを に距離 だけ近づけるという操作を繰り返す。
操作ができなくなった人を負けとすると,お互いに最善を尽くしたときどちらが勝つかを求めよ。
(問題文は https://atcoder.jp/contests/agc033/tasks/agc033_c に記載されている。)
制約
- 与えられるグラフは木
アイディア
回の操作でコインは選んだ頂点に少しずつ引き寄せられるので,葉となる頂点からはコインは出ていくが入ってこない。つまり,回の操作はグラフから葉となる頂点をすべて消す(ただし選んだ頂点については葉であっても当然消されない)のと同値である。
ここで,木上の最長パスのうちのつ(このパスをとする)についてのみ考えることとする。この最長パスの端点をとするとかを選ぶとこのパスの長さは減り,以外の頂点を選んだ場合についてはこのパスの長さは減ることになる。
次に,上にない頂点について考える。葉ではない頂点を選ぶとき,すべての葉が消されるので,よりも長いパスができることはなく,のパスを減らすこととなる。また,のどちらかを選ぶときも同様にのうち選ばれなかった頂点とすべての葉が消されるので,よりも長いパスができることはなく,のパスを減らすこととなる。
さらに,上にない葉を消すことを考えると,もしもその葉がとは別の最長パスの端点であったとしたらそのパスを新たにとして考えることができ(つまりの長さがだけ減る),それ以外のときはより長いパスができることはないので結局のパスを減らすこととなる。
以上のことから,1回の操作はの長さをまたは減らすことと同値であることが分かる。
実装
木の最長パスを一般に直径という。直径の長さ(及びその長さを持つパス)はどの頂点でもよいので頂点を1つ選びとし,から最も遠い頂点のつをとして,からもっとも遠い頂点のつをとすると,とを端点とするパスの長さと一致する。とは簡単なBFSで求められるので,直径の長さはで求まる。
そして,直径の長さをもとに青木くんが勝つか高橋くんが勝つかは単純なメモ化再帰*1で求めることができる。この計算量はである。
よって全体的な計算量はである。
コード
#include <iostream> #include <vector> using namespace std; const int INF = (1<<30); vector<int> g[225816],q[2]; int dist[225816]; bool done[225816],memo[225816]; bool solve(int r){ if(done[r]){ return memo[r]; } done[r] = true; if(r==0){ memo[r] = true; }else if(r==1){ memo[r] = false; }else{ memo[r] = (!(solve(r-1)&&solve(r-2))); } return memo[r]; } int main(void){ int N,a,b,i,j,k; cin >> N; for(i=1; i<N; ++i){ cin >> a >> b; --a; --b; g[a].push_back(b); g[b].push_back(a); } a = 0; for(k=0; k<2; ++k){ fill(dist,dist+N,INF); q[k].push_back(a); dist[a] = 0; for(i=0; i<q[k].size(); ++i){ a = q[k][i]; for(j=0; j<g[a].size(); ++j){ b = g[a][j]; if(dist[b] > dist[a]+1){ dist[b] = dist[a]+1; q[k].push_back(b); } } } } cout << ((solve(dist[a]))?"First":"Second") << endl; return 0; }
感想
コンテスト中にを通したので喜んでいたら任意人類が通していて「うーん」となってしまった。問題自体はとても綺麗で好き。