Processing math: 59%

AtCoder Beginner Contest 136 D~F問題メモ

AtCoder Beginner Contest 136

久々の全完。6問体制になってから普通にむずい。

D - Gathering Children

問題

  • 横一列の N 個のマス目に'L'か'R'が書かれている
    • 各マスにどちらが書かれているかは入力で与えられる
    • 1番目のマスには'R'が、N 番目のマスには'L'が書かれている
  • はじめ、各マスに1人ずつ人がいる
  • 以下の移動を10100回繰り返す
    • 現在、'L'のマスにいる人は左に、'R'のマスにいる人は右に移動する
  • 終了後、それぞれのマスに何人の人がいるか
  • 2N105

解法

昔、各マスに矢印が書かれていて「矢印の書いた方向にしか進めません」みたいな迷路があった記憶がある。 その迷路では、「→←」みたいなマスがあると、実質行き止まりである。

この問題もそうで、10100 回も移動すると全ての人は「RL」と連続するマスに集約され、そこを行ったり来たりするだけになる。

マスを、1区分が 'RRR…RRRLLL…LLL' となるように分割する。

RRLLLLRRRLRLRRRLL
↓
RRLLLL  RRRL  RL  RRRLL

人は区分を超えて移動できず、ある区分上にいた人は全て、終了後にはその区分の 'RL' に集約される。他のマスには残れないので0人。

次に考えることは、10100 回ちょうどの後には、'RL'の2マスのどちらにどれだけの人がいるかである。

これは、10100 が偶数なので、偶数回の移動では、偶数個離れたマスにしか移動できないことを使う。

'RL' の 'R' が、区分の左から奇数番目なら、その区分の中で最初に奇数番目のマスにいた人は'R' に、偶数番目のマスにいた人は'L'に集約されることが分かる。

偶数番目なら逆。

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
def update(ans, switch, i, prev_i):
    total = i - prev_i
    if (switch - prev_i) % 2 == 0:
        ans[switch - 1] = total // 2
        ans[switch] = total - total // 2
    else:
        ans[switch] = total // 2
        ans[switch - 1] = total - total // 2
 
 
s = input()
switch = -1  # RRR...RL...LLL の、'L'に切り替わったindex
prev_c = 'R'  # すぐ左のマスの文字
prev_i = 0  # 現在の区分の左端のindex
ans = [0] * len(s)
for i, c in enumerate(s):
    if prev_c == 'R' and c == 'L':
        switch = i
        prev_c = 'L'
        continue
    if prev_c == 'L' and c == 'R':
        update(ans, switch, i, prev_i)
        prev_i = i
        prev_c = c
update(ans, switch, len(s), prev_i)
print(*ans)

ダブリング解法

解説pdfにあった解法。

             i   0  1  2  3  4  5  6  7
                 R  R  L  L  L  R  R  L
1回の移動後のi   1  2  3  2  3  6  7  6

最初、i にいた人が1回の移動後にいるマスは、i から'R'なら+1、'L'なら-1される。

すると、2回の移動後にいるマスはどうなるか。

例えば i=0 の人は、1回目で0→1、2回目で1→2と移動する。

             i   0  1  2  3  4  5  6  7
                ↓↗↓
1回の移動後のi   1  2  3  2  3  6  7  6

1回の移動後にいる i を取得し、再度同じ配列で i の場所を辿れば、それが2回の移動後にいる場所になる。

2回の移動後のi   2  3  2  3  2  7  6  7

すると、今度は2回の移動後の配列に同じ事をすれば、4回の移動後の位置を求めることが出来る。

             i   0  1  2  3  4  5  6  7
                ↓,---'↓
2回の移動後のi   2  3  2  3  2  7  6  7

これを繰り返すと、8回、16回、32回……後の移動後の位置を知ることが出来る。

