D - 都市巡回ラリー / City Tour Rally 解説 by admin
Claude 4.6 Opus (Thinking)概要
有向グラフ上で \(K\) 日間の滞在計画を立て、各日のスコア \((P_{c_j} \times j) \bmod Q\) の合計を最大化する問題。動的計画法(DP)で解ける。
考察
重要な気づき
この問題は「有向グラフ上で長さ \(K\) のパス(頂点の再訪問あり)を見つけ、重み付きスコアの合計を最大化する」問題と言い換えられます。
素朴なアプローチの問題
全ての経路を列挙しようとすると、最大で \(N^K\) 通りの計画が存在し得るため、到底間に合いません。
解決の方針
「\(j\) 日目にどの都市にいるか」だけが次の遷移に影響するため、状態を \((\text{日目}, \text{都市})\) の組で管理する DP が有効です。
具体的には:
\[dp[j][i] = \text{1日目から } j \text{ 日目まで有効な計画で、} j \text{ 日目に都市 } i \text{ にいるときの最大スコア}\]
と定義します。
アルゴリズム
DP の定式化
初期状態(1日目):
任意の都市 \(i\) から出発できるので:
\[dp[1][i] = (P_i \times 1) \bmod Q\]
遷移(\(j = 2, 3, \ldots, K\)):
\(j\) 日目に都市 \(i\) にいるためには、前日(\(j-1\) 日目)に都市 \(i\) への辺を持つ都市 \(u\) にいる必要があります。そこで 逆隣接リスト(都市 \(i\) への入辺を持つ都市の集合)を使います:
\[dp[j][i] = \max_{u \in \text{rev\_adj}(i)} dp[j-1][u] + (P_i \times j) \bmod Q\]
入辺が存在しない場合、\(dp[j][i] = -\infty\)(到達不可能)とします。
最終的な答え:
\[\text{answer} = \max_{i=1}^{N} dp[K][i]\]
具体例
例えば \(N=3, K=3\) で辺が \(1 \to 2, 2 \to 3, 2 \to 1\) の場合:
- 1日目:都市1, 2, 3 のどれでも選べる
- 2日目:前日の都市から辺がある都市に移動
- 3日目:同様に移動
DP の各ステップで「前日にどの都市にいれば最大スコアになるか」を効率的に求めています。
計算量
- 時間計算量: \(O(K \times (N + M))\)
- \(K\) 日分のループがあり、各日で全辺を1回ずつ見る(逆隣接リストの走査は全体で \(M\) 回)
- 空間計算量: \(O(N + M)\)
- DP 配列は前日分と当日分の2つだけ保持すればよい(\(O(N)\))
- 隣接リストに \(O(N + M)\)
制約 \(N \leq 1000, M \leq 50000, K \leq 1000\) より、計算量は最大で約 \(1000 \times 50000 = 5 \times 10^7\) 程度であり、十分高速です。
実装のポイント
逆隣接リストを事前に構築しておくと、遷移の際に「都市 \(i\) に来られる都市はどこか」を効率的に列挙できる。
DP 配列は2本(前日分
dp_prevと当日分dp_curr)だけで十分。毎日dp_currを計算した後、dp_prevに上書きする。到達不可能な状態を \(-1\)(または \(-\infty\))で管理し、遷移時に到達可能な前日の状態が1つもなければその都市は到達不可能とする。
\(K = 1\) の場合は移動が不要なので、単に全都市のスコアの最大値を出力すればよい。
ソースコード
import sys
def main():
input_data = sys.stdin.buffer.read().split()
idx = 0
N = int(input_data[idx]); idx += 1
M = int(input_data[idx]); idx += 1
K = int(input_data[idx]); idx += 1
Q = int(input_data[idx]); idx += 1
P = [0] * (N + 1)
for i in range(1, N + 1):
P[i] = int(input_data[idx]); idx += 1
# adj[v] = list of cities that have an edge TO v (reverse adjacency)
# We need: for each city v, which cities u have edge u -> v
rev_adj = [[] for _ in range(N + 1)]
# Also store forward adjacency to know which cities have outgoing edges
fwd_adj = [[] for _ in range(N + 1)]
for _ in range(M):
a = int(input_data[idx]); idx += 1
b = int(input_data[idx]); idx += 1
fwd_adj[a].append(b)
rev_adj[b].append(a)
# DP approach:
# dp[j][i] = maximum score achievable by staying at city i on day j
# using a valid plan from day 1 to day j.
#
# Base case: dp[1][i] = (P[i] * 1) % Q for all i
# Transition: dp[j][i] = max over u in rev_adj[i] of dp[j-1][u] + (P[i] * j) % Q
#
# But we also need to ensure that from day j, the plan can continue to day K.
# Actually, the problem says "有効な滞在計画が少なくとも1つ存在する" is guaranteed,
# and we just need to maximize over all valid K-day plans.
# A plan must have length exactly K, so we need dp[K][i] for some i.
#
# However, we need to be careful: if K >= 2, on day j < K, city i must have
# at least one outgoing edge to continue. But actually, we just need the full
# path to exist. The DP forward ensures connectivity, and as long as dp[K][i]
# is reachable, it's fine.
#
# Wait, but what if at day j < K we're at city i with no outgoing edges?
# Then we can't continue. So we need to handle reachability.
#
# Approach: dp[j][i] = best score for days 1..j ending at city i, or -infinity if impossible.
# For day j < K, city i must have at least one outgoing edge (otherwise we can't continue,
# but actually that's handled naturally: if city i has no outgoing edge and j < K,
# then dp[j][i] won't contribute to any dp[j+1][*], so it's fine).
# Actually we just need max over i of dp[K][i].
NEG_INF = -1
# Use arrays for efficiency
# dp_prev[i] for i in 0..N (1-indexed)
dp_prev = [NEG_INF] * (N + 1)
# Day 1
for i in range(1, N + 1):
dp_prev[i] = (P[i] * 1) % Q
# If K == 1, answer is max of dp_prev
if K == 1:
print(max(dp_prev[1:]))
return
for j in range(2, K + 1):
dp_curr = [NEG_INF] * (N + 1)
score_j = [0] * (N + 1)
for i in range(1, N + 1):
score_j[i] = (P[i] * j) % Q
for i in range(1, N + 1):
# Find max dp_prev[u] for u in rev_adj[i]
best = NEG_INF
for u in rev_adj[i]:
if dp_prev[u] > best:
best = dp_prev[u]
if best != NEG_INF:
dp_curr[i] = best + score_j[i]
dp_prev = dp_curr
print(max(dp_prev[1:]))
if __name__ == '__main__':
main()
この解説は claude4.6opus-thinking によって生成されました。
投稿日時:
最終更新: