TECH PLAY

フォルシア

フォルシア の技術ブログ

å…š241ä»¶

技術本郚の束本です。フォルシアでは、2019幎新卒の孊生を察象に、ワヌクショップや䌚瀟説明䌚を実斜しおいたす。゚ンゞニア志望者向けのワヌクショップでは、膚倧か぀耇雑なデヌタから高速に最適解を導く独自の「高速怜玢技術」を、プログラミングの実践を亀えながら孊べるプログラムを甚意しおいたす。 本日は特別に、4月24日火に青山で開催した゚ンゞニアワヌクショップで出題した問題を解説したす。 問題 Level1 Level2 Level3 たずめ 今埌のワヌクショップのご案内 問題 それでは、実際に出題した問題を芋おいきたしょう。 問題文 N人の身長 h_i ずQ個の質問ク゚リが䞎えられたす。 各質問ク゚リは、 (h_min_j, h_max_j) の2芁玠で構成されたす。 このずき、それぞれのク゚リに぀いお h_min_j <= h_i <= h_max_j を満たす人数を答えなさい。 ※身長、ク゚リはすべお敎数である。 入力 入力は暙準入力から以䞋の圢匏で䞎えられる。「←」以降は泚釈です N Q ← このあず身長のデヌタがN個、次いでク゚リがQ個入力される h_1 ← 1番目の身長 ïž™ h_N ← N番目の身長 h_min_1 h_max_1 ← 1番目のク゚リ ïž™ h_min_Q h_max_Q 出力 各ク゚リに察しお答えずなる倀を敎数で出力せよ。 Sample Input 0 3 1 4 9 5 3 5 Sample Output 0 2 Explanation 0 3人の身長 4,9,5 ずク゚リ (3, 5) が䞎えられたす。身長が3以䞊5以䞋の人は1番目(4)ず3番目(5)の2人です。したがっおク゚リ (3, 5) に察しお出力すべき倀は2です。 Sample Input 1 5 2 2 3 4 4 4 1 1 4 4 Sample Output 1 0 3 Explanation 1 身長が1以䞊1以䞋の人は0人です。身長が4以䞊4以䞋の人は3人です。 Sample Input 2 6 2 10 2 5 6 10 8 6 7 1 9 Sample Output 2 1 4 Explanation 2 身長が6以䞊7以䞋の人は1人です。身長が1以䞊9以䞋の人は4人です。 Level 1 䞊蚘の問題蚭定に具䜓的な制玄入力の倀域を䞎えお、各レベルの問題を芋おいきたいず思いたす。たず非垞に倀の小さい問題を考えたす。 制玄(Level1) 1 <= N <= 10 1 <= Q <= 10 1 <= h_i <= 10 1 <= h_min_j <= h_max_j <= 10 この問題を解決する単玔なプログラムを実装したす。 党員の身長を配列に栌玍し、 各質問に぀いお各人の身長が範囲内に収たっおいるかを刀定し、 条件に圓おはたる人数を答えたす。 䞋蚘がPython3の実装䟋です。 Python:Level1実装䟋 N, Q = map(int, input().split()) P = [] for i in range(N): h = int(input()) P.append(h) # 各質問ごずに for i in range(Q): h_min, h_max = map(int, input().split()) count = 0 # 各人の身長を刀定する for p in P: if h_min <= p <= h_max: count += 1 print(count) 䞊蚘アルゎリズムの蚈算量はQ回のルヌプ(8行目)の䞭にN回のルヌプ(13行目)が入っおいるため O(QN) になりたす。手元の環境で実行しおみるず0.1秒以䞋で出力が返っおきたす。 Level2 制玄(Level2) 1 <= N <= 10,000 = 10**4 1 <= Q <= 10,000 = 10**4 1 <= h_i <= 10,000 = 10**4 1 <= h_min_j <= h_max_j <= 10,000 = 10**4 Level2ではそれぞれの倀が1000倍になりたす。Level1で実装したプログラムでは、答えを出すたでに玄8秒の時間が必芁になりたす。 Level1で実装したプログラムのどこに時間がかかっおいるかを考えたしょう。2重ルヌプによっお合蚈 Q * N 回も身長の比范が行われおいたす。 実際に自分がこの䜜業を行うずしたらず考えおみるず、身長が明らかに範囲から倖れおいる人には䜕床も質問しないでしょう。 質問ごずに各人の身長を刀定するアルゎリズムは "毎回「党員」の身長を確かめる必芁がある" 点が非効率です。 それでは、どうすれば "「党員」に察しおの刀定" を避けられるかを考えおみたしょう。 〈Level2のポむント〉 数えるのではなく、蚈算する。 䞊限以䞋の人数から䞋限に満たない人数を匕くこずで条件を満たす人数を蚈算するこずができたす。 孊校の倧掃陀で「12番から16番の5人は窓拭き」のようなシヌンがありたせんでしたか 事前に゜ヌトしおおく。 ゜ヌトされおいるずいうのは匷い性質で、昇順に゜ヌトされおいお、i番目の人が䞋限未満の堎合、i-1番目以䞋の人も䞋限未満であるこずが明らかです。 これらのポむントをふたえるず、問題は次のように蚀い換えられたす。 〈問題の蚀い換え〉 ゜ヌトされた配列から䞋限未満の最埌の人、䞊限以䞋の最埌の人を芋぀けたい。 ここで、泚意したい点がありたす。前から順番に芋おいくず、探しおいる人が配列の埌ろの方にいた堎合、結局ほがすべおの人の身長を確認するこずになりたす。 もっず効率良く蚈算するために、䜕人かをピックアップしお質問をしたいず思いたす。 基本情報技術者詊隓を勉匷しおいる人や情報系の人なら聞いたこずがあるかもしれたせんが、 "真ん䞭の人から聞いお解の存圚空間を半分にする" ずいう考え方がありたす。 これは "二分探玢" ず呌ばれる探玢アルゎリズムの䞀぀で、今回の問題蚭定ず盞性が良いため、二分探玢を䜿っおみたしょう。 二分探玢を䜿甚しお「゜ヌトされた配列からある倀T以䞊の最初の人のむンデックス」を探したす。 P を身長が昇順に゜ヌトされた配列、配列 P 䞭で身長 T 以䞊の最初の人のむンデックスを探すずしお説明を進めたす。 P[lb] < T <= P[ub] (※)を満たすように lb, ub を初期化したす。この条件を保ちながら曎新するこずが二分探玢のポむントです。 m = (lb+ub)//2 (平均の小数点以䞋切捚)ずしお P[m] < T ⇒ lb = m , T <= P[m] ⇒ ub = m ず解の存圚範囲を曎新したす。 lb+1 == ub が成り立぀ずき、(※)の条件から ub こそが「ある倀T以䞊の最初の人のむンデックス」であるこずが分かりたす。 䟋ずしお䞋蚘の入力が䞎えられたずき、身長がh_min=3以䞊の最初の人のむンデックスを求めたす。 N = 12 H = {1, 2, 2, 4, 4, 5, 6, 6, 7, 7, 7, 10} h_min=3, h_max=6 P[0]=1 < T=3 であるこずを確認した䞊で、 lb=0, ub=len(P)=12 で初期化したす。※配列䞭にT以䞊の倀がない堎合はPの芁玠数(=12)を返したす。 m = (lb+ub)//2=(0+11)//2=5 ずなりたす。 P[m]=P[5]=6 >= T=3 なので ub = m=5 に曎新したす。 m = (lb+ub)//2=(0+5)//2=2 ずなりたす。 P[m]=P[2]=2 < T=3 なので lb = m=2 に曎新したす。 m = (lb+ub)//2=(2+5)//2=3 ずなりたす。 P[m]=P[3]=4 >= T=3 なので ub = m=3 に曎新したす。 lb+1 == ub が成り立ち、「身長がh_min=3以䞊の最初の人のむンデックス」は ub=3 であるこずが分かりたす。 凊理の流れが分かったずころで、実装をしおみたす。 Python:Level2実装䟋 """ ゜ヌトしお二分探玢 """ import bisect N, Q = map(int, input().split(" ")) P = [] for i in range(N): h = int(input()) P.append(h) # O( N*logN ) P.sort() # O( Q*logN ) for i in range(Q): h_min, h_max = map(int, input().split()) count = 0 lower = bisect.bisect_left(P, h_min) upper = bisect.bisect_left(P, h_max + 1) print(upper - lower) 暙準ラむブラリを䜿っおいる二分探玢郚分を曞き䞋すずこうなりたす。 """ 二分探玢 """ def bisect_left(array, target): if target <= array[0]: return 0 lb = 0 ub = len(array) while lb + 1 < ub: m = (lb + ub) // 2 if target <= array[m]: ub = m else: lb = m return ub 予め゜ヌトする手間はかかるものの、1回゜ヌトしおおくだけで、Q回のク゚リ凊理にかかる時間を倧幅に短瞮するこずが出来たす。事前にN人の身長を゜ヌトする蚈算量が O(NlogN) 、Q個のク゚リそれぞれに察しお、二郚探玢を行う蚈算量がO(QlogN)で、合蚈の蚈算量は O(NlogN) + O(QlogN) になりたす。 Level3 Level3では制玄を曎に10倍にした問題を考えたす。 制玄(Level3) 1 <= N <= 100,000 = 10**5 1 <= Q <= 100,000 = 10**5 1 <= h_i <= 100,000 = 10**5 1 <= h_min_j <= h_max_j <= 100,000 = 10**5 Level2でも高速に怜玢できおはいたすが、ただ無駄な郚分がありたす。1回のク゚リを凊理するずきに2回の二分探玢を実行し、前埌の境界ずなる人を探す䜜業をしおいたす。今回の問題蚭定では具䜓的な人を特定する必芁はなく、人数だけに着目すれば十分です。情報の持ち方を倉えお、各身長ごずに䜕人いるかを持っおみたしょう。 人数だけを配列に持った堎合、ク゚リに察する凊理は「䞋限から䞊限たでの各身長ごずの人数の和」ずしお衚珟するこずができたす。ここでも泚意が必芁ですが、ク゚リごずに䞋限から䞊限たでを足しおいったのでは、 O(QH) の蚈算量になり、身長の幅が倧きい堎合に改善がされたせん。 情報の持ち方を曎に䞀工倫しお、身長i以䞋の合蚈人数をi番目の芁玠ずしお持぀ような配列を考えたしょう。これは O(N+H) で準備できたす。Level2ず同じ具䜓䟋を䜿っお説明したす。 初めに身長の倀域分の配列を確保しおおき、各人の身長を読み蟌んだら、察応する倀を+1したす。この凊理は加算N回で O(N) です。次に P[i] = P[i] + P[i-1] ずしお P[i] が身長i以䞋の人数を衚すようにデヌタを持ちたす。この凊理は身長の䞊限たで足す必芁があるため、加算H_MAX回が必芁で O(H_MAX) です。 この配列䞊で、䞊限ず䞋限の匕き算を行うこずで、範囲に含たれる人数を答えるこずが可胜です。事前に人数を足しお持っおおくこずで、ク゚リに察しおは回の匕き算( O(1) )でレスポンスする事ができたす。この手法を环積和ずいいたす。 Level3のポむントを考えおみたしょう。 〈Level3のポむント〉 身長情報の持ち方を倉える 毎回の探玢さえ必芁ない 匕き算1回で蚈算すべし Python:Level3実装䟋 """ 环積和 """ H_MAX = 10**5 N, Q = map(int, input().split(" ")) P = [] lower_count = [0] * (H_MAX + 1) # 身長i以䞋の人数をlower_count[i]に栌玍する。 for i in range(N): h = int(input()) lower_count[h] += 1 # O( H_MAX ) for i in range(H_MAX): lower_count[i + 1] += lower_count[i] # O( Q ) for i in range(Q): h_min, h_max = map(int, input().split()) upper = lower_count[h_max] lower = lower_count[h_min - 1] print(upper - lower) # 人数が分かっおいるので匕き算だけで答えがわかる。 蚈算量は O(N) + O(H) + O(Q) になりたす。毎回の探玢が必芁ないため、高速に凊理をするこずが出来たす。 たずめ 今回のワヌクショップでは、制玄の倧きさに合わせおアルゎリズムの改善ず考え方を玹介したした。 デヌタ数が少ないなら、実装が簡単な党探玢。 デヌタ数が倚い堎合は゜ヌトしおおいお二分探玢。前凊理 デヌタ数だけが必芁になる堎合、デヌタ数だけ数えおおいお、匕き算する环積和の手法が䜿える。デヌタの持ち方を倉える ぀い先日も瀟内slackで「O(N)をO(logN)に改善したした」ずいう話題が䞊がっおいたした。蚈算量を考えながらアルゎリズムの改善をするこずに面癜さを感じた方、䞀緒に働けるのを楜しみにしおいたす 今埌のワヌクショップのご案内 実際のワヌクショップでは参加者の皆さんが、Level1で問題を理解しお、Level2の実装に取り組むずいう様子でした。競技プログラミングに慣れおいる方は、Level2たでは瞬殺、Level3にチャレンゞしおいたしたよ。 今埌もフォルシアでは、ワヌクショップの開催を予定しおいたす。゚ンゞニアずしおの知芋を広げたい方フォルシアのこずを知りたい方、ぜひご応募お埅ちしおおりたす ワヌクショップの日皋、お申し蟌みは こちら
アバタヌ