Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ private BipartiteGraphDFS() {
* During DFS traversal, this method attempts to color each vertex in such a way
* that no two adjacent vertices share the same color.
*
* <p>Traversal invariant: every reachable vertex transitions from {@code -1} (uncolored) to
* {@code {0, 1}} before recursion and is never reset, so each reachable node is expanded at
* most once and cycles cannot cause an infinite walk.
*
* @param v Number of vertices in the graph
* @param adj Adjacency list of the graph where each index i contains a list of adjacent vertices
* @param color Array to store the color assigned to each vertex (-1 indicates uncolored)
Expand All @@ -64,6 +68,10 @@ private static boolean bipartite(int v, ArrayList<ArrayList<Integer>> adj, int[]
/**
* Method to check if the graph is bipartite.
*
* <p>Invariant: each reachable vertex is colored and expanded at most once across all
* components; on a non-bipartite component (such as an odd cycle), traversal terminates and
* returns {@code false} immediately without visiting later disconnected components.
*
* @param v Number of vertices in the graph
* @param adj Adjacency list of the graph
* @return True if the graph is bipartite, otherwise false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package com.thealgorithms.datastructures.graphs;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import java.util.Random;
import org.junit.jupiter.api.Test;

/**
* Invariant / property tests for {@link BipartiteGraphDFS}.
*
* <p>Source facts ({@code BipartiteGraphDFS.java}):
* <ul>
* <li>line 32: private constructor {@code BipartiteGraphDFS()}</li>
* <li>lines 51-66: {@code bipartite(v, adj, color, node)} assigns {@code color[node] = 1} when
* {@code -1} (lines 52-54), expands {@code adj.get(node)} (line 55), marks uncolored neighbors
* {@code color[it] = 1 - color[node]} before recursing (lines 56-60), and returns {@code false}
* on same-color conflict (lines 61-63)</li>
* <li>lines 79-90: {@code isBipartite(v, adj)} initializes {@code color} to {@code -1}
* (lines 80-81) and invokes {@code bipartite} for each unvisited component, returning
* {@code false} immediately when a component is non-bipartite (lines 82-88)</li>
* </ul>
*
* <p>Invariant: every vertex is colored at most once ({@code -1 -> {0, 1}}), so reachable nodes
* are expanded at most once and cycles cannot cause an infinite walk.
*/
public class BipartiteGraphDFSInvariantTest {

private static final long FIXED_SEED = 20261111L;
private static final int GENERATED_CASES = 120;

/**
* Subclass of {@code ArrayList<ArrayList<Integer>>} that records every vertex index passed to
* {@link #get(int)} by {@code BipartiteGraphDFS.bipartite(..., node)} without altering behavior.
*/
private static final class TrackingAdjacencyList extends ArrayList<ArrayList<Integer>> {
private static final long serialVersionUID = 1L;
private final transient List<Integer> expandedOrder = new ArrayList<>();
private final transient int[] expansionCounts;

TrackingAdjacencyList(int v) {
super(v);
this.expansionCounts = new int[v];
for (int i = 0; i < v; i++) {
super.add(new ArrayList<>());
}
}

void addDirectedEdge(int from, int to) {
super.get(from).add(to);
}

void addUndirectedEdge(int u, int w) {
super.get(u).add(w);
if (u != w) {
super.get(w).add(u);
}
}

@Override
public ArrayList<Integer> get(int index) {
expandedOrder.add(index);
expansionCounts[index]++;
return super.get(index);
}

List<ArrayList<Integer>> snapshot() {
List<ArrayList<Integer>> copy = new ArrayList<>(size());
for (int i = 0; i < size(); i++) {
copy.add(new ArrayList<>(super.get(i)));
}
return copy;
}
}

private static boolean independentBfsBipartiteOracle(int v, List<ArrayList<Integer>> adj) {
int[] color = new int[v];
Arrays.fill(color, -1);
for (int start = 0; start < v; start++) {
if (color[start] != -1) {
continue;
}
color[start] = 0;
Queue<Integer> queue = new ArrayDeque<>();
queue.add(start);
while (!queue.isEmpty()) {
int curr = queue.poll();
for (int next : adj.get(curr)) {
if (color[next] == -1) {
color[next] = 1 - color[curr];
queue.add(next);
} else if (color[next] == color[curr]) {
return false;
}
}
}
}
return true;
}

private static void logCase(String id, String input, Object expected, Object actual) {
System.out.println("[R45-T014] " + id + " | input=" + input
+ " | expected=" + expected + " | actual=" + actual);
}

/**
* Required fixture: {@code A->B, B->C, C->A} (vertices 0, 1, 2) and isolated {@code D}
* (vertex 3). Starting at {@code A} (0), the cycle terminates with each reachable node
* expanded once ({@code [0, 1, 2]}), returns {@code false}, and never visits {@code D} (3).
*/
@Test
public void cycleWithIsolatedVertexTerminatesOncePerNodeWithoutVisitingIsolatedD() {
TrackingAdjacencyList directedFixture = new TrackingAdjacencyList(4);
directedFixture.addDirectedEdge(0, 1); // A -> B
directedFixture.addDirectedEdge(1, 2); // B -> C
directedFixture.addDirectedEdge(2, 0); // C -> A
// vertex 3 (D) is isolated

boolean result = BipartiteGraphDFS.isBipartite(4, directedFixture);
logCase("fixture-directed-cycle-isolated-D",
"A(0)->B(1),B(1)->C(2),C(2)->A(0),isolated D(3)",
"bipartite=false,visited=[0,1,2],D_visits=0",
"bipartite=" + result + ",visited=" + directedFixture.expandedOrder
+ ",D_visits=" + directedFixture.expansionCounts[3]);

assertFalse(result);
assertEquals(List.of(0, 1, 2), directedFixture.expandedOrder);
assertEquals(1, directedFixture.expansionCounts[0]);
assertEquals(1, directedFixture.expansionCounts[1]);
assertEquals(1, directedFixture.expansionCounts[2]);
assertEquals(0, directedFixture.expansionCounts[3]);

TrackingAdjacencyList undirectedFixture = new TrackingAdjacencyList(4);
undirectedFixture.addUndirectedEdge(0, 1);
undirectedFixture.addUndirectedEdge(1, 2);
undirectedFixture.addUndirectedEdge(2, 0);

boolean undirectedResult = BipartiteGraphDFS.isBipartite(4, undirectedFixture);
logCase("fixture-undirected-cycle-isolated-D",
"A(0)-B(1)-C(2)-A(0),isolated D(3)",
"bipartite=false,visited=[0,1,2],D_visits=0",
"bipartite=" + undirectedResult + ",visited=" + undirectedFixture.expandedOrder
+ ",D_visits=" + undirectedFixture.expansionCounts[3]);

assertFalse(undirectedResult);
assertEquals(List.of(0, 1, 2), undirectedFixture.expandedOrder);
assertEquals(0, undirectedFixture.expansionCounts[3]);
}

/**
* Smallest failing non-bipartite example (self-loop {@code 0->0}) and smallest bipartite
* even cycle ({@code 0-1-2-3-0} + isolated {@code 4}).
*/
@Test
public void smallestCycleBoundariesTerminateWithAtMostOneExpansionPerNode() {
TrackingAdjacencyList selfLoop = new TrackingAdjacencyList(1);
selfLoop.addDirectedEdge(0, 0);
boolean selfLoopResult = BipartiteGraphDFS.isBipartite(1, selfLoop);
logCase("smallest-failing-self-loop", "v=1,edge=(0->0)",
"bipartite=false,visited=[0]",
"bipartite=" + selfLoopResult + ",visited=" + selfLoop.expandedOrder);
assertFalse(selfLoopResult);
assertEquals(List.of(0), selfLoop.expandedOrder);

TrackingAdjacencyList evenCyclePlusIsolated = new TrackingAdjacencyList(5);
evenCyclePlusIsolated.addUndirectedEdge(0, 1);
evenCyclePlusIsolated.addUndirectedEdge(1, 2);
evenCyclePlusIsolated.addUndirectedEdge(2, 3);
evenCyclePlusIsolated.addUndirectedEdge(3, 0);
// vertex 4 isolated

boolean evenResult = BipartiteGraphDFS.isBipartite(5, evenCyclePlusIsolated);
logCase("bipartite-even-cycle-plus-isolated",
"v=5,cycle=(0-1-2-3-0),isolated=4",
"bipartite=true,maxVisits=1",
"bipartite=" + evenResult + ",visited=" + evenCyclePlusIsolated.expandedOrder);
assertTrue(evenResult);
assertEquals(List.of(0, 1, 2, 3, 4), evenCyclePlusIsolated.expandedOrder);
for (int i = 0; i < 5; i++) {
assertEquals(1, evenCyclePlusIsolated.expansionCounts[i]);
}
}

/**
* Deterministic property test across 120 bounded valid graphs generated with fixed seed
* {@code 20261111L}: verifies at-most-once vertex expansion (cycle termination), no input
* mutation, and exact agreement with an independent BFS two-coloring oracle.
*/
@Test
public void seededGeneratedGraphsSatisfySingleVisitAndOracleAgreement() {
Random rng = new Random(FIXED_SEED);
int bipartiteCount = 0;
int nonBipartiteCount = 0;

for (int caseIdx = 0; caseIdx < GENERATED_CASES; caseIdx++) {
int v = 1 + rng.nextInt(12);
TrackingAdjacencyList adj = new TrackingAdjacencyList(v);
boolean[][] seenEdge = new boolean[v][v];
int maxEdges = v * (v - 1) / 2;
int targetEdges = maxEdges == 0 ? 0 : rng.nextInt(maxEdges + 1);
for (int e = 0; e < targetEdges; e++) {
int u = rng.nextInt(v);
int w = rng.nextInt(v);
if (u != w && !seenEdge[u][w]) {
seenEdge[u][w] = true;
seenEdge[w][u] = true;
adj.addUndirectedEdge(u, w);
}
}

List<ArrayList<Integer>> before = adj.snapshot();
boolean expected = independentBfsBipartiteOracle(v, before);
boolean actual = BipartiteGraphDFS.isBipartite(v, adj);

if (expected) {
bipartiteCount++;
} else {
nonBipartiteCount++;
}

assertEquals(expected, actual, "Oracle mismatch on seeded case " + caseIdx);
assertEquals(before, adj.snapshot(), "Input mutated on seeded case " + caseIdx);
for (int node = 0; node < v; node++) {
assertTrue(adj.expansionCounts[node] <= 1,
"Node " + node + " expanded more than once on seeded case " + caseIdx);
if (expected) {
assertEquals(1, adj.expansionCounts[node],
"Bipartite graph must expand every node once on seeded case " + caseIdx);
}
}
}

logCase("seeded-property-120",
"seed=" + FIXED_SEED + ",cases=" + GENERATED_CASES + ",v in [1..12]",
"all_agree=true,maxVisits<=1",
"bipartite=" + bipartiteCount + ",nonBipartite=" + nonBipartiteCount + ",maxVisits<=1");
assertTrue(bipartiteCount > 0 && nonBipartiteCount > 0);
}
}
70 changes: 70 additions & 0 deletions work/R45-T014/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Runner for task R45-T014: invariant/property checks for BipartiteGraphDFS.

Runs the focused JUnit test through Maven, requires a nonzero test count,
saves inputs, expected versus actual results, and tool versions into work/R45-T014/,
and exits non-zero on any failure.
"""
import glob
import subprocess
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

HERE = Path(__file__).resolve().parent
REPO = HERE.parent.parent
TEST_CLASS = "BipartiteGraphDFSInvariantTest"
CMD = ["mvn", "-B", "test", f"-Dtest={TEST_CLASS}", "-Dsurefire.failIfNoSpecifiedTests=false"]


def tool_version(cmd):
try:
out = subprocess.run(cmd, capture_output=True, text=True, cwd=REPO)
return "\n".join(l for l in (out.stdout + out.stderr).splitlines()
if not l.startswith("Picked up")).strip()
except FileNotFoundError:
return f"{cmd[0]}: not found"


def main():
(HERE / "tool_versions.txt").write_text(
"java:\n" + tool_version(["java", "-version"]) + "\n\nmvn:\n"
+ tool_version(["mvn", "-v"]) + "\n\npython:\n" + sys.version + "\n")

print("Running:", " ".join(CMD))
proc = subprocess.run(CMD, capture_output=True, text=True, cwd=REPO)
combined_output = proc.stdout + proc.stderr
(HERE / "mvn_output.txt").write_text(combined_output)

records = [line.strip() for line in combined_output.splitlines()
if line.strip().startswith("[R45-T014]")]
(HERE / "comparisons.txt").write_text(
("\n".join(records) + "\n") if records else "No invariant records captured.\n")

reports = glob.glob(str(REPO / "target/surefire-reports" / f"TEST-*{TEST_CLASS}.xml"))
if not reports:
(HERE / "result.txt").write_text(
f"command: {' '.join(CMD)}\nFAIL: no surefire report found\n")
print("FAIL: no surefire report found")
return 1

suite = ET.parse(reports[0]).getroot()
tests = int(suite.get("tests", 0))
failures = int(suite.get("failures", 0)) + int(suite.get("errors", 0))
skipped = int(suite.get("skipped", 0))
passed = proc.returncode == 0 and failures == 0 and tests > 0 and len(records) > 0
status = "PASS" if passed else "FAIL"
summary = (
f"command: {' '.join(CMD)}\n"
f"tests={tests} failures/errors={failures} skipped={skipped} "
f"records={len(records)} mvn_exit={proc.returncode}\n"
f"status={status}\n"
)
(HERE / "result.txt").write_text(summary)
print(summary)

return 0 if passed else 1


if __name__ == "__main__":
sys.exit(main())
5 changes: 5 additions & 0 deletions work/R45-T014/comparisons.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[R45-T014] smallest-failing-self-loop | input=v=1,edge=(0->0) | expected=bipartite=false,visited=[0] | actual=bipartite=false,visited=[0]
[R45-T014] bipartite-even-cycle-plus-isolated | input=v=5,cycle=(0-1-2-3-0),isolated=4 | expected=bipartite=true,maxVisits=1 | actual=bipartite=true,visited=[0, 1, 2, 3, 4]
[R45-T014] seeded-property-120 | input=seed=20261111,cases=120,v in [1..12] | expected=all_agree=true,maxVisits<=1 | actual=bipartite=59,nonBipartite=61,maxVisits<=1
[R45-T014] fixture-directed-cycle-isolated-D | input=A(0)->B(1),B(1)->C(2),C(2)->A(0),isolated D(3) | expected=bipartite=false,visited=[0,1,2],D_visits=0 | actual=bipartite=false,visited=[0, 1, 2],D_visits=0
[R45-T014] fixture-undirected-cycle-isolated-D | input=A(0)-B(1)-C(2)-A(0),isolated D(3) | expected=bipartite=false,visited=[0,1,2],D_visits=0 | actual=bipartite=false,visited=[0, 1, 2],D_visits=0
54 changes: 54 additions & 0 deletions work/R45-T014/mvn_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[INFO] Scanning for projects...
[INFO] Loaded 24447 auto-discovered prefixes for remote repository central (prefixes-central.txt)
[INFO]
[INFO] -----------------------< com.thealgorithms:Java >-----------------------
[INFO] Building Java 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- jacoco:0.8.16:prepare-agent (default) @ Java ---
[INFO] Loaded 64 auto-discovered prefixes for remote repository apache.snapshots (prefixes-apache.snapshots.txt)
[INFO] argLine set to -javaagent:/Users/shyamlibakale/.m2/repository/org/jacoco/org.jacoco.agent/0.8.16/org.jacoco.agent-0.8.16-runtime.jar=destfile=/Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/target/jacoco.exec
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ Java ---
[INFO] skip non existing resourceDirectory /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/src/main/resources
[INFO]
[INFO] --- compiler:3.16.0:compile (default-compile) @ Java ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 848 source files with javac [debug release 21] to target/classes
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ Java ---
[INFO] skip non existing resourceDirectory /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/src/test/resources
[INFO]
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ Java ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 797 source files with javac [debug release 21] to target/test-classes
[INFO]
[INFO] --- surefire:3.6.0:test (default-test) @ Java ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.thealgorithms.datastructures.graphs.BipartiteGraphDFSInvariantTest
[R45-T014] smallest-failing-self-loop | input=v=1,edge=(0->0) | expected=bipartite=false,visited=[0] | actual=bipartite=false,visited=[0]
[R45-T014] bipartite-even-cycle-plus-isolated | input=v=5,cycle=(0-1-2-3-0),isolated=4 | expected=bipartite=true,maxVisits=1 | actual=bipartite=true,visited=[0, 1, 2, 3, 4]
[R45-T014] seeded-property-120 | input=seed=20261111,cases=120,v in [1..12] | expected=all_agree=true,maxVisits<=1 | actual=bipartite=59,nonBipartite=61,maxVisits<=1
[R45-T014] fixture-directed-cycle-isolated-D | input=A(0)->B(1),B(1)->C(2),C(2)->A(0),isolated D(3) | expected=bipartite=false,visited=[0,1,2],D_visits=0 | actual=bipartite=false,visited=[0, 1, 2],D_visits=0
[R45-T014] fixture-undirected-cycle-isolated-D | input=A(0)-B(1)-C(2)-A(0),isolated D(3) | expected=bipartite=false,visited=[0,1,2],D_visits=0 | actual=bipartite=false,visited=[0, 1, 2],D_visits=0
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.042 s -- in com.thealgorithms.datastructures.graphs.BipartiteGraphDFSInvariantTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jacoco:0.8.16:report (generate-code-coverage-report) @ Java ---
[INFO] Loading execution data file /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/target/jacoco.exec
[INFO] Analyzed bundle 'Java' with 1073 classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.224 s
[INFO] Finished at: 2026-11-11T17:34:11+05:30
[INFO] ------------------------------------------------------------------------
Loading