これで、例えば100回の移動後の位置を知りたいとき、100を2の冪乗の和にすると64+32+4であるので、これらの回数移動後の配列を順に通せば良い。

              i   0  1  2  3  4  5  6  7
                  v
  4回の移動後のi  2  3  2  3  2  7  6  7
                   `----v
 32回の移動後のi  2  3  2  3  2  7  6  7
                        v
 64回の移動後のi  2  3  2  3  2  7  6  7

100回の移動後のi  2  3  2  3  2  7  6  7  (例が短いので2回目以降の配列がみんな同じになっているが)

10100 回の移動は、2乗計算を繰り返すと333回程度でたどり着く。

ただし、今回の場合は正確に 10100 でなくとも「無限ループに陥った後の偶数回の移動後」ということさえ満たしていればよい。

無限ループには |S| 回移動すれば必ず到達するので、2乗計算を繰り返し、|S| を超えた時点で打ち切れば、その状態が答えと一致する。

最後に、移動後のindexから「それぞれの数が何個あるか?」を集計したものが答えだが、これはnumpy.bincountが便利。

この解法なら、無限ループに陥る前の状態や、そもそも無限ループに陥らないような移動法則の問題でも、解くことが出来る。

1
2
3
4
5
6
7
8
9
10
import numpy as np
 
s = input()
n = len(s)
a = np.arange(n) + np.array([1 if c == 'R' else -1 for c in s])
i = 1
while i < n:
    a = a[a]
    i <<= 1
print(*np.bincount(a, minlength=n))

E - Max GCD

問題

  • N 個の整数 A1,A2,...,AN がある
  • 以下の操作を K 回まで行える。行わなくてもよい。
    • 操作: 異なる2数を選び、一方を+1、もう一方を-1する
    • 数が負になってもよい
  • 操作後、N 個の全ての整数の最大公約数を g とすると、上手く操作を行うことで得られる g の最大値を求めよ
  • 2N500
  • 1Ai106

解法

約数列挙+貪欲。

N 個の整数の全体の和を S とすると、S は操作を行っても変わらない。また、各整数の最大公約数 g は、当然 S の約数でもある。

なので、S の約数を大きい順に列挙して、可能かどうか順に試していく。S は最大 5×108 である。

約数の個数の目安は、高度合成数を調べるとおおよそつけることができる。これは「自分未満に、約数の個数を自分以上持つ数が存在しない」数で、551350800が1200個らしい。

なので、多くともこれ以下の約数しか調べなくてよい。 それぞれにつき O(N) とか O(NlogN) とかで処理できればよい。

決め打ったdでの判定

ある約数 d を決め打ちして、それが K 回以内の操作で可能かどうかを判定したい。

Aid の倍数にするには、Ai%dm として、m 回引くか、dm 回足せばよい。(10を7の倍数にするには、10%7=3なので、3回引くか、4回足せばよい)

なるべく少ない回数の方を選びたい気持ちはあるが、全体で「引く回数」と「足す回数」は揃えないといけない。

d = 7
A      10   1   2  22
引く    3   1   2   1
足す    4   6   5   6

いずれの数も引く方が足すより操作回数が少ないが、
数を揃えるためにはどれかを足さないといけない

たとえば10を足すことにすれば、引く数と足す数を同じにできる

引く    3  ①  ②  ①
足す   ④   6   5   6

同じ数に「引く」「足す」操作を両方することは無駄なので、全ての数は「引くのみ」「足すのみ」に分離できる。

よって、引くべき数が小さい順にソートし、(このとき、当然「足すべき数が大きい順」でもある) 小さい方から t 個を「引くのみ」、残りを「足すのみ」に分類する。

両者の必要回数合計が同じで、かつ K 以下となれるような t が存在すれば、達成可能となる。累積和を用いればよい。

引く      1   1   2   3
足す      6   6   5   4
引_累積   1   2   4 | 7
足_累積  21  15   9 | 4
                    |  ←ここに境界を引いた時、最小回数4を達成。
                         これがK以下なら答えはd (大きい順に調べているので)

ほんとぉ?

このままでは、以下の証明が曖昧である。

  • 本当に分類は、引く数の小さい順に「引く」グループに加えていっていいの? 引く数が小さくても足すグループに使わないと達成不可能になったりしない?
  • 本当にその時に最小回数が達成されるの?

以下で説明できる。ある分類で、引くと足すの合計回数が同じにできたとする。

d=11
引く   1   2   4 |  6   9     1+2+4=7
足す  10   9   7 |  5   2     5+2  =7

この時、「引く」から1つ、「足す」から1つを入れ替えてみる。すると合計回数は再び同じになった。

引く   1   2   9 |  6   4      1+2+9=12
足す  10   9   2 |  5   7      5+7  =12

しかしこれ、そもそも ()=d() なので、「引く方での増分」と「足す方での増分」は必ず同じになる。 引く数が a の数字と b の数字(a<b)を入れ替えると、引く方は ba 増え、足す方は (da)(db)=ba 増え、同じになる。 よって、合計回数が同じ状態は必ず保たれる。

1個ずつの入れ替えで合計が同じ状態が必ず保たれるので、 片方から片方へ一方的に移したり、2個と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
from itertools import accumulate
 
 
def divisors(n):
    ret = set()
    for i in range(1, int(n ** 0.5) + 1):
        d, m = divmod(n, i)
        if m == 0:
            ret.add(i)
            ret.add(d)
    return sorted(ret, reverse=True)
 
 
def solve(n, k, aaa):
    s = sum(aaa)
    div = divisors(s)
    for d in div:
        mods = [a % d for a in aaa]
        mods.sort()
        subs = [0] + list(accumulate(mods))
        adds = [0] + list(accumulate(d - p for p in reversed(mods)))
        adds.reverse()
        for m, a in zip(subs, adds):
            if m == a:
                if m <= k:
                    return d
                break
            if m > a:
                break
 
 
n, k = list(map(int, input().split()))
aaa = list(map(int, input().split()))
print(solve(n, k, aaa))

F - Enclosed Points

問題

  • 「バウンディングボックス」とは、ある点の集合が与えられたときに「全ての点を含む、各辺が軸に平行な最小の長方形」である
  • 2次元平面上に N 個の宝石があり、i 番目の宝石は (xi,yi)(整数)
  • ここで、宝石の集合の取り方は、2N1 個ある(空集合を除く)
  • ある集合を s で表し、f(s) を「s のバウンディングボックス内部に含まれる宝石の個数」とする
  • 全ての集合に対して f(s) を求め、その総和を、\mod{998244353} で求めよ
  • 1 \le N \le 10^5
  • -10^9 \le x_i,y_i \le 10^9
  • 全ての宝石の x 座標は異なる
  • 全ての宝石の y 座標も異なる

解法

1つの集合を決め打った時、その内部に含まれる宝石の個数を各集合につき考えてもよいが、ちょっと面倒。

視点を変えると、「ある宝石を決め打った時、そいつがカウントされるような集合はいくつあるか?」を、 全ての宝石について求めて合計すれば、求める答えと同じである。

自身がカウントされる集合の個数

まず、自分自身が含まれる集合は、必ずそのバウンディングボックス内に自身が含まれる。このような集合は 2^{N-1} 通りある。

また、自身が含まれない集合でも、以下の2つはバウンディングボックスに自身が含まれる。

  • 1.「自身の左上のどれか」と「自身の右下のどれか」が共に含まれる集合
  • 2.「自身の左下のどれか」と「自身の右上のどれか」が共に含まれる集合

この個数はどのようにして求めるか。

いま、ある宝石からナナメ4方向にある宝石の個数が分かっているとする。

  b  |  d
-----o-----
  a  |  c

この時、「自身の左下 a 個のどれかが1つは含まれる集合」というのは、 空集合でなければよいので、2^a-1 (×a以外の宝石の選び方)となる。

よって、上記1.は (2^b-1)(2^c-1)2^{a+d} で求められる。これをBCとする。

同様に、2.は (2^a-1)(2^d-1)2^{b+c} で求められる。これをADとする。

しかし、この中には「4方向の全てに、どれか1つが含まれる集合」が重複して数えられているので、それを除く。

  • (2^a-1)(2^b-1)(2^c-1)(2^d-1)、これをABCDとする。

これで、ある宝石について、自身が数え挙げられる集合の個数は、以下で求めることができた。

  • 2^{N-1}+BC+AD-ABCD

a,b,c,dの求め方

まず、x 座標が小さい順にソートすると、その順番から a+bc+d は分かる。

また、座標の範囲が大きいので、あらかじめ y座標を圧縮して、小さい順に 1,2,3,... と座標を振り直しておく。 大小関係さえ保たれていれば、答えは変わらない。

すると、y_i の値によって a+cb+d もわかる。

ここで x 座標が小さい方から処理すれば、Binary Indexed Tree 等を使って、 a、つまり「x 座標が x_i より小さく、y 座標が y_i より小さいものの数」は O(log{N}) で求めることができる。

すると、残りも以下のように連鎖的に求められる。

i = a+b なので、b=i-a。(i は0-indexedとする)

y_i が1からの連番に振り直されているので、y_i = a+c+1 なので、c=y_i-a-1

N-i-1 = c+d より、d=n-i-1-c

これで、i 番目の宝石のa~dがわかり、答えが求められた。

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
import sys
 
 
class Bit:
    def __init__(self, n):
        self.size = n
        self.tree = [0] * (n + 1)
 
    def sum(self, i):
        s = 0
        while i > 0:
            s += self.tree[i]
            i -= i & -i
        return s
 
    def add(self, i, x):
        while i <= self.size:
            self.tree[i] += x
            i += i & -i
 
 
MOD = 998244353
n = int(input())
xys = []
ys = []
for line in sys.stdin:
    x, y = map(int, line.split())
    xys.append((x, y))
    ys.append(y)
ys.sort()
y_dict = {y: i + 1 for i, y in enumerate(ys)}
xys = [(x, y_dict[y]) for x, y in xys]
xys.sort()
bit = Bit(n)
 
p2d = [1]
for i in range(n):
    p2d.append(p2d[-1] * 2 % MOD)
 
ans = 0
for i, (x, y) in enumerate(xys):
    a = bit.sum(y)
    b = i - a
    c = y - a - 1
    d = (n - i - 1) - c
    bit.add(y, 1)
    pa, pb, pc, pd = p2d[a], p2d[b], p2d[c], p2d[d]
    tmp = p2d[n - 1]
    tmp += (pa - 1) * (pd - 1) * (pb + pc - 1) % MOD
    tmp += (pb - 1) * (pc - 1) % MOD * pa * pd % MOD
    ans = (ans + tmp) % MOD
print(ans)

programming_algorithm/contest_history/atcoder/2019/0804_abc136.txt · 最終更新: 2019/08/06 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0