−目次
AtCoder Beginner Contest 169 D,E,F問題メモ
なんか、厳密な証明はともかく「多分こうじゃね」って解法が思いつきやすい問題というのはある。今回は比較的そういう回だった。
D - Div Game
問題
- 正整数 N が与えられる
- N に対して、以下の操作を最大何回行うことができるか、求めよ
- 操作: N を、以下の条件を全て満たす整数 z で割った値で置き換える
- 素数 p と正整数 e で、z=pe と表せる
- N は z で割り切れる
- 過去の操作で z として選んだどの数とも異なる
- 1≤N≤1012
解法
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+4≤12<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=1~N のそれぞれについて、Ai≤Xi≤Bi であることは分かっている
- 中央値としてあり得る値の個数を求めよ
- 2≤N≤2×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 が与えられる
- この数列の、空でない部分集合は 2N−1 通りある
- ある部分集合を T として、f(T) を以下で定める
- T の部分集合であって、総和が S であるものの個数
- T としてあり得る全ての集合について f(T) を求め、その総和を mod998244353 で求めよ
- 1≤N≤3000
- 1≤S≤3000
解法
ある集合の、冪集合の、さらに部分集合を考えるので、こんがらがって問題文の意味の把握が難しい。
とりあえず、元の数列の全ての要素を使えるとして、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通りを考える。
- もともと T に i 番目の要素が選ばれていない
- T に i 番目の要素は選ばれたものの、和が 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]) |