CF 167E - Wizards and Bets

We are given a directed acyclic graph. Some vertices are sources, meaning no edge enters them. Some vertices are sinks, meaning no edge leaves them. The number of sources and sinks is guaranteed to be equal.

CF 167E - Wizards and Bets

Rating: 2900
Tags: dfs and similar, graphs, math, matrices
Solve time: 3m 52s
Verified: yes

Solution

Problem Understanding

We are given a directed acyclic graph. Some vertices are sources, meaning no edge enters them. Some vertices are sinks, meaning no edge leaves them. The number of sources and sinks is guaranteed to be equal.

A valid configuration consists of vertex-disjoint directed paths such that every source is connected to exactly one sink, and every sink receives exactly one path. Since the paths are vertex-disjoint, every vertex can belong to at most one chosen path.

Suppose the sinks are ordered increasingly by vertex number, and the sources are ordered the same way. If sink i is connected to source a[i], then the permutation a determines the sign of the configuration. Even inversion count contributes +1, odd inversion count contributes -1.

The task is to sum these signs over all valid collections of disjoint paths, modulo a prime p.

This is not asking us to enumerate matchings. It asks for the alternating sum over all perfect source-to-sink path systems.

The graph has at most 600 vertices and 100000 edges. That immediately rules out any exponential approach over subsets of sources or sinks. Even 2^20 would already be too large, while here the number of terminals may itself approach 600.

The graph is acyclic, which is the structural property the whole solution depends on. In a DAG, path counting can be done with dynamic programming in topological order. The small bound on n strongly suggests an O(n^3) or similar algebraic solution is intended.

Multiple edges are allowed. A careless implementation that compresses parallel edges into one edge would undercount paths. For example:

2 2
1 2
1 2

There are two distinct paths from 1 to 2, not one.

Vertices may simultaneously be a source and a sink. For example:

1 0

The single vertex is both source and sink. There is exactly one valid path system, consisting of the empty path from the vertex to itself, so the answer is 1.

Another subtle case is when some source cannot reach some sink. A naive determinant implementation that forgets unreachable pairs become zero may accidentally count invalid matchings. Example:

4 2
1 3
2 3

Sources are {1,2}, sinks are {3,4}. Sink 4 is unreachable, so no valid path system exists. The answer is 0.

The hardest pitfall is understanding why the alternating sum appears at all. If we tried to count only non-intersecting path systems directly, inclusion-exclusion would become messy. The key observation is that determinants already encode signs of permutations, and intersecting path systems cancel automatically.

Approaches

The brute-force approach is conceptually simple. We could enumerate every possible path from each source to each sink, then try every combination of paths, check whether they are vertex-disjoint, compute the induced permutation, and add +1 or -1.

This works because the definition of the answer is literally a signed sum over valid path systems.

The problem is that the number of paths in a DAG can already be exponential in n. Even a layered graph with only two outgoing choices per layer creates exponentially many source-to-sink paths. Trying all combinations becomes completely impossible.

The first useful observation is that the sign depends only on the source-to-sink permutation. That is exactly the structure that determinants encode.

Suppose we build a matrix A where:

A[i][j] = number of paths from source i to sink j

Then:

$\det(A)=\sum_{\pi} \operatorname{sgn}(\pi) \prod_i A_{i,\pi(i)}$

Each product corresponds to choosing one path from every source to the assigned sink.

At first glance this seems wrong, because the determinant counts all tuples of paths, including intersecting ones. The miracle is the Lindström-Gessel-Viennot lemma: in a DAG, all intersecting path tuples cancel pairwise in the determinant expansion.

Only vertex-disjoint path systems survive.

So the entire problem reduces to:

  1. Compute path counts between every source and sink.
  2. Build the matrix.
  3. Compute its determinant modulo p.

Since n ≤ 600, cubic Gaussian elimination is completely feasible.

Approach Time Complexity Space Complexity Verdict
Brute Force Exponential Exponential Too slow
Optimal O(n^3 + km) O(n^2) Accepted

Here k is the number of sources and sinks.

Algorithm Walkthrough

  1. Read the graph and compute indegrees and outdegrees.

Sources are vertices with indegree zero. Sinks are vertices with outdegree zero. Their ordering must follow increasing vertex index exactly as required by the statement. 2. Compute a topological ordering of the DAG.

Since the graph is acyclic, Kahn's algorithm produces a valid order. This order lets us perform path DP correctly. 3. For each source s, compute the number of paths from s to every vertex.

Initialize:

dp[s] = 1

Then process vertices in topological order. For every edge u -> v:

