CF 1547G - How Many Paths?

We are given a directed graph where we start from node 1 and consider all possible directed walks that end at each vertex. A walk can revisit nodes and edges arbitrarily many times.

CF 1547G - How Many Paths?

Rating: 2100
Tags: dfs and similar, dp, graphs, trees
Solve time: 8m 54s
Verified: no

Solution

Problem Understanding

We are given a directed graph where we start from node 1 and consider all possible directed walks that end at each vertex. A walk can revisit nodes and edges arbitrarily many times. For every vertex, we are not asked to compute the exact number of such walks, but only to classify that number into four categories: no reachability, exactly one walk, multiple but finitely many walks, or infinitely many walks.

The key subtlety is that cycles completely change the meaning of “number of paths”. A cycle does not just add more paths, it can be repeated arbitrarily many times, which immediately creates infinitely many distinct walks as long as it is reachable from the start and can also reach the target.

The input size forces us into a linear or near-linear solution per test case. The total number of vertices and edges across all test cases is bounded by 4e5, so any algorithm that is more than O(n + m) per test case or that revisits edges excessively will not pass. This already rules out any explicit path counting, any exponential DFS over all walks, and any naive dynamic programming that does not collapse cycles.

There are a few failure modes that appear in straightforward attempts. A naive DFS that counts paths will immediately blow up on graphs like a single directed cycle reachable from node 1, since the number of distinct walks becomes infinite due to repeated traversal of the cycle. Another common mistake is treating reachability as a DAG problem and doing DP in arbitrary order. That fails when cycles exist, because DP assumes acyclicity and finite propagation order. Finally, marking a node as “visited” once is not enough: revisiting is exactly what creates multiple paths and infinite counts.

A small illustrative failure case is a cycle reachable from 1 such as 1 → 2 → 3 → 2. A naive path counter would keep enumerating 1-2-3-2-3-2-... indefinitely, while the correct answer is immediately “infinite paths” for nodes 2 and 3.

Approaches

If we ignore the constraints, the most direct idea is to enumerate all walks from node 1 using DFS and count how many distinct ways each node is reached. This is correct in acyclic graphs because each path is finite and the number of paths is bounded by combinatorics of DAG structure. However, as soon as cycles exist, this approach fails because a cycle can be traversed any number of times, producing infinitely many distinct walks even if we only care about reachability patterns.

The key structural observation is that cycles are the only source of infinity. If we could contract every strongly connected component into a single node, the resulting graph becomes a DAG. Inside a strongly connected component, once you enter it, you can reach every node in it and potentially leave in multiple ways. If that component is also reachable from a cycle that can still reach outward, then all nodes downstream become infinite.

This suggests a two-phase reasoning. First we identify all nodes that are part of or reachable from any cycle that is reachable from node 1. These nodes automatically get answer -1 because we can loop indefinitely before continuing or after returning. Second, on the remaining graph, which is effectively a DAG after removing “bad” nodes, we can compute the number of ways from node 1 using a topological DP, but we only need to distinguish between 0, 1, and 2. The moment a node accumulates more than one way, we cap it at 2.

To detect cycles efficiently, we use DFS with recursion state tracking or SCC decomposition. The standard CF solution uses DFS colors: unvisited, visiting, and finished. Any edge to a “visiting” node indicates a cycle. Once we find any cycle node reachable from 1, we propagate “infinite” forward through all nodes reachable from that cycle in the original graph.

After marking infinite nodes, we ignore them in DP and run a topological-style traversal on the remaining graph starting from 1. Because cycles are removed, this subgraph is a DAG, and path counting becomes linear propagation.

Approach Time Complexity Space Complexity Verdict
Brute DFS path enumeration exponential O(n) Too slow
Cycle-aware DFS + DAG DP O(n + m) O(n + m) Accepted

Algorithm Walkthrough

  1. Build adjacency list for the graph and also compute a reversed adjacency list. The reverse graph will later help us propagate “infinite reachability” backwards from detected cycles.
  2. Run a DFS from node 1 to determine which nodes are reachable at all. Any node not visited here immediately gets answer 0, since no path exists from 1.
  3. During DFS, maintain a recursion stack state to detect back edges. When we find an edge from a node to another node currently in the recursion stack, we identify a directed cycle. Mark all nodes involved in or reachable from such cycles as “bad”.

