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

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

AtCoder Beginner Contest 169

なんか、厳密な証明はともかく「多分こうじゃね」って解法が思いつきやすい問題というのはある。今回は比較的そういう回だった。

D - Div Game

問題

  • 正整数 N が与えられる
  • N に対して、以下の操作を最大何回行うことができるか、求めよ
    • 操作: N を、以下の条件を全て満たす整数 z で割った値で置き換える
      • 素数 p と正整数 e で、z=pe と表せる
      • Nz で割り切れる
      • 過去の操作で z として選んだどの数とも異なる
  • 1N1012

解法

N を素因数分解したとき、N=2a3b5c と表せるとすると、z を作るのに使える素数は 2,3,5 のいずれか。他の素数を使うと N を割り切れない。

1つの素数しか使わないので、例えば p=2 として z を作って N を割っても 3b5c の部分には影響がない。素数ごとに考えることができる。

さらに、同じ z を使ってはいけないので、p=2,e=1 として z=2 を使うと、次からは p=2 の時は別の e を使わなければならない。

操作をできるだけ多くしたいので、e は小さい方から 1,2,3,... と使っていくのがよい。

よって、素因数分解の後、指数 a,b,c に着目し、それぞれが a=1+2+3+... でどこまでの和で表せるかを調べればよい。

たとえば N=21237547 なら、1+2+3+412<1+2+3+4+5 のため、z=21,22,23,24 まで操作できて、25 は無理。

同様に3は3回、5は2回、7は1回処理できるので、あわせて10回が答え。

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
from collections import defaultdict
 
 
def prime_factorize(n):
    ret = defaultdict(int)
    while n % 2 == 0:
        n //= 2
        ret[2] += 1
    d = 3
    while d * d <= n:
        while n % d == 0:
            n //= d
            ret[d] += 1
        d += 2
    if n > 1:
        ret[n] += 1
    return dict(ret)
 
 
n = int(input())
pf = prime_factorize(n)
ans = 0
for c in pf.values():
    i = 1
    while i <= c:
        c -= i
        i += 1
        ans += 1
print(ans)

E - Count Median

問題

  • ある長さ N の数列の中央値を、以下で定義する
    • N が奇数の時、ソートしてちょうど真ん中の値
    • N が偶数の時、ソートして真ん中の2つの値を足して2で割った値
  • さて、いま数列 X1,X2,...,XN があるが、具体的な値は分かっていない
  • i=1N のそれぞれについて、AiXiBi であることは分かっている
  • 中央値としてあり得る値の個数を求めよ
  • 2N2×105

解法

ソートが必要なため実際は O(NlogN) であるものの、それ以外の実装はほぼ O(1) の、机の上で解く問題。

偶数の場合、複数の値がからんでややこしそうなため、とりあえず1つの値のみで決まる奇数の場合を考える。

「仮に k が中央値と仮定して、これが可能か?」を考える。「あり得る値」なので、他の値は k が中央値となるよう都合よく(問題文の条件の範囲で)決めてよい。

奇数の場合

中央値は、「自分より小さい値が N2 個」「自分より大きい値が N2 個」存在すればよい。(切り捨て)

Ai をソートして、小さい方から N+12 番目(Ai の中での中央値)が、中央値が取れる最小値となる。これより小さくなると、自分より小さい値が N2 個作れない。

この場合、自分より Ai の大きな数が確実に N2 個あるので、大きい方の個数の条件も満たせる。

      ┃-------------┃
    ┃------|            |----┃
         ┃-----|  |--------┃
         ↑          ↑
        最小        最大

中央値の取り得る最大値も、同様に考えると、Bi の中央値であることが分かる。

後は、最小と最大の間の整数なら、どんな数でも作れるか? を考える必要がある。

         Aj            Bk
|---|    ┃---|   |----┃   |-----|
       |----------------------|

最大・最小を取る Aj,Bk について、Xj,Xk の区間同士が断絶していたとしても、それを覆うような区間があれば、そいつが中央値になることはできる。 (Aj より右の区間では常に左に N2 個以上の数を存在させられ、Bk より左の区間にも常に右に N2 個以上の値を存在させられる)

つまり作れない場合、最小と最大の間に、どんな Ai,Bi でも作れない数があることになる。しかし、その場合、

|------|      |---|
   |-----|      |---|
     |--|         |---|

左に N+12 個、右にも同数の区間が必要になり、あわせて N+1 個の区間が必要となり矛盾する。

よって、どんな数でも作れる。+1 が答えとなる。

偶数の場合

