CF 1093D - Beautiful Graph

We are given an undirected graph and asked to assign each vertex one value from the set {1, 2, 3}. The assignment is valid only if every edge connects two vertices whose values sum to an odd number. The task is to count how many such assignments exist, taken modulo 998244353.

CF 1093D - Beautiful Graph

Rating: 1700
Tags: dfs and similar, graphs
Solve time: 3m 33s
Verified: yes

Solution

Problem Understanding

We are given an undirected graph and asked to assign each vertex one value from the set {1, 2, 3}. The assignment is valid only if every edge connects two vertices whose values sum to an odd number. The task is to count how many such assignments exist, taken modulo 998244353.

The condition “sum is odd” immediately restricts which pairs can appear on an edge. A sum is odd exactly when one endpoint is odd and the other is even. Among the allowed labels, 1 and 3 are odd, while 2 is even. This transforms the problem from working with three arbitrary labels into working with a bipartition constraint: every edge must connect a vertex labeled odd to a vertex labeled even.

So every connected component of the graph must be bipartite, otherwise there is no way to consistently assign parity along edges.

The constraints are large, with up to 3×10^5 vertices and edges summed over all test cases. This rules out any approach that tries to assign values independently per vertex with backtracking or brute force enumeration of assignments, since even a single component of size n would lead to 3^n possibilities. The intended solution must be linear or near-linear per test case, using graph traversal.

A few edge cases matter:

A triangle graph such as a cycle of length 3 makes the answer zero. For example, n=3 with edges (1-2, 2-3, 3-1). Any attempt to alternate parity fails because it forces contradictions along the cycle.

An isolated vertex is always valid and contributes independently to the count. For instance, a single node graph with n=1 and m=0 has 3 valid assignments: {1,2,3}.

A disconnected graph requires combining answers across components multiplicatively, since assignments on different components do not interact.

Approaches

A brute-force approach would try all assignments of values 1, 2, or 3 to each vertex and check whether every edge satisfies the parity constraint. This requires checking 3^n assignments and validating each in O(m), giving O(3^n · m), which is infeasible even for n around 20.

The key simplification comes from interpreting values as parity classes. Each vertex is either odd (1 or 3) or even (2). The edge constraint only depends on parity, not the exact odd value chosen. If we first decide a bipartition of the graph into two parity classes, we can then refine assignments inside each class.

For a connected bipartite component, there are exactly two valid ways to assign parity: choose a starting node as even or odd and propagate. If a component is not bipartite, propagation leads to a contradiction, meaning the answer is zero.

Once parity is fixed, every vertex assigned odd can independently be either 1 or 3, giving 2 choices per vertex in the odd side. Every vertex assigned even must be 2, giving no freedom. However, we are free to swap the global parity orientation of each component, which flips which side is considered odd. This means for a bipartite component with sizes a and b, the number of valid assignments is 2^a + 2^b.

Since components are independent, we multiply these contributions across all components.

Approach Time Complexity Space Complexity Verdict
Brute Force O(3^n · m) O(n + m) Too slow
DFS + Bipartite Counting O(n + m) O(n + m) Accepted

Algorithm Walkthrough

We process each test case independently.

  1. Build the adjacency list of the graph. This representation is necessary because we will traverse components repeatedly, and adjacency lists allow linear-time traversal.
  2. Maintain a color array where each node is unvisited or assigned one of two parity colors. We use DFS or BFS to assign alternating colors along edges.
  3. For each unvisited node, start a traversal and attempt to 2-color its connected component. During traversal, we count how many nodes land in each color class. This gives the bipartition sizes for the component.
  4. If during traversal we find an edge connecting two nodes with the same color, the graph is not bipartite. In that case, no valid assignment exists because parity constraints would force contradiction, so we return 0 immediately.
  5. For a valid component, compute its contribution as 2^a + 2^b, where a and b are sizes of the two color classes. We multiply this value into the global answer.
  6. Repeat for all components and output the final product modulo 998244353.

Why this works is tied to how parity propagates. Once a node is assigned a parity, all neighbors are forced to take the opposite parity, and this constraint spreads across the component. This means each connected component behaves like a bipartite graph problem with exactly two global orientation choices. After fixing orientation, each node in the odd side independently chooses between two odd labels, while even nodes are fixed to 2.

Python Solution

import sys
input = sys.stdin.readline

MOD = 998244353

