日々drdrする人のメモ

今日も明日もdrdr

CodeChef June Challenge 2018: Archi and Tree

CodeChef June Challenge 2018の問題: Archi and Tree (Code: ARCTR)
問題ページ: https://www.codechef.com/JUNE18A/problems/ARCTR

問題概要

各辺がそれぞれの長さを持つ、 {N}個の頂点を含む木がある。
 {i}番目の辺は {a_i} {b_i}を繋ぐ、長さ {w_i}の辺である。

この木の上に {M}個の流れ星が流れる。
具体的に、 {i}番目の流れ星は時間 {t_i}に頂点 {u_i}からスタートし、速度 {s_i}でまっすぐ {v_i}に向かい、頂点 {v_i}に到達したら消える。

各頂点で最初にいずれかの流れ星が観測できる最小の時刻を出力せよ。観測できない頂点については-1を出力せよ。

制約

1つのテストケースに含まれるケース数:  {1 \le T \le 100}
 {1 \le N, M \le 2*10^5}
 {1 \le u_i, v_i, a_i, b_i \le N}
 {1 \le s_i, t_i, w_i \le 10^9}
入力されるグラフは木
全てのケースの {N}の総和は {2*10^5}を超えない
全てのケースの {M}の総和は {2*10^5}を超えない

解法

Heavy-Light Decomposition + Li Chao Tree で解いた。

例えば、下の図のようなケースであれば、
f:id:smijake3:20180614011655p:plain
下の図のようにHeavy-Light Decompositionを用いてheavy-pathに沿って木をいくつかの木に分割し、
f:id:smijake3:20180614011705p:plain
下の図のように分割された木の中で {M}本の線分の最小値を計算すればよい。
f:id:smijake3:20180614011713p:plain

HL分解された各木に線分を追加する処理は、頂点u, vが含まれたそれぞれの分割木から、頂点u, vのLCAの頂点wが含まれた分割木まで1つずつ親の木に遡りながら線分を追加する処理を行うことで達成できる。HL分解した木は、高さが {O(\log N)}になるため、この線分追加操作は {M}本の線分に対し {O(M \log N)}回になる。


ある点における、これらの線分が取る最小値はLi Chao Treeを用いることで計算できる。
Li Chao Treeでは、 {N}個の点(x座標)について計算を行う場合、1本の直線追加処理を {O(\log N)}、ある点xにおける最大値・最小値の計算を1回 {O(\log N)}で計算できる。このLi Chao Treeで更に、線分が含まれる範囲のみに直線を追加するように更新することで、ある点における各線分のうちの最大値・最小値を計算できる。
参考: Convex hull trick and Li Chao tree - Competitive Programming Algorithms

実装

計算量は {O((M \log N + N) \log N)}

HL分解を実装したつもりが、heavy-pathに沿ってないため実装できてない()。
雑な実装してる。

提出コード(Python3): https://www.codechef.com/viewsolution/18771280

