AtCoder Beginner Contest 177 E,F問題メモ

E - Coprime

問題

  • $N$ 個の正整数 $A_1,A_2,\ldots,A_N$ が与えられる
  • 以下を出力せよ
    • 全てのペアについて互いに素なら「pairwise coprime
    • $N$ 個全体に1以外の共通の約数が無ければ($GCD(A_1,A_2,\ldots,A_N)=1$ なら)「setwise coprime
    • そうでないなら「not coprime
  • $1 \le N \le 10^6$
  • $1 \le A_i \le 10^6$

解法

まず最初に、全てのgcdを取ることで「pairwise or setwise」か「not」かは判別できる。結果が1以外なら「not」確定。

pairwise と setwise の判別をどうするか。

まず、pairwise な状態では、どの2数の素因数も被っていない。
しかし、$N$ から $A_i=1$ であるものを除いた個数が、 $10^6$ 以下の素数の個数以上あったら、その時点でどれか2つには共通の素因数が含まれざるを得ない。

$10^6$ 以下の素数の個数は、検索すれば78498個とわかる。つまり、$A_i \ge 2$ の個数がこれより大きければ「setwise」と判定できる。
これにより、実質的に考慮すべき $N$ がぐっと小さくなった。

そしたら、後は $\sqrt{A_{max}} = 1000$ までの全ての素数で各 $A_i$ を試し割っていく。
「これまでの素数で割られた後の $A_i$ の値」を $B_i$ として、集合で持っておく。

もしどれか2つが共通の素因数を持つなら、$B_i$ にかぶりが発生する。

$B_i$ を素数 $p$ で割ったとき、

  • 2つ以上の $B_i$ が $p$ で割り切れてしまったら setwise
  • $p$ で割った後の新しい $B_i$ が1以外の時、他の $B_j$ と被ってしまったら setwise

上記のようなことがなく最後まで処理できたら pairwise となる。

重複の判定には、$B_i$ を set で管理して、別途「$B_i=1$ の個数」を数えて、合わせて要素数が $N$ になるかでチェックした。
だが、$10^6$ 程度ならその分の配列を用意して、存在する・しないフラグを使った方が楽だったね。

計算量は、$P(N)$ を「$N$ 以下の素数の個数」として、$O(\max(N,P(A_{max})) \times P(\sqrt{A_{max}}))$

import os
import sys
from math import gcd

import numpy as np


def solve(inp):
    n = inp[0]
    aaa = inp[1:]

    g = 0
    for i in range(n):
        g = gcd(g, aaa[i])
    if g != 1:
        return -1

    PRIMES = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103,
              107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223,
              227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347,
              349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463,
              467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607,
              613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743,
              751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883,
              887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997)

    one_count = (aaa == 1).sum()
    val = set(aaa)
    if len(val) + max(0, one_count - 1) < n:
        return 0

    for p in PRIMES:
        multiple_count = 0
        for a in val:
            b = a
            if b % p == 0:
                multiple_count += 1
                b //= p
                while b % p == 0:
                    b //= p
                if b == 1:
                    val.remove(a)
                else:
                    if b in val:
                        return 0
                    val.remove(a)
                    val.add(b)

        if multiple_count > 1:
            return 0

    return 1


if sys.argv[-1] == 'ONLINE_JUDGE':
    from numba.pycc import CC

    cc = CC('my_module')
    cc.export('solve', '(i8[:],)')(solve)
    cc.compile()
    exit()

if os.name == 'posix':
    # noinspection PyUnresolvedReferences
    from my_module import solve
else:
    from numba import njit

    solve = njit('(i8[:],)', cache=True)(solve)
    print('compiled', file=sys.stderr)

inp = np.fromstring(sys.stdin.read(), dtype=np.int64, sep=' ')
ans = solve(inp)
print(['setwise coprime', 'pairwise coprime', 'not coprime'][ans])

F - I hate Shortest Path Problem

問題

  • $(H+1) \times W$ のグリッドを、右か下のみに移動する
  • 始点は、1段目の好きな位置を選んで決める
  • $i=1~H$ について、$i$ 段目 $A_i~B_i$ 列目のマスから下に移動することは出来ない(そこに移動してくることは出来る)
  • $k=2~H+1$ について、$k$ 段目のいずれかのマスまで降りるために必要な最小移動回数を求めよ
    • 到達できない場合は-1を答えよ

  1234   A B  答
1 v___   2 4
2 __vv   1 2   1
3 v__v   2 3   4
4 vvv_   4 4   6
5 ....        -1

_: 降りれない
v: 降りれる

