.gemini/SKILL.md
This skill defines the conventions and standards for an educational algorithms repository. The goal is to make every algorithm implementation clear, well-tested, and accessible to learners who may not have deep CS backgrounds.
Goal: Every file should teach, not just implement.
Every public method gets a doc comment that explains:
/**
* Finds the shortest path from a source node to all other nodes
* using Bellman-Ford's algorithm. Unlike Dijkstra's, this handles
* negative edge weights and detects negative cycles.
*
* @param graph - adjacency list where graph[i] lists edges from node i
* @param start - the source node index
* @param n - total number of nodes in the graph
* @return dist array where dist[i] = shortest distance from start to i,
* or Double.NEGATIVE_INFINITY if node i is in a negative cycle
*
* Time: O(V * E) — relaxes all edges V-1 times
* Space: O(V) — stores distance array
*/
Comment the why, not the what. Focus on lines where the logic isn't obvious:
// Relax all edges V-1 times. After V-1 passes, shortest paths
// are guaranteed if no negative cycles exist.
for (int i = 0; i < n - 1; i++) {
for (Edge e : edges) {
if (dist[e.from] + e.cost < dist[e.to]) {
dist[e.to] = dist[e.from] + e.cost;
}
}
}
// If we can still relax an edge after V-1 passes, that node
// is reachable from a negative cycle — mark it as -infinity.
for (int i = 0; i < n - 1; i++) {
for (Edge e : edges) {
if (dist[e.from] + e.cost < dist[e.to]) {
dist[e.to] = Double.NEGATIVE_INFINITY;
}
}
}
Every file starts with a comment block explaining the algorithm in the file
/**
* Bellman-Ford Shortest Path Algorithm
*
* Computes single-source shortest paths in a weighted graph.
* Handles negative edge weights and detects negative cycles.
*
* Use cases:
* - Graphs with negative weights (where Dijkstra fails)
* - Detecting negative cycles (e.g., currency arbitrage)
*
* Run with:
* bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:BellmanFordAdjacencyList
*
* @see <a href="https://en.wikipedia.org/wiki/Bellman-Ford_algorithm">Wikipedia</a>
*/
Goal: Every algorithm has tests that prove it works and teach edge cases.
Place tests alongside source files or in a tests/ directory. Name test files
to mirror the source: BellmanFord.java → BellmanFordTest.java.
For every algorithm, cover these categories:
Use descriptive names that read like a sentence:
@Test
public void testShortestPathSimpleGraph() { ... }
@Test
public void testDetectsNegativeCycle() { ... }
@Test
public void testSingleNodeGraph() { ... }
@Test
public void testDisconnectedNodes() { ... }
Each test method gets a brief comment explaining what scenario it covers and why that scenario matters:
/**
* Graph with a negative cycle reachable from the source.
* Bellman-Ford should mark affected nodes as NEGATIVE_INFINITY.
*
* 0 --5--> 1 --(-10)--> 2 --3--> 1
* (creates cycle 1→2→1 with net cost -7)
*/
@Test
public void testDetectsNegativeCycle() {
// ... test body
}
Every code change must be accompanied by:
Goal: Keep the codebase clean without losing educational value.
Remove code that is:
Keep alternative implementations when they teach different approaches:
// ✓ KEEP — BFS and DFS solutions to the same problem teach different techniques
public int[] bfsSolve(int[][] grid) { ... }
public int[] dfsSolve(int[][] grid) { ... }
// ✓ KEEP — iterative vs recursive shows tradeoffs
public int fibRecursive(int n) { ... }
public int fibIterative(int n) { ... }
// ✗ REMOVE — identical logic, just different variable names
public int search_v1(int[] arr, int target) { ... }
public int search_v2(int[] arr, int target) { ... }
When keeping alternatives, clearly label them with a comment explaining the educational purpose:
/**
* Recursive implementation of binary search.
* Compare with binarySearchIterative() to see the iterative approach.
* The iterative version avoids stack overhead for large arrays.
*/
When refactoring, scan for:
Goal: Uniform style across the entire repository.
Use short, clear variable names. Prefer readability through simplicity:
// ✓ GOOD — short and clear
int n = graph.length;
int[] dist = new int[n];
boolean[] vis = new boolean[n];
List<int[]> adj = new ArrayList<>();
Queue<Integer> q = new LinkedList<>();
int src = 0;
int dst = n - 1;
// ✗ BAD — verbose names that clutter algorithm logic
int numberOfNodesInGraph = graph.length;
int[] shortestDistanceFromSource = new int[numberOfNodesInGraph];
boolean[] hasNodeBeenVisited = new boolean[numberOfNodesInGraph];
List<int[]> adjacencyListRepresentation = new ArrayList<>();
Queue<Integer> breadthFirstSearchQueue = new LinkedList<>();
int sourceNodeIndex = 0;
int destinationNodeIndex = numberOfNodesInGraph - 1;
Common short names (use consistently across the repo):
| Name | Meaning |
|---|---|
n | number of elements/nodes |
m | number of edges |
i, j | loop indices |
from, to | graph node endpoints |
cost | edge weight |
dist | distance array |
vis | visited array |
adj | adjacency list |
q | queue |
pq | priority queue |
st | stack |
dp | dynamic programming table |
ans | result/answer |
lo | low pointer/bound |
hi | high pointer/bound |
mid | midpoint |
src | source node |
dst | destination node |
cnt | counter |
sz | size |
cur | current element |
prev | previous element |
next | next element (use nxt if shadowing keyword) |
if (...) {)Always use explicit multiplication and parentheses in Big-O expressions for clarity:
// ✓ GOOD — explicit and unambiguous
// Time: O(n*log(n))
// Time: O(n*log^2(n))
// Time: O(n^2*log(n))
// ✗ BAD — missing multiplication and parentheses
// Time: O(n log n)
// Time: O(n log^2 n)
// Time: O(n^2 log n)
// Simple expressions without multiplication are fine as-is
// Time: O(n)
// Time: O(n^2)
// Time: O(log(n))
// Space: O(n)
Always place the body of a for loop on its own line, even for single statements.
This improves readability, especially in nested loops:
// ✗ BAD — body on same line as for
for (int j = 0; j < n; j++) augmented[i][j] = matrix[i][j];
// ✓ GOOD — body on its own line
for (int j = 0; j < n; j++)
augmented[i][j] = matrix[i][j];
// ✓ GOOD — nested for loops, each level on its own line
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
for (int k = 0; k < n; k++)
result[i][j] += m1[i][k] * m2[k][j];
Streams hurt readability for learners. Use plain loops instead:
// ✗ AVOID — streams obscure the logic for beginners
int sum = Arrays.stream(arr).filter(x -> x > 0).reduce(0, Integer::sum);
// ✓ PREFER — a loop is immediately readable
int sum = 0;
for (int x : arr) {
if (x > 0) sum += x;
}
Goal: The simplest correct code teaches the best.
// ✗ AVOID — deep nesting
if (node != null) {
if (node.left != null) {
if (node.left.val == target) {
return true;
}
}
}
return false;
// ✓ PREFER — early returns keep code flat
if (node == null) return false;
if (node.left == null) return false;
return node.left.val == target;
Extract repeated logic — but only if it genuinely reduces complexity
Use standard library where it clarifies — Arrays.sort(), Collections.swap(),
Math.min(), etc. are fine because learners need to know these exist
Remove unnecessary wrappers — don't wrap a single method call in another method
Prefer arrays over complex data structures when the problem allows it —
int[] is clearer than ArrayList<Integer> when the size is known
Goal: Catch bugs proactively whenever touching code.
When modifying any lines of code, actively check for and report:
== vs <=, < vs <= in loop conditionsi+1, i-1 without range checksWhen a bug is found, report it clearly:
🐛 BUG FOUND in BellmanFord.java line 42:
Loop runs `i < n` but should be `i < n - 1`.
The extra iteration incorrectly marks reachable nodes as
being in a negative cycle.
FIX: Change `i < n` to `i < n - 1`
Goal: Help learners understand the why behind each algorithm.
Goal: The main java method should be near the bottom of the Java file for consistency throughout the project