Processing math: 95%

AtCoder Regular Contest 126 B,C,D問題メモ

B - Cross-free Matching

問題

  • 平面座標上に M 本の線分がある
  • i 番目の線分は (ai,0)(bi,1) を結ぶ
  • どの2本の線分も点を共有しない(両端を含めて)ように、M 本の中から線分を選びたい
  • 選べる最大数を求めよ
  • 1M2×105
  • 1ai,bi2×105

解法

最長増加部分列(Longest Increase Subsequence)の亜種。

(ai,0)(bi,1) を結ぶ線分を、単に (ai,bi) と表現することにする。

DPを以下のように定義する。

  • DP[i][k]=ai 以下である線分だけで k 本選ぶときの、最も右の線分のもう一方の端点 b がとれる最小値

a が同じなら b がなるべく小さい方が次に採用できる線分の可能性が広がるので、小さく保って損しない。

(ai,bi) を採用するとき、 「既に採用された一番右の線分の a<ai」かつ「既に採用された一番右の線分の b<bi」という状態に1本加えることができ、自身が新たに一番右の線分となる。

前者の条件は ai が小さい方から順に調べればよい。

この DP の更新方法はLISと似通っていて、

  • 常に狭義単調増加になる
  • 二分探索で DP 配列から b の挿入位置を探す
  • 位置が p だったとすると p 本目として採用できるので、DP[i][p]b に更新できる
DP[4]
[0, 3, 4, 8]      ai=5 の線分は (a, b)=(5, 1),(5, 5),(5, 6),(5, 10) の4本があるとする

[0, 1, 4, 8]      (5, 1) を使うと、1本採用したときのbの最小値を1に更新できる
[0, 3, 4, 5]      (5, 5) を使うと、3本採用したときのbの最小値を5に更新できる
[0, 3, 4, 6]      (5, 6) を使うと、3本採用したときのbの最小値を6に更新できる
[0, 3, 4, 8, 10]  (5, 10)は、新たに1本採用できる

[0, 1, 4, 5, 10]  以上をまとめると、DP[5]は左のようになる

3 本目としては (5,5)(5,6) も採用できるが、より小さい (5,5) が採択される。

a が同じ線分は「全ての挿入位置を求める」→「まとめて更新する」という方法をとる。
さもないと、逐次更新していたら上記の例では (5,6) の時に新たに1本採用できることになってしまう。

[0, 3, 4, 5]      (5, 5) を使って左のように更新した後に
                  (5, 6)について更新しようとすると、
[0, 3, 4, 5, 6]   こうなってしまう

だが、これは ai=5 である2本を同時に採用することになるので、共有点ができてしまう。

Python3

C - Maximize GCD

問題

  • N 個の整数 A=(A1,A2,...,AN) がある
  • 好きな整数を1つ選んで1を足す、という操作を K 以下行える
  • gcd(A1,A2,...,AN) を最大化せよ
  • 2N3×105
  • 1Ai3×105
  • 1K1018

解法

工夫してまとめられる計算をまとめる。

まず、K が馬鹿でかい時は、なるべく均して全てを同じ数にするのが最もgcdを大きくできる。

全てを均すと m=Sum(A)+KN(切り捨て)となるとして、 実際には数字は減らせないので、mAmax なら全てを同じ値にでき、答えは m となる。

以下、そうで無い場合を考える。

操作後の A には Amax1 以下の値が含まれ、gcdが Amax1 より大きくなることは無いので、 gcdの候補としては Amax1 以下だけ考えればよいことになる。

Ai の制約が中途半端に小さいのも、gcd候補を全探索できるようにしている感じがある。

大きい順に、gcd候補を g として「合計 K 以下を足すことで全てを g の倍数に出来るか?」を試していく。
一番最初に見つかった可能な g が答え。

制約上、この判定を1回あたり O(logN)O(logAmax) くらいで行いたい。

基本方針

g を固定した時、各 Aig の倍数にする必要がある。

1つの Aig の倍数にするためには、ceil(Aig)×gAi だけ足す必要がある。

この ceil(Aig) が同じ Ai ごとに計算をまとめることができる。

ceil(Aig)=c となる Ai の個数を n、その和を s とすると、cngs が、加算する必要数となる。

ceilが取り得る値の種類は、g が大きい内は少ない。具体的には Amaxg 個くらい。

従って、g を全て試しても Amax1+Amax2+...+AmaxAmax1 で、 全体で O(AmaxlogAmax) となる。