dp[v] += dp[u]

modulo p.

Because the graph is acyclic, by the time we process u, all ways to reach u are already finalized. 4. Fill the matrix A.

A[i][j] equals the number of paths from source i to sink j. 5. Compute the determinant modulo p.

Use Gaussian elimination with modular inverses.

Whenever the pivot row differs from the current row, swap rows and multiply the determinant by -1.

Since p is prime, modular inverses exist for every nonzero pivot. 6. Output the determinant modulo p.

Why it works

The determinant expansion sums over all permutations of source-to-sink assignments. For a fixed permutation, the product term counts all tuples of paths realizing that assignment.

Intersecting tuples appear twice with opposite signs and cancel each other. This cancellation is the core statement of the Lindström-Gessel-Viennot lemma.

Only vertex-disjoint path systems remain, each weighted by the sign of its permutation. That is exactly the quantity the problem asks us to compute.

Python Solution

import sys
from collections import deque

input = sys.stdin.readline

def modinv(a, p):
    return pow(a, p - 2, p)

def determinant(mat, p):
    n = len(mat)
    det = 1

    for col in range(n):
        pivot = -1

        for row in range(col, n):
            if mat[row][col] % p != 0:
                pivot = row
                break

        if pivot == -1:
            return 0

        if pivot != col:
            mat[pivot], mat[col] = mat[col], mat[pivot]
            det = (-det) % p

        pivot_val = mat[col][col] % p
        det = (det * pivot_val) % p

        inv_pivot = modinv(pivot_val, p)

        for row in range(col + 1, n):
            if mat[row][col] == 0:
                continue

            factor = mat[row][col] * inv_pivot % p

            for k in range(col, n):
                mat[row][k] = (mat[row][k] - factor * mat[col][k]) % p

    return det % p

def solve():
    n, m, p = map(int, input().split())

    g = [[] for _ in range(n)]
    indeg = [0] * n
    outdeg = [0] * n

    for _ in range(m):
        u, v = map(int, input().split())
        u -= 1
        v -= 1

        g[u].append(v)
        indeg[v] += 1
        outdeg[u] += 1

    sources = [i for i in range(n) if indeg[i] == 0]
    sinks = [i for i in range(n) if outdeg[i] == 0]

    k = len(sources)

    q = deque()

    cur_indeg = indeg[:]

    for i in range(n):
        if cur_indeg[i] == 0:
            q.append(i)

    topo = []

    while q:
        u = q.popleft()
        topo.append(u)

        for v in g[u]:
            cur_indeg[v] -= 1
            if cur_indeg[v] == 0:
                q.append(v)

    mat = [[0] * k for _ in range(k)]

    for i, s in enumerate(sources):
        dp = [0] * n
        dp[s] = 1

        for u in topo:
            if dp[u] == 0:
                continue

            val = dp[u]

            for v in g[u]:
                dp[v] += val
                if dp[v] >= p:
                    dp[v] %= p

        for j, t in enumerate(sinks):
            mat[i][j] = dp[t] % p

    ans = determinant(mat, p)
    print(ans % p)

solve()

The first section constructs the DAG and identifies sources and sinks exactly in increasing vertex order. The statement defines the numbering implicitly this way, so sorting differently would change the sign convention and produce wrong answers.

The topological sort is standard Kahn processing. We copy the indegree array because the original values are still needed later to identify sources.

For each source, we run a path-count DP over the DAG. Since n is only 600, running one DP per source is cheap enough. Multiple edges are handled naturally because every edge contributes separately during propagation.

The determinant implementation uses modular Gaussian elimination. A common mistake is forgetting that row swaps change the determinant sign. Another common bug is dividing normally instead of multiplying by modular inverse.

The matrix is modified in place during elimination. That is fine because the original values are never needed afterward.

Worked Examples

Sample 1

Input:

4 2 1000003
1 3
2 4

Sources are [1,2]. Sinks are [3,4].

Path matrix:

Source Sink 3 Sink 4
1 1 0
2 0 1

So:

$\det\begin{pmatrix}1&0\0&1\end{pmatrix}=1$

The determinant is 1.

This trace demonstrates the simplest possible non-intersecting system. The determinant reduces to the identity permutation with positive sign.

Sample 2

Consider:

4 2 1000003
1 4
2 3

Sources are [1,2]. Sinks are [3,4].

Path matrix:

Source Sink 3 Sink 4
1 0 1
2 1 0

So:

$\det\begin{pmatrix}0&1\1&0\end{pmatrix}=-1$

Modulo 1000003, the answer becomes 1000002.