def solve():
    n, m = map(int, input().split())
    g = [[] for _ in range(n)]
    for _ in range(m):
        u, v = map(int, input().split())
        u -= 1
        v -= 1
        g[u].append(v)
        g[v].append(u)

    color = [-1] * n
    ans = 1

    for i in range(n):
        if color[i] != -1:
            continue

        stack = [i]
        color[i] = 0
        cnt = [0, 0]

        ok = True

        while stack:
            u = stack.pop()
            cnt[color[u]] += 1

            for v in g[u]:
                if color[v] == -1:
                    color[v] = color[u] ^ 1
                    stack.append(v)
                elif color[v] == color[u]:
                    ok = False

        if not ok:
            return 0

        a, b = cnt[0], cnt[1]

        pow2_a = pow(2, a, MOD)
        pow2_b = pow(2, b, MOD)

        ans = ans * (pow2_a + pow2_b) % MOD

    print(ans)

if __name__ == "__main__":
    solve()

The adjacency list construction ensures we can traverse each edge once during DFS. The color array encodes the bipartition attempt; -1 means unvisited, and 0/1 represent the two sides.

The stack-based DFS avoids recursion depth issues since n can be large. Each node is processed once, and each edge is examined twice, keeping complexity linear.

The key implementation detail is counting nodes per color during traversal. This directly gives the two possible interpretations of which side is “odd”. We compute powers of 2 separately for both sides because each vertex labeled odd has two choices: 1 or 3.

If a conflict is found, returning 0 immediately avoids unnecessary computation.

Worked Examples

Example 1: single edge

Input:

n=2, edge (1-2)

Step Node Color assignment Counts (0,1) Conflict
Start 1 0 (1,0) no
Visit 2 1 (1,1) no

Component sizes are a=1, b=1.

Contribution is 2^1 + 2^1 = 2 + 2 = 4.

Final answer: 4.

This confirms that swapping parity roles produces distinct valid assignments.

Example 2: triangle

Input: 1-2-3-1

Step Node Color assignment Counts Conflict
Start 1 0 (1,0) no
Visit 2 1 (1,1) no
Visit 3 1 (from 2), but edge 3-1 (2,1) conflict

When processing edge (3,1), both endpoints already have color 0/1 mismatch requirement violated, so a conflict appears.

Answer is 0.

This demonstrates that non-bipartite components immediately invalidate the entire graph.

Complexity Analysis

Measure Complexity Explanation
Time O(n + m) Each vertex is visited once and each edge is processed at most twice during DFS
Space O(n + m) Adjacency list and color array

The sum of n and m across all test cases is 3×10^5, so a linear traversal per test case is well within limits.

Test Cases

import sys, io

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

    MOD = 998244353

    def solve():
        n, m = map(int, input().split())
        g = [[] for _ in range(n)]
        for _ in range(m):
            u, v = map(int, input().split())
            u -= 1
            v -= 1
            g[u].append(v)
            g[v].append(u)

        color = [-1] * n
        ans = 1

        for i in range(n):
            if color[i] != -1:
                continue

            stack = [i]
            color[i] = 0
            cnt = [0, 0]
            ok = True

            while stack:
                u = stack.pop()
                cnt[color[u]] += 1
                for v in g[u]:
                    if color[v] == -1:
                        color[v] = color[u] ^ 1
                        stack.append(v)
                    elif color[v] == color[u]:
                        ok = False

            if not ok:
                return "0"

            a, b = cnt
            ans = ans * (pow(2, a, MOD) + pow(2, b, MOD)) % MOD

        return str(ans)

    return solve()

# provided samples
assert run("""2
2 1
1 2
4 6
1 2
1 3
1 4
2 3
2 4
3 4
""") == """4
0
"""

# single node
assert run("""1
1 0
""") == "3"

# triangle (odd cycle)
assert run("""1
3 3
1 2
2 3
3 1
""") == "0"

# disconnected bipartite components
assert run("""1
4 2
1 2
3 4
""") == str((pow(2,1)+pow(2,1))*(pow(2,1)+pow(2,1))%998244353)

# path graph
assert run("""1
3 2
1 2
2 3
""") == str((pow(2,2)+pow(2,1)))
Test input Expected output What it validates
single node 3 isolated vertex contribution
triangle 0 odd cycle rejection
two edges product of components multiplicative structure
path bipartite counting general correctness

Edge Cases

An isolated vertex is handled as a one-node component. The DFS assigns it color 0, producing counts (1,0). The contribution becomes 2^1 + 2^0 = 2 + 1 = 3, which matches the three possible labels.

A disconnected graph is processed component by component, and the multiplication step ensures independence of assignments across components.

A non-bipartite cycle is detected during DFS when an already colored neighbor violates parity consistency. At that moment the algorithm aborts and returns zero, preventing any incorrect partial counting.