Loading [MathJax]/jax/output/CommonHTML/jax.js

AtCoder Beginner Contest 155 D~F問題メモ

AtCoder Beginner Contest 155

アルストツカに栄光あれ

D - Pairs

問題

  • N 個の整数 A1,A2,...,AN がある
  • ここから2個選んで積をとってできる N(N1)2 個の整数の内、K 番目に小さいものを求めよ
  • 2N2×105
  • 109Ai109

解法

実装がちょっと面倒な二分探索。

このように、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 を 小→大 で動かすと、ji のループごとにリセットせず、全体で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より大きく、K8 なら答えは12以下である。

しかし、今回は負の数も絡むというのが厄介で、

  • ペアの一方が正の場合は、もう一方も大きくなるほど積も大きくなる
  • ペアの一方が負の場合は、もう一方は小さくなるほど積は大きくなる

このため、尺取りや二分探索がそのままでは使えず、場合分けが必要となる。

Ai を正の要素と負の要素に分けた数列を AB とし、昇順にソートしておく。(0は捨て置く)

負の要素は |A|×|B| 個、正の要素は |A|(|A|1)2+|B|(|B|1)2 個、それ以外は0なので、K の値によって答えが正か負か0かはわかる。

負の場合

ペアの正負は(負, 正)となる。負の方 x を固定し、正の数列 Aax 「以上」となる数がいくつあるかを尺取りで求めていけばよい。

絶対値が大きいほど小さい数である、という点に注意。

正の場合

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() があるので、そちらを使ってもよい。

Python実装例

E - Payment

問題

  • ある国には、通貨として1円、10円、100円、……、10 円玉がある
  • お店で N 円の品物を購入したとき、「支払う硬貨の枚数」と「お釣りとして受け取る硬貨の枚数」の合計を最小化せよ
  • 1N101000000

解法

実生活でもよくやるやつ。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にすることが可能か判定し、可能なら操作手順も求めよ
  • 1N105
  • 1M2×105
  • 1Ai,Li,Ri109

解法

区間に対する一律の操作は、隣接要素の差分に変換し、両端への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)

programming_algorithm/contest_history/atcoder/2020/0216_abc155.txt · 最終更新: 2020/02/21 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0