O(AmaxlogAmaxlogN) 解法

一見、間に合いそうにないが、枝刈りがそれなりに有効に働いて実際は間に合う。

ceil が同じになる Ai は、Ai をソートしたときに連続している。
最初に Ai をソートし、累積和を取ることで「g に対してceilが同じ Ai の和」を高速に取得できる。

ceil(Aig)=c となる Ai は、 cg<Ai(c+1)g なる最小と最大の i(それぞれ l,r とする)を二分探索で求めることで得られる。

実際には1つ前の r は次の l となるので、

  • l=0 から始める
  • c=ceil(Alg) を計算する
  • A から (c+1)g の挿入位置を二分探索で求め、r とする
  • lr の個数と累積和から、当該範囲の Ai を全て g の倍数にするために加算すべき値が求まる
  • lr に更新する

とすると、余計な c を調べることがなくなる。
(※余計な c: ceil(Aig)=c となる i が1つも存在しない c

また、l を増やして加算すべき値を求めていく過程で、途中で K を超えたら、 その時点で現在の g は不可能として打ち切ってよい。

前者(余計な c の省略)はあまり効かないテストケースを作れるが、 後者(K を超えたら打ち切り)は「多くの g に対して、ギリギリまで K を超えないで、 でも最終的には K を超えてしまうような A」というのは作れないので、有効に働く。

O(AmaxlogAmax) 解法

Ai の上限は配列で表現できる範囲なので、はじめから数直線上にプロットしてから累積和を取ってしまえばよい。

つまり、先ほどは

A = (3, 5, 6, 9)  だったら累積和は
 [0, 3, 8,14,23]

としていて、c ごとに挿入可能位置を二分探索しなければならなかったが、

A = (3, 5, 6, 9)

i 0  1  2  3  4  5  6  7  8  9
[ 0  0  0  3  0  5  6  0  0  9 ]   数直線上にプロットして
[ 0  0  0  3  3  8 14 14 14 23 ]   累積和をとる

[ 0  0  0  1  0  1  1  0  0  1 ]   同様に個数も取っておく
[ 0  0  0  1  1  2  3  3  3  4 ]

こうすると、二分探索をせずとも c ごとに (cg,(c+1)g] の範囲の累積和を調べればよくなる。

余計な c を調べてしまうかも知れないが、二分探索を省略できることの方が大きく、上記より高速になる。

Python3

なんか、g を1減らしたときに、足す必要数が増減する差分を管理する、みたいな方向に行っちゃったな。 これだと g が小さくなったときに対象が多くなりすぎてTLEとなる。

D - Pure Straight

問題

  • 1K の値からなる長さ N の数列 A=(A1,...,AN) がある
    • 1K はそれぞれ少なくとも1個は A に出現する
  • 隣接swapを繰り返すことで、A のどこかに「1,2,3,...,K」という並びが現れるようにしたい
  • 必要な最小swap回数を求めよ
  • 2K16
  • KN200

解法

不要な要素を追い出す操作と、最終的に 1,...,K の並びを作る要素同士の並べ替えで、分けて考える。

並べ替えはおなじみ転倒数だが、不要な要素を追い出す回数の考え方が面白い。

先頭からDPをしていく。

  • DP[i,S]=Ai まで見て、集合 S の要素を整列した上で並べるための、最小操作回数

大まかにはこんな感じだが、「どこにそれを並べるか」によって操作回数が変わってくるため、少し定義不足である。

まぁ、とりあえず遷移を考える。

Ai を採用

Ai を新たに S に加える場合、「S に含まれる、Ai より大きい要素の個数」だけ、操作が発生する。

これは転倒数の更新方法そのものなので、わりとわかりやすい。

Ai を不採用

スルーする場合、Ai を追い出す必要が発生する。

そのコストは、min となる。

つまり、|S| がまだ半分以下なら左から追い出した方がよく、 半分を超えているなら右から追い出した方がよい、ということを表す。

(答えには直接関係ないが)最終的に 1,2,...,K の並びができる位置は、 採用した K 個の要素の内、初期状態で真ん中にあった要素を中心とした位置、となる。

S に含まれない要素はまだ位置や並びも見えていないが、 個数さえ分かっていれば A_i という単独の要素を追い出すコストはわかる、というのがポイント。

Python3

programming_algorithm/contest_history/atcoder/2021/0919_arc126.txt · 最終更新: 2023/07/11 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0