E - 観光ルートの平均スコア / Average Score of Tourist Routes 解説 by admin
Claude 4.6 Opus (Thinking)Overview
This problem asks us to find the average satisfaction score across all simple paths (paths that don’t visit the same vertex twice) from start vertex \(S\) to end vertex \(T\) on a graph. We use bitmask DP to efficiently compute the total score sum across all paths and the number of paths.
Analysis
Naive Approach and Its Issues
The most intuitive method is to enumerate all simple paths from \(S\) to \(T\), compute the score of each path, and take the average. However, in the case of a complete graph with \(N\) vertices, the number of simple paths can be as large as \((N-2)!\) in the worst case, which for \(N = 18\) becomes \(16! \approx 2 \times 10^{13}\), making full enumeration far too slow.
Key Insight: State Compression
What matters for a simple path is “which vertex we are currently at” and “which vertices have already been visited.” By representing the set of visited vertices as a bitmask (an \(N\)-bit integer), the number of states is at most \(2^N \times N\). Even for \(N = 18\), this is about \(2^{18} \times 18 \approx 4.7\) million, which is perfectly manageable.
Technique: Directly Managing the Score Sum
We manage not only the “number of paths” but also the “sum of scores” simultaneously in the DP. When there are \(cnt\) paths reaching a state \((mask, cur)\) with a total score sum of \(scr\), moving to the next vertex \(v\) gives:
- New path count: \(cnt\) paths are carried over as-is
- New score sum: \(scr + cnt \times cost[v]\) (the satisfaction of spot \(v\) is added to each of the \(cnt\) paths)
Algorithm
State Definition: \(dp\_count[(mask, cur)]\) = number of paths where the visited set is \(mask\) and the current position is \(cur\). \(dp\_score[(mask, cur)]\) = the sum of scores (cumulative satisfaction so far) across all such paths.
Initial State: When \(mask = \{S\}\) (the set with only \(S\)’s bit set) and \(cur = S\): \(dp\_count = 1\), \(dp\_score = c_S\).
Transition: Process \(mask\) values in increasing order. From state \((mask, cur)\), transition to each adjacent vertex \(v\) of \(cur\) that is not in \(mask\).
- \(new\_mask = mask \cup \{v\}\)
- \(dp\_count[(new\_mask, v)]\) += \(cnt\)
- \(dp\_score[(new\_mask, v)]\) += \(scr + cnt \times cost[v]\)
Termination Condition: A state where \(cur = T\) means the path is complete, so no further transitions are made from it. The path count and score sum upon reaching \(T\) are accumulated.
Answer: Total score sum of all collected paths ÷ total number of paths.
Concrete Example
If \(S=1, T=3\) and there are two paths \(1 - 2 - 3\) and \(1 - 3\): - Path \(1 \to 3\): score = \(c_1 + c_3\) - Path \(1 \to 2 \to 3\): score = \(c_1 + c_2 + c_3\) - Average = \(\frac{(c_1 + c_3) + (c_1 + c_2 + c_3)}{2}\)
Complexity
- Time complexity: \(O(2^N \times N^2)\) (for each of the \(2^N \times N\) states, searching adjacent vertices takes at most \(O(N)\))
- Space complexity: \(O(2^N \times N)\) (only reachable states are stored using a dictionary)
Implementation Notes
Fast enumeration of adjacent vertices using bit operations: Obtain the bitmask of unvisited adjacent vertices with
adj[cur] & ~mask, then extract them one by one from the lowest bit using the techniquev = (neighbors & -neighbors).bit_length() - 1.Stop transitions at \(cur = T\): By not extending paths that have reached the endpoint, we correctly count only simple paths from \(S\) to \(T\).
Sparse state management using a dictionary: This saves memory and time when many states are unreachable.
Conversion to 0-indexed: Internally, vertex numbers are converted to \(0, 1, \ldots, N-1\) for convenience of bitmask operations.
Source Code
import sys
from functools import lru_cache
def main():
input_data = sys.stdin.read().split()
idx = 0
N = int(input_data[idx]); idx += 1
M = int(input_data[idx]); idx += 1
S = int(input_data[idx]); idx += 1
T = int(input_data[idx]); idx += 1
c = [0] * (N + 1)
for i in range(1, N + 1):
c[i] = int(input_data[idx]); idx += 1
# Adjacency as bitmask (0-indexed internally)
adj = [0] * N
for j in range(M):
u = int(input_data[idx]); idx += 1
v = int(input_data[idx]); idx += 1
adj[u - 1] |= (1 << (v - 1))
adj[v - 1] |= (1 << (u - 1))
# Convert S, T to 0-indexed
S -= 1
T -= 1
# dp[visited_mask][current_node] = (count_of_paths, sum_of_scores)
# We use memoization with bitmask DP
# visited_mask includes all visited nodes including current
# We'll do iterative DP or recursive with lru_cache
# State: (mask, cur) where mask is the set of visited nodes, cur is current position
# Transition: move to neighbor not in mask
# If cur == T, we have a complete path (don't extend further)
cost = [0] * N
for i in range(N):
cost[i] = c[i + 1]
# dp[mask][cur] = (number_of_paths_from_cur_to_T, total_score_sum_of_remaining_path_including_cur)
# Actually, let's think forward:
# Let's accumulate from S.
# State: (mask, cur) - mask is visited set, cur is where we are
# For each state, count = number of simple paths from cur to T using only unvisited nodes
# score_sum = sum of scores of all such paths (where score includes all nodes from S to T)
# Better approach: enumerate paths from S to T
# dp[mask][cur]: number of paths that visited exactly 'mask' nodes and ended at 'cur'
# and the total accumulated score sum across all such paths
# Initialize: mask = {S}, cur = S, count=1, score_sum = cost[S]
# Transition: from (mask, cur), go to neighbor v not in mask
# new_mask = mask | (1 << v)
# new_count += count
# new_score_sum += score_sum + count * cost[v] (each of the 'count' paths gains cost[v])
# Answer: sum over all masks where T is in mask, of dp[mask][T].score_sum / dp[mask][T].count total
# Actually: total_score = sum of score_sum for all (mask, T), total_count = sum of count for all (mask, T)
# answer = total_score / total_count
# Use dictionary or array. With N<=18, mask up to 2^18 = 262144, nodes up to 18
# Total states: 2^18 * 18 ~ 4.7M, manageable
size = 1 << N
# Use arrays: dp_count[mask][cur], dp_score[mask][cur]
# Too much memory for full array? 262144 * 18 = ~4.7M entries, fine with dicts or arrays
# Let's use dictionaries keyed by (mask, cur) for efficiency since many states are unreachable
dp_count = {}
dp_score = {}
init_mask = 1 << S
dp_count[(init_mask, S)] = 1
dp_score[(init_mask, S)] = cost[S]
total_count = 0
total_score = 0
# BFS/iteration in order of popcount of mask
from collections import deque
queue = deque()
queue.append((init_mask, S))
# We need to process each state once. Use a set or process by mask order.
# Better: iterate over masks in order (increasing), then nodes.
# Rebuild with iteration over masks
for mask in range(size):
for cur in range(N):
if (mask, cur) not in dp_count:
continue
cnt = dp_count[(mask, cur)]
scr = dp_score[(mask, cur)]
if cur == T:
total_count += cnt
total_score += scr
continue # Don't extend past T
neighbors = adj[cur] & ~mask
while neighbors:
v = (neighbors & -neighbors).bit_length() - 1
neighbors &= neighbors - 1
new_mask = mask | (1 << v)
key = (new_mask, v)
if key in dp_count:
dp_count[key] += cnt
dp_score[key] += scr + cnt * cost[v]
else:
dp_count[key] = cnt
dp_score[key] = scr + cnt * cost[v]
print(f"{total_score / total_count:.10f}")
main()
This editorial was generated by claude4.6opus-thinking.
投稿日時:
最終更新: