2019年5月2日木曜日

実装方法の逆引き

実装でよくつまづいているポイントをメモ。

二乗するとnn以上になるような最小の自然数

n\lfloor \sqrt{n} \rfloor(isqrt n)で求まる。n\lceil \sqrt{n} \rceilには対応する関数がないが、やはりisqrtが使える:

(+ 1 (isqrt (- n 1)))

ただしn=0n=0の時に壊れているので注意。(SBCL x86-64では、速度が必要な場合にはsqrtを使ったほうがよい。)

2kn2^k \ge nであるような最小の整数kk

nNn \in {\mathbb N}が与えられたとき、2kn2^k \ge nであるような最小のkNk \in {\mathbb N}log2n\lceil \log_2 n \rceilと書ける。計算の際は

(integer-length (- n 1))

とするのがよい。同様に、2kn2^k \le nであるような最大のkNk \in {\mathbb N}、つまりlog2n\lfloor \log_2 n \rfloor

(- (integer-length n) 1)

で得られる。

ちなみに、nn以上であるような最小の22の累乗が欲しい場合は、(ash 1 (integer-length (- n 1)))だが、これはsb-int:power-of-two-ceilingとしてSBCLにあったりする。

ppで割るとqqあまる整数でxx以上/以下のもの

ppで割るとqqあまる整数でxx以上の最小のものはp(xq)/p+qp \lceil (x-q)/p \rceil + q。あるいは、位相の差を足すと考えてx+(qx)%px + (q-x) \% pのほうがわかりやすい?

ppで割るとqqあまる整数でxx以下の最大のものはp(xq)/p+qp \lfloor (x-q)/p \rfloor + q。同様に、x(xq)%px - (x-q) \% pでもよい。

nn以下のkkの正の倍数の総数

nn以下のkkの正の倍数の総数はn/k\lfloor n/k \rfloorである。

nn以下の、kkを法としてa{0,1,...,k1}a \in \{0,1,...,k−1 \}と合同な正の整数の数は

n+(ka)%kk \Bigl \lfloor \frac{n+(k-a)\%k}{k} \Bigr \rfloor

がいちばんシンプル? (ただし、%\%はあまりを求める演算とする。)非負整数を数える場合は%k\%kをしなければよい。

nn以下のkkの正の倍数の総和

nn以下のkkの正の倍数の総和は

12knk(1+nk) \frac{1}{2}k \Bigl \lfloor \frac{n}{k} \Bigr \rfloor \Bigl( 1+\Bigl \lfloor \frac{n}{k} \Bigr \rfloor \Bigr)

である。ただ、等差数列の和が両端の和に長さ/2を掛けたものであることがわかっていれば、確認しなくても問題なさそう。

自然数をできるだけ等分する

nn個のものをkk人に配る際にできるだけ等分になるようにすると、n/k+1\lfloor n/k \rfloor + 1個をn%kn \%k人に、n/k\lfloor n/k \rfloor個をkn%kk-n \%k人に配ることになる。ちょうど割り切れる時は大きいほうがが00個、小さいほうがkk個になっていることに気を付ける。

x/k\lfloor x/k \rfloorが一定であるような区間

n,kNn, k \in {\mathbb N}が与えられたとき、x/k=n/k\lfloor x/k \rfloor = \lfloor n/k \rfloorであるようなxxの区間は

[nkk,n+1kk1] \Bigl [ \Bigl \lfloor \frac{n}{k} \Bigr \rfloor \cdot k, \: \Bigl \lceil \frac{n+1}{k} \Bigr \rceil \cdot k -1\Bigr ]

であり、x/k=n/k\lceil x/k \rceil = \lceil n/k \rceilであるようなxxの区間は

[n1kk+1,nkk] \Bigl [ \Bigl \lfloor \frac{n-1}{k} \Bigr \rfloor \cdot k + 1, \: \Bigl \lceil \frac{n}{k} \Bigr \rceil \cdot k \Bigr ]

である。

TZCNT

tzcnt (trailing zero count) はLSBからの連続する0の数を数える命令である(例: 00101000300101000 \mapsto 3)。これを実現する効率の良い書き方は

(- (integer-length (logand x (- x))) 1)

だろう。SBCLならinteger-lengthはbsr命令に落ちる。 xが0の時に-1が返ることに注意。

integer-lengthの代わりにlogcountを使って

(logcount (- (logand x (- x)) 1))

と書いても良い。こちらはxxが0の時に0が返るという違いがある。logcountはSBCL (バージョン1.2.8以降)ではpopcnt命令になり、だいたい上と同程度の効率になる。

2の累乗かどうか判定する

(= 1 (logcount x))(zerop (logand x (- x 1)))など。後者は00でも真になることに注意。

実数rrより小さい/大きいもっとも近い整数

実数rrより小さいもっとも大きな整数はr1\lceil r \rceil -1r=a/b (b>0)r = a/b \ (b >0)なら(a1)/b\lfloor (a-1)/b \rfloorでもよい。

