問題
$N$ 個の都市が有向辺で接続されている(DAG、$i < j$ 形式)。辺のコストは $w_{ij} = a_i \times b_j$(各頂点に属性 $a_i, b_j$ が付与)。都市 $1$ から都市 $N$ への最大重み経路を求めよ。
制約
| パラメータ | 範囲 |
|---|---|
| $N$ | $2 \le N \le 2 \times 10^5$ |
| $M$ | $1 \le M \le 3 \times 10^5$ |
| $a_i, b_i$ | $1 \le a_i, b_i \le 10^9$ |
| グラフ形状 | DAG(トポロジカル順 = 頂点番号順) |
入出力例
入力例 1
4 4
3 1 4 1
5 9 2 6
1 2
1 3
2 4
3 4
出力例 1
33
経路 $1 \to 2 \to 4$: $a_1 b_2 + a_2 b_4 = 3 \times 9 + 1 \times 6 = 27 + 6 = 33$ が最大。
概念図: Li Chao Tree による傾き最適化
ヒント(段階的開示)
ヒント1: 方向性
DP で $dp[v]$ = 頂点 $1$ から $v$ への最大重み経路長。遷移: $dp[v] = \max_{(u,v) \in E} \{dp[u] + a_u \times b_v\}$。
これは $b_v$ を固定したとき $\max_u \{a_u \cdot b_v + dp[u]\}$ という「直線 $y = a_u \cdot x + dp[u]$ の $x = b_v$ での最大値」問題。
Li Chao Tree で $O(\log N)$ per クエリ。
ヒント2: アプローチ
- DAG のトポロジカル順(= 頂点番号順)に処理
- 頂点 $u$ が確定したら直線 $(a_u, dp[u])$ を Li Chao Tree に追加
- 頂点 $v$ を処理するとき: $x = b_v$ でクエリ → $dp[v]$ を更新
- 全 $b$ 値を座標圧縮して Li Chao Tree を構築
ヒント3: コード骨格
# Li Chao Tree(最大値版)
class LiChaoTree:
def __init__(self, xs):
self.xs = sorted(set(xs))
self.n = len(self.xs)
self.idx = {x: i for i, x in enumerate(self.xs)}
self.seg = [None] * (4 * self.n) # (slope, intercept)
def add(self, a, b): # y = a*x + b
self._add(1, 0, self.n, (a, b))
def query(self, x):
return self._query(1, 0, self.n, self.idx[x])
# トポロジカル順に処理
lct = LiChaoTree(b_values)
dp = [-INF] * N
dp[0] = 0
lct.add(a[0], 0)
for v in range(1, N):
val = lct.query(b[v])
if val > -INF: dp[v] = max(dp[v], val)
if dp[v] > -INF: lct.add(a[v], dp[v])
模範解答 (Python)
import sys
input = sys.stdin.readline
def solve():
N, M = map(int, input().split())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
graph_rev = [[] for _ in range(N)]
for _ in range(M):
u, v = map(int, input().split())
u -= 1; v -= 1
graph_rev[v].append(u)
INF = float('inf')
# Li Chao Tree (max)
xs = sorted(set(b))
n = len(xs)
idx = {x: i for i, x in enumerate(xs)}
seg = [None] * (4 * n)
def _add(node, l, r, line):
if l >= r: return
mid = (l + r) // 2
if seg[node] is None:
seg[node] = line; return
cur = seg[node]
line_mid = line[0] * xs[mid] + line[1]
cur_mid = cur[0] * xs[mid] + cur[1]
if line_mid > cur_mid:
seg[node], line = line, cur
if r - l == 1: return
if line[0] > cur[0]:
_add(2*node, l, mid, line)
else:
_add(2*node+1, mid, r, line)
def _query(node, l, r, i):
if seg[node] is None: val = -INF
else: val = seg[node][0] * xs[i] + seg[node][1]
if r - l == 1: return val
mid = (l + r) // 2
if i < mid: return max(val, _query(2*node, l, mid, i))
else: return max(val, _query(2*node+1, mid, r, i))
def add(slope, intercept):
_add(1, 0, n, (slope, intercept))
def query(x):
return _query(1, 0, n, idx[x])
dp = [-INF] * N
dp[0] = 0
add(a[0], 0)
for v in range(1, N):
val = query(b[v])
if val != -INF:
dp[v] = max(dp[v], val)
if dp[v] != -INF:
add(a[v], dp[v])
print(dp[N-1] if dp[N-1] != -INF else -1)
solve()
Step-by-Step 解説
1DP 遷移式の変形
$dp[v] = \max_{(u,v)} \{dp[u] + a_u \cdot b_v\}$。$b_v$ を固定すると、これは直線 $f_u(x) = a_u \cdot x + dp[u]$ の $x = b_v$ での最大値問題。「最大値クエリ」なので上凸包(Li Chao Tree の最大化版)を使う。
$dp[v] = \max_{(u,v)} \{dp[u] + a_u \cdot b_v\}$。$b_v$ を固定すると、これは直線 $f_u(x) = a_u \cdot x + dp[u]$ の $x = b_v$ での最大値問題。「最大値クエリ」なので上凸包(Li Chao Tree の最大化版)を使う。
2Li Chao Tree の構築
$b$ の全値を座標圧縮。Li Chao Tree は各ノードで「中点で優位な直線」を保持。追加時、中点で負けた直線を子ノードに渡す再帰的実装で $O(\log N)$ per add。
$b$ の全値を座標圧縮。Li Chao Tree は各ノードで「中点で優位な直線」を保持。追加時、中点で負けた直線を子ノードに渡す再帰的実装で $O(\log N)$ per add。
3トポロジカル順での処理
$i < j$ の DAG なので頂点番号順 = トポロジカル順。頂点 $u$ が確定 → 直線追加。頂点 $v$ → クエリ。順序が保証されているため、常に前辺の情報が Li Chao Tree に存在する。
$i < j$ の DAG なので頂点番号順 = トポロジカル順。頂点 $u$ が確定 → 直線追加。頂点 $v$ → クエリ。順序が保証されているため、常に前辺の情報が Li Chao Tree に存在する。
4到達不可能の処理
$dp[u] = -\infty$ の頂点の直線は追加しない。クエリ結果が $-\infty$ なら先行辺から到達不可能。最終的に $dp[N-1] = -\infty$ なら $-1$ を出力。
$dp[u] = -\infty$ の頂点の直線は追加しない。クエリ結果が $-\infty$ なら先行辺から到達不可能。最終的に $dp[N-1] = -\infty$ なら $-1$ を出力。
計算量
Li Chao Tree 構築: $O(N \log N)$(座標圧縮 + 初期化)
各辺の追加・クエリ: $O((N+M) \log N)$
全体: $O((N+M) \log N)$
空間: $O(N)$(Li Chao Tree)
各辺の追加・クエリ: $O((N+M) \log N)$
全体: $O((N+M) \log N)$
空間: $O(N)$(Li Chao Tree)
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 最小化と最大化の混同 | Li Chao Tree の不等号の向き | 最大化: line_mid > cur_mid のとき入れ替え |
| 座標圧縮の漏れ | クエリ値が登録リストにない | xs = sorted(set(b)) で全 b を事前登録 |
| 到達不可能頂点の直線追加 | $dp[u]=-\infty$ の直線が他クエリに影響 | if dp[v] != -INF: add(...) |
| Li Chao Tree の子方向ミス | 傾きで左右を決める | 最大化では傾きが大きい方が左(小x)で有利 |
次のステップ
- 発展問題: $w_{ij} = a_i + b_j$(傾き固定、切片が異なる CHT)
- 関連: Kinetic Heap との比較(動的最小値追跡)
- 応用: 3次元の場合($w_{ijk} = a_i b_j c_k$ の多段 CHT / 分割統治)