デラノイ数に関するメモ
デラノイ数って?
(以降「縦」と表現)
(以降「横」と表現)
(以降「斜め」と表現)
漸化式
表現方法その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;
}
感想
コンテスト中にを通したので喜んでいたら任意人類が通していて「うーん」となってしまった。問題自体はとても綺麗で好き。