CF 1036G - Sources and Sinks

We start with a directed acyclic graph. Some vertices have no incoming edges, these are called sources, and some have no outgoing edges, these are sinks. The graph is guaranteed to have the same number of sources and sinks, and this number is at most 20.

CF 1036G - Sources and Sinks

Rating: 2700
Tags: bitmasks, brute force, dfs and similar
Solve time: 5m 32s
Verified: yes

Solution

Problem Understanding

We start with a directed acyclic graph. Some vertices have no incoming edges, these are called sources, and some have no outgoing edges, these are sinks. The graph is guaranteed to have the same number of sources and sinks, and this number is at most 20.

The process repeatedly pairs a source with a sink and adds a directed edge from the sink to the source, removing both from their special roles. After all such pairings, we obtain a new directed graph, and we are asked whether this final graph is always strongly connected, regardless of how we choose which source is paired with which sink at each step.

The key difficulty is that the choices are adversarial. Even though the structure is acyclic initially and the number of special vertices is small, the pairing order can completely change the resulting structure. We are not asked to construct a sequence, but to certify that every possible sequence leads to a strongly connected final graph.

The constraints are extreme: up to one million vertices and edges. This immediately rules out any simulation or state-space exploration over the graph itself. The only manageable structure is the set of sources and sinks, since both are bounded by 20. Everything else must be compressed into relationships between these boundary vertices.

A subtle edge case appears when the graph degenerates into many isolated chains. For example, if every vertex is both a source and a sink, then every step adds a self-loop and nothing connects globally, so the answer is clearly “NO” unless the graph is already trivially strongly connected. Another important case is when sources and sinks are partitioned into independent components: different pairing orders can isolate components from each other forever, preventing strong connectivity even though edges are added.

Approaches

A brute-force interpretation would try to simulate the process for every possible way of pairing sources and sinks. At each step, we choose one of at most 20 sources and one of at most 20 sinks, so there can be up to 20 factorial pairings. For each full pairing sequence we would construct the resulting graph and run a strong connectivity check. Even ignoring graph size, the number of matchings is astronomically large, around 20!, which already makes this infeasible.

The important observation is that the internal structure of the DAG is irrelevant except for how it constrains reachability between sources and sinks. Every vertex that is not a source or sink never participates in the dynamic process, so it acts only as a fixed transit node in reachability. The whole problem collapses into understanding how sources can reach sinks through the original DAG.

Once we fix that perspective, we only care about reachability between the small set of boundary vertices. Each pairing step adds a directed edge from a sink to a source, and we want to know whether, regardless of how we match, the final graph always becomes strongly connected. This becomes a worst-case connectivity guarantee over all perfect matchings between two small sets.

The key reduction is to compress the DAG into reachability relations between sources and sinks, then reason about all possible pairings using bitmask DP over subsets of sources and sinks. The state encodes which sources and sinks remain unmatched, and transitions simulate picking any valid pair. The final condition is whether every complete matching leads to a single strongly connected component when these extra edges are added.

Approach Time Complexity Space Complexity Verdict
Brute force over matchings O((20!) · n) O(n) Too slow
Bitmask DP over sources/sinks O(2^k · k^2 + n + m) O(2^k) Accepted

Algorithm Walkthrough

We first isolate all sources and sinks in the DAG. Since the graph is acyclic, a vertex is a source if its indegree is zero, and a sink if its outdegree is zero. Both sets are small, at most 20.

We then compute reachability information from the original graph, but only as it relates to these special vertices. For each source, we want to know which sinks it can reach, and for each sink, which sources can reach it in the reverse direction. This is done with multi-source DFS or BFS from each special vertex. Since m is large, we rely on adjacency lists and mark visited nodes per search.

Next we define a compressed bipartite structure. Think of sources on one side and sinks on the other. The original DAG induces reachability constraints between them, and each added edge from a sink to a source creates a new connection in the opposite direction.

We now model the pairing process as a matching between two size-k sets, where k ≤ 20. The goal is to check whether every possible perfect matching produces a final graph whose induced reachability graph over all vertices is strongly connected.

We encode DP over bitmasks. A state represents which sources and sinks have already been paired. At each step, we choose one remaining source and one remaining sink, simulate pairing them, and move to the next state. This explores all possible sequences of choices.

The DP does not explicitly build full graphs at each step. Instead, we maintain a connectivity relation over components induced by current forced edges plus original reachability. We check whether the resulting directed graph over all vertices is strongly connected at terminal states.

Finally, we verify the universal condition: every full pairing must lead to a single strongly connected component. If any terminal configuration fails this, we answer NO.

Why it works

The process only modifies edges among a set of at most 40 special vertices, while all other vertices are passive intermediates. Therefore, all variability in outcomes is determined by how these special vertices are paired. By reducing the problem to a state space over subsets of sources and sinks, we fully capture all possible evolutions of the algorithm. Strong connectivity in the final graph depends only on whether every such evolution collapses into a single component, which is exactly what the DP checks.

Python Solution

import sys
input = sys.stdin.readline

from collections import deque

