2019年10月26日土曜日

yukicoder No.917 - No.917 Make One With GCD

yukicoder No.917 - No.917 Make One With GCD

AAに登場する素因数を(重複なしで)すべて集めてp1,p2,...,pkp_1, p_2, ..., p_kとする時、これらのどれも公約数とならないような部分列の数を求めればよい。包除を考えれば計算できそう。

正整数xxが与えられたとする。A1,A2,...,ANA_1, A_2, ..., A_Nのうちxxで割り切れるものの総数をf(x)f(x)とするとき、xxが公約数であるような部分列の数は(空の部分列を除いて)2f(x)12^{f(x)}-1である。したがって、求める数は包除原理に従って

(2f(1)1) (2f(p1)1)(2f(p2)1)...(2f(pk)1) +(2f(p1p2)1)+(2f(p1p3)1)+...+(2f(pk1pk)1) ... +(1)k(2f(p1p2...pk)1) (2^{f(1)}-1) \\ \ - (2^{f(p_1)}-1) - (2^{f(p_2)}-1) - ... - (2^{f(p_k)}-1) \\ \ + (2^{f(p_1p_2)}-1) + (2^{f(p_1p_3)}-1) + ... + (2^{f(p_{k-1}p_k)}-1) \\ \ - ... \ + (-1)^k(2^{f(p_1p_2...p_k)}-1)

のようになる。素数の数が奇数なら引いて偶数なら足せばよい。ただし、登場する素数の総数kkはだいぶ大きくなることがあるので、上の式の2k2^k項全部を計算していては間に合わない。10810^8を超えるものを枝刈りすれば間に合う。


これ、計算量がわからない。正整数AAと素数列のprefix{2,3,5,7,...,pk}\{2, 3, 5, 7, ..., p_k\}が与えられたとき、prefixの部分集合で総積がAAを超えないようなものが何通りあるか? という問題に帰着しそうだけど……


50
97132843 98363449 89581363 82864427 98796221 99147481 74352463 99477713 93924871 65541251 99070949 98476523 99270979 99661321 99880747 98045347 99838183 98904791 99582083 89757337 99752171 98440403 99331283 98037839 99472123 98862697 99813167 99238807 99743993 99408347 99727399 97821397 99594503 98248609 98207933 99677881 90900499 81927497 68034287 54143561 90426598 35950241 73138803 82613935 68388971 92946349 33984931 12780049 64097801 1062347

最悪っぽいケースを作ってみたらだいぶ時間が怪しかった。

maphashによる各エントリへのアクセスはO(1)とは限らない

maphashによる各エントリへのアクセスはO(1)とは限らない

SBCLのハッシュテーブルについて。

ハッシュテーブルのキーをなんでもいいから1つ消したいとする。ANSI CLのハッシュテーブルに該当する機能はない。しかし、そういう関数を定義するのは特に難しくなさそう:

(defun pophash (hash-table)
  (maphash (lambda (key _)
             (remhash key hash-table)
             (return-from pophash key))
           hash-table))

maphashの中で受け取ったエントリを消去するのは許されているので、pophashは意図通り動作する。しかし……

(defun test-pophash (size)
  (let ((table (make-hash-table :size size)))
    (dotimes (i size)
      (setf (gethash i table) t))
    (dotimes (i size)
      (pophash table))))

(time (test-pophash 200000))
;; Evaluation took:
;;   15.268 seconds of real time
;;   15.265625 seconds of total run time (15.265625 user, 0.000000 system)
;;   99.99% CPU
;;   38,084,922,378 processor cycles
;;   5,848,704 bytes consed

たった4×1054 \times 10^5回の操作に時間が掛かりすぎである。

もちろん、次のようにremhashするだけなら計算量の問題はない。

(defun test-pophash2 (size)
  (let ((table (make-hash-table :size size)))
    (dotimes (i size)
      (setf (gethash i table) t))
    (dotimes (i size)
      (remhash i table))))

(time (test-pophash2 200000))

;; Evaluation took:
;;   0.016 seconds of real time
;;   0.015625 seconds of total run time (0.015625 user, 0.000000 system)
;;   100.00% CPU
;;   37,052,672 processor cycles
;;   5,848,704 bytes consed

そもそもchainingによるハッシュテーブルがキーをどう管理するか考えてみると、pophashがだめなのはそれはそうという感じだった。以下、手短に動作を見てみる:

CL-USER> (let ((table (make-hash-table)))
	   (setf (gethash 3 table) 8
		 (gethash 5 table) 32
		 (gethash 7 table) 128)
	   (print (sb-impl::hash-table-pairs table))
	   (remhash 5 table)
	   (print (sb-impl::hash-table-pairs table))
	   (setf (gethash 11 table) 2048)
	   (print (sb-impl::hash-table-pairs table)))