実数xxより大きいもっとも小さな整数はr+1\lfloor r \rfloor + 1r=a/b (b>0)r=a/b \ (b > 0)なら(a+1)/b\lceil (a+1)/b \rceilでもよい。

xx以上/以下の偶数/奇数

  • 偶数の場合だけ1足す。つまり、xx以上の奇数にする: (logior 1 x)
  • 奇数の場合だけ1足す。つまり、xx以上の偶数にする: (logand -2 (+ x 1))
  • 偶数の場合だけ1引く。つまり、xx以下の奇数にする: (logior 1 (- x 1))
  • 奇数の場合だけ1引く。つまり、xx以下の偶数にする: (logand -2 x)

区間の重なり

2つの半開区間[l1,r1),[l2,r2)[l_1, r_1), [l_2, r_2)が重なっている必要十分条件はl1<r2l_1 < r_2かつl2<r1l_2 < r_1であること。 2つの閉区間[l1,r1],[l2,r2][l_1, r_1], [l_2, r_2]が重なっている必要十分条件はl1r2l_1 \le r_2かつl2r1l_2 \le r_1であること。

区間の入れ替え

隣り合った区間[a,b],[b,c][a, b], [b, c]を長さを保つように入れ替えると、新しい切れ目の位置はa+cba+c-bになる。

lower_boundupper_bound

upper_boundlower_boundを使った添え字の処理のまとめ

列が昇順にソートされている場合

  • xx以上でもっとも左にある数の位置: lower_bound(x)
  • xx以下でもっとも右にある数の位置: upper_bound(x)-1
  • xxより大きいもっとも左にある数の位置:: upper_bound(x)
  • xxより小さいもっとも右にある数の位置: lower_bound(x)-1
  • xx以上の数の総数: N - lower_bound(x)
  • xx以下の数の総数: upper_bound(x)
  • xxより大きい数の総数: N - upper_bound(x)
  • xxより小さい数の総数: lower_bound(x)
  • ちょうどxxに等しい数の総数: upper_bound(x) - lower_bound(x)
  • xxに等しい数の存在する区間: [lower_bound(x), upper_bound(x))

