Processing math: 2%

目次

AtCoder Beginner Contest 221 E,F,G,H問題メモ

AtCoder Beginner Contest 221

E - LEQ

E - LEQ

勝手な早とちりにより誤読が2回発生した。

問題

解法

たとえば取り出す部分列の左端と右端のindexを (2,7) とすると、

これを左端、右端ともに全て調べて合計すると答えとなるが、計算量が O(N^2) となってしまう。

各右端につき、左端はまとめて計算できないか考える。

転倒数や増加部分列と似た考え方が適用できそう。
これらの典型問題では、A_i を添字に持つFenwick Treeなどを用意することで、 「i より左に位置する、A_i 以下の値の個数」を高速に得られる。

【iより左に位置するAi以下の値の個数を、各iについて求める】

i     0 1 2 3 4 5 6 7 8
A = [ 2 7 1 2 4 5 7 4 9 ... ]    (※以降、Aiは0-indexとする)

  Ai  0  1  2  3  4  5  6  7 ...
Tree  0  1  2  0  1  0  0  1     i=0~4 までのAiの個数を反映
      
      ~~~~~~~~~~~~~~~~           i=5 より左に出てくる
                                 A5=5 以下の数の個数は、波線部の合計=4

Tree  0  1  2  0  1  1  0  1     "5" の位置に+1して次へ

      ~~~~~~~~~~~~~~~~~~~~~~     i=6 より左に出てくる
                                 A6=7 以下の数の個数は、波線部の合計=6

Tree  0  1  2  0  1  1  0  2     "7" の位置に+1して次へ
...
繰り返し

これと似たことをしたい。

今回は、たとえば右端を i=5 とするのだったら、

と、位置毎に 2^{i-j-1} が足し込まれていると、今回の問題の答えが求められる。
具体的には、16+4+2+1=23 個となる。

ただし、次の i=6 を考える際には、

のように、足されている数は2倍ずつになっていないと正しく求まらない。

実際に毎回2倍にするわけにはいかないので、これは累積和を取った後で調整する。

つまり、

このように値を加算しておく。

こうすると、i=5 のとき、Fenwick Tree上で 5 以下の値の合計は 1 + \dfrac{1}{2^2} + \dfrac{1}{2^3} + \dfrac{1}{2^4} = \dfrac{23}{2^4} となるが、 ここに 2^{i-1} をかけることで、正しい値である 23 が得られる。

これを各右端につき合計すると答えとなる。

Python3

F - Diameter set

F - Diameter set

問題

解法

どんなケースが当てはまるか、過不足無く見つけるのが難しい。

例えば以下のような木だと、

①-,                  ,-③
   ○-○-○- ... -○-○-④
②-'                  `-⑤

なので6通りとなる。

だが、このようなグループがたくさんある木も考えられる。

       ,-①
      ○
②-,  |  ,-④
③-○-⑪-○-⑤
      |  `-⑥
   ⑦-○-⑩
    ⑧'`⑨

①、②③、④⑤⑥、⑦⑧⑨⑩がそれぞれグループとなっており、各グループからは多くとも1つしか選べない。

このグループをどうやって見つけるか?

グループの見つけ方

木の直径を見つけるアルゴリズムといえばBFSを2回行う方法がある。

たとえば、適当な頂点から1回目のBFSを行った結果、①が直径をなす頂点の1つとわかったとする。

①から2回目のBFSを行って、最も遠い頂点として②~⑩が得られる。
だが、そのどれとどれがグループなのかを判別しないといけない。

これには、木の直径が持つ1つの性質を利用する。

木の中心とは、直径をなすパスの真ん中の点または辺のことを指す。
上記の例では、⑪が中心である。

木の中心から、直径をなす頂点までの距離を「木の半径」という。

まず木の中心を求め、そこから伸びる各辺ごとに、 中心からの距離が木の半径と一致する頂点を求めれば、それがグループとなる。

木の直径が奇数の場合は辺が木の中心となるが、 便宜的に辺の間に N+1 個目の頂点を追加するようにつなぎ替えれば、頂点と同様に考えられる。

①--②------③--④
        ↓
①--②--⑤--③--④

木の中心から、各辺方向の距離を求める際には、TLEに注意。

スターグラフなど、木の中心から多数の辺が伸びていることがある。
1つの辺ごとに、毎回BFSを初期化して、N 要素の距離配列を作って、などとしていると、いつの間にか O(N^2) の計算量になっていることがある。

なるべく配列は使い回すなど、全体として O(N) に収まるように実装する。

パターン数の求め方

木の中心からそれぞれの辺の先に、1,2,3,4個のグループが存在するとわかった。

各グループにつき、「どれか1点を選ぶ」か「1点も選ばない」かの、(size+1) 通りの選択肢がある。

これをまず掛け合わせる。

ただしこの中には、1点以下の頂点しか選ばれていないものが混ざっている。

その数は、

なので、これを掛け合わせた結果から除けば、答えとなる。

Python3

G - Jumping sequence

G - Jumping sequence

問題

解法

数式で表現すると、2つの \{-1,0,1\} をとる係数列 k=(k_1,k_2,...,k_N)l=(l_1,l_2,...,l_N) を、

となるように決定したいのだが、 この時に「k_i=0 なら l_i=-1 or 1」「k_i=-1 or 1 なら l_i=0」でなければならないという制約がある。

