Processing math: 55%

ACL Contest 1 A,B,C問題メモ

A - Reachable Towns

問題

  • xy 平面上に N 個の点 (x1,y1),...,(xN,yN)
    • x 座標が 1,2,...,N の箇所に点が1個ずつある
    • y 座標が 1,2,...,N の箇所に点が1個ずつある
  • k=1N について、以下の問いに答えよ
    • 今いる点から「x 座標も y 座標も小さい点」と「x 座標も y 座標も大きい点」に移動できる
    • k から出発して、好きなだけ移動を繰り返して到達できる点の個数を(点 k を含めて)求めよ
  • 1N2×105

解法

ある点から到達できる点のグループは、グループ内のどの点から出発しても同じグループに到達できる。
なので、Union-Find(Disjoint Set Union)を使えばよさそう。

ただ問題は、行き来できる点をどのように見つければよいか。
各点から、他の点が移動できる点かどうかを全て確かめていては O(N2) で無理。

x,y 座標が大きい点の方から、小さい方向に移動できる点を探す。

  ↙●
  ●  
      ●
          ↙●
        ●  ↙
          ●
              ●
x 1234567

まず点を x 座標でソートし、その順に、該当する点を探していく。

自分より後に処理される点は、当然 x 座標が大きいので、該当する点ではあり得ない。
自分より前に処理された中で、y 座標が小さい点に対して、Uniteすればよい。

これは、std::set やFenwick Tree などで管理できる。

ただ、この方法も、以下のようなケースではUniteする対象がどんどん増え、結局 O(N2) かかってしまう。

            ...
          ●
        ●
      ●
    ●
  ●  
x 1234567

x=5 まで処理された時点では、x=14 の点は既にUniteされているので、5から辺を張りに行くのはどれか1点でよい。
そう考えると、一番左下の点のみ残しておけばいい具合に無駄を減らせそう。

x 座標も y 座標も自分より小さい点が無い点」のみをFenwickTreeに登録し、探索対象とする。
その点がハブとなってくれて、Uniteされるグループは正しいまま、探す対象が少なくなる。

    ・
  ●  
      ●
            ・ 
        ●
          ●
              ●
x 1234567

(Pythonでは std::set が無いから代用とは言え) いきなりA問題から FenwickTreeとUnionFindの合わせ技が出てくるのは飛ばしてるなあ、と思ったけど、 実は使わなくても解ける方法があるらしい。先入観よ。

Python3

B - Sum is Multiple

問題

  • 正整数 N が与えられる
  • (1+2+...+k)N の倍数となるような、最小の k を求めよ
  • 1N1015

解法

シンプルですごい。

まずパッと見、(1+2+...+k)=k(k+1)2 と式変形できる。
よって、「k(k+1)2N の倍数となる」と言い換えられるのだが、 ここで重要なのは kk+1 は互いに素であるということ。

つまり、2N2a3b5c などと素因数分解されたとして、kk+1 の両方に2や3が素因数として入ることは無い。

2a3b5c も、それぞれ kk+1 のどちらかに全入れしなければならない。

素因数のユニークな個数なんて、素数を小さい方からかけていってもまぁ15個もしない内に 1015 を越えるので、 たかだか15個の数についてどっちに入れるか、215 通り試すのは十分可能である。

さて、kA=2a5c の倍数、k+1B=3b の倍数と割り振ったとして、これについての答えを求める。
(ここでの割り振りは一例であって、重要ではない)

中国剰余定理の考え方を使って、以下のように求められる。

k+1=Bmm:整数)と表すとして、これを k \equiv 0 \mod{A} に代入すると、Bm \equiv 1 \mod{A} となる。

ここで、AB は互いに素なので、B は逆元 B^{-1} を持つ。これを使えば m \equiv B^{-1} \mod{A} となり、 m は、B^{-1} をベースとして、それに A の倍数を足し引きした数に絞られる。

正整数の中での最小値を求めたいので、0 \le m \lt A である m を使って k+1 を求めればよい。

ただし、A=1B=1 の場合は、うまく逆元を計算できない(というか、何でもよい)ので、個別に考える。

A=1 の時、k \equiv 0 \mod{1} かつ k+1 \equiv 0 \mod{2N} である。よって、k=2N-1 が最小となる。
B=1 も同様に考えると、k=2N となる。

Python3

C - Moving Pieces