This example shows how the determinant automatically encodes permutation parity. The only matching swaps the two terminals, giving one inversion and negative sign.

Complexity Analysis

Measure Complexity Explanation
Time O(k(n + m) + k^3) Path DP for each source plus determinant computation
Space O(k^2 + n + m) Matrix, graph, and DP storage

Since k ≤ n ≤ 600, the cubic determinant step is at most about 2.1 × 10^8 primitive arithmetic operations in the absolute worst theoretical case, but in practice Python handles this size comfortably because the inner work is simple modular arithmetic and many matrices become sparse during elimination. The path DP is linear in graph size per source and easily fits the limits.

Test Cases

# helper: run solution on input string, return output string
import sys
import io
from collections import deque

def run(inp: str) -> str:
    sys.stdin = io.StringIO(inp)

    input = sys.stdin.readline

    def modinv(a, p):
        return pow(a, p - 2, p)

    def determinant(mat, p):
        n = len(mat)
        det = 1

        for col in range(n):
            pivot = -1

            for row in range(col, n):
                if mat[row][col] % p != 0:
                    pivot = row
                    break

            if pivot == -1:
                return 0

            if pivot != col:
                mat[pivot], mat[col] = mat[col], mat[pivot]
                det = (-det) % p

            pivot_val = mat[col][col] % p
            det = (det * pivot_val) % p

            inv_pivot = modinv(pivot_val, p)

            for row in range(col + 1, n):
                if mat[row][col] == 0:
                    continue

                factor = mat[row][col] * inv_pivot % p

                for k in range(col, n):
                    mat[row][k] = (
                        mat[row][k] - factor * mat[col][k]
                    ) % p

        return det % p

    n, m, p = map(int, input().split())

    g = [[] for _ in range(n)]
    indeg = [0] * n
    outdeg = [0] * n

    for _ in range(m):
        u, v = map(int, input().split())
        u -= 1
        v -= 1

        g[u].append(v)
        indeg[v] += 1
        outdeg[u] += 1

    sources = [i for i in range(n) if indeg[i] == 0]
    sinks = [i for i in range(n) if outdeg[i] == 0]

    k = len(sources)

    q = deque()

    cur = indeg[:]

    for i in range(n):
        if cur[i] == 0:
            q.append(i)

    topo = []

    while q:
        u = q.popleft()
        topo.append(u)

        for v in g[u]:
            cur[v] -= 1
            if cur[v] == 0:
                q.append(v)

    mat = [[0] * k for _ in range(k)]

    for i, s in enumerate(sources):
        dp = [0] * n
        dp[s] = 1

        for u in topo:
            for v in g[u]:
                dp[v] = (dp[v] + dp[u]) % p

        for j, t in enumerate(sinks):
            mat[i][j] = dp[t]

    return str(determinant(mat, p)) + "\n"

# provided sample
assert run(
"""4 2 1000003
1 3
2 4
"""
) == "1\n", "sample 1"

# single isolated vertex
assert run(
"""1 0 13
"""
) == "1\n", "single source-sink vertex"

# impossible matching
assert run(
"""4 2 17
1 3
2 3
"""
) == "0\n", "unreachable sink"

# odd permutation
assert run(
"""4 2 1000003
1 4
2 3
"""
) == "1000002\n", "negative determinant"

# multiple parallel edges
assert run(
"""2 2 97
1 2
1 2
"""
) == "2\n", "parallel edges counted separately"
Test input Expected output What it validates
Single isolated vertex 1 Empty path counts correctly
Unreachable sink 0 Impossible systems vanish
Crossed matching p-1 Negative permutation sign
Parallel edges 2 Multiple edges are distinct paths

Edge Cases

Consider the isolated vertex case:

1 0 13

The single vertex has indegree zero and outdegree zero, so it is both a source and a sink.

The path matrix is:

v1
v1 1

The empty path from the vertex to itself is valid in DAG path counting. The determinant is 1, so the algorithm outputs 1.

Now consider unreachable sinks:

4 2 17
1 3
2 3

Sources are [1,2], sinks are [3,4].

No path reaches sink 4, so the matrix becomes:

3 4
1 1 0
2 1 0

Its determinant is zero. During elimination, the second pivot column has no nonzero pivot, so the algorithm immediately returns 0.

Finally, consider parallel edges:

2 2 97
1 2
1 2

There are exactly two distinct directed paths from 1 to 2, one for each edge.

The DP processes both edges independently:

dp[2] += dp[1]
dp[2] += dp[1]

giving dp[2] = 2.

The determinant of the 1 × 1 matrix [2] is 2, which is the correct answer.