困っていること
AtcoderのABC081のC問題を解いていたところ、私がはじめに書いたコードはTLE(実行制限時間超過)となってしまいました。
その後、ソースコードを修正し、そのコードは通ったのですが、最初に書いたコードのどの処理にコストが高くかかってしまったのかいまいちわからないため、教えていただきたいです。
問題とそのテストケースのリンクは以下の通りです。
ABC081 C問題
テストケース(DropBox)
pythonのバージョンは3.6.5を使用しています。
TLEとなったソースコード
from collections import Counter n, k = map(int, input().split()) a = Counter((i for i in input().split())) sort_count = sorted(a.items(), key=lambda x: x[1]) total = 0 while len(sort_count) > k: _, n = sort_count.pop(0) total += n print(total)
修正後のソースコード
from collections import Counter n, k = map(int, input().split()) a = Counter((i for i in input().split())) sort_count = sorted(a.values()) total = 0 l = len(sort_count) if len(sort_count) <= k: print(0) else: print(sum(sort_count[:l-k]))
試したこと
修正前のコードでは、出現回数を昇順にソートしたものから1つずつpopしてtotalを算出しています。
修正後のコードでは、出現回数のみを格納したソートされたリストからpythonの組込関数であるsumを用いてtotalを算出しています。
勉強不足で申し訳ございませんが、どの部分の処理にコストが高くかかっているのか知りたいです。
よろしくお願いします。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答3件
0
面白そうなので、line_profilerというものを使ってプロファイリングしてみました。
参考:
Python: line_profiler でボトルネックを調べる - CUBE SUGAR CONTAINER
道具立てのためにbefore.pyとafter.pyを作り、それぞれline_profilerでプロファイリングします。
before.py
python
1from collections import Counter 2 3with open("1.txt", "r") as f: 4 text_str = f.read() 5 6n_k, data = text_str.split("\n")[:2] 7 8@profile 9def process(): 10 for x in range(100): 11 n, k = map(int, n_k.split()) 12 a = Counter((i for i in data.split())) 13 14 sort_count = sorted(a.items(), key=lambda x: x[1]) 15 16 total = 0 17 while len(sort_count) > k: 18 _, n = sort_count.pop(0) 19 total += n 20 21 #print(total) 22 total 23 24if __name__ == "__main__": 25 process()
after.py
python
1from collections import Counter 2 3with open("1.txt", "r") as f: 4 text_str = f.read() 5 6n_k, data = text_str.split("\n")[:2] 7 8@profile 9def process(): 10 for x in range(100): 11 n, k = map(int, n_k.split()) 12 a = Counter((i for i in data.split())) 13 14 sort_count = sorted(a.values()) 15 16 total = 0 17 l = len(sort_count) 18 19 if len(sort_count) <= k: 20 # print(0) 21 0 22 else: 23 # print(sum(sort_count[:l-k])) 24 sum(sort_count[:l-k]) 25 26if __name__ == "__main__": 27 process()
実行結果
text
1$ kernprof -lv before.py 2Wrote profile results to before.py.lprof 3Timer unit: 1e-06 s 4 5Total time: 0.017229 s 6File: before.py 7Function: process at line 8 8 9Line # Hits Time Per Hit % Time Line Contents 10============================================================== 11 8 @profile 12 9 def process(): 13 10 101 141.0 1.4 0.8 for x in range(100): 14 11 100 504.0 5.0 2.9 n, k = map(int, n_k.split()) 15 12 100 8778.0 87.8 50.9 a = Counter((i for i in data.split())) 16 13 17 14 100 2164.0 21.6 12.6 sort_count = sorted(a.items(), key=lambda x: x[1]) 18 15 19 16 100 163.0 1.6 0.9 total = 0 20 17 1100 1858.0 1.7 10.8 while len(sort_count) > k: 21 18 1000 2092.0 2.1 12.1 _, n = sort_count.pop(0) 22 19 1000 1397.0 1.4 8.1 total += n 23 20 24 21 #print(total) 25 22 100 132.0 1.3 0.8 total 26 27$ kernprof -lv after.py 28Wrote profile results to after.py.lprof 29Timer unit: 1e-06 s 30 31Total time: 0.006542 s 32File: after.py 33Function: process at line 8 34 35Line # Hits Time Per Hit % Time Line Contents 36============================================================== 37 8 @profile 38 9 def process(): 39 10 101 71.0 0.7 1.1 for x in range(100): 40 11 100 258.0 2.6 3.9 n, k = map(int, n_k.split()) 41 12 100 5397.0 54.0 82.5 a = Counter((i for i in data.split())) 42 13 43 14 100 354.0 3.5 5.4 sort_count = sorted(a.values()) 44 15 45 16 100 70.0 0.7 1.1 total = 0 46 17 100 70.0 0.7 1.1 l = len(sort_count) 47 18 48 19 100 80.0 0.8 1.2 if len(sort_count) <= k: 49 20 # print(0) 50 21 0 51 22 else: 52 23 # print(sum(sort_count[:l-k])) 53 24 100 242.0 2.4 3.7 sum(sort_count[:l-k]) 54
とりあえずわかることはa = Counter((i for i in data.split()))
の行が壮絶に遅いことですが、これはどちらにもあるので気にしないこととして、あとはbeforeの方はぜんぶ遅いですね。
遅い理由の説明
- sort_count = sorted(a.items(), key=lambda x: x[1])
これをやるとaの中身をぜんぶ手繰り寄せた上で、キーでソートする(無駄にオブジェクトをたくさん作ったり、ポインタ見る回数が増えたり色々する)ので遅い。sorted(a.values())は値をぜんぶ取ってきてソートするだけなのでだいぶマシ。
以下はループ内のお話なので、とてもシビアに効いてきます。個々の計算はそうでもなくても、ループ回数がそれなりにありますから。
- while len(sort_count) > k:
毎回len計算して比較するから。
- _, n = sort_count.pop(0)
popという操作はオブジェクトを変更します。当然それなりに遅いです。
- total += n
ただの加算なのに意外と時間取ってますね。ループの中だとこうなります(というか逆に、これと比較するとlen, 比較, popは優秀なのか・・・)。
投稿2018/05/03 14:32
編集2018/05/03 21:36総合スコア30933
0
ベストアンサー
cProfile & snakeviz & timeitを使ったプロファイルの仕方です。
<< チューニングの原則 >>
1,プロファイラを使う。代表的なプロファイラとしてcProfileがあります。
2,チューニングのデッドラインを決める。
今回の場合は、質問文のリンク先の問題にある、実行時間制限: 2 sec が該当します。
チューニングすることによってこれ以下の実行時間にしないといけません。
◇手順
1,before.py
を作成しその後コピーしafter.py
を作成します。
Python
1from collections import Counter 2n, k = map(int, input().split()) 3a = Counter((i for i in input().split())) 4 5sort_count = sorted(a.items(), key=lambda x: x[1]) 6 7total = 0 8while len(sort_count) > k: 9 _, n = sort_count.pop(0) 10 total += n 11 12print(total)
変更するのはafter.py
です。
2,初回のプロファイルを取ります。入力ファイルはリダイレクトで渡します。
python -m cProfile -o before.prof before.py < 13.txt
python -m cProfile -o after.prof after.py < 13.txt
3,snakeviz
で、プロファイル結果を表示します。
snakeviz before.prof
snakeviz after.prof
4,プロファイル結果を元にボトルネックを特定します。
実行時間が0.0998 sかかり、 そのうち0.0714 sがcollection
のcount
が占めているのが分かります。
直前の呼び出し部分がupdate
なので、ソースコードのCounter
をチェックします。
Python
1 a = Counter((i for i in data.split()))
Counter
の引数がジェネレータ式(※1)になっているため、試しにinput().split()
に変更してみます。
※1 hayataka2049さん指摘ありがとうございました。
※注意
ここから変更するソースコードはafter.py
です。
Python
1 a = Counter(input().split())
6,after.py
のプロファイルを再度取ります。
python -m cProfile -o after.prof after.py < 13.txt
snakeviz after.prof
◇結果
0.0714 sから0.0598 sに高速化されました。
このような形で一番CPU時間を専有している部分から、最初に求めたデッドラインの時間を満たすまでチューニングをします。
◇参考情報
SNAKEVIZ
pythonのバージョンは3.6.5を使用しています。
すべての提出一覧を見る限りではAtCoderの実行環境はPython3 (3.4.3)のなためvenvなどで、仮想環境を構築して同じ環境(3.4.3)にしたほうが良いと思われます。
投稿2018/05/03 21:16
編集2018/05/03 23:18総合スコア5846
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/05/03 21:28
2018/05/03 21:32
2018/05/03 23:04
2018/05/03 23:36
2018/05/04 00:01
2018/05/04 00:03
2018/05/04 00:22
2018/05/04 00:25
2018/05/04 03:32
2018/05/04 07:19
2018/05/04 14:09
0
2秒で20万のデータをですか。。
こんなのどうでしょう
空の配列用意してデータを読み込みながら重複を省く。
(javascript風の動かないやつですが単純ですのでご勘弁を)
javascript
1let a = [] 2for(i of input().split() ){ 3 a[parseInt(i)] = i 4} 5 6return max ( 0, a.length - k) 7
投稿2018/05/03 21:15
編集2018/05/03 21:17総合スコア192
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/05/04 14:18