#(3 0 3 8 5 32 7 128 #<unbound> #<unbound> ... #<unbound>
  #(0 60102308 60102306 60102304 0 0 0 0 0 0 0 0 0 0 0)) 
;; キー5を消す
#(3 0 3 8 #<unbound> #<unbound> 7 128 #<unbound> #<unbound> ... #<unbound>
  #(0 60102308 60102306 60102304 0 0 0 0 0 0 0 0 0 0 0)) 
;; キー11を追加する
#(3 0 3 8 11 2048 7 128 #<unbound> #<unbound> ... #<unbound>
  #(0 60102308 60102317 60102304 0 0 0 0 0 0 0 0 0 0 0)) 

上のように1、ハッシュテーブルの中身は単に配列に並んでいて、remhashによって消されるとそこが消えるだけである。unboundになったインデックスはうまく連鎖的に管理されていて、次に追加する時にはそこが埋まるようになっている。2

さて、maphashだが、実装を見ればわかるように、pairsを先頭から末尾まで走査するだけだ。つまり、上のpophashは常に先頭から走査して見つけたキーを消すように動くので、全体ではO(N2){\mathcal O}(N^2)になってしまう。

また、同じような理由で次のコードも計算量の問題がある:

(defun test-maphash (size)
  (let ((table (make-hash-table :size size))
        (sum 0))
    (dotimes (i size)
      (setf (gethash i table) t))
    (dotimes (i (- size 1))
      (remhash i table))
    (dotimes (i size)
      (maphash (lambda (x _) (incf sum x)) table))
    sum))

(time (test-maphash 200000))
;; Evaluation took:
;;   34.567 seconds of real time
;;   34.562500 seconds of total run time (34.562500 user, 0.000000 system)
;;   99.99% CPU
;;   86,197,938,782 processor cycles
;;   5,848,704 bytes consed

ハッシュテーブルに200000200000個のキーを登録したあと11個まで減らし、そのテーブルに対してmaphash200000200000回適用している。これも、同じ原理でO(N2){\mathcal O}(N^2)になる。maphashによる走査は確かにO(N){\mathcal O}(N)だけれど、テーブルの今の要素数に対するO(N){\mathcal O}(N)ではなく、過去に到達した最大要素数に対するO(N){\mathcal O}(N)なので、各エントリへのアクセスがO(1){\mathcal O}(1)になるとは限らないのだった。3

この最大要素数はSBCL内部ではhigh-water-markと呼ばれている。こういう実装になったのはバージョン1.5.5からで、それ以前は事前に確保したサイズに対するO(N){\mathcal O}(N)だったようだ。該当コミットはこれ。次のコードで違いがわかる:

(defun test-maphash2 (size)
  (let ((table (make-hash-table :size size))
        (sum 0))
    (setf (gethash 0 table) t)
    (dotimes (i size)
      (maphash (lambda (x _) (incf sum x)) table))
    sum))

(time (test-maphash2 200000))
;; SBCL 1.5.8, O(n)
;; Evaluation took:
;;   0.016 seconds of real time
;;   0.015625 seconds of total run time (0.015625 user, 0.000000 system)
;;   100.00% CPU
;;   16,126,752 processor cycles
;;   5,848,704 bytes consed
  
;; SBCL 1.4.14, O(n^2)
;; Evaluation took:
;;   47.615 seconds of real time
;;   47.625000 seconds of total run time (47.625000 user, 0.000000 system)
;;   100.02% CPU
;;   118,798,016,334 processor cycles
;;   8,497,264 bytes consed

ハッシュテーブルの要素数が大きくなるかもしれない場合に、リハッシュの回数を減らそうとして最初に:sizeを大きめに取っておくという発想は普通っぽい感じだけど、1.5.5より前のバージョンでは逆に遅くなる可能性がある。


maphashが今の要素数に対するO(N){\mathcal O}(N)になるような実装って可能かな? と少し考えていたけど、とりあえず既存のエントリ全体(と消したエントリ全体)のチェインをnext-vectorとは別に持てばできそう? 定数倍が重くなるわりに、できるようになることが大したことない感じだけど……


  1. pairsの最初の2つのエントリと末尾のエントリはテーブルの管理に使われている。また、SBCLのハッシュテーブルの実装は最近変わったので、上のコードはたぶんバージョン1.5.5以降でないと動かない。 ↩︎

  2. sb-impl::hash-table-next-vectorがインデックスの管理に使われる。 ↩︎

  3. 最初は「ならしでO(1){\mathcal O}(1)」と書いたけど、よく考えるとおかしかった。アクセスが最悪ω(1)\omega (1)で償却O(1){\mathcal O}(1)にするには別の例を挙げる必要がありそう。NN個登録した後N/2N/2個消すとか? ↩︎