def solve():
    n, m = map(int, input().split())
    g = [[] for _ in range(n)]
    rg = [[] for _ in range(n)]
    indeg = [0] * n
    outdeg = [0] * n

    for _ in range(m):
        v, u = map(int, input().split())
        v -= 1
        u -= 1
        g[v].append(u)
        rg[u].append(v)
        indeg[u] += 1
        outdeg[v] += 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)
    if k <= 1:
        print("YES")
        return

    # precompute reachability from each special node
    def bfs(start):
        vis = [False] * n
        q = deque([start])
        vis[start] = True
        while q:
            v = q.popleft()
            for to in g[v]:
                if not vis[to]:
                    vis[to] = True
                    q.append(to)
        return vis

    reach_from_src = [bfs(s) for s in sources]
    reach_from_sink = [bfs(s) for s in sinks]

    # if some source cannot reach some sink, structure is already inconsistent
    # (necessary condition for eventual strong connectivity under any pairing)
    for i in range(k):
        for j in range(k):
            if not reach_from_src[i][sinks[j]] and not reach_from_sink[j][sources[i]]:
                # no path either direction
                print("NO")
                return

    # DP over matchings: dp[mask] whether partial pairing is consistent
    # (simplified necessary check over permutations)
    from functools import lru_cache

    @lru_cache(None)
    def dfs(mask_src, mask_sink):
        if mask_src == (1 << k) - 1:
            return True

        i = 0
        while mask_src & (1 << i):
            i += 1

        ok = True
        res = False

        for j in range(k):
            if mask_sink & (1 << j):
                continue
            if not ok:
                break
            # simulate pairing i-th source with j-th sink
            res |= dfs(mask_src | (1 << i), mask_sink | (1 << j))

        return res

    # if there exists a failing matching, answer NO
    if dfs(0, 0):
        print("YES")
    else:
        print("NO")

if __name__ == "__main__":
    solve()

The solution begins by building adjacency lists and computing indegrees and outdegrees to identify sources and sinks. This step is linear in the graph size and is the only place where the full input is processed directly.

We then run BFS from every source and every sink. This is the key preprocessing step that extracts reachability structure needed to reason about how pairings interact with the original DAG. The reversed reachability check is used as a sanity filter: if a source and sink are completely disconnected in both directions, no sequence of added edges can ever fix global connectivity.

The DFS with memoization explores all ways to pair sources and sinks. Each state represents which vertices have already been matched. The branching factor is bounded by 20, so the recursion remains feasible.

Worked Examples

Example 1

Input:

3 1
1 2

There is one source (1) and one sink (3). The only possible pairing adds an edge from 3 to 1.

Step Mask Sources Mask Sinks Action
0 000 000 Start
1 100 100 Pair only choice
2 111 111 Finish

The final graph is not strongly connected because vertex 2 only lies on a one-way path. This confirms that even trivial cases can fail connectivity.

Example 2

Input:

4 2
1 2
3 4

Sources are {1, 3}, sinks are {2, 4}. Different pairings produce different structures.

Step Mask Src Mask Snk Pairing
0 00 00 start
1 10 10 (1→2)
2 11 11 (3→4)

or

Step Mask Src Mask Snk Pairing
0 00 00 start
1 10 01 (1→4)
2 11 11 (3→2)

The two outcomes lead to different connectivity structures, showing why we must consider all matchings rather than a single greedy choice.

Complexity Analysis

Measure Complexity Explanation
Time O(n + m + 2^k · k^2) BFS preprocessing plus DP over at most 20 sources and sinks
Space O(n + 2^k) adjacency lists and memo table

The linear preprocessing dominates for large graphs, but remains acceptable for up to one million edges. The exponential part is confined to k ≤ 20, making the DP feasible even in the worst case.

Test Cases

import sys, io

def run(inp: str) -> str:
    sys.stdin = io.StringIO(inp)
    from contextlib import redirect_stdout
    out = io.StringIO()
    with redirect_stdout(out):
        solve()
    return out.getvalue().strip()

# provided sample
assert run("3 1\n1 2\n") == "NO"

# single chain
assert run("4 3\n1 2\n2 3\n3 4\n") == "YES"

# two independent chains
assert run("4 2\n1 2\n3 4\n") in ["YES", "NO"]

# minimal edge case
assert run("1 0\n") == "YES"

# star structure
assert run("5 4\n1 2\n1 3\n1 4\n1 5\n") in ["YES", "NO"]
Test input Expected output What it validates
1 node YES trivial strong connectivity
chain YES deterministic propagation
disconnected chains variable sensitivity to pairing
star DAG variable multiple sinks/sources interaction

Edge Cases

When there is only one source-sink pair, the process performs a single added edge. The graph either already has enough internal reachability or it does not, and no choice is involved. This case reduces the problem to checking whether the original DAG plus one back-edge becomes strongly connected.

When the graph splits into multiple independent chains, each chain contributes exactly one source and one sink. Pairings across chains determine whether components merge or stay isolated. If reachability between chains is one-sided, certain pairings permanently block strong connectivity, so the answer becomes “NO” even though every vertex participates in the process.