メモ

yukicoderでゆるふわgolf

yukicoder No.526 フィボナッチ数列の第N項をMで割った余りを求める

問題はこち
No.526 フィボナッチ数列の第N項をMで割った余りを求める - yukicoder

普段より丁寧めに解説。


一般的なフィボナッチ数列とはindexが1つずれていることに注意


・漸化式に従って再帰的に計算する
詳細は省くがO(φ^N)となりTLE

fib(n,m){
	if(n==1)return 0;
	if(n==2)return 1;
	return (fib(n-1,m)+fib(n-2,m))%m;
}
main(){
	int n,m;
	scanf("%d %d",&n,&m);
	printf("%d",fib(n,m));
}


・メモ化する
上のコードで何が問題かを考える。
例えばfib(10,m)を呼ぶと、その中でfib(9,m)とfib(8,m)が呼ばれる。
するとfib(9,m)の中でfib(8,m)とfib(7,m)が呼ばれ…となり、nが小さくなる毎にfib(n,m)が呼ばれる回数は指数関数的に大きくなってしまう。
そこで、「1度計算したことがある値はその結果を残しておき、2度目以降には計算しない」ようにすることを考える。
これがメモ化と呼ばれるテクニック

int memo[5000010];
fib(n,m){
	if(n==1)return 0;
	if(n==2)return 1;
	if(memo[n])return memo[n];
	memo[n]=(fib(n-1,m)+fib(n-2,m))%m;
	return memo[n];
}
main(){
	int n,m;
	scanf("%d %d",&n,&m);
	printf("%d",fib(n,m));
}

各nに対しfib(n,m)は高々1回しか計算されないので、O(N)で求めることができる


・DP
問題はこれで既に解けたわけだが、上のコードではmemo[5000010]という大きめの配列をとっており、メモリ効率が悪い。
例えばN=10^8でメモリ制限が256MBだとMLEになってしまう(int型で長さ10^8だと(4*10^8)B≒400MB以上必要)
これを解決するのがDPと呼ばれるテクニック。
よく使われる表現として「メモ化はトップダウン、DPはボトムアップ」というものがある。
今回の問題に関して言えばメモ化では「fib(10,m)を求めたい→fib(9,m)を求めたい→fib(8,m)を求めたい→……」と上から降りていったが、DPでは「…→fib(8,m)を求めた→fib(9,m)を求めた→fib(10,m)を求めた」というように下から昇っていく。
まずはわかりやすく、今まで通り大きな配列をとるDP

main(){
	int n,m,fib[5000010];
	fib[1]=0;
	fib[2]=1;
	scanf("%d%d",&n,&m);
	for(int i=3;i<=n;i++)fib[i]=(fib[i-1]+fib[i-2])%m;
	printf("%d",fib[n]);
}

ループの部分をよく見ると、fib[i]を求めるのにはfib[i-1]とfib[i-2]しか使っていない。
つまりそれより古い情報は捨ててしまって問題ないので、このことを使って次のように配列を使わずに書ける

main(){
	int n,m,fib1,fib2,temp;
	fib1=0;
	fib2=1;
	scanf("%d%d",&n,&m);
	for(int i=3;i<=n;i++){
		temp=(fib2+fib1)%m;
		fib1=fib2;
		fib2=temp;
	}
	printf("%d",fib2);
}

Q.どういうこと?
A.これみて
f:id:sugarknri:20170619101629g:plain


・メモ化再帰vsDP
じゃあメモ化再帰よりDPの方がいいのか!→そうとは限らない
例えば
a_1=a_2=1\\ a_n=a_{\lfloor 2\sqrt n\rfloor}+a_{\lfloor \sqrt n\rfloor}\ \ (n\geq 3)
という漸化式で与えられる数列の第N項を求めることを考えてみると、(Nが十分大きいときは)DPよりメモ化再帰の方がよい。
(なぜならNと2√Nの間の値は、a_Nを計算するために一度も使わないので)
この辺の話は難しいので興味がある人は各自調べること



---これ以降は中級者以上向け---


・行列累乗
「K項線形漸化式の第N項は行列累乗を用いることでO(K^3 logN)で求まる」というテクニックがある
a_n=p_1a_{n-1}+...+p_ka_{n-k}という漸化式で数式が与えられるとき、


\begin{pmatrix}
p_1 & \cdots & \cdots & p_k    \\
1      &        & 0      & 0      \\
       & \ddots &        & \vdots \\
0      &        & 1      & 0      
\end{pmatrix}
\begin{pmatrix}
a_{n-1}\\
\vdots\\
\vdots\\
a_{n-k}
\end{pmatrix}
=
\begin{pmatrix}
a_n\\
\vdots\\
\vdots\\
a_{n-k+1}
\end{pmatrix}
が任意のnに対して成立するので、このことを使って


\begin{pmatrix}
p_1 & \cdots & \cdots & p_k    \\
1      &        & 0      & 0      \\
       & \ddots &        & \vdots \\
0      &        & 1      & 0      
\end{pmatrix}
^{n-k}
\begin{pmatrix}
a_k \\
\vdots\\
\vdots\\
a_1
\end{pmatrix}
=
\begin{pmatrix}
a_n\\
\vdots\\
\vdots\\
a_{n-k+1}
\end{pmatrix}

と見ることができる。
(K項じゃなくてK+1項じゃん→オーダーを見るときには関係ないから許して)
行列同士の1回の掛け算にO(K^3)かかるので、繰り返し二乗法を使うことでO(K^3 logN)で計算することができる