このため独立に考えることが出来ず、DPを考えるにしても「x が○○のときに y は△△が可能」という2次元で行わなければならず、TLEとなる。

ここで(やや天啓的だが)2数の和と差を考えれば、

k    +1  -1   0   0
l     0   0  +1  -1
-------------------
k+l  +1  -1  +1  -1
k-l  +1  -1  -1  +1

独立な2つの {1,-1} の組合せで4通りの移動を区別できる。
この独立に割り当ててよくなるという点が大きい。

座標で和と差を取るといえば、45度回転に相当する。
座標を45度回転させて(かつ整数になるように \sqrt{2} 倍して)考える。

2つの {-1,1} をとる係数列 k',l' があったとして、

となるようにすればよく、たとえば以下のような k',l' があったら、組合せに 'RLUD' を対応づけ、操作列を構成することが出来る。

            k'  +1  -1  +1  +1  -1
            l'  +1  +1  -1  +1  -1
対応する操作列   R   U   D   R   L

従って、1次元のDPがそのまま使えて、

この情報を使って合計を A-B にできるように k' を決め、A+B にできるように l' を決めれば、答えが求められる。

高速化

これにて一件落着めでたしめでたし、とはならず、上のDPを計算しようとすると最悪 O(N^2 D_{max}) かかる。

制約の値を単純に当てはめると 7.2 \times 10^9 となり、間に合いそうにない。

今回のDPでは管理する値が真偽値 0/1 だけでよいため、bitset化できることを利用する。
Pythonなど多倍長整数が可能な言語では、いつものように整数をbit列として扱うだけでもよい。

 j  9876543210
値  0011001101  →  205 として扱う

64個の j の値を1つの64bit整数にまとめることができるので、 (細かい部分は言語と実装に依るだろうけど)大体64倍くらいになることが期待される。 7.2 \times 10^9 / 64 = 1.125 \times 10^8 となり、 実行時間制限が長めなこともあってまぁ間に合わなくはないレベルになる。

さらなる高速化

このままのDPでも言語によっては間に合うかも知れないが、以下の改善の余地を残している。

j は負の値も取り得るため、bitsetとして扱うなら Sum(D) だけ前もってオフセットでずらす必要がある。

また、遷移は

j     10 9 8 7 6 5 4 3 2 1 0
DP[0]  0 0 0 0 0 1 0 0 0 0 0
              ↙   ↘             D1=3
DP[1]  0 0 1 0 0 0 0 0 1 0 0
         ↙ ↘       ↙ ↘        D2=2
DP[2]  1 0 0 0 1 0 1 0 0 0 1

のように、左右にbit-shiftしたそれぞれをbit-orしたものとなり、1回の遷移で3回のbit演算が行われる。

あと、これだと各時点で奇数indexか偶数indexのどちらは必ず0となるため、ちょっと空間の効率が悪い。

ここで、公式Editorialにあるように以下のように式変形する。

ただし、k'',l''{0,1} をとる係数列で、0k',l'-1 に相当する。

こうすると、

これらの改善により、もうあと何倍か速くなる。

Python3

H - Count Multiset

H - Count Multiset

このタイプの解法、たまに出題されては毎回解説見て「頭いいな」っていってる。

問題

解法

Nx 個の自然数の和に分割する方法の個数を示す「分割数」に、同じ数の個数上限が加わった感じの問題となっている。

DPを行うのだが、前から1要素ずつ決めるとかの考え方からの転換が必要。
発想がわかれば、理解・実装はさほど難しくない。

以下、単に「多重集合」といったとき、正整数からなり、同じ要素の個数は M 個を超えないという条件は満たしているとする。

DP[i][j] を求めることを考える。

ここで唐突に、多重集合の全要素を1ずつ減らしてみる。

元の集合での'1'が'0'となり多重集合から除外されるが、他は1減らした値での集合と1対1対応する。

M=2

i=4  j=10
1が0個  {2, 2, 3, 3}  →  {1, 1, 2, 2} ─ DP[4][6] と一致
1が1個  {1, 2, 2, 5}  →  {1, 1, 4}    ┐
        {1, 2, 3, 4}  →  {1, 2, 3}    ┴ DP[3][6] と一致
1が2個  {1, 1, 2, 6}      {1, 5}       ┐
        {1, 1, 3, 5}  →  {2, 4}       ┼ DP[2][6] と一致
        {1, 1, 4, 4}      {3, 3}       ┘

(Mの条件と合致しないためDP[4][10]には計上されないが続きとして)
1が3個  {1, 1, 1, 7}  →  {6}             DP[1][6] と一致

要は、以下のようになる。

つまり、自分より小さな i,j について DP[i][j] が埋まっていれば、答えはその和で求められる。

さらに参照する i は連続しているので、i 方向に累積和を取った状態で管理しておけば、

以下のように、累積和の差分で求められる。(indexが負の場合の例外処理はするとして)

DPを埋め終わったら、DP[1][N]~DP[N][N] がそれぞれの答え。

初期値としては、要素数 i=1 のとき明らかに j=1~N に対して DP[1][j]=1 なので (\{j\} 1要素のみからなる多重集合の1通り)、それを埋めておけばよい。

Python3