何の話かと言うと
上記の問題をネタに「ギリギリ間に合う系」、もしくは、「より一般にはもっと多くの種類で考えてもよさそうなのに、なぜか具体的な少数のものに限定されている」系の話をします。
実行時間の見積もり
PyPy3 での実行時間を調べてみます。
for _ in range(10**9): pass # 902 ms
for _ in range(10**10): pass # 8416 ms
これより、制限時間 2s の問題であれば、 回のループまではなんとか間に合うことがわかります。
素朴な解法
例えば、4 文字の部分列を作るのであれば、最初の文字は、
・文字列 S の最後の3文字を除いた中で、最も小さい文字
を選びます。
・2文字目は、
・文字列 S の 1 文字目を選んだ場所より後ろで、最後の2文字を除いた中で、最も小さい文字
を選びます。
つまり、検索の末尾を1文字づつ増やしながら、直前に選んだ場所以降で最小の文字を選ぶ操作を繰り返せばOKです。
これを実装するとループの回数はいくらになるでしょう?
全部で K 文字選ぶので、K 回のループがあります。
次に、それぞれのループの中で、文字列 S に対する最小文字の検索をします。最悪ケースで N-K 文字を検索するので、N-K 回のループになります。(最後の K 文字を除く事と、検索の末尾が増えると同時に検索の先頭も後ろに移動する事を考えてください。)
というわけで、全体として、 回のループになります。
計算量の正確な見積もり
という条件があるので、上記の計算量を大雑把に と考えると、 でアウトになります。
なのですが、厳密に、 を計算すると、 という条件があることから、この値は で最大値 を取ります。つまり、正確なループ回数は最大で に抑えられます。
回で 900ms という結果を考えると、ギリギリ感はありますが、試してみる価値はありそうです。
結果は、最遅ケースで 1778ms、なんとかセーフでした!
より効率的な解法
さきほどの素朴な解法は、次の様な本当の最悪ケースを手で作ると TLE になります。
N = 10**5 K = N//2 S = ['a'] * N
これでもパスできるための効率化は、「アルファベットは26文字」という特殊性を利用して、
・a が選べるかチェック
・b が選べるかチェック
・z が選べるかチェック
という(ある意味愚直な)確認を繰り返します。具体的には、k 文字目を選ぶ際に「直前に選んだ場所以降でアルファベット X が最初に存在する位置 index」を検索して、index が後ろ過ぎなければ(残り m 文字選ぶのであれば、末尾から m-1 文字目までは選べない)これを採用する、ということを繰り返していきます。
・「直前に選んだ場所以降でアルファベット X が最初に存在する位置 index」
については、事前に DP で準備ができます。
dp = {} # dp['a'][n] # n 文字目以降で最初に a が来る位置 for o in range(ord('a'), ord('z')+1): # O(26*N) tmp = [-1] * (N+2) for n in range(N, 0, -1): if S[n] == chr(o): tmp[n] = n else: tmp[n] = tmp[n+1] dp[chr(o)] = tmp
1 文字に付き、N 文字の検索で用意できるので、全体として で用意できます。
すると、さきほどのチェックは、 で済むので、全部で K 文字選ぶ処理は、 で終わります。
このように、与えられた条件の中で、「より一般にはもっと多くの種類で考えてもよさそうなのに、なぜか具体的な少数のものに限定されている」部分に注目して、
・少数に限定されたもののループを利用する(このループは と見なせる)
という手法を考えるとよいことがあります。次の問題もそのような例です。
おまけ
巨大なリストの作成は、初期化だけでも時間がかかるので注意しましょう。
N = 10**8 a = [None] * N # 1015 ms
リストを作るだけで、1sec 以上かかってます。