2019年10月16日水曜日

Maximum-Cup 2013 F - 3人の騎士と1匹の犬

Maximum-Cup 2013 F - 3人の騎士と1匹の犬

魔力00の保有者はいかなる場合も不要なので最初に取り除き、MM11以上の魔力の保有者の総数としておく。

NMN \ge Mのケースは簡単で、マンハッタン距離をコストにして、MM個の騎士・魔力保有者のペアを作る最小重みマッチングを求めればよい。

N<MN < Mのケースが難しかった。魔力保有者をあらかじめ魔力の降順にソートしておいて、上位NN人の魔力保有者とのマッチングを考えればよいのだけれど、MAGICN\operatorname{MAGIC}_Nとちょうど同じ魔力を持つ者が複数人いたときに誰を選ぶかが問題になる。端的には、次のようにすればよい: (s,ts, tをフローの始点、終点とする)

  • ssから各騎士に、容量11、コスト00の辺を張る。
  • 各魔力保有者からttに、容量11、コスト00の辺を張る。
  • 各騎士からMAGICN\operatorname{MAGIC}_Nより大きな魔力の保有者に、容量11、コストがマンハッタン距離20000-20000の辺を張る。
  • 各騎士からちょうどMAGICN\operatorname{MAGIC}_Nの魔力の保有者に、容量11、コストがマンハッタン距離の辺を張る。

MAGICN\operatorname{MAGIC}_Nより大きな魔力の保有者が必ずマッチングに含まれるようにしたいので、20000-20000の補正を入れている。あとは流量NNの最小コストを求めて、20000×MAGICN20000 \times \operatorname{MAGIC}_Nより大きな魔力の保有者の数を足して戻せば答えになっている。

2019年10月15日火曜日

Indeedなう E - Page Rank

Indeedなう E - Page Rank

全部11のベクトルからスタートして、漸化式を1000回くらい回せば収束する。

正当性があまりわかってない。漸化式中の行列をAAとしたとき、つまりPR=0.1+APRPR = 0.1 + A \cdot PRとしたとき、

  1. AAの列毎の絶対値の和が11未満なのでA1<1\| A \|_1 <111ノルムについてはApAp\|A^p\| \le \|A\|^p成り立つ。したがってlimpAp\lim_{p \rightarrow \infty} A^p収束するので漸化式も収束する。(本当に?)1
  2. 収束するなら極限は漸化式の不動点になるから、漸化式を回すだけでよい。

くらいでわかった気になっておいた。


  1. 式変形で非正則な行列が出てこなければ大丈夫そう。非正則な場合は知らない。 ↩︎

2019年10月14日月曜日

KUPC 2019 D - Maximin Game

KUPC 2019 D - Maximin Game

とりあえずDPっぽいものから考えた。カード11から順にカードNNまで二人に配っていくとして、千咲さんにxx枚、月乃瀬さんにyy枚配った時点で、条件に合っているような配り方の総数をdp[x][y]dp[x][y]とする。遷移としては

(Sx=1(S_x=1かつxy)x\le y)または(Sx=0(S_x = 0かつx>y)x > y)なら

dp[x][y]+=dp[x1][y]dp[x][y] += dp[x-1][y]

(Sy=0(S_y=0かつyx)y \le x)または(Sy=1(S_y = 1かつy>x)y > x)なら

dp[x][y]+=dp[x][y1]dp[x][y] += dp[x][y-1]

で求まるが、O(N2){\mathcal O}(N^2)なので制約的に無理だし、実はメモ化再帰で枝刈りされるみたいなこともないし、うまい高速化も思いつかないし……となって困っていた。

こういう問題の言い換えとして、原点OOからスタートして千咲さんがカードを取ったらベクトル(1,1)(1, 1)に沿って進み、月乃瀬さんが取ったらベクトル(1,1)(1, -1)に沿って進むというのがあったなと思い出して実験してみると、次のような性質が見える:

  • OOからスタートして(2N,0)(2N, 0)に着く
  • yy座標が正の時は月乃瀬さんが勝っていて、yy座標が負の時は千咲さんが勝っている。つまり、文字列SSで勝ち負けが切り替わるところでxx軸と交わる。

すなわち、SS中で00が連続している部分ではxx軸の上にいなければならず(xx軸を踏んでも良い)、11が連続している部分では下にいなければならない。このパスの数え上げは有名なのがあった気がすると思ってググったらカタラン数だった。同じ要素が連続している各部分列についてカタラン数を求めてかけ合わせれば答えになっている。