整数列に対して有理数(割り算の結果)で評価する場合、floorceilで考えたいかもしれない。その場合の書き方:

  • xx以上でもっとも左にある数の位置: lower_bound(ceil(x))
  • xx以下でもっとも右にある数の位置: upper_bound(floor(x))-1
  • xxより大きいもっとも左にある数の位置:: upper_bound(floor(x))
  • xxより小さいもっとも右にある位置の数: lower_bound(ceil(x))-1
  • xx以上の数の総数: N - lower_bound(ceil(x))
  • xx以下の数の総数: upper_bound(floor(x))
  • xxより大きい数の総数: N - upper_bound(floor(x))
  • xxより小さい数の総数: lower_bound(ceil(x))
  • ちょうどxxに等しい数の総数: max(0, upper_bound(floor(x)) - lower_bound(ceil(x)))
  • xxに等しい数の存在する区間: [lower_bound(ceil(x)), upper_bound(floor(x)) ただし、無効な区間は空とする。
  • x[L,R]x \in [L, R]であるようなxxの総数: upper_bound(floor(R)) - lower_bound(ceil(L))
  • x[L,R)x \in [L, R)であるようなxxの総数: lower_bound(ceil(R)) - lower_bound(ceil(L))
  • x(L,R]x \in (L, R]であるようなxxの総数: upper_bound(floor(R)) - upper_bound(floor(L))
  • x(L,R)x \in (L, R)であるようなxxの総数: lower_bound(ceil(R)) - upper_bound(floor(L))

1-basedでkk番目に小さい数がxxということは、tt以下の数がkk個存在するような最小のttxxに等しいということなので、upper_bound(t)tt以下の数の総数を求めてupper_bound(t) >= kになるような最小のttを求めればよい。列全体をいくつかの(昇順の)部分列にわけて、upper_boundをそれぞれに対して計算して総和を取ることも可能。

1-basedでkk番目に大きい数がxxということはtt以上の数がkk個存在するような最大のttxxに等しいということなので、N - lower_bound(t)tt以上の数の総数を求めてN - lower_bound(t) >= kになるような最大のttを求めればよい。列全体をいくつかの(昇順の)部分列にわけて、N-lower_bound(t)をそれぞれに対して計算して総和を取ることも可能。

列が降順にソートされている場合

  • xx以下でもっとも左にある数の位置: lower_bound(x)
  • xx以上でもっとも右にある数の位置: upper_bound(x)-1
  • xxより小さいもっとも左にある数の位置: upper_bound(x)
  • xxより大きいもっとも右にある位置の数: lower_bound(x)-1
  • xx以下の数の総数: N - lower_bound(x)
  • xx以上の数の総数: upper_bound(x)
  • xxより小さい数の総数: N - upper_bound(x)
  • xxより大きい数の総数: lower_bound(x)
  • ちょうどxxに等しい数の総数: upper_bound(x) - lower_bound(x)
  • xxに等しい数の存在する区間: [lower_bound(x), upper_bound(x))

累積和上のDP

元のDP配列ff上のf[y]+=f[x]\operatorname{f}[y] \mathrel{{+}{=}} \operatorname{f}[x]という操作が、累積和SS上ではS[y]+=S[x]\operatorname{S}[y] \mathrel{{+}{=}} S[x], S[y+1]=S[x]\operatorname{S}[y+1] \mathrel{{-}{=}} S[x]に対応することを覚えておく。SSの先頭に00を追加せず、S[0]=f[0]S[0] = f[0]としたほうがわかりやすいことが多いと思う。

S[x]=f[0]+...+f[x]S[x] = f[0] + ... + f[x]とする。たとえばxx[x+l,x+r)[x+l, x+r)に足す、つまりf[x]=f[xl]+...+f[x(r1)]f[x] = f[x-l] + ... + f[x-(r-1)]という漸化式がなりたっている場合、もらうDPで考えると

S[0]=f[0]S[x]=S[x1]+f[x]=S[x1]+f[xl]+...f[x(r1)]=S[x1]+S[xl]S[xr] \begin{aligned} S[0] &= f[0] \\ S[x] & = S[x-1] + f[x] \\ & = S[x-1] + f[x-l] + ... f[x-(r-1)] \\ & = S[x-1] + S[x-l] - S[x-r] \end{aligned}

配るDPで実装する場合、S[x+l]+=S[x],S[x+r]=S[x],S[x+1]+=S[x]S[x + l] \mathrel{{+}{=}} S[x], S[x+r] \mathrel{{-}{=}} S[x], S[x+1] \mathrel{{+}{=}} S[x]と遷移すればいいことになる。

平方分割

バケットサイズをBBとする。区間[l,r)[l, r)を処理するときはbl=l/B,br=r/Bb_l = \lceil l/B \rceil, b_r = \lfloor r/B \rfloorとして

  • bl>brb_l > b_r:
    • 区間[l,r)[l, r)を処理
  • blbrb_l \le b_r:
    • 区間[l,blB)[l, b_lB)を処理
    • バケット列上の区間[bl,br)[b_l, b_r)を処理
    • 区間[brB,r)[b_rB, r)を処理

する。

引き算ができる場合は[0,l),[0,r)[0, l), [0, r)を処理するほうが簡単か。

巡回置換を戻す

巡回置換(p1,p2,...,pk)(p_1, p_2, ..., p_k)が配列上の部分列として現れている場合に、その部分を恒等に戻したい場合、p1p2p_1とp_2を交換してp2p_2p3p_3を交換して……pk1p_{k-1}pkp_{k}を交換する。一般に(p1p2...pk)=(pk1pk)...(p2p3)(p1p2)(p_1 p_2 ... p_k) = (p_{k-1}p_k)...(p_2p_3)(p_1p_2)(操作としては左から右に適用することに注意)なので、その逆をやればよいということになる。

配列上の操作としては、配列AAにおけるpip_iの初期位置をviv_iとして、A[v1]A[v_1]A[v2]A[v_2]をスワップしてA[v1]A[v3]A[v_1]とA[v_3]をスワップして……A[v1]A[v_1]A[vk]A[v_k]をスワップすればよい。

2次元配列の回転

高さHH, 幅WWの行列AAを反時計回りに回転する。回転後の行列をAA'とすると。添え字を0-basedとして

  • 90°の回転: A[i][j]:=A[W1j][i]A[i][j]:=A'[W−1−j][i]
  • 180°の回転: A[i][j]:=A[H1i][W1j]A[i][j]:=A'[H−1−i][W−1−j]
  • 270°の回転: A[i][j]:=A[j][H1i]A[i][j]:=A'[j][H−1−i]

転置

  • 左上-右下の対角線に関する転置: A[i][j]=A[j][i]A[i][j] = A'[j][i]
  • 左下-右上の対角線に関する転置: A[i][j]=A[W1i][H1j]A[i][j] = A'[W-1-i][H-1-j]

グリッド上の正方形

グリッド上の正方形S={0,1,...,N1}2S = \{0, 1, ..., N-1\}^2について。

SS上の点(a,b)(a, b)を通る傾き-1の直線x+y=a+bx+y = a+bSS上のマスをmin(a+b+1,2N(a+b+1))\min(a+b+1, 2N - (a+b+1))個含む。また、(a,b)(a,b)を通る傾き1の直線xy=abx-y=a-bSS上のマスをmin(N,ab)\min(N, |a-b|)個含む。

SS上の点(a,b)(a, b)を通る直線xy=abx-y=a-bは、直線x+y=ab,x+y=2N2abx+y=|a-b|, x+y=2N - 2 - |a-b|のそれぞれと正方形の端で交わる。

極座標と直交座標

complexに対する極座標はabsphaseで求まる。逆に極座標からcomplexを得たい場合は(* scale (cis radian))とする。