Processing math: 100%

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

E - LEQ

E - LEQ

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

問題

  • 長さ N の数列 A1,A2,...,AN
  • 長さ2以上の部分列のうち、(最初の要素) (最後の要素) となるものの個数を mod998244353 で求めよ
  • 部分列として同じであっても、取り出す添字が異なるものは区別する
  • 2N3×105

解法

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

  • A2A7 であることが条件
  • 満たす場合、A3A6 は入れるか入れないか自由なので、24 個の部分列が当てはまる

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

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

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

【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 とするのだったら、

  • j=0 の “2” を左端とするときは、24
  • j=1 の “7” を左端にはできない(が、“7” の位置に足しておくのは 23 個)
  • j=2 の “1” を左端とするときは、22
  • j=3 の “2” を左端とするときは、21
  • j=4 の “4” を左端とするときは、20

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

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

  • j=0 の “2” を左端とするときは、25
  • j=1 の “7” を左端とするときは、24
  • j=2 の “1” を左端とするときは、23

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

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

つまり、

  • A0=2 の位置には 1
  • A1=7 の位置には 12
  • A2=1 の位置には 122
  • A3=2 の位置には 123

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

こうすると、i=5 のとき、Fenwick Tree上で 5 以下の値の合計は 1+122+123+124=2324 となるが、 ここに 2i1 をかけることで、正しい値である 23 が得られる。

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

Python3

F - Diameter set

問題

  • N 頂点の木が与えられる
  • いくつかの頂点を選んで赤く塗る方法のうち、以下の条件を全て満たすものの個数を mod998244353 で求めよ
  • 条件
    • 赤く塗る頂点は2個以上
    • どの2点を選んでも、その距離が木の直径と等しい
  • 2N2×105

解法

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

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

①-,                  ,-③
   ○-○-○- ... -○-○-④
②-'                  `-⑤
  • ①と②は同時に選べないが、少なくともどちらかから1点
  • ③④⑤は同時に選べないが、少なくともどれかから1点

なので6通りとなる。

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

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

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

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

グループの見つけ方

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

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

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

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

  • 木の直径をなす任意の2点間を結ぶパスは、必ず木の中心を通過する

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

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

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

  • ⑪から↑方向の辺の先には①の1点
  • ⑪から←方向の辺の先には②③の2点
  • ⑪から→方向の辺の先には④⑤⑥の3点
  • ⑪から↓方向の辺の先には⑦⑧⑨⑩の4点

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

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

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

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

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

パターン数の求め方

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

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

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

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

その数は、

  • どの頂点も選ばれていないもの: 1通り
  • 1点しか選ばれていないもの: (全グループのsizeの合計)通り

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

Python3

G - Jumping sequence

問題

  • 2次元座標の原点 (0,0) からちょうど N 回、上下左右いずれかへのジャンプを繰り返し、(A,B) に移動したい
  • i 回目のジャンプで移動できる距離は Di と決まっている
  • 可能か判定し、可能ならジャンプした方向の列を1つ求めよ
  • 1N2000
  • 1Di1800

解法

数式で表現すると、2つの {1,0,1} をとる係数列 k=(k1,k2,...,kN)l=(l1,l2,...,lN) を、

  • Ni=1kiDi=A
  • Ni=1liDi=B

となるように決定したいのだが、 この時に「ki=0 なら li=1or1」「ki=1or1 なら li=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度回転させて(かつ整数になるように 2 倍して)考える。

  • それぞれの方向へのジャンプは以下のようになる
    • 'R': (X,Y)(X+D,Y+D)
    • 'L': (X,Y)(XD,YD)
    • 'U': (X,Y)(XD,Y+D)
    • 'D': (X,Y)(X+D,YD)
  • 目標地点は (AB,A+B)

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

  • Ni=1kiDi=AB
  • Ni=1liDi=A+B

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

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

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

  • DP[i][j]=D1Di のそれぞれを足し引きすることで合計を j にできるなら 1、不可能なら 0

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

高速化

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

制約の値を単純に当てはめると 7.2×109 となり、間に合いそうにない。

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

 j  9876543210
値  0011001101  →  205 として扱う

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

さらなる高速化

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

  • オフセットの必要性
  • 1回の遷移の演算回数
  • bitsetの桁数

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にあるように以下のように式変形する。

  • Ni=1kiDi=Sum(D)+AB2
  • Ni=1liDi=Sum(D)+A+B2

ただし、k,l0,1 をとる係数列で、0k,l1 に相当する。

こうすると、

  • 左辺の合計値は負になり得ないのでオフセットの必要は無くなる
    • 右辺が負や非整数になったらその時点でアウト
  • 1回の遷移は1回のbit-shiftと1回のbit-orだけで済むので演算回数が2/3に減る
  • 最大桁数も半分になるので1回ずつのbit演算も高速になる

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

Python3

H - Count Multiset

H - Count Multiset

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

問題

  • 整数 N,M が与えられる
  • f(x) を、以下の条件を全て満たす多重集合の個数とする
  • 条件
    • x 個の正整数からなる
    • 総和が N
    • 同じ要素の個数は M 個を超えない
  • f(1),f(2),...,f(N) をそれぞれ mod998244353 で求めよ
  • 1MN5000

解法

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

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

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

  • DP[i][j]= 多重集合の内、要素数が i、総和が j のものの個数

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] と一致

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

  • '1'が0個: 要素数が i、総和が ji の多重集合の個数と一致する
  • '1'が1個: 要素数が i1、総和が ji の多重集合の個数と一致する
  • '1'が2個: 要素数が i2、総和が ji の多重集合の個数と一致する
  • '1'がM個: 要素数が iM、総和が ji の多重集合の個数と一致する

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

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

  • DP[i][j]= 多重集合の内、要素数が「1i」、総和が j のものの個数

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

  • DP[i][j]=DP[i][ji]DP[iM1][ji]

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

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

Python3

programming_algorithm/contest_history/atcoder/2021/1002_abc221.txt · 最終更新: 2021/10/08 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0