問題

  • N \times M の盤面
    • . が空きマス
    • o がコマ(複数個あり得る)
    • # が壁
  • コマを1つ選んで、右または下に動かすことを繰り返す
    • 壁や、他のコマがいるマスには移動できない
  • どの駒も動かせなくなったら終わり
  • 全ての駒の移動回数の総和の最大値を求めよ
  • 1 \le N,M \le 50
  • 1 \le コマの個数 \le 100

解法

どのコマをどのマスで終わらせるか、の組み合わせで最小費用流。

各コマは右も下も外周か # のマス(以降、「行き止まりマス」)のいずれかを目指すんだけど、 同じマスを目指すコマが複数あると、1つ以外は手前で止まって移動回数を減らさざるを得ない。

最終状態は、行き止まりマスの多くにはコマが置かれ、その周辺にも溢れたコマが密集している形となる。

その「周辺」がたとえば1本道だと、ちょっとのコマが集まっただけで詰まってしまい、より手前で止まらねばならず、移動回数の損失は大きくなる。
一方、周辺が開けていると、多くのコマが集まっても比較的、損失の増え方は小さい。

はじめ「コマ と 目指す行き止まりマス」の対応を考えたが、それだとこの違いを上手く吸収できない。

「コマ と 最終的なマス」の対応を考えることで、1対1のマッチングを求めればよくなり、最小費用流に持ち込める。

  • 仮想的な始点・終点(各1つずつ)、コマ(oの数だけ)、各マス(N \times M)、の分だけ頂点を作る
  • 始点→コマ→そのコマが移動できるマス→終点 と通るように辺を張る
    • 始点からコマに、容量1、コスト0の辺を張る
    • 各コマから経路探索し、到達できるマスに、容量1、コスト「-移動距離」の辺を張る
    • 各マスから終点に、容量1、コスト0の辺を張る

最大スコアを最小費用流によって求めたいときは、正負を逆転させれば使えるようになる。
これで始点から終点に、コマの個数分の流量を流したときの最小費用を求めれば、それが答えとなる。

負辺ができてしまうが、始点から終点への全ての経路で負辺(コマ→マスに張られる辺)を通るのは1回固定なので、適当に下駄を履かせておけばよい。

ところで、この方針ではコマ同士の、途中経路での衝突が考慮されていない。
「あるコマから到達できるマスであっても、他のコマを考慮すると邪魔されて動けなくなることはないか?」という点が気になる。

初期状態      最終状態
                            このような実際は不可能な
①②....  →  ....②①      マッチングが発生してしまう

が、衝突する場合はマッチング結果の対応を入れ替えると解消でき、総移動回数は変わらないので問題ない。
適宜入れ替えれば問題ない、ということが言えたので、実際に矛盾しないマッチングを求める必要は無い。

Python3

よりよい解法

コマ1つ1つについて各マスへのコストを求めなくても、 通常のグリッド問題のように、隣接するマスにコスト -1 の辺を張る方法でも、最小費用流が可能である。

そうすると各コマで辺を共有できるので、高速になる。

  • 先ほどの方法では、コマの数 p、壁でないマスの数が NM に比例するとして、だいたい p+NM+pNM 本の辺が必要
    • p \le 100, NM \le 2500 より、ざっくり25万本
  • この方法では、だいたい p+3NM 程度でいける
    • ざっくり7500本

ただ、その場合、負辺を防ぐために下駄を履かせたくても、各コマによって負辺を通る回数が違う=足される下駄の回数が異なるので、正しく求められない。

以下のように考えると下駄の回数を統一した上で辺を非負にできる。

  • 全てのコマは、(1,1) から (N,M) への N+M-2 回の移動分のコストがデフォルトでかかる(下駄)
  • その中で、実際におこなった移動分だけを0にできる
  • (N+M-2) \times コマの個数 分の下駄が履かされた状態なので、ここから最小費用を引くと、答えとなる

これは、以下のように実装する。

  • 始点、終点、空きマスの数だけの頂点を用意
  • 始点から、各コマが初期で置かれているマス (x_i,y_i) へ辺を張る
    • 容量は1、コストは x_i+y_i-2 ((1,1) からの移動分)
  • 壁以外のマス (x_j,y_j) から、終点へ辺を張る
    • 容量は1、コストは N+M-x_j-y_j ((N,M) への移動分)
  • マスから右か下に隣接する移動できるマスに辺を張る
    • 容量は∞、コストは 0

Python3

programming_algorithm/contest_history/atcoder/2020/0920_acl1.txt · 最終更新: 2020/09/24 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0