import sys
sys.setrecursionlimit(10**6)
T = int(input())
for _ in range(T):
    N = int(input())
    G = [[] for i in range(N)]
    for i in range(N-1):
        u, v, w = map(int, input().split())
        G[u-1].append((v-1, w))
        G[v-1].append((u-1, w))
 
    # Heavy-Light Decomposition
    # DFSで探索しながら、木をいくつかの直線木に分割
    # lg[i]: 頂点iが属する分割木番号
    # li[i]: 頂点iの、分割木におけるインデックス
    # ld[i]: 頂点iの、分割木における左からの距離
    # grp[i]: 分割木iに含まれる頂点リスト
    # gprv[i]: 分割木番号iが繋がる親の分割木の情報 (gp, gi, gd)
    #    gp: 親の分割木番号
    #    gi: 分割木の中で繋がっている頂点インデックス
    #    gd: 分割木の中で繋がっている頂点の左からの距離
    # gdep[i]: 分割木iのrootの分割木からの深さ(距離)
    # gdist[i]: 分割木iに含まれる、各頂点の左からの距離
    lg = [0]*N; li = [0]*N; ld = [0]*N
    prv = [0]*N
    grp = []; gprv = []; gdep = []; gdist = []
    def hld(v, p, d, gp, gi, gd):
        L = []; D = []
        g = len(grp)
        grp.append(L)
        gdist.append(D)
        gprv.append((gp, gi, gd))
        gdep.append(d)
        di = i = 0
        while v is not None:
            lg[v] = g
            li[v] = i
            ld[v] = di
            D.append(di)
            L.append(v)
            nxt = None
            for w, co in G[v]:
                if w == p:
                    continue
                if nxt is None:
                    # vから最初に見つけた頂点を、現在の分割木の次の頂点とする
                    # 本来はheavy-pathにしなければならない
                    nxt = w
                    di += co
                else:
                    # 2つめ以降は、新たな分割木に含ませる
                    hld(w, v, d+1, g, i, co)
            p, v = v, nxt
            i += 1
    hld(0, -1, 0, -1, -1, 0)
 

    # Li Chao Tree

    # x=xにおける線分lineの値を計算
    def f(line, x):
        # 誤差を回避するため、r, p, qを保持し、f(x) = (r*x + q)/pと計算
        r, p, q = line
        if r == 1:
            return (x + q)/p
        return (-x + q)/p
 
    def _add_line(line, a, b, data, xs, k, l, r):
        if r <= a or b <= l:
            return
        m = (l + r) // 2
        if not (a <= l and r <= b):
            # 線分が含まれる区間まで移動
            _add_line(line, a, b, data, xs, 2*k+1, l, m)
            _add_line(line, a, b, data, xs, 2*k+2, m, r)
            return
        # 以下は線分が含まれる区間[l, r]での処理
        # 区間[l, r]に線分が存在しなければ、ここに線分追加して終了
        if data[k] is None:
            data[k] = line
            return
        lx = xs[l]
        mx = xs[m]
        rx = xs[r-1]
        r0, p0, q0 = line; r1, p1, q1 = data[k]
        left = (f(line, lx) < f(data[k], lx))
        mid = (f(line, mx) < f(data[k], mx))
        right = (f(line, rx) < f(data[k], rx))
        # 元の区間[l, r]の線分より全て値が小さければ、入れ替えて終了
        if left and right:
            data[k] = line
            return
        # 元の区間[l, r]の線分より全て値が大きければ、何もせず終了
        if not left and not right:
            return
        # 元の区間[l, r]の線分よりも、xs[m]における値が小さければ、ここで入れ替える
        if mid:
            data[k], line = line, data[k]
        # 今持っている線分が、区間[l, r]の線分よりも小さくなれる方の区間をさらに探索
        if left != mid:
            _add_line(line, a, b, data, xs, 2*k+1, l, m)
        else:
            _add_line(line, a, b, data, xs, 2*k+2, m, r)
 
    # 区間[xs[a], xs[b]]の線分lineを追加する
    def add_line(line, a, b, i):
        return _add_line(line, a, b+1, ldata[i], gdist[i], 0, 0, NN[i])
 
    # xs[k] (= x)におけるM本の線分がとる最小値を計算
    def query(k, x, data, n):
        k += n-1
        s = 1e30
        while k >= 0:
            if data[k]:
                s = min(s, f(data[k], x))
            k = (k - 1) // 2
        return s
 
    LL = len(grp)
    NN = [2**(len(e)-1).bit_length() for e in grp]
    ldata = [[None]*(2*n+1) for n in NN]
 
    M = int(input())
    for i in range(M):
        u, v, t, s = map(int, input().split()); u -= 1; v -= 1
        ug = lg[u]; vg = lg[v]
        if lg[u] == lg[v]:
            # 同じ分割木にu, vが存在
            ui = li[u]; vi = li[v]
            ud = ld[u]; vd = ld[v]
            if ui <= vi:
                add_line((1, s, t*s-ud), ui, vi, ug)
            else:
                add_line((-1, s, t*s+ud), vi, ui, ug)
        else:
            # 異なる分割木にu, vが存在

            # 同じ高さになるように親の木に遡る
            udp = gdep[ug]; vdp = gdep[vg]
            ud = ld[u]; ui = li[u]
            d = 0
            while udp > vdp:
                # 根頂点に近づくにつれて時間が増大する線分を追加
                # w = LCA(u, v)とすると、u -> w -> vのうちのu -> wの部分
                ud = gdist[ug][ui]
                d += ud
                add_line((-1, s, t*s+d), 0, ui, ug)
                ug, ui, ud = gprv[ug]
                d += ud
                udp -= 1
            st = []
            vd = ld[v]; vi = li[v]
            while udp < vdp:
                # w = LCA(u, v)とすると、u -> w -> vのうちのw -> vの部分は、uからの距離が
                # 分かってから計算を行うためスタックする
                vd = gdist[vg][vi]
                st.append((vd, vg, vi))
                vg, vi, vd = gprv[vg]
                vdp -= 1
 
            # 同じ木に到達するまで1つずつ親の木に遡る
            while ug != vg:
                ud = gdist[ug][ui]
                d += ud
                add_line((-1, s, t*s+d), 0, ui, ug)
                ug, ui, ud = gprv[ug]
                d += ud
                udp -= 1
 
                vd = gdist[vg][vi]
                st.append((vd, vg, vi))
                vg, vi, vd = gprv[vg]
                vdp -= 1
 
            # 同じ木に到達した時の線分追加処理
            ud = gdist[ug][ui]; vd = gdist[vg][vi]
            if ui <= vi:
                add_line((1, s, t*s+(d-ud)), ui, vi, ug)
            else:
                add_line((-1, s, t*s+(d+ud)), vi, ui, ug)
            d += abs(ud - vd)

            # 根頂点に近づくにつれて時間が減少する線分を追加
            # w = LCA(u, v)とすると、u -> w -> vのうちのw -> vの部分
            while st:
                e, vg, vi = st.pop()
                vd = gprv[vg][2]
                d += vd
                add_line((1, s, t*s+d), 0, vi, vg)
                d += e
 
    ans = []
    for i in range(N):
        # 各頂点について、最小値を計算
        g = lg[i]
        j = li[i]
        d = ld[i]
 
        y = query(j, d, ldata[g], NN[g])
        if y >= 1e29:
            ans.append("-1")
        else:
            ans.append("%.08f" % y)
    print(*ans, sep='\n')