−目次
AtCoder Beginner Contest 155 D~F問題メモ
アルストツカに栄光あれ
D - Pairs
問題
- N 個の整数 A1,A2,...,AN がある
- ここから2個選んで積をとってできる N(N−1)2 個の整数の内、K 番目に小さいものを求めよ
- 2≤N≤2×105
- −109≤Ai≤109
解法
実装がちょっと面倒な二分探索。
このように、X 個中の K 番目を求めたいが、X が巨大すぎて全部を実際に計算してられない問題は、 「答えが a だとして、積が a 以下となるペアが K 個以上できるか?」をチェックする二分探索で求められることがある。 (ペアが K 個以上できる最小の a が答えとなる)
まず問題を仮に Ai が全て正として単純化し「積が M 以下となるペアが K 個以上できるか」を判定する方法を説明する。
積が M 以下のペアの個数は、ペアの一方を Ai と固定するともう一方は MAi 以下(切り捨て)なので、各 i についてこれを求めて合計すればよい。
これは Ai をあらかじめ昇順ソートしておくことで、尺取りまたは二分探索で求めることができる。
各 i について、Ai とペアになると M を超過する最小の Aj の位置 j を尺取りで探索する。
i が大きい方が j は小さくなる。 従って、i を 大→小、j を 小→大 で動かすと、j は i のループごとにリセットせず、全体で1回 A を走査すればよいことを利用する。
M = 12 凡例 A 1 2 3 4 6 o: チェックしてM以下と判明 i ! *: M以下にならない最小のj(jの現在位置) j o o * v: 既にK以下と判明しているためチェックの必要なし i ! ↖Ai = 6 のとき、2個 j v v o * ←Ai = 4 のとき、3個 i ! j v v v o * Ai = 3 のとき、4個 i ! j v v v v o * Ai = 2 のとき、5個 i ! j v v v v v * Ai = 1 のとき、5個 → 計 19個が積が12以下
※これは(a,a)など同じ要素を選んでいたり、(a,b)と(b,a)を区別した上での数なので、この問題の定義におけるペア数とは異なる。が、後述の方法で簡単に求められる。
結果、積が12以下であるペアが8個と判明したとすると、K>8 であれば答えは12より大きく、K≤8 なら答えは12以下である。
- 類題: E - Handshake
しかし、今回は負の数も絡むというのが厄介で、
- ペアの一方が正の場合は、もう一方も大きくなるほど積も大きくなる
- ペアの一方が負の場合は、もう一方は小さくなるほど積は大きくなる
このため、尺取りや二分探索がそのままでは使えず、場合分けが必要となる。
Ai を正の要素と負の要素に分けた数列を A と B とし、昇順にソートしておく。(0は捨て置く)
負の要素は |A|×|B| 個、正の要素は |A|(|A|−1)2+|B|(|B|−1)2 個、それ以外は0なので、K の値によって答えが正か負か0かはわかる。
負の場合
ペアの正負は(負, 正)となる。負の方 x を固定し、正の数列 A に ax 「以上」となる数がいくつあるかを尺取りで求めていけばよい。
絶対値が大きいほど小さい数である、という点に注意。
正の場合
A,B それぞれ、同じ方から2つ選べばよい。組合せなので「同じ要素は選べない」「(a,b)と(b,a)は同じ」ことに留意する必要がある。
とはいえ、まずは気にせず全て数えてしまった方が楽なので数える。
その後 「数列に含まれる √a 以下の値の数だけ同じ要素が選ばれているので引く」 「残る値は全て2回ずつ同じペアが数えられているので、2で割る」とすると、正しい値となる。
境界に注意
- ax を整数に丸める際の方向
- 小数のままでもいいのだが、誤差が発生するとヤなのと、一般に型を揃えた方が高速に動作する場合が多い
- 「以下」か「未満」か
実装においては、これらの丸める方向のために1だけズレた、みたいなことが発生しやすい。
特に負の数では、int(-2.5)
はどちらに切り捨てられるか、など言語仕様にも絡んでくる。(Pythonでは絶対値の小さい方に丸められ、-2
となる)
何か、混乱しない考え方みたいなのはないだろうか……。とりあえずこの辺に注意して実装する必要がある。
Pythonでは
計算量を最悪ケースで考えると二分探索は全体で O(Nlog2N)、尺取りでは O(NlogN) となり、尺取りの方が速い。
実際、判定に二分探索を用いる方法では、PyPyを使ってもTLEが解消できないケースがあった。
二分探索はbisectモジュール一発で楽なのだが、尺取りでの書き方も覚えておいた方がよい。
また、NumPyでは複数の値を一気に二分探索できる searchsorted() があるので、そちらを使ってもよい。
E - Payment
問題
- ある国には、通貨として1円、10円、100円、……、10∞ 円玉がある
- お店で N 円の品物を購入したとき、「支払う硬貨の枚数」と「お釣りとして受け取る硬貨の枚数」の合計を最小化せよ
- 1≤N≤101000000
解法
実生活でもよくやるやつ。300円なら100円玉×3で払うけど、800円なら1000円札出して100円玉×2受け取った方が楽、みたいな。
すると、各桁につき、有効な出し方はある程度制限されてくる。
- ぴったり出す
- 1枚多く出す
- 出さない
ただ、「出さない」を選択できるのは上位桁で1枚多く出しているときのみ、など、それぞれを取れる「状態」がある。
上の桁からDPで決めていくことを考えると、いろいろな N で試した結果、状態は以下の2つに集約できる。
- ぴったり出している状態(ぴ)
- 上位桁で1枚多く出している状態(超)
遷移は、以下のケースが考えられる。現在の桁の N における数字を d として、
- (ぴ)→ぴったり出す→(ぴ) … d 枚
- (ぴ)→1枚多く出す→(超) … d+1 枚
- (超)→ぴったり出す→(ぴ) … d+1 枚
- (超)→1枚多く出す→(超) … d+2 枚
- (超)→出さない→(超) … 9-d 枚お釣り
(ぴ)は、N の最上位から完全にぴったり、というわけではなく、途中で1枚多い状態になって再度この状態になる、ということも可能である。 引き算の筆算で、繰り下がり用の「1」を保持しているか否か、と言い換えた方がいいかも。
たとえば、N=28119を支払うとき、30120を払って2001返されるのが最適である。
2 8 1 1 9 3 0 1 2 0 超 超 ぴ 超 超
この時、真ん中の1は、超過からぴったりに戻す、と考える。
既に上位の桁で N 円以上であることは確定しているので、金額的には次の桁で「出さない」を選択することも可能だが、そうすると必ずくり下がりが発生し、お釣りが9枚になる桁が出来て得にならない。
2 8 1 1 9 3桁目でぴったりの枚数出した後、4桁目で出さない 3 0 1 0? → 3桁目でお釣りが9枚となることが確定する → なら最初から0枚で(超)のまま遷移させておいた方が8枚となり損しない
先頭の処理
N=99の時、100円出して1円お釣りが最適である。このように、N より1桁上位の硬貨を出して、いきなり(超)状態ではじめることも考慮に入れる必要がある。
末尾の処理
(超)の状態は、繰り下がり用の1を常に用意しているようなものなので、それが不要になったとき、および末尾が(超)で終わった場合については、お釣りに1枚プラスされる。
N 2 8 1 1 9 1 9 3 0 1 2 0 2 0 超 超 ぴ 超 超 超 超 +3 +1 +1 +2 +0 +2 +0 +1 +1 +1 ↑ ↑ ↑ こことこことここで、上位桁から引き継がれていた1がお釣りとして清算される
(超)の状態からぴったりまたは1枚多く出すと、その時点で上位桁からの繰り下がりが不要になり、1つ上位桁の硬貨のお釣りが1枚増える。
遷移では、これを下位桁の遷移時に+1することで、辻褄を合わせている。
1 2 3 4 5 6 7 8 9 10 |
n = input () precise = 0 over = 1 for d in n: d = int (d) new_over = min (precise + d + 1 , over + d + 2 , over + 9 - d) precise = min (over + d + 1 , precise + d) over = new_over over + = 1 print ( min (precise, over)) |
F - Perils in Parallel
問題
- ON/OFFのスイッチが N 個あり、i 番目のスイッチは座標 Ai に置かれ初期状態は Bi (0/1) である
- 取れる操作が M 個あり、i 番目の操作では、区間 [Li,Ri] にあるスイッチのON/OFFを一斉に切り替える
- 全てのスイッチをOFFにすることが可能か判定し、可能なら操作手順も求めよ
- 1≤N≤105
- 1≤M≤2×105
- 1≤Ai,Li,Ri≤109
解法
区間に対する一律の操作は、隣接要素の差分に変換し、両端への2回の操作に置きかえるのがよくある解法。
今回の場合、スイッチを並べ、隣の要素のON/OFFと同じなら0、異なるなら1とした差分列を作る。
スイッチ 0 0 1 1 0 1 0 1 差分 0 0 1 0 1 1 1 1 1 ↓ |---------------| この区間を反転 スイッチ 0 1 0 0 1 1 0 1 差分 0 1 1 0 1 0 1 1 1 差分において、反転区間の両端の0/1のみが切り替わっているのがわかる
で、差分を全て0にできれば、スイッチは全て同じ状態となっている。 そのままでは全て“0”か“1”かわからないので、元のスイッチ列の先頭か末尾にダミーで“0”を入れておくと、“0”となっていることが保証できる。
下準備
スイッチを座標 Ai の昇順に並べ、indexを振りなおす。ON/OFF状態 Bi も、連動して並べ替えておく。
区間 [Li,Ri] を、座標ではなくスイッチのindex(何番目から何番目のスイッチを切り替えるのか)に置きかえ、[li,ri) の形にしておく。二分探索が使える。
B に、両端にダミーの“0”を追加した上で隣接要素の差分をとり、C とする。
さて、これで各操作は「Cli,Cri の0/1を切り替える」という処理に変わり、これを上手く選んで C を全て0にすればよいということになった。
入力例1 A スイッチ座標 5 8 10 B スイッチON/OFF 1 0 1 C 差分 1 1 1 1 操作1 * * 操作2 * * 操作4 * * ※操作3は意味が無いので除外
操作の組合せの探索
ここからどうしようか倦ねる。
解説では、(li,ri) を辺として結んだグラフから全域森を作るといいらしい。
全域森以外の辺は考えなくていいのかと思うが、一例を示せばいいので「全域木でできること」が示せるならそれでいい。
一応、ざっくりした理解としては、全域森以外の辺を採用すると閉路が出来るが、
i 0 1 2 3 4 5 操作1 *-----------* 操作2 *-------* 操作3 *---* 操作4 *---------------*
ここで、「閉路のどれか1つの操作」を行った結果は、「閉路のそれ以外の全ての操作」を行った結果と一致する。 今回は特に操作回数の最小化などは求められていないので、等価なものがあるなら考慮から省いてよい。
さて、木構造に辺を絞ると、ある辺(に相当する操作)が必要かどうか、考えやすくなる。
全域森の1つの木 T に含まれる Ci のXORを ST とすると、操作によって、ST は変化しない。(常に T 内の2つに対して操作を行うため)
よって、ST=1 である木が1つでもあれば、その木に含まれる Ci の全てを0にすることは不可能であり、全体としても不可能である。
逆に ST=0 なら、必ず全てを0にすることができる。
手順としては、適当に根を決め、葉から順に Ci が1なら親 j と結ばれる辺を採用し、Ci,Cj を反転させる、を繰り返せばよい。
ST も、あらかじめ求めずとも、最終的に根の Ci が1であれば ST=1 であったとわかる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
import sys from bisect import bisect_left, bisect sys.setrecursionlimit( 10 * * 5 + 5 ) class UnionFind: def __init__( self , n): self .table = [ - 1 ] * n def _root( self , x): if self .table[x] < 0 : return x else : self .table[x] = self ._root( self .table[x]) return self .table[x] def find( self , x, y): return self ._root(x) = = self ._root(y) def union( self , x, y): r1 = self ._root(x) r2 = self ._root(y) if r1 = = r2: return d1 = self .table[r1] d2 = self .table[r2] if d1 < = d2: self .table[r2] = r1 self .table[r1] + = d2 else : self .table[r1] = r2 self .table[r2] + = d1 def solve(n, m, aaa, bbb, lll, rrr): aaa, bbb = map ( list , zip ( * sorted ( zip (aaa, bbb)))) ccc = [bbb[ 0 ]] + [b0 ^ b1 for b0, b1 in zip (bbb, bbb[ 1 :])] + [bbb[ - 1 ]] cords = [ set () for _ in range (n + 1 )] uft = UnionFind(n + 1 ) for i, (l, r) in enumerate ( zip (lll, rrr), start = 1 ): li = bisect_left(aaa, l) ri = bisect(aaa, r) if li = = ri or uft.find(li, ri): continue cords[li].add((ri, i)) cords[ri].add((li, i)) uft.union(li, ri) use_cords = set () def dfs(v, p): res = ccc[v] for u, cord in cords[v]: if u = = p: continue ret = dfs(u, v) if ret = = 1 : use_cords.add(cord) ccc[u] = 0 res ^ = ret return res for v in range (n + 1 ): if ccc[v]: res = dfs(v, - 1 ) if res = = 1 : print ( - 1 ) return print ( len (use_cords)) print ( * sorted (use_cords)) inp = list ( map ( int , sys.stdin. buffer .read().split())) n, m = inp[: 2 ] aaa = inp[ 2 : 2 * n + 2 : 2 ] bbb = inp[ 3 : 2 * n + 2 : 2 ] lll = inp[ 2 * n + 2 :: 2 ] rrr = inp[ 2 * n + 3 :: 2 ] solve(n, m, aaa, bbb, lll, rrr) |