解法

順序付き集合(std::setなど)による状態管理。

$k$ 段目までの下移動の回数は $k-1$ 回と決まっているので、同じ段では右移動の回数の最小値を考えればよい。
右移動の回数は「降り立つ列番号 - 始点の列番号」なので、なるべく右には移動せず、下に降りられる限りは下に降りるように移動するとする。

すると、壁に出会すたびに、降り立つ列番号は徐々にまとめられていく。

例えば、1段目の $A_1~B_1$ 列目に壁があったら、$A_1~B_1+1$ 列目を始点とした時の2段目に降り立つ位置は全て $B_1+1$ になる。
この時、考慮する始点は $B_1+1$ だけ残して、$A_1~B_1$ は最小値になり得ないので除いてしまってよい。

 1  2 ...   A1 ... B1 B1+1
↓ ↓    ↓ → →  → ↓
            ~~~~~~~~~

同様に、$i$ 列目で $(A_i,B_i)=(2,10)$ として、$2~11$ の中で降り立つ可能性のある列番号が $3,4,8,9$ だった場合、
9だけを11に更新しつつ残して、3,4,8に降り立つルートは、以降の段目では除いてしまってよい。

1  2  3  4  5  6  7  8  9 10 11
     ↓ ↓          ↓ ↓
     → → → → → → → → ↓
  ~~~~~~~~~~~~~~~~~~~~~~~~~~ ↓

1段目から順に「現在の段目まで降り立つ可能性のある列番号」を、順序付き集合で管理する。$desc$ とする。

  12345678   desc
1 v__vvvvv   { 1 2 3 4 5 6 7 8 }
2 vvvvvv__   { 1 4 5 6 7 8 }
3 _vvvvvvv   { 1 4 5 6 }
4 vvvvv__v   { 2 4 5 6 }
5 vv__vvvv   { 2 4 5 8 }
6 v____vvv   { 2 5 8 }
7 vvvvvv__   { 6 8 }
8 ........   { 6 }

ある段目まで考慮したときの $desc$ の各要素を $D_1,D_2,\ldots$、またそれぞれに対応する、そこに降り立つために最も効率のよい始点を $S_1,S_2,\ldots$ とする。 $S_i$ と $D_i$ は、段目が進む毎に更新されることはあるが、各時点では1対1対応する。

以下のデータを管理する。

  • $rel$: $D_j→S_j$ 対応辞書
  • $getmin$: 現在有効な $D_j-S_j$ の集合を管理し、最小値を取得、また不要になった値を削除できるデータ構造

2番目は、C++ならstd::multisetで実装できるし、機能が無い言語なら、よくある代替手段として遅延削除優先度付きキュー1)を使うと可能になる。

  • 遅延削除優先度付きキュー
    • 優先度キューと、現在の正しい状態を保持するデータ構造を用意(今回は各 $S_i$ に対応する $D_i-S_i$ の値など)
    • 優先度キューから最小値を取得するとき、現在の正しい状態と一致するまでpopして捨てる

$i$ 段目を考える時、以下の要領で更新する。

  • $desc$ の中から、$A_i$ 以上 $B_i+1$ 以下の要素を抽出
  • 抽出された最大の要素を $D_{max}$ とし、
    • $desc$ から抽出された要素を全て削除、$B_i+1$ を追加
    • $rel[B_i+1]←S_{max}$ に更新($S_{max}$ は $D_{max}$ に対応する始点)
    • $getmin$ から、$D_j-S_j$ を全て削除、$B_i+1-S_{max}$ を追加
    • その時の $getmin$ における最小値に、縦移動の回数 $i$ 回を加えたものが、$i+1$ 段目の答え
  • ただし $B_i=W$(右端まで追いやられて降りれない)場合、$desc$ や $getmin$ への追加は無し
  • 途中で $getmin$ が無くなったら、以下は全て -1

計算量は、各段について「$desc$ に残る $A_i$ 以上 $B_i+1$ 以下のそれぞれに対して、削除したり更新したりの操作」を行う必要があるが、1段につき1列以外は削除され、1回削除した列は再び対象となることは無いので、全体を通して操作は最大でも $O(H+W)$ に抑えられる。1回の操作では順序付き集合から値を削除したり追加したりで、これは $O(\log{W})$ で可能である。

よって、$O((H+W)\log{W})$ となり、十分に間に合う。

std::setの代替

この問題では、$desc$($D_i$ の管理)と $getmin$($D_i-S_i$ の管理)に2種類の順序付き集合のデータ構造を用いる。

Pythonでは、前者は Binary Indexed Tree (Fenwick Tree)、後者は heapq で代替できる。

Fenwick Treeを用いた代替実装は、機能的にはおよそheapqの上位互換だが2)、indexの扱いなどで混乱しやすい。heapqは制約があるが比較的実装で迷いにくい。(個人の感想)

前者は、「$A_i$ 以上の最小の要素」~「$B_i+1$ 以下の最大の要素」を求める必要があるが、このように特定の値以上・以下を探すのはheapqではできない。
Fenwick Treeでは、要素がある場所に+1、無い場所は0として累積和を取ることで「$A_i$ 未満にいくつの要素があるか→$k$ とする」「累積和が $k+1$ となる最小の(つまり $k+1$ 番目の)要素は何か」を求めることで、「$A_i$ 以上の最小の要素」を取得できる。

対して $getmin$ は、参照は最小値さえできればよいので、値が有効かどうか別途管理しておけばheapqで代替できる。

import os
import sys

import numpy as np
from heapq import heappop, heappush, heapify


def solve(inp):
    def fenwick_add(arr, n, i, x):
        while i <= n:
            arr[i] += x
            i += i & -i

    def fenwick_sum(arr, i):
        result = 0
        while i > 0:
            result += arr[i]
            i ^= i & -i
        return result

    def fenwick_lower_bound(arr, n, log_n, x):
        sum_ = 0
        pos = 0
        for i in range(log_n, -1, -1):
            k = pos + (1 << i)
            if k < n and sum_ + arr[k] < x:
                sum_ += arr[k]
                pos += 1 << i
        return pos + 1

    def bit_length(x):
        res = 0
        while x:
            res += 1
            x >>= 1
        return res

    h = inp[0]
    w = inp[1]
    aaa = inp[2::2]
    bbb = inp[3::2]

    w1 = w + 1
    log_w = bit_length(w1)
    arr = np.zeros(w1 + 1, dtype=np.int64)
    for j in range(1, w1):
        arr[j] = j & -j
    fenwick_add(arr, w1, w1, 10 ** 18)
    horizontal = np.zeros(w + 1, dtype=np.int64)
    horizontal_rev = np.arange(w + 1, dtype=np.int64)
    q = [(0, j) for j in range(1, w + 1)]
    heapify(q)

    ans = np.full(h, -1, dtype=np.int64)

    for row in range(h):
        a = aaa[row]
        b = bbb[row]
        k = 1 if a == 1 else fenwick_sum(arr, a - 1) + 1
        c = fenwick_lower_bound(arr, w1, log_w, k)
        stack = []
        while c <= b + 1:
            stack.append(c)
            if c == w1:
                break
            k += 1
            c = fenwick_lower_bound(arr, w1, log_w, k)
        # print(a, b, k, c, horizontal, stack)

        if len(stack) == 0:
            ans[row] = row + q[0][0] + 1
            continue

        if stack[-1] == w1:
            found_nearest = True
            stack.pop()
        else:
            found_nearest = False
        stack.reverse()

        for c in stack:
            if found_nearest:
                fenwick_add(arr, w1, c, -1)
                horizontal[horizontal_rev[c]] = -1
            elif c == b + 1:
                found_nearest = True
            else:
                orig = horizontal_rev[c]
                horizontal[orig] = b + 1 - orig
                horizontal_rev[b + 1] = orig
                fenwick_add(arr, w1, c, -1)
                fenwick_add(arr, w1, b + 1, 1)
                heappush(q, (horizontal[orig], orig))
                found_nearest = True

        while q and q[0][0] != horizontal[q[0][1]]:
            heappop(q)

        if len(q) == 0:
            break

        ans[row] = row + q[0][0] + 1

    return ans


if sys.argv[-1] == 'ONLINE_JUDGE':
    from numba.pycc import CC

    cc = CC('my_module')
    cc.export('solve', '(i8[:],)')(solve)
    cc.compile()
    exit()

if os.name == 'posix':
    # noinspection PyUnresolvedReferences
    from my_module import solve
else:
    from numba import njit

    solve = njit('(i8[:],)', cache=True)(solve)
    print('compiled', file=sys.stderr)

inp = np.fromstring(sys.stdin.read(), dtype=np.int64, sep=' ')
ans = solve(inp)
print('\n'.join(map(str, ans)))

1)
なんて呼べばいいんだろう
2)
$O(1)$ と $O(\log{N})$ 程度の計算量の違いは重要視しないとする
programming_algorithm/contest_history/atcoder/2020/0829_abc177.txt · 最終更新: 2020/09/03 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0