真ん中の2数を別々に考えて M1,M2 として、それぞれが取り得る最小・最大は、N が奇数の場合と同様に考えられる。

  • たとえば N=12 として
    • M1…最小: Ai の小さい方から 6番目の数、最大: Bi の小さい方から 6番目の数
    • M2…最小: Ai の小さい方から 7番目の数、最大: Bi の小さい方から 7番目の数

すると、中央値の最小は、(M1)+(M2)2、最大は (M1)+(M2)2 となる。

また、こちらは2で割るので、0.5刻みで値を取ることができる点に注意。

奇数の場合と同様に考えると、M1 の取り得る範囲、M2 の取り得る範囲はそれぞれ連続であることが示せるので、中央値も最小・最大の間の全ての数を取れる。

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
import sys
 
 
def solve_even(n, aaa, bbb):
    li = n // 2 - 1
    ri = n // 2
    aaa.sort()
    bbb.sort()
    ll = aaa[li]
    lr = aaa[ri]
    rl = bbb[li]
    rr = bbb[ri]
    return (rl + rr) - (lr + ll) + 1
 
 
def solve_odd(n, aaa, bbb):
    k = n // 2
    aaa.sort()
    bbb.sort()
    return bbb[k] - aaa[k] + 1
 
 
n, *ab = map(int, sys.stdin.buffer.read().split())
if n % 2 == 0:
    ans = solve_even(n, ab[0::2], ab[1::2])
else:
    ans = solve_odd(n, ab[0::2], ab[1::2])
print(ans)

F - Knapsack for All Subsets

問題

  • 長さ N の正整数列 A1,A2,...,AN と、正整数 S が与えられる
  • この数列の、空でない部分集合は 2N1 通りある
  • ある部分集合を T として、f(T) を以下で定める
    • T の部分集合であって、総和が S であるものの個数
  • T としてあり得る全ての集合について f(T) を求め、その総和を mod998244353 で求めよ
  • 1N3000
  • 1S3000

解法

ある集合の、冪集合の、さらに部分集合を考えるので、こんがらがって問題文の意味の把握が難しい。

とりあえず、元の数列の全ての要素を使えるとして、Ai+Aj+...=S を満たすものの個数は、よくあるナップサック問題として求められる。

  • DP[i][w]=i 個目の要素まで見て、和が w となるような場合の数

で、そういう集合が、たとえば以下の例で、5通りあったとする。

N = 6   S = 7
A = {1, 1, 2, 2, 3, 6}

{1, 6}        1の選び方で2通り
{1, 1, 2, 3}  2の選び方で2通り
{2, 2, 3}

ある部分集合 T が、たとえば {2,2,3}f(T) に数え挙げるなら、T にこの3個の要素は当然入ってないといけなくて、残りの要素はどうだっていい。

よって、{2,2,3} が数えられるような T は、残りの4要素の決め方で 24 個存在する。

他の要素も同様に {1,6}25 個、{1,1,2,3}23 個存在するので、これらを全て総計して、答えは96となる。

しかし、通常のナップサック問題は、「Ai+Aj+...=S を満たす {Ai,Aj,...} の要素数」まではわからない。

上記の例では、「5通り」という部分しか分からない。これでは答えが求められない。

かといって、以下のようなDPでは、計算量が O(N3) となり、間に合わない。

  • DP[i][j][w]=i 番目の要素まで見て、そのうちの j 個を使い、和が w となるような場合の数

ここでは、T の個数を直接求めに行くとよい。日本語が難しいが、

  • DP[i][w]=i 個目の要素まで見て、「和が w となるような集合を作れるような T の個数」

つまり、通常のナップサック問題では i 番目の要素が選ばれなければそのままだが、このDPでは、以下の2通りを考える。

  • もともと Ti 番目の要素が選ばれていない
  • Ti 番目の要素は選ばれたものの、和が S になる集合には選ばれなかった

よって、DPの遷移において、i 番目の要素を選ばない場合の遷移を2倍する。

    w  0  1  2  3  4
初期   1
    x2↓   ↘           "↓"の遷移を2倍して行う
 3     2        1
      ↓↘     ↓↘
 1     4  2     2  1
 ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import sys
import numpy as np
 
n, s, *aaa = map(int, sys.stdin.buffer.read().split())
MOD = 998244353
 
dp = np.zeros(s + 1, np.int64)
dp[0] = 1
 
for a in aaa:
    ndp = dp * 2 % MOD
    if a <= s:
        ndp[a:] += dp[:-a]
    dp = ndp % MOD
 
print(dp[s])

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