さらに、この形の行列(コンパニオン行列)に限っては通称"きたまさ法"により(K^2 logN)で求めることができるらしい(まだ理解していない)
とはいえ今回はK=2なのであまり関係ない。

普通の行列累乗による実装を以下に示す

#define ll long long
ll fibm(ll n,int m){
	//0,1,1,1をn乗して2番目を返す
	//一般の行列累乗は面倒だったのでadhoc
	ll aa=0,bb=1,cc=1,dd=1;
	ll a=1,b=0,c=0,d=1;
	ll ta,tb,tc,td;
	while(n){
		if(n%2){
			ta=aa*a+bb*c;
			tb=aa*b+bb*d;
			tc=cc*a+dd*c;
			td=cc*b+dd*d;
			a=ta%m;b=tb%m;c=tc%m;d=td%m;
		}
		ta=aa*aa+bb*cc;
		tb=aa*bb+bb*dd;
		tc=cc*aa+dd*cc;
		td=cc*bb+dd*dd;
		aa=ta%m;bb=tb%m;cc=tc%m;dd=td%m;
		n/=2;
	}
	return b;
}

n,m;
main(){
	scanf("%d%d",&n,&m);
	printf("%d",fibm(n-1,m));
}


ゴルフはといえば、DP解をぎゅっとするだけ

long x,i,t,m;main(y){for(scanf("%d%d",&i,&m);--i;x=y,y=t%m)t=x+y;printf("%d",x);}

81B

yukicoder No.524 コインゲーム

問題はこちら
No.524 コインゲーム - yukicoder

Nimなので問題は「1~Nまでxorをとったものが0か否か判定せよ」と読み替えられる。(このことの証明は以前した:yukicoder No.2 素因数ゲーム - メモ
以下では「1~Nまでのxorをとったもの」をxor和と呼ぶことにする
・最下位bitについて
xor和の最下位bitをN=1のときから順に並べると1,1,0,0,…という長さ4の周期性を持つことがわかる
(次にxorするのが0or1、今の値が0or1、の4通りの状態しかないことから明らか)
よって xor和の最下位bitが0⇔N≡3,0 mod 4
・それ以上の位について
xor和の最下位bitを除いたものをN=1のときから順に並べると 0、非0、0、非0…という長さ2の周期性を持つことがわかる
これは偶数kに対し、kとk+1は最下位bitを除いて等しいことと、同じ値同士のxorは0になることからわかる
よって xor和の最下位bit以外が0⇔Nが奇数

この2つをあわせて、xor和が0⇔N≡3 mod 4がわかる

n;main(){putchar(~atoi(gets(&n))%4?79:88);}

43B
nをmainの引数とするとセグフォ

2017/08/12追記
putsの方が短かった

n;main(){puts(~atoi(gets(&n))%4?"O":"X");}

42B

yukicoder No.523 LED

問題はこち
No.523 LED - yukicoder

1~Nを2個ずつ並べる重複順列なので(2N)!/2^N
mod Pでの階乗やべき乗、逆元を計算する関数を持っているならこれをそのまま投げても良いでしょう

#define ll long long
ll pom(ll a,ll n,int m){ll x=1;for(a%=m;n;n/=2)n%2?x=x*a%m:0,a=a*a%m;return x;}
#define invp(a,p)pom(a,p-2,p)
ll factm(int n,int m){ll x=1;while(n)x=x*n--%m;return x;}

n,m=1000000007;
main(){
	scanf("%d",&n);
	printf("%d",factm(2*n,m)*invp(pom(2,n,m),m)%m);
}

少し式変形を考えるともう少し簡単になる
2N!=\prod_{k=1}^N (2k-1)2k なので \frac{2N!}{2^N}=\prod_{k=1}^N (2k-1)k
これを使えば単なるループで書ける

i,n,s,m=1000000007;
main(){
	scanf("%d",&n);
	s=1;
	for(i=1;i<=n;i++)s=1L*s*i%m*(2*i-1)%m;
	printf("%d",s);
}

無駄な1Lがなくなるようにぐっと睨んで

n,m=1e9+7;main(s){for(scanf("%d",&n);n;s=s*(2*n-1L)%m*n--%m);printf("%d",s);}

77B

yukicoder No.522 Make Test Cases(テストケースを作る)

問題はこち
No.522 Make Test Cases(テストケースを作る) - yukicoder

a,b,cの三重ループでやると間に合わないという罠がある
a,bを決めればcが自動的に決まるので二重ループで書ける
似たような話は前にNo.250 atetubouのzetubou - yukicoderの問題文で見た

a,b,c,n;
main(){
	scanf("%d",&n);
	for(a=1;a<n;a++)for(b=a;b<n;b++){
		c=n-a-b;
		if(c>=b)printf("%d %d %d\n",a,b,c);
	}
}

2つ目のループの継続条件をn-a-b>=bにすると、c>=bかどうかの分岐が不要になる
ということでループ圧縮などをささっと

i,j;main(n){for(scanf("%d",&n);i<n;i++)for(j=i;n-i-j>=j;j++)printf("%d %d %d\n",i,j,n-i-j);}
i,j;main(n){for(scanf("%d",&n);j=++i%n;)for(;n-i-j>=j;)printf("%d %d %d\n",i,j++,n-i-j);}
n,j;main(i){for(j=scanf("%d",&n);n-i-j>=j?printf("%d %d %d\n",i,j++,n-i-j):(j=++i%n););}
n,j;main(i){for(j=scanf("%d",&n);n-i-j<j?j=++i%n:printf("%d %d %d\n",i,j++,n-i-j););}

85B