This works because any node on a cycle allows repeated traversal, producing infinitely many distinct walks. 4. After initial cycle detection, propagate “bad” status forward: if a node can reach a bad node, it is also bad. This is done using BFS/DFS over the original graph starting from all cycle nodes.

The reason this propagation is necessary is that once a path can enter a cycle and then continue onward, every downstream node inherits infinite multiplicity. 5. Now remove all bad nodes from consideration. On the remaining graph, all paths from node 1 are guaranteed finite. 6. Compute number of paths in this reduced graph using DFS memoization or topological DP. Since the graph is acyclic after removing bad nodes, each node’s value depends only on previously computed nodes.

We store only three states per node: 0 (unreachable), 1 (exactly one path), 2 (more than one path). Any sum exceeding 2 is clamped. 7. Output results: bad nodes get -1, unreachable nodes get 0, and remaining nodes get DP values.

Why it works

The correctness hinges on separating two phenomena: multiplicity due to branching and infiniteness due to cycles. Strongly connected components are exactly the maximal structures where cycles live, and any SCC reachable from node 1 that can be revisited implies unbounded repetition of walks. Once these are isolated and propagated forward, the remaining graph becomes acyclic, so path counts behave like standard DAG DP. The clamping to 2 preserves only the distinction between unique and multiple finite paths, which is stable under DP transitions in a DAG.

Python Solution

import sys
sys.setrecursionlimit(10**7)
input = sys.stdin.readline

def solve():
    n, m = map(int, input().split())
    g = [[] for _ in range(n + 1)]
    rg = [[] for _ in range(n + 1)]

    for _ in range(m):
        a, b = map(int, input().split())
        g[a].append(b)
        rg[b].append(a)

    # 1. reachable from 1
    vis = [False] * (n + 1)
    stack = [1]
    vis[1] = True
    order = []
    while stack:
        u = stack.pop()
        order.append(u)
        for v in g[u]:
            if not vis[v]:
                vis[v] = True
                stack.append(v)

    if not vis[1]:
        vis[1] = True

    # 2. detect cycles via DFS colors
    color = [0] * (n + 1)
    bad = [False] * (n + 1)

    def dfs(u):
        color[u] = 1
        for v in g[u]:
            if not vis[v]:
                continue
            if color[v] == 0:
                if dfs(v):
                    bad[u] = True
            elif color[v] == 1:
                bad[v] = True
                bad[u] = True
        color[u] = 2
        return bad[u]

    for i in range(1, n + 1):
        if vis[i] and color[i] == 0:
            dfs(i)

    # 3. propagate bad forward
    from collections import deque
    q = deque([i for i in range(1, n + 1) if bad[i]])
    while q:
        u = q.popleft()
        for v in g[u]:
            if vis[v] and not bad[v]:
                bad[v] = True
                q.append(v)

    # 4. DP on remaining graph
    dp = [-1] * (n + 1)

    def solve_dp(u):
        if bad[u]:
            return 0
        if dp[u] != -1:
            return dp[u]
        total = 1 if u == 1 else 0
        for v in g[u]:
            if not vis[v] or bad[v]:
                continue
            total += solve_dp(v)
            if total > 2:
                total = 2
        dp[u] = total
        return dp[u]

    for i in range(1, n + 1):
        if vis[i] and not bad[i]:
            solve_dp(i)

    res = []
    for i in range(1, n + 1):
        if not vis[i]:
            res.append("0")
        elif bad[i]:
            res.append("-1")
        else:
            res.append(str(dp[i]))

    print(" ".join(res))

t = int(input())
for _ in range(t):
    input()
    solve()

The implementation first restricts attention to nodes reachable from 1, because everything else is trivially 0. It then uses DFS coloring to detect cycles and marks all nodes involved. The additional propagation step ensures that any node downstream of a cycle is also treated as infinite, which is essential because cycles affect not just their own nodes but everything reachable after them.

The DP stage carefully avoids double counting by clamping values at 2. This is important because the graph can have multiple merging paths, and exact counting is not required.

Worked Examples

Consider a small graph where 1 splits into two paths, 1 → 2 → 4 and 1 → 3 → 4. Node 4 should be marked as having two finite paths.

Step Reachable Cycle nodes DP(2) DP(3) DP(4)
after reachability {1,2,3,4} none - - -
after DP init same none 1 1 -
after DP same none 1 1 2

This confirms how merging paths increase counts but remain capped.

Now consider a cycle 1 → 2 → 3 → 2 with 3 → 4. Node 4 is reachable after a cycle.

Step Reachable Cycle detected bad nodes DP(4)
after DFS {1,2,3,4} {2,3} {2,3} -
propagation same same {2,3,4} -
final same same 2,3,4 = -1 -

This shows how cycle reachability forces downstream nodes into infinite classification.

Complexity Analysis

Measure Complexity Explanation
Time O(n + m) Each node and edge is processed a constant number of times across DFS, cycle detection, propagation, and DP
Space O(n + m) adjacency lists, state arrays, and recursion/DP storage

The constraints allow 4e5 total nodes and edges, so linear-time processing per test case is sufficient. The algorithm stays within this bound because every phase is strictly linear and avoids recomputation.

Test Cases

import sys, io

def run(inp: str) -> str:
    sys.stdin = io.StringIO(inp)
    from collections import defaultdict

    def solve():
        n, m = map(int, input().split())
        g = [[] for _ in range(n + 1)]
        vis = [False]*(n+1)
        for _ in range(m):
            a,b = map(int,input().split())
            g[a].append(b)

        vis[1]=True
        stack=[1]
        while stack:
            u=stack.pop()
            for v in g[u]:
                if not vis[v]:
                    vis[v]=True
                    stack.append(v)

        color=[0]*(n+1)
        bad=[False]*(n+1)

        def dfs(u):
            color[u]=1
            for v in g[u]:
                if not vis[v]:
                    continue
                if color[v]==0:
                    dfs(v)
                elif color[v]==1:
                    bad[u]=bad[v]=True
            color[u]=2

        for i in range(1,n+1):
            if vis[i] and color[i]==0:
                dfs(i)

        from collections import deque
        q=deque([i for i in range(1,n+1) if bad[i]])
        while q:
            u=q.popleft()
            for v in g[u]:
                if vis[v] and not bad[v]:
                    bad[v]=True
                    q.append(v)

        dp=[-1]*(n+1)
        def f(u):
            if bad[u]: return 0
            if dp[u]!=-1: return dp[u]
            res = 1 if u==1 else 0
            for v in g[u]:
                if vis[v] and not bad[v]:
                    res += f(v)
                    if res>2: res=2
            dp[u]=res
            return res

        for i in range(1,n+1):
            if vis[i] and not bad[i]:
                f(i)

        out=[]
        for i in range(1,n+1):
            if not vis[i]:
                out.append("0")
            elif bad[i]:
                out.append("-1")
            else:
                out.append(str(dp[i]))
        return " ".join(out)

    return solve()

# provided samples (basic sanity placeholders)
assert run("""1

6 7
1 4
1 3
3 4
4 5
2 1
5 5
5 6
""").split()[:6], "sample sanity"
Test input Expected output What it validates
single node 1 empty path base case
cycle only -1 -1 -1 infinite propagation
DAG branching 1 1 2 multiple finite paths
unreachable nodes 1 0 0 0 reachability filtering

Edge Cases

A self-loop at a node reachable from 1 immediately turns that node into infinite, because the loop can be repeated arbitrarily before exiting. The algorithm marks it as a cycle during DFS and then propagates that status forward, ensuring correctness.

A deeper cycle that is not directly connected to node 1 is ignored unless it becomes reachable from node 1. This is handled correctly because the first phase restricts processing to reachable nodes only, preventing false positives.

A node with two distinct incoming DAG paths but no cycles must return 2. The DP cap at 2 ensures we do not attempt full counting, while still distinguishing uniqueness from multiplicity.

A node that is unreachable except through a cycle should still be -1, not 0, since reachability through 1 implies existence of paths but infinite multiplicity. The propagation phase ensures such nodes inherit the infinite marker.