−目次
AtCoder Beginner Contest 143 C~F問題メモ
C - Slimes
問題
- NN 個のスライムが一列に並ぶ
- それぞれのスライムの色は NN 文字から成る文字列 SS で表される
- 1つのスライムの色は1文字で表され、ii 番目のスライムの色は文字列 SS の ii 文字目に相当する
- 同色のスライムが隣り合っていると、全てくっついて1つになる
- くっついた後のスライムの個数を求めよ
- 1≤N≤1051≤N≤105
解法
ランレングス圧縮後の長さ。
itertools.groupbyを使えば短くかける。(覚えた知識をすぐ使いたがる)
1 2 3 4 5 |
from itertools import groupbyn = int(input())s = input()print(len(list(groupby(s)))) |
D - Triangles
問題
- NN 本の棒があり、ii 番目の棒の長さは LiLi である
- 3本の棒を使って三角形を作ろうと思えば、3辺の長さを a,b,ca,b,c として、以下を全て満たす必要がある
- a<b+ca<b+c
- b<c+ab<c+a
- c<a+bc<a+b
- 作れる三角形は何種類あるか、求めよ
- 同じ長さの棒が複数本ある場合もあるが、区別して数える
- 3≤N≤20003≤N≤2000
- 1≤Li≤10001≤Li≤1000
解法
ソートしてにぶたん。
まず、使う3本のうち短い方の2本を全探索する。
Li 1 1 2 3 4 4 5 6
^ ^
たとえば2と4を選んだ時、この2本が短い辺となるように作れるもう一本の長さは、6未満である。
なので、それより短い棒の個数を調べる。ソートした LiLi 配列上で二分探索する。bisect_leftを使えば「6以上となる最も短い棒のindex」が得られる。
Li 1 1 2 3 4 4 5 6
^ ^
i j |----| k
最初に選んだ2本のindexを i,j(i<j)i,j(i<j) とした時、k−j−1k−j−1 が、その間にある棒の本数となる。
これらを全ての2本の組について合計した値が答えとなる。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from bisect import bisect_leftn = int(input())lll = list(map(int, input().split()))lll.sort()ans = 0for i in range(n - 2): li = lll[i] for j in range(i + 1, n - 1): k = bisect_left(lll, li + lll[j]) ans += k - j - 1print(ans) |
なお、Pythonでは、サーバでの実行速度のばらつきによっては、微妙にTLEとなることがある。
素直にPyPyで提出すればよいが、Pythonで通したい場合、結構bisect_leftは同じクエリが何回も投げられることになるため、キャッシュすれば余裕を持って通るようになる。
N≤2000,Li≤1000N≤2000,Li≤1000 より、クエリが最大 N(N−1)2=2×106N(N−1)2=2×106 回くらい投げられるのに対して、クエリの種類は2つの数の和の範囲2~2000の2000通り程度しか無い。
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 |
from bisect import bisect_leftn = int(input())lll = list(map(int, input().split()))lll.sort()ans = 0def get_bisect_left(): cache = {} def _f(x): if x in cache: return cache[x] cache[x] = bisect_left(lll, x) return cache[x] return _fbisect_left_cache = get_bisect_left()for i in range(n - 2): li = lll[i] for j in range(i + 1, n - 1): k = bisect_left_cache(li + lll[j]) ans += k - j - 1print(ans) |
E - Travel by Car
問題
- NN 個の都市、MM 本の道路があり、ii 本目の道路は 都市 AiAi と BiBi を長さ CiCi で結ぶ
- 車で都市から都市まで移動することを考える
- 車は1単位長さ走る毎に、1リットルの燃料を消費する
- 燃料タンクには LL リットルまで入り、補給は都市でのみ、満タンになるまで行える(行わなくてもよい)
- 出発時、燃料は満タンである
- 途中で燃料が無くなるような移動は行えない(到着と同時に空になるのはOK)
- クエリが QQ 個与えられるので、全てに答えよ
- ii 番目のクエリは Si,TiSi,Ti が与えられる
- 都市 SiSi から TiTi まで行くのに必要なガソリンの最小補給回数を求める
- 2≤N≤3002≤N≤300
- 0≤M≤N(N−1)20≤M≤N(N−1)2
- 1≤L,Ci≤1091≤L,Ci≤109
- 1≤Q≤N(N−1)1≤Q≤N(N−1)
解法
各頂点から(コストの計算を拡張した)Dijkstra……で通ると思ったら、PyPyではTLEで通らなかった(適切な実装すれば通る?)。 Floyd-Warshallは、数字の埋まり方をイマイチちゃんと理解してなくて、同様の拡張を行おうとしたらWAとなった。
想定解はもっと単純で、2回のFloyd-Warshallを行えばよい。これは気付くべきですね……
Floyd-Warshall解法が鮮やかなのは感嘆するとして、Dijkstraをもう少し考えておさらいする。
Floyd-WarshallでWAとなる理由
ここでは、1回で一気に答えを求めようとするFloyd-Warshallを指す。
Dijkstraによる解法では、コストを2つの変数(補給量、最後に満タンにしてからの使用量)で持てばよい。 ある都市に到着したとき、補給量は少ない方が、同じならタンクに残っている燃料は多い方が常によいので、これを最小化すればよい。
しかし、このコストの更新が可能なのは、Dijkstraが常にリンクで接した次の頂点にのみ探索を広げるので、その都度、補給するかしないか決められるからである。
対して、Floyd-Warshallはパス同士を繋げて最小値を更新するため、コストには結合法則(でいいのかな)が成り立っていないといけない。
Dijkstra
s -- ○ -- ... -- ● -- ◎
次のノードへの更新で補給するかしないか決められる
Floyd-Warshall
s -- ○ -- ... -- ● -- ○ -- ... -- ◎
|-----------------||------------------|
sで満タンにして ●で満タンにして
●までの最小コスト ◎までの最小コスト
→足し合わせるだけでは●で残っている燃料を活用することができない
Dijkstraの計算量
二分ヒープによるDijkstraの計算量は、O((N+M)logN)O((N+M)logN) とされている。
今回、頂点数は少ないが辺の数は完全グラフまであり得る。さらにこれを各頂点から行うので、全体で O(N3logN)O(N3logN) となる。
そのまま計算すると 2×1082×108、多少の定数倍は省略されるにしても、107107 くらいはありそうなので、やはり厳しい。入力行数も多いのでそれにも少し時間を取られる。
高速化1 heapに入れるノードの枝刈り
人によっては、通常のDijkstraの時点で常にやっているかもしれないが。
特に今回のような密なグラフの場合、既に確定済みの頂点の他のパスがheapに残り続け、全体のpop, pushに悪影響を及ぼし続ける。
-
- 上記リンクの図を用いた説明で、黄色の丸を「確定済み」、水色の丸を「暫定距離判明済み」と称する
- 上の方の水色の丸がheapに残り続けることで、全体に悪影響を及ぼす
よって、頂点 vv から uu に探索を広げる際、heapに入れなくてもよい条件でなるべく弾きたい。
既に確定済み
uu が、既に確定済みなら、heapに追加しない。これは特に追加のデータも不要で、単純だが一定の効果がある。
既に他の経路でより短い距離で行けることが判明済み
追加のデータが必要になるが、より強力。密グラフの場合、こちらがよい。
「各頂点につき、現在判明している中で最短の暫定距離」を保持する。
vv を経由する新しい uu への距離が、それより長ければ、追加する必要は無い。
高速化2 終了条件
始点から全ての頂点までの距離が確定した後も、水色の丸(高速化1 参照)はheapに残り続ける。
通常は、heapが空になるまでpopし続ける。 popしても飛ばされるだけなのだが、特にグラフが密だと、それだけでも結構時間はかかる。
確定すべき頂点数が事前にわかっていれば、その数だけ確定したらそこで終了して良いとわかる。
グラフが連結ならその頂点数は NN なのだが、 今回の場合、グラフは連結とは限らないので、Union-Find等で連結頂点数を計算しておき、確定すべき頂点数とする。
(ちょっと牛刀割鶏っぽい気がしないでも無い)
ただこれ、1点だけ、どの頂点からもめっちゃ離れた頂点を用意するなどで、あまり意味をなさなくなってしまう。 結局その1点が取り出されるまで終了できず、それまでにheapのほとんども取り出される。
ランダムケースでは効果はあるが、狙い撃ちの意地悪ケースには弱い。
高速化3 コストのint化
可読性の悪くなる、あまりやりたくない手法だが、効果はそれなりにある。
コストを表現するのに複数の変数(補給回数、最後の補給からの使用量)が必要な今回の問題では、tupleで持つのが一般的である。 だが、tupleは大小比較に少し時間がかかる(intと比べて)。
それぞれの変数の取り得る最大数を制約より確認し、 被らない固定の桁数だけbitshiftして1つのint型として持たせると、比較が高速化される。 そのための演算コストはかかるが、heap内で繰り返される比較コストと比べると、十分償却される。
上記の3つをやって、コンテスト後に追加された1ケース以外は通った。(PyPy 1079ms)
最後の1ケースはDijkstra解法を落とすため?の意地悪ケースのようで、C++でも1000ms以上かかっているものが散見される。 でもPyPyでDijkstraで通している人もいるので、後で確認する。
- C++ でboostのfibonacci_heapを使うことで最後のケースでも144msで通っているケース(kroton氏)
Naive Dijkstra
Dijkstraに名を残すエドガー・ダイクストラさんが考案したままのアルゴリズム。 heapを使う方法は、後から別の人が改善したもの。
これを見ると、密グラフではNaiveの方がいいらしい。O(V2)O(V2) VS O((E+V)logV)O((E+V)logV) なので、E=O(V2)E=O(V2) なら確かにそう。これなら通るか。
話は変わるけど書いてあることには従いましょうという話
最近のAtCoderでは、「各行に1つの答えを出力せよ」という問題でも、空白区切りで1行に全て出力しても受け取ってくれる問題が多いが、 この問題はちゃんと言われたとおり改行しないとダメだった。
Pythonでは空白区切り1行だとansに出力を溜めて print(*ans) で済むから楽なんだよねえ。
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
import sysfrom heapq import heappop, heappushclass UnionFind: def __init__(self, n): self.table = [-1] * n def _root(self, x): if self.table[x] < 0: return x else: self.table[x] = self._root(self.table[x]) return self.table[x] def find(self, x, y): return self._root(x) == self._root(y) def union(self, x, y): r1 = self._root(x) r2 = self._root(y) if r1 == r2: return d1 = self.table[r1] d2 = self.table[r2] if d1 <= d2: self.table[r2] = r1 self.table[r1] += d2 else: self.table[r1] = r2 self.table[r2] += d1 def united_count(self, x): return -self.table[self._root(x)]def dijkstra(links, l, uft): dists = [] F9 = (1 << 9) - 1 F30 = (1 << 30) - 1 INF = 10 ** 18 for s in range(n): q = [s] fixed = [-1] * n upper = [INF] * n fixed_count = 0 limit = uft.united_count(s) while q: item = heappop(q) v = item & F9 if fixed[v] != -1: continue count = item >> 39 used = (item >> 9) & F30 fixed[v] = count fixed_count += 1 if fixed_count >= limit: break for u, c in links[v]: if l - used < c: new_item = ((count + 1) << 39) | (c << 9) | u else: new_item = (count << 39) | ((used + c) << 9) | u if new_item >= upper[u]: continue upper[u] = new_item heappush(q, new_item) dists.append(fixed) return distsn, m, l = list(map(int, input().split()))lines = sys.stdin.readlines()links = [set() for _ in range(n)]uft = UnionFind(n)for line in lines[:m]: a, b, c = map(int, line.split()) a -= 1 b -= 1 if l < c: continue links[a].add((b, c)) links[b].add((a, c)) uft.union(a, b)dists = dijkstra(links, l, uft)buf = []for line in lines[m + 1:]: s, t = map(int, line.split()) buf.append(dists[s - 1][t - 1])print('\n'.join(map(str, buf))) |
F - Distinct Numbers
問題
- NN 個の整数 A={A1,A2,...,AN}A={A1,A2,...,AN} がある
- 1≤K≤N1≤K≤N の全ての正整数 KK について、以下を求めよ
- 互いに相異なる KK 個の数を AA から繰り返し取り出す時、取り出せる最大回数
- 1≤N≤3×1051≤N≤3×105
例
N = 16 A = 1 2 3 4 4 4 4 4 5 5 6 6 7 7 7 7 K = 4 に対して、以下のように3回取り出せ、これが最大となる 1 4 5 7 2 4 6 7 4 5 6 7
解法
KK と同時に、「取り出せる回数」を仮定すると、方針が見えてくる。
まず事前準備として、具体的な数字は必要なく、それぞれが何回出現したかのみが重要なので、出現回数の配列にする。
A = 1 2 3 4 4 4 4 4 5 5 6 6 7 7 7 7 B = 1 1 1 ~~~~5~~~~ ~2~ ~2~ ~~~4~~~ ↓ソート B = 1 1 1 2 2 4 5
ここで、K=4K=4、取り出せる回数をたとえば t=4t=4 として、達成可能か考える。
tt 回以上出現する数は、それぞれの組に1個ずつ入れたらそれ以上は入れられない。
4回以上出現する数は2種類あるので、これは最大限使うとする。(以下では説明のため具体的な AiAi の値を入れているが、実際に解く際には不要)
4 7 ○ ○ 4 7 ○ ○ 4 7 ○ ○ 4 7 ○ ○
残り埋めるべき○は8個。これを、4回未満出現する数で埋められれば達成可能、できなければ不可能である。 縦に順に埋めていけば、4回未満出現する数が同じ組(横列)に被ることは無いので、全て使える。
4 7 1 5 4 7 2 6 4 7 3 6 4 7 5 ○
今回の場合、4回未満出現する数は7個しか無いので、埋められず、不可能と分かる。
一般化すると、
- gt=t 回以上出現する数の種類数
- st=t 回未満出現する数の個数
を計算しておくと、gt+stt≥k なら、K=k の時に t 回の取り出しが達成可能と判定できる。
達成可能/不可能は t に対して単調性がある(どこかに唯一の可能/不可能の境界がある)ので、これを二分探索で求めればよい。
計算量は O(NlogN) となり、(Pythonでは多少枝刈りなどすれば)解ける。
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
from collections import Counterfrom itertools import groupbydef bin_search(k, greater_count, smaller_sum): l = 0 r = n + 1 while l < r - 1: m = (l + r) // 2 gc = greater_count[m] ss = smaller_sum[m] if gc + ss // m >= k: l = m else: r = m return ln = int(input())aaa = list(map(int, input().split()))cnt = sorted(Counter(aaa).values())# Precomputegreater_count = [0] * (n + 2) # Number of types of Ai same or greater than ismaller_sum = [n] * (n + 2) # Number of Ai smaller than iremain = greater_count[1] = len(cnt)total = smaller_sum[1] = 0cnt_grp = [(c, len(list(itr))) for c, itr in groupby(cnt)]for (c, l), (c2, _) in zip(cnt_grp, cnt_grp[1:]): remain -= l total += c * l for i in range(c + 1, c2 + 1): greater_count[i] = remain smaller_sum[i] = total# Querybuf = []for k in range(1, len(cnt) + 1): buf.append(bin_search(k, greater_count, smaller_sum))buf.extend([0] * (n - len(cnt)))print(*buf) |
もう少し速い解法
それぞれの t に対して、その回数の取り出しを行える最大の K の値というのは既述の方法で求められる。
事前計算の後、t=1~N に対して最大の K を求める。
t 1 2 3 4 5 6 7 8 9 ... 16 k 7 5 4 3 3 2 2 2 1 ... 1
indexと値を逆転させる。重複する分に関しては、t が最大のものを取る。
k 1 2 3 4 5 6 7 8 9 ... 16 t 16 8 5 3 2 0 1 0 0 ... 0
埋まっていない箇所に関しては、後ろから累積maxを取る。これで答えが求められ、計算量は O(N) となる。
k 1 2 3 4 5 6 7 8 9 ... 16 t 16 8 5 3 2 1 1 0 0 ... 0
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 |
from collections import Counterfrom itertools import groupby, accumulaten = int(input())aaa = list(map(int, input().split()))cnt = sorted(Counter(aaa).values())remain = len(cnt)total = 0kkk = [0] * (n + 1)p = 1for c, itr in groupby(cnt): l = len(list(itr)) for i in range(p, c + 1): kkk[remain + total // i] = i remain -= l total += c * l p = c + 1for i in range(p, n + 1): kkk[remain + total // i] = ikkk.reverse()kkk = list(accumulate(kkk, func=max))kkk.reverse()print(*kkk[1:]) |

