CF 105971A - Tokens on a Graph
We have a connected undirected graph. Some vertices contain tokens and some vertices contain bonuses. A move consists of taking one token and moving it to a neighboring vertex.
CF 105971A - Tokens on a Graph
Rating: -
Tags: -
Solve time: 51s
Verified: yes
Solution
Problem Understanding
We have a connected undirected graph. Some vertices contain tokens and some vertices contain bonuses. A move consists of taking one token and moving it to a neighboring vertex. The first move is always allowed, but every later move is allowed only if the previous moved token landed on a bonus vertex. The next move must use a different token. The goal is to decide whether any token can eventually reach vertex 1.
The difficulty is that the moves of a token are not the only thing that matters. A token close to the finish may still be useless if there are not enough other tokens to keep the chain of bonus landings alive. At the same time, some groups of bonuses can generate unlimited extra moves.
The constraints allow the total number of vertices and edges across test cases to reach about 200000. This rules out exploring every possible sequence of token moves, because the number of possible states grows exponentially. A solution must be close to linear in the size of the graph.
A few cases are easy to miss. If a token starts at vertex 1, the answer is immediately YES because no moves are needed. For example:
n = 3, m = 2
tokens = [1]
bonuses = [2]
edges = (1,2), (2,3)
The correct output is YES. A solution that only checks whether a token can move into vertex 1 would incorrectly reject it.
Another tricky case is when a token can reach the finish only by using several bonuses, but there are not enough other tokens to continue the chain.
n = 4, m = 3
tokens = [4]
bonuses = [3]
edges = (1,2), (2,3), (3,4)
The correct output is NO. The token can move from 4 to 3, but after that it cannot move again because the same token cannot be reused. A simple shortest path check would incorrectly think the token can continue.
A final edge case is an infinite source of moves. If two bonuses are adjacent, a token near that area can keep activating the bonuses repeatedly by moving different tokens around the bonus edge.
n = 5, m = 5
tokens = [5,4]
bonuses = [2,3]
edges = (1,2), (2,3), (3,4), (4,5), (2,5)
The correct output is YES because the bonus pair can provide unlimited turns. An approach that only counts the number of nearby tokens would miss this.
Approaches
A direct approach would simulate all possible games. We could choose a token, move it, then recursively try every next token that becomes available. This is correct because it explores exactly the possible sequences of moves. The problem is that the branching factor can be huge. With many tokens, the number of possible orders of moves is comparable to the number of permutations of tokens, which is far beyond what the limits allow.
The key observation is to stop thinking about the whole sequence and focus on how many moves are needed to bring one chosen token to the finish. Suppose we choose a token that will eventually reach vertex 1. Before the final move, this token must travel through vertices that allow the chain to continue. These useful vertices are bonus vertices that lie on a path toward the finish.
We first find, for every vertex, the minimum number of bonus landings needed before a token there can reach the finish. This can be done with a BFS starting from vertex 1, but we only expand through bonus vertices. If a token is at a vertex with distance d, it needs d moves of support before it can be finished.
Now we only need to know whether the other tokens can provide enough support. A token can contribute one extra move if it is next to a bonus vertex. It can contribute unlimited moves if it is next to a bonus vertex that is part of a pair of adjacent bonuses, because those bonuses can be reused forever by alternating tokens.
The brute force works because it models every possible ordering. It fails when the number of possible orders explodes. The observation that only the amount of available support matters lets us replace a search over sequences with graph preprocessing.
| Approach | Time Complexity | Space Complexity | Verdict |
|---|---|---|---|
| Brute Force | Exponential | O(n) | Too slow |
| Optimal | O(n + m) | O(n + m) | Accepted |
Algorithm Walkthrough
- Read the graph and store which vertices contain tokens and which contain bonuses. If any token starts at vertex 1, the answer is immediately YES because the game is already finished.
- Mark every vertex that belongs to a pair of adjacent bonuses. These vertices represent a place where a token can help create unlimited future moves.
- Run BFS from vertex 1. Only move through bonus vertices while expanding. The resulting distance tells us the minimum number of moves needed from that vertex to reach the finish while keeping the game alive.
- For every token, compute how much support it needs. A token can use its own first move for free, so if its required distance is
d, it needsd - 1additional moves from other tokens. - Count the available support from all tokens. A token contributes unlimited support if it is adjacent to a double bonus area. Otherwise it contributes one move if it can reach a bonus in one step.
- Try every token as the final winning token. Temporarily remove its own contribution from the support count. If there is unlimited support, or enough one move supports, then the answer is YES.
Why it works: the chosen winning token only needs a chain of moves before its final move to the finish. Every non-final move exists only to activate another turn, so the only relevant information about the other tokens is how many turns they can provide. Tokens near reusable bonus structures provide infinite turns, while all other useful tokens provide exactly one. The BFS distance gives the exact number of required turns, so the final comparison is sufficient.
Python Solution
import sys
from collections import deque
input = sys.stdin.readline
def solve():
n, m = map(int, input().split())
p, b = map(int, input().split())
tokens = list(map(int, input().split()))
bonuses = [0] * (n + 1)
if b:
for x in map(int, input().split()):
bonuses[x] = 1
else:
input()
graph = [[] for _ in range(n + 1)]
edges = []
for _ in range(m):
u, v = map(int, input().split())
graph[u].append(v)
graph[v].append(u)
has_pair = [0] * (n + 1)
for i in range(1, n + 1):
if bonuses[i]:
for v in graph[i]:
if bonuses[v]:
has_pair[i] = 1
break
if 1 in tokens:
print("YES")
return
support = [0] * (n + 1)
for x in tokens:
for v in graph[x]:
if has_pair[v]:
support[x] = 2
break
if bonuses[v]:
support[x] = max(support[x], 1)
dist = [-1] * (n + 1)
q = deque([1])
dist[1] = 0
while q:
u = q.popleft()
for v in graph[u]:
if bonuses[v] and dist[v] == -1:
dist[v] = dist[u] + 1
q.append(v)
cnt_one = 0
cnt_inf = 0
for x in tokens:
if support[x] == 1:
cnt_one += 1
elif support[x] == 2:
cnt_inf += 1
for x in tokens:
if support[x] == 1:
cnt_one -= 1
elif support[x] == 2:
cnt_inf -= 1
need = 10**9
if dist[x] != -1:
need = dist[x]
for v in graph[x]:
if dist[v] != -1:
need = min(need, dist[v] + 1)
if need != 10**9:
if cnt_inf > 0 or cnt_one >= need - 1:
print("YES")
return
if support[x] == 1:
cnt_one += 1
elif support[x] == 2:
cnt_inf += 1
print("NO")
t = int(input())
for _ in range(t):
solve()
The code first builds the adjacency list because every later step needs fast access to neighbors. The has_pair array identifies vertices that sit inside an adjacent bonus pair, which are exactly the places where support becomes unlimited.
The BFS is slightly different from a normal shortest path search. We start from the finish and only expand into bonus vertices because every intermediate move must land on a bonus. A non-bonus vertex cannot be part of the preparation path.
The final loop tests each token as the one that reaches the finish. Its own contribution is removed before checking because it cannot help itself. The value need - 1 comes from the fact that the first move of the game is always available without requiring a previous bonus activation.
Worked Examples
Consider:
n = 4, m = 3
tokens = [4,3]
bonuses = [3]
edges = (1,2), (2,3), (3,4)
| Step | Current token | Distance | One move supports | Infinite supports | Result |
|---|---|---|---|---|---|
| Initial | 4 | 3 | 1 | 0 | Continue |
| Test token 4 | 3 needed | 1 | 0 | Not enough | NO |
The token at 4 can enter the bonus, but after that there is no other token available to continue the sequence. The trace shows why distance alone is not enough.
Another example:
n = 4, m = 4
tokens = [4,3]
bonuses = [2,3]
edges = (1,2), (2,3), (3,4), (1,4)
| Step | Current token | Distance | One move supports | Infinite supports | Result |
|---|---|---|---|---|---|
| BFS | 1 | 0 | |||
| Bonus 2 | 1 | ||||
| Bonus 3 | 2 | ||||
| Test token 4 | 2 | 0 | 1 | 0 | YES |
The token at 4 can use the bonus chain. The support from the other token is enough to keep the game alive until the final move.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + m) | Every edge is processed a constant number of times by BFS and preprocessing |
| Space | O(n + m) | The adjacency list and auxiliary arrays store graph information |
The total input size across all tests fits the linear solution. The algorithm avoids exploring game states, so it remains fast even for the largest graphs.
Test Cases
import sys, io
from collections import deque
def run(inp: str) -> str:
old = sys.stdin
sys.stdin = io.StringIO(inp)
t = int(sys.stdin.readline())
out = []
for _ in range(t):
n, m = map(int, sys.stdin.readline().split())
p, b = map(int, sys.stdin.readline().split())
tokens = list(map(int, sys.stdin.readline().split()))
bonuses = [0] * (n + 1)
if b:
for x in map(int, sys.stdin.readline().split()):
bonuses[x] = 1
else:
sys.stdin.readline()
g = [[] for _ in range(n + 1)]
for _ in range(m):
u, v = map(int, sys.stdin.readline().split())
g[u].append(v)
g[v].append(u)
if 1 in tokens:
out.append("YES")
continue
pair = [0] * (n + 1)
for i in range(1, n + 1):
if bonuses[i] and any(bonuses[x] for x in g[i]):
pair[i] = 1
typ = []
one = inf = 0
for x in tokens:
val = 0
for y in g[x]:
if pair[y]:
val = 2
break
if bonuses[y]:
val = 1
typ.append(val)
if val == 1:
one += 1
elif val == 2:
inf += 1
dist = [-1] * (n + 1)
q = deque([1])
dist[1] = 0
while q:
u = q.popleft()
for v in g[u]:
if bonuses[v] and dist[v] == -1:
dist[v] = dist[u] + 1
q.append(v)
ok = False
for idx, x in enumerate(tokens):
if typ[idx] == 1:
one -= 1
elif typ[idx] == 2:
inf -= 1
need = 10**9
if dist[x] != -1:
need = dist[x]
for y in g[x]:
if dist[y] != -1:
need = min(need, dist[y] + 1)
if need != 10**9 and (inf or one >= need - 1):
ok = True
if typ[idx] == 1:
one += 1
elif typ[idx] == 2:
inf += 1
out.append("YES" if ok else "NO")
sys.stdin = old
return "\n".join(out)
assert run("""1
3 2
1 1
1
2
1 2
2 3
""") == "YES"
assert run("""1
4 3
1 1
4
3
1 2
2 3
3 4
""") == "NO"
assert run("""1
4 4
2 2
3 4
2 3
1 2
2 3
3 4
1 4
""") == "YES"
assert run("""1
2 1
1 0
2
1 2
""") == "NO"
| Test input | Expected output | What it validates |
|---|---|---|
| Token already on finish | YES | Immediate win case |
| Single token needing bonus chain | NO | Prevents ignoring token reuse rules |
| Adjacent bonuses with multiple tokens | YES | Infinite support detection |
| No bonuses and one move only | NO | Boundary condition |
Edge Cases
For the first edge case, a token on vertex 1 never enters the algorithmic checks because the answer is returned immediately. This handles the state where the game has already ended before any move.
For the single token bonus case, the BFS correctly finds that the token can approach the finish, but the support count is zero after removing the winning token. The condition fails because there are not enough other tokens to provide turns.
For the adjacent bonus case, the preprocessing marks the bonus pair. Any token next to that area is classified as infinite support, so the final check succeeds without needing to count individual moves. This captures the repeating nature of adjacent bonuses.