Processing math: 100%

AtCoder Beginner Contest 172 D,E,F問題メモ

D - Sum of Divisors

問題

  • 正整数 N が与えられる
  • f(x)=x の約数の個数、と定義する
  • Nx=1x×f(x) を求めよ
  • 1N107
  • 制限時間: 3sec

解法

公式解説に加え、maspy氏のHPに複数の解法が計算量別にまとめられている。

xa を約数に持つ」という関係を2次元表にすると以下のような感じになる

a \ x   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16
1       o   o   o   o   o   o   o   o   o   o   o   o   o   o   o   o
2           o       o       o       o       o       o       o       o    (2個おき)
3               o           o           o           o           o        (3個おき)
4                   o               o               o               o    (4個おき)
5                       o                   o                   o        (5個おき)
6                           o                       o
...

個々の 'o' について、その上にある x の値を全て合計したものが、答えとなる。

問題文はこれを縦に見て x の軸を 1N まで合計するように言っているが、視点を変えて a の軸を中心に横に見ても答えは変わらない。

ある数 a の約数は a,2a,3a,...N を超えるまで Na=k(切り捨て)個ある。

この合計は等差数列の和で a×k(k+1)2 となる。

これを a=1N まで合計すればよい。

1
2
3
4
5
6
7
8
9
10
def solve(n):
    ans = 0
    for i in range(1, n + 1):
        k = n // i
        ans += i * k * (k + 1) // 2
    return ans
 
n = int(input())
ans = solve(n)
print(ans)

O(N) 解法

上記のように「N までに a の倍数は b 個ある」は b=Na で求められる。

しかし、b が小さい場合(1個や2個など)、b が同じになる a は数多く存在する。b が小さい範囲については、それらをまとめて計算してやりたい。

a \ x   1   2   3   4   5   6   7   8   9  10  11  12
1       o   o   o   o   o   o   o   o   o   o   o   o     約数12個 
2           o       o       o       o       o       o ~~~ 約数6個 ~~~
3               o           o           o           o ~~~ 約数4個 ~~~ ↑aを中心に計算
4                   o               o               o ~~~ 約数3個 ~~~ ~~~~~~~~~~~~~
5                       o                   o         ~~~~~~~~~~~~~~~ ↓bを中心に計算
6                           o                       o     約数2個
7                               o                     ~~~~~~~~~~~~~~~
8                                   o                 以下約数1個
...

N までに倍数が b 個ある最大の a」は、a=Nb で求められる。

要は a×bN なので、N 付近で a,b の大小が切り替わる。

a<b の範囲は a を中心に計算し、a>b の範囲は b を中心に計算すると、探索範囲の上限は N となる。

注意すべきは a=b となるような場合で、重複して数えないようにする。

N までに倍数が b 個ある最大の数を q=Nbb+1 個(以上)ある最大の数を p=Nb+1 とすると、 a=p+1,p+2,...,q について、a+2a+3a+...+ba を求める計算になる。

これは、2つの等差数列の和のかけ算で、(p+1+q)(qp)2b(b+1)2 で求められる。

1
2
3
4
5
6
7
8
9
n = int(input())
ans = 0
for i in range(1, int(n ** 0.5) + 1):
    k = n // i
    ans += i * k * (k + 1) // 2
    if i != k:
        l = n // (i + 1)
        ans += i * (i + 1) * (k + l + 1) * (k - l) // 4
print(ans)

E - NEQ

問題

  • 以下の条件を満たす長さ N の2つの数列 A,B の組が何通りあるか mod109+7 で求めよ
  • 条件
    • 全要素は 1 以上 M 以下の整数
    • A の要素は全て異なる
    • B の要素は全て異なる
    • 全ての i について、AiBi
  • 1NM5×105

解法

包除原理。

AiBi という条件がなければ、1つの数列の作り方は M×(M1)×...N 個かけた数なので、MPN、それが2つで2乗。

この中から、条件を満たさないものを消していく。

必ず Ai=Bi となっている i の個数を k 個と固定し、残りはどっちでもよいとすると、

  • N 個中どの要素が同じか NCk
  • 同じ要素の具体的な値の決め方 MPk
  • 残りの具体的な値の決め方 MkP2Nk

これをあわせて、N!M!(Mk)!k!(Nk)!(MN)!2 となる。これを f(k) と表す。

包除原理を利用すると、f(0)f(1)+f(2)+...+f(N) が答えとなる。

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
def prepare(n, MOD):
    f = 1
    factorials = [1]
    for m in range(1, n + 1):
        f *= m
        f %= MOD
        factorials.append(f)
    inv = pow(f, MOD - 2, MOD)
    invs = [1] * (n + 1)
    invs[n] = inv
    for m in range(n, 1, -1):
        inv *= m
        inv %= MOD
        invs[m - 1] = inv
    return factorials, invs
 
 
n, m = map(int, input().split())
MOD = 10 ** 9 + 7
facts, finvs = prepare(m, MOD)
c = 1
ans = 0
base = facts[n] * facts[m] * finvs[m - n] * finvs[m - n] % MOD
for k in range(n + 1):
    tmp = base * facts[m - k] * finvs[k] * finvs[n - k] % MOD
    ans = (ans + c * tmp) % MOD
    c *= -1
print(ans)

F - Unfair Nim

問題

  • 2人で ニム を行う
  • 石山は N 個、山 i の初期の石の個数は Ai
  • ゲームを始める前に、1度だけ山1から山2に石を移して、後手必勝にしたい
    • 移せる個数は 0 個以上 A1 個未満(山1を0個にしてはいけない)
  • 可能か判定し、可能ならその最小個数を求めよ
  • 2N300
  • 1Ai1012

解法

まずニムの必勝法を知っている前提となる。全ての山のXORの値が0なら後手必勝、違うなら先手必勝。

なので、A3AN のXORはまず計算してやる(X とする)。操作後の山1、山2の個数を p,q として、pqX=0 ならよい。

これを含め、問題設定を考えると、以下の条件に言い換えられる。A1+A2=S とする。

  • pq=X
  • p+q=S
  • 0<pA1
  • p は上記の条件を満たす中での最大値
  • A1p を求めよ

2進数で考えて、p,q を0で初期化して上から貪欲に決めていく。(厳密には貪欲には決められないケースが1つあるけど、それは後述)

上から d 桁目が表す数を b とする。

Xd 桁目が'1'のとき

p,qd 桁目は、どちらかが'1'、どちらかが'0'。

ここで、S<p+q+b の場合、どちらかに'1'を立てたら S をオーバーしてしまうので、不可能。

もし pb を加えても A1 をオーバーしないのであれば、最大化したいので p の方に足す。

オーバーしてしまうなら、q の方に足す。

Xd 桁目が'0'のとき

p,qd 桁目は、どっちも'1'か、どっちも'0'か。

ここで、S の値により、どちらを取るかがわかる。

  • S<p+q+2b の場合
    • 両方'1'を立てたら S をオーバーしてしまうので、どっちも'0'確定
  • Sp+q+2b の場合
    • もし両方'0'なら、それ以下の p,q の桁をすべて'1'にしても S に足りなくなってしまうので、どっちも'1'確定

ただし、どっちも'1'の場合に問題が起こる。ひょっとしたら、p に'1'を立てたら A1 をオーバーしてしまうかも知れない。

d     123456
------------
A1    010010
A2    100100
X     110000
------------
p     01001_  (S 残り 2)
q     10001_

6桁目、両方1を立てなければならないが、立てたら A1 をオーバーする。

ここで、「それより上の桁に、p が'1'、q が'0'の桁があったか?」がポイントとなる。 あったら、そのうち最も低い桁の '1' を q に移すことで、p+q を変えずに p は減り、6桁目に'1'を立ててもオーバーしなくなる。

p     000011
q     110011
       ~

そういう桁が無かったら不可能。(というのが未証明)

Xd 桁目が'1'のとき、最後に p の方に'1'を立てた桁」を記録しておけばよい。


本番では貪欲で実装後、1ケースだけWAになり、そのケースを見つけるのができなかった。 未証明貪欲は危ないので、最初からきちんとDPで考えないとね。 (今回、貪欲解が少しだけの補正で済んだのはたまたまな気がする)

偽貪欲という結構根本的な間違いでも、WAとなるのは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
import sys
 
 
def solve(n, aaa):
    if n == 2:
        h, m = divmod(sum(aaa), 2)
        if m == 1 or h > aaa[0]:
            return -1
        return aaa[0] - h
 
    x = 0
    for a in aaa[2:]:
        x ^= a
    a0, a1 = aaa[:2]
    s = a0 + a1
 
    if s & 1 != x & 1:
        return -1
 
    p, q = 0, 0
 
    b = 1 << 40
    last_b = -1
    while b:
        bx = x & b
        if bx == 0:
            if s >= 2 * b:
                if p | b <= a0:
                    p |= b
                    q |= b
                    s -= 2 * b
                elif last_b > 0:
                    p ^= last_b
                    q ^= last_b
                    p |= b
                    q |= b
                    s -= 2 * b
                else:
                    return -1
        else:
            if s < b:
                return -1
            if p | b <= a0 and q | b > a1:
                p |= b
                last_b = b
            else:
                q |= b
            s -= b
 
        b >>= 1
 
    if p == 0:
        return -1
    assert s == 0
    assert p ^ q == x
    assert a0 - p == q - a1
    return a0 - p
 
 
n, *aaa = map(int, sys.stdin.buffer.read().split())
print(solve(n, aaa))

上記をメモ化DFSにしたもの。あまり変わっていないが、こちらなら貪欲で破綻したとき、戻って違う方を試す、というのが明確になる。

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
import sys
from functools import lru_cache
 
 
def solve(n, aaa):
    if n == 2:
        h, m = divmod(sum(aaa), 2)
        if m == 1 or h > aaa[0]:
            return -1
        return aaa[0] - h
 
    x = 0
    for a in aaa[2:]:
        x ^= a
    a0, a1 = aaa[:2]
    s = a0 + a1
 
    if s & 1 != x & 1:
        return -1
 
    b = 1 << 40
    MASK = (b << 1) - 1
 
    @lru_cache(maxsize=None)
    def dfs(p, b):
        if b == 0:
            return p
        q = p ^ x & (MASK ^ ((b << 1) - 1))
        if x & b == 0:
            if s >= p + q + 2 * b:
                if p | b <= a0:
                    return dfs(p | b, b >> 1)
                else:
                    return -1
            return dfs(p, b >> 1)
        else:
            if s < p + q + b:
                return -1
            if p | b <= a0:
                ret = dfs(p | b, b >> 1)
                if ret != -1:
                    return ret
            return dfs(p, b >> 1)
 
    ret = dfs(0, b)
 
    if ret <= 0:
        return -1
    return a0 - ret
 
 
n, *aaa = map(int, sys.stdin.buffer.read().split())
print(solve(n, aaa))

programming_algorithm/contest_history/atcoder/2020/0627_abc172.txt · 最終更新: 2020/06/28 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0