Algorithms Design & Analysis
Unit 2: Disjoint Sets & Backtracking
Disjoint Sets
Disjoint Set Operations
Core Concept
In many computer science problems, we begin with a finite universe of elements, represented as:
We partition these elements into several sets that are pairwise disjoint. This means that no element exists in more than one set:
Disjoint sets are managed by a data structure called the Union-Find (or Disjoint Set Union - DSU) structure, which supports two primary operations:
- Disjoint Set Union ():
- Combines two distinct sets and into a single new set .
- Once combined, the original sets and are destroyed/replaced by .
- Find ():
- Identifies and returns the unique representative (or root ID) of the set containing element .
- If two elements and return the same representative, they belong to the same set:
Union and Find Algorithms (Tree Representation)
Tree-Based Data Structure
The most efficient way to implement disjoint sets is by representing each set as a tree:
- Each node in the tree points upward to its parent.
- The root of the tree has no parent (indicated by a self-loop or a parent value ). The root acts as the set representative.
For example, let’s represent three disjoint sets:
- (Root: )
- (Root: )
- (Root: )
graph BT
subgraph S1 ["Set S1"]
7((7)) --> 1((1))
8((8)) --> 1
9((9)) --> 1
style 1 stroke-width:3px
end
subgraph S2 ["Set S2"]
5((5)) --> 2((2))
10((10)) --> 2
style 2 stroke-width:3px
end
subgraph S3 ["Set S3"]
4((4)) --> 3((3))
6((6)) --> 3
style 3 stroke-width:3px
end
In array representation, we use a 1-indexed array PARENT[1..n]:
- If
PARENT[i] = 0(or negative), then node is a root. - If
PARENT[i] = p > 0, then is the parent of node .
The initial state of the array for elements (each element in its own singleton set):
Index i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
PARENT[i] | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Simple Union and Find Algorithms
Algorithm for Simple Union
This procedure links the root of one set directly as a child of the root of another set.
Algorithm SimpleUnion(i, j)
// i and j are the roots of two disjoint trees
{
PARENT[i] := j;
}
Algorithm for Simple Find
This procedure traverses upward from node i along the parent links until it encounters a root node (parent value ).
Algorithm SimpleFind(i)
{
j := i;
while (PARENT[j] > 0) do
j := PARENT[j]; // Move up to parent
return j; // Return root
}
The Degenerate Tree Problem (Worst-Case Analysis)
If we use SimpleUnion and SimpleFind without checks, we can construct highly unbalanced trees.
Degenerate Trace Example:
Suppose we start with elements in separate sets and execute the sequence of operations:
graph BT
1((1)) --> 2((2))
2((2)) --> 3((3))
3((3)) --> dots["..."]
dots --> n(("n (Root)"))
style n stroke-width:3px
- Result: The tree degenerates into a linear chain of length .
- Cost:
- Performing
SimpleFind(1)requires traversing all nodes, taking steps. - A sequence of such find operations would take an inefficient worst-case time. If , the total time complexity scales quadratically as .
- Performing
Efficiency Improvements
To prevent tree degeneration, two optimizations are applied: Weighted Union and Collapsing Find (Path Compression).
Weighting Rule for Union
- Concept: When merging two trees, always attach the root of the tree with fewer nodes as a child of the root of the tree with more nodes.
- Implementation: To avoid using extra memory, we store the size of the tree as a negative number inside the root’s
PARENTfield:
Weighted Union Pseudocode
Algorithm WeightedUnion(i, j)
// i and j are the roots of two disjoint trees.
// PARENT[i] and PARENT[j] contain negative sizes.
{
temp := PARENT[i] + PARENT[j]; // Combined negative count
if (PARENT[i] > PARENT[j]) then
{
// Tree i has fewer nodes than Tree j (since, e.g., -2 > -5)
PARENT[i] := j; // Make j the parent of i
PARENT[j] := temp; // Update size of j
}
else
{
// Tree j has fewer or equal nodes than Tree i
PARENT[j] := i; // Make i the parent of j
PARENT[i] := temp; // Update size of i
}
}
Mathematical Proof (Lemma 2.3)
Theorem: Let be a tree with nodes created by the
WeightedUnionalgorithm. The depth of any node in is at most .
Proof by Mathematical Induction:
-
Base Case ():
- A tree with node has depth .
- Formula: . The theorem holds.
-
Inductive Step:
- Assume the theorem holds for all trees with size nodes.
- Let tree (size ) be created by joining two trees (size ) and (size ) using
WeightedUnion. Let . - Without loss of generality, assume . According to the weighting rule, is attached under the root of .
- Let’s analyze the new depths of the nodes in :
- Nodes originating from : Their depths do not change. By induction hypothesis:
- Nodes originating from : Their depths increase by exactly because they have a new parent link to the root of .
- Since and , we know that:
- Substitute this inequality back:
- The theorem holds for a tree of size . By induction, the proof is complete.
-
Impact: Since the tree height is bounded by , the worst-case time for a single
Finddrops to .
Collapsing Rule for Find (Path Compression)
- Concept: During a
Find(i)operation, we traverse up to the root. Once the root is identified, we make another pass up the tree and change the parent pointer of every node on the path to point directly to .
graph TD
subgraph Before ["Path Before Compression"]
root1["(root)"] --> a1["(a)"]
a1 --> b1["(b)"]
b1 --> c1["(c) [Find(c)]"]
end
subgraph After ["Path After Compression"]
root2["(root)"] --> a2["(a)"]
root2 --> b2["(b)"]
root2 --> c2["(c)"]
end
Collapsing Find Pseudocode
Algorithm CollapsingFind(i)
// Finds the root of the tree containing element i and compresses the path
{
// Pass 1: Find the root r
r := i;
while (PARENT[r] > 0) do
r := PARENT[r];
// Pass 2: Collapse path
curr := i;
while (curr != r) do
{
parent_node := PARENT[curr];
PARENT[curr] := r; // Redirect parent directly to root
curr := parent_node; // Move to next node in original path
}
return r;
}
Asymptotic Bound of Combined Operations (Tarjan’s Lemma)
When we combine both WeightedUnion and CollapsingFind, the tree structure becomes incredibly flat.
Let be the number of Find operations and be the number of elements. The total worst-case time required to process an intermixed sequence of Finds and Unions satisfies:
Where is the Inverse Ackermann Function.
- Ackermann’s function grows incredibly fast:
- Because the function grows so rapidly, its inverse grows extremely slowly.
- For all practical inputs in computer science:
- Therefore, the average time per operation is practically constant , making Union-Find one of the most efficient data structures in computer science.
Practical Application: Equivalence Relations
The Union-Find data structure is standard for computing equivalence classes online. Given variables and a series of equivalence relations (such as ), we group them into classes.
Trace Example:
Let variables, initially in singleton sets:
PARENT = [-1, -1, -1, -1, -1] (Using to represent tree size of ).
graph BT
1((1))
2((2))
3((3))
4((4))
5((5))
style 1 stroke-width:3px
style 2 stroke-width:3px
style 3 stroke-width:3px
style 4 stroke-width:3px
style 5 stroke-width:3px
- Relation
1 = 2:root1 = CollapsingFind(1) = 1root2 = CollapsingFind(2) = 2- Since
root1 != root2, performWeightedUnion(1, 2). - Size of 1 is 1, size of 2 is 1 (both -1). Let’s make 2 the child of 1.
PARENT[2] = 1,PARENT[1] = -2.
graph BT
2((2)) --> 1((1))
3((3))
4((4))
5((5))
style 1 stroke-width:3px
style 3 stroke-width:3px
style 4 stroke-width:3px
style 5 stroke-width:3px
- Relation
3 = 4:- Perform
WeightedUnion(3, 4). PARENT[4] = 3,PARENT[3] = -2.
- Perform
graph BT
2((2)) --> 1((1))
4((4)) --> 3((3))
5((5))
style 1 stroke-width:3px
style 3 stroke-width:3px
style 5 stroke-width:3px
- Relation
1 = 3:root1 = CollapsingFind(1) = 1(size 2,PARENT[1] = -2)root2 = CollapsingFind(3) = 3(size 2,PARENT[3] = -2)- Since size is equal, make 3 the child of 1.
PARENT[3] := 1,PARENT[1] := -2 + (-2) = -4.- Array state:
PARENT = [-4, 1, 1, 3, -1].
graph BT
2((2)) --> 1((1))
3((3)) --> 1
4((4)) --> 3
5((5))
style 1 stroke-width:3px
style 5 stroke-width:3px
- Query
CollapsingFind(4):- Path: (root).
- Path collapse changes
PARENT[4]to point directly to1. - Array state becomes:
PARENT = [-4, 1, 1, 1, -1].
graph BT
2((2)) --> 1((1))
3((3)) --> 1
4((4)) --> 1
5((5))
style 1 stroke-width:3px
style 5 stroke-width:3px
Priority Queues: Heaps and Heapsort
Priority Queues
Core Concept
A Priority Queue is an abstract data type that maintains a set of elements, where each element is associated with a “priority.” In a standard queue, elements are processed in a First-In-First-Out (FIFO) manner. In a priority queue:
- Elements are extracted based on their priority (e.g., the element with the highest/lowest priority is served first).
- The two primary operations are:
- Insertion: Add a new element with a given priority to the queue.
- Extraction: Retrieve and remove the element with the highest (or lowest) priority.
Motivation (Comparison of Implementations)
To understand the efficiency of heaps, consider alternative data structures for implementing a priority queue of size :
| Implementation Scheme | Insertion Complexity | Finding / Deleting Max Complexity |
|---|---|---|
| Unsorted Array / List | (Append to the end) | (Must scan the entire array) |
| Sorted Array / List | (Must shift elements to keep sorted) | (Remove from the end) |
| Binary Search Tree (Balanced) | ||
| Binary Heap |
A Binary Heap provides a highly efficient balance: both insertion and deletion run in logarithmic time, and it requires no extra pointers, unlike tree-based node representations.
Heaps
Definition
A Heap is a complete binary tree represented sequentially in memory (typically in an array) that satisfies the Heap Property.
- Complete Binary Tree: A binary tree in which all levels are completely filled except possibly the last level, which is filled from left to right.
- Max-Heap Property: The key (value) at any parent node is greater than or equal to the keys of its children: Consequently, the maximum element in a Max-Heap is always at the root.
- Min-Heap Property: The key at any parent node is less than or equal to the keys of its children: Consequently, the minimum element in a Min-Heap is always at the root.
graph TD
subgraph MaxHeap ["Max-Heap Example"]
90_1((90)) --> 80_1((80))
90_1 --> 70_1((70))
80_1 --> 30_1((30))
80_1 --> 40_1((40))
70_1 --> 50_1((50))
70_1 --> 60_1((60))
end
subgraph MinHeap ["Min-Heap Example"]
10_2((10)) --> 15_2((15))
10_2 --> 30_2((30))
15_2 --> 40_2((40))
15_2 --> 50_2((50))
30_2 --> 60_2((60))
30_2 --> 70_2((70))
end
Array Representation of Complete Binary Trees
Because a heap is a complete binary tree, we can map it directly into a 1-indexed array without using child/parent pointers. For any element at index :
- Parent of : (for )
- Left Child of : (if )
- Right Child of : (if )
graph TD
%% Node relationships based on array indices
1(90) --> 2(80)
1(90) --> 3(70)
2(80) --> 4(30)
2(80) --> 5(40)
3(70) --> 6(50)
3(70) --> 7(60)
%% Styling to make it clean
style 1 fill:#2d3748,stroke:#4a5568,stroke-width:2px,color:#fff
classDef default fill:#1a202c,stroke:#4a5568,color:#fff;
Heap Operations
Insertion (Up-Heap / Sift-Up)
To insert an element:
- Append the element at the end of the array (maintaining the complete binary tree property).
- Compare the new element with its parent. If the new element is larger than its parent (in a Max-Heap), swap them.
- Repeat this process (moving upward) until the heap property is satisfied or the element becomes the root.
Insert Pseudocode
Algorithm InsertMaxHeap(A, n, item)
// Inserts item into a Max-Heap A of size n.
// n is incremented before or during this operation.
{
n := n + 1;
i := n;
while (i > 1 and item > A[floor(i/2)]) do
{
A[i] := A[floor(i/2)]; // Move parent down
i := floor(i/2); // Move pointer up
}
A[i] := item; // Place item in its correct position
}
- Time Complexity: (since the maximum height of the tree is ).
Deletion and Adjustment (Down-Heap / Sift-Down)
To remove the maximum element (the root):
- Copy the element at the root (which is the maximum).
- Replace the root with the last element of the array and decrement the heap size .
- Run the Adjust procedure to push the root element down to its correct position by comparing it with its children and swapping with the larger child.
Adjust Pseudocode
Algorithm Adjust(A, i, n)
// Adjusts the binary tree A[i..n] to satisfy the Max-Heap property.
// The left and right subtrees of node i are assumed to be heaps.
{
j := 2 * i; // Left child of i
item := A[i];
while (j <= n) do
{
// Find the larger child of parent i
if (j < n and A[j] < A[j+1]) then
j := j + 1; // j is now the right child
// Compare parent item with the larger child
if (item >= A[j]) then
break; // Heap property satisfied
else
{
A[floor(j/2)] := A[j]; // Move child up
j := 2 * j; // Move down to next level
}
}
A[floor(j/2)] := item; // Place item in final position
}
- Time Complexity: (sinking a node from root to leaf takes at most steps).
Heap Creation (HEAPIFY)
There are two ways to build a heap from an arbitrary array :
Method 1: Repeated Insertion
- Process: Start with an empty heap and call
InsertMaxHeapfor each of the elements. - Worst-Case Cost: If elements are inserted in ascending order, each element rises to the root, taking time.
Method 2: Heapify (Bottom-Up Method)
- Process: Treat the array directly as a complete binary tree. Note that all leaf nodes at indices to are already valid heaps. Therefore, we call
Adjuststarting from the parent of the last leaf (index ) down to the root (index 1).
Heapify Pseudocode
Algorithm Heapify(A, n)
// Converts an arbitrary array A[1..n] into a Max-Heap.
{
for i := floor(n/2) downto 1 do
Adjust(A, i, n);
}
Mathematical Proof of Heapify Complexity
A common question is why Heapify runs in rather than .
- The maximum number of nodes at height in a complete binary tree of size is:
- The time required to run
Adjuston a node at height is proportional to its height, (since it can swap down at most times). - The total work for the entire heap construction is:
- For an infinite sum:
- Substituting this result back into our equation:
- Therefore, the worst-case time complexity of
Heapifyis .
Heapsort
Heapsort Algorithm
Heapsort utilizes the Max-Heap structure to sort an array in-place.
- Build a Max-Heap from the array using
Heapifyin time. - The maximum element is now at . Swap with the last element of the heap .
- Reduce the heap size by and run
Adjust(A, 1, n-1)to restore the heap property. - Repeat this process until all elements are sorted.
Algorithm HeapSort(A, n)
// Sorts the array A[1..n] in non-decreasing order.
{
Heapify(A, n); // Step 1: Build Max-Heap
for i := n downto 2 do // Step 2-4: Extract max and adjust
{
Swap(A[1], A[i]); // Move maximum to sorted section
Adjust(A, 1, i - 1); // Adjust remaining elements
}
}
Step-by-Step Numerical Trace of Heapsort
Let’s sort the array:
Phase 1: Build Max-Heap (Heapify)
We start with the unsorted tree:
graph TD
12((12)) --> 11((11))
12 --> 13((13))
11 --> 5((5))
11 --> 6((6))
13 --> 7((7))
style 12 stroke-width:3px
- Leaves are at indices: (values ).
- Non-leaves are at indices: down to .
- Adjust at index ():
- Children: Left child .
- No change.
- Adjust at index ():
- Children: , . Larger child is .
- No change.
- Adjust at index ():
- Children: , . Larger child is .
- Since , swap and .
- Tree becomes:
graph TD
13((13)) --> 11((11))
13 --> 12((12))
11 --> 5((5))
11 --> 6((6))
12 --> 7((7))
style 13 stroke-width:3px
- Node is pushed down to index . Children of index : .
- Since , adjustments are complete.
- Max-Heap Array:
Phase 2: Sort Loop (Extraction and Adjust)
- Iteration 1 ():
- Swap Swap .
- Array: (Sorted section: ).
- Call
Adjust(A, 1, 5)to fix the root :- Children of index 1: , . Larger is . Swap .
- Node is now at index 3. Child is (ignored as it’s sorted).
- Array becomes:
- Iteration 2 ():
- Swap Swap .
- Array: (Sorted section: ).
- Call
Adjust(A, 1, 4)to fix root :- Children of 1: , . Larger is . Swap .
- Node is now at index 2. Child is . Since stop.
- Array becomes:
- Iteration 3 ():
- Swap Swap .
- Array:
- Call
Adjust(A, 1, 3)to fix root :- Children of 1: , . Larger is . Swap .
- Array becomes:
- Iteration 4 ():
- Swap Swap .
- Array:
- Call
Adjust(A, 1, 2)to fix root :- Children of 1: . Swap .
- Array becomes:
- Iteration 5 ():
- Swap Swap .
- Array:
- Loop ends.
Final Sorted Array:
Complexity Summary
- Time Complexity:
- Worst-Case: (Heapify is , loop runs times doing adjustments of cost ).
- Best-Case: .
- Average-Case: .
- Space Complexity: auxiliary space. Heapsort is an in-place sorting algorithm.
Backtracking
General Method
Core Concept
Backtracking is a systematic, depth-first search strategy used for finding solutions to problems where the solution can be expressed as an -tuple:
where each variable is chosen from a finite set .
Unlike brute-force search, which generates all possible combinations to evaluate, backtracking builds the solution tuple one component at a time, checking constraints at each step.
graph TD
Root["(Root Node)"]
A["(a)"]
B["(b)"]
C["(c) [Killed/Pruned]"]
A1["x2 = 1"]
A2["x2 = 2"]
B1["x2 = 1"]
B2["x2 = 2"]
Root -->|x1 = 1| A
Root -->|x1 = 2| B
Root -->|x1 = 3| C
A -->|x2 = 1| A1
A -->|x2 = 2| A2
B -->|x2 = 1| B1
B -->|x2 = 2| B2
State Space Tree Terminology
To conceptualize the search space, backtracking structures it as a tree called the State Space Tree:
- Problem State: Any node in the tree that represents a partial or full state of the variable assignments.
- Solution Space: The set of all possible tuples defined by the explicit constraints (the total leaf nodes of the unpruned tree).
- Solution State: Any state in which the path from the root defines a valid tuple in the solution space.
- Answer State: A solution state that satisfies all implicit constraints (represents a valid final solution to the problem).
- Live Node: A node that has been generated, but its children have not yet been fully expanded.
- E-Node (Expanding Node): The currently active live node whose children are being generated.
- Dead Node: A node that has been generated and either:
- Has had all its children expanded.
- Has been determined by a bounding function to have no chance of leading to an answer node. It is pruned immediately.
Explicit vs. Implicit Constraints
- Explicit Constraints: Rules that restrict each variable to take values from a specific set (e.g., , or ). These define the dimensions of the state space tree.
- Implicit Constraints: Rules that determine how the variables must relate to one another to satisfy the problem criteria (e.g., no two queens can attack each other, or the sum of chosen elements must equal ).
General Control Abstraction
Backtracking uses a recursive template to perform the depth-first traversal of the state space tree:
Algorithm Backtrack(k)
// k is the index of the variable currently being decided (x[k])
{
// Generate all candidate values for x[k]
for (each x[k] in T(x[1], x[2], ..., x[k-1])) do
{
if (Bound(x[1], ..., x[k]) = true) then
{
// If it is a full solution, record/print it
if (Solution(x[1], ..., x[k]) = true) then
Print(x[1], ..., x[k]);
else
Backtrack(k + 1); // Recurse for the next variable
}
}
}
Applications
The -Queens Problem
Problem Definition
The problem is to place non-attacking queens on an chessboard so that no two queens share the same row, column, or diagonal.
Formulation
- Solution Vector: An -tuple , where represents the column index of row where the queen is placed.
- Explicit Constraint: for all .
- Implicit Constraints:
- No two queens in the same column: for all .
- No two queens on the same diagonal: For any two queens placed at and , they share a diagonal if and only if:
Algorithms
Algorithm Place(k, col)
// Returns true if a queen can be placed in row k, column col.
// x[1..k-1] store the column positions of queens placed in previous rows.
{
for i := 1 to k - 1 do
{
// Check column conflict or diagonal conflict
if (x[i] = col or abs(x[i] - col) = abs(i - k)) then
return false;
}
return true;
}
Algorithm NQueens(k, n)
// Recursive backtracking search for n-Queens
{
for col := 1 to n do
{
if Place(k, col) then
{
x[k] := col;
if (k = n) then
Print(x[1..n]);
else
NQueens(k + 1, n);
}
}
}
Step-by-Step Trace of the 4-Queens Problem
Let’s solve the 4-Queens problem ():
- Row 1: Try . (Board:
[1, 0, 0, 0]) - Row 2:
- Try (attacks ).
- Try (attacks diagonally).
- Try . Valid! (Board:
[1, 3, 0, 0])
- Row 3:
- Try (attacks ).
- Try (attacks diagonally).
- Try (attacks ).
- Try (attacks diagonally).
- All columns fail! Backtrack to Row 2.
- Row 2 (Resume):
- Try . Valid! (Board:
[1, 4, 0, 0])
- Try . Valid! (Board:
- Row 3:
- Try (attacks ).
- Try . Valid! (Board:
[1, 4, 2, 0])
- Row 4:
- Try (attacks and diagonally).
- Try (attacks ).
- Try (attacks diagonally).
- Try (attacks ).
- All columns fail! Backtrack to Row 3, then Row 2, then Row 1.
- Row 1 (Resume): Try . (Board:
[2, 0, 0, 0]) - Row 2: Try . Valid! (Board:
[2, 4, 0, 0]) - Row 3: Try . Valid! (Board:
[2, 4, 1, 0]) - Row 4: Try . Valid! (Board:
[2, 4, 1, 3])- Solution Found:
Solution 1: (2, 4, 1, 3) Solution 2: (3, 1, 4, 2)
. Q . . . . Q .
. . . Q Q . . .
Q . . . . . . Q
. . Q . . Q . .
Sum of Subsets Problem
Problem Definition
Given positive numbers (weights) and a target sum , find all subsets of these weights whose sum is exactly .
Formulation
- Solution Vector: A boolean tuple where if weight is chosen, and otherwise.
- Pre-condition: Sort the weights in non-decreasing order: .
- Bounding Functions:
Let be the current sum. A partial solution can be pruned if:
- The sum exceeds the target: .
- The remaining weights are insufficient to reach : .
Algorithm
Algorithm SumOfSub(s, k, r)
// s is current sum, k is current index, r is remaining weight sum
{
// Generate left child (Include w[k])
x[k] := 1;
if (s + w[k] = M) then
Print(x[1..k]); // Found solution
else if (s + w[k] + w[k+1] <= M) then
SumOfSub(s + w[k], k + 1, r - w[k]);
// Generate right child (Exclude w[k])
if (s + r - w[k] >= M and s + w[k+1] <= M) then
{
x[k] := 0;
SumOfSub(s, k + 1, r - w[k]);
}
}
Step-by-Step Trace of Sum of Subsets
- Weights: , Target .
- Initial Sum: , index , remaining weight sum .
- Try (Include ): .
- Try (Include ): .
- Try (Include ): .
- Try (Include ): (Killed).
- Try (Exclude ): Remaining sum . Recurse:
- Try (Include ): (Killed).
- Try (Exclude ): Remaining sum . Recurse:
- Try (Include ): (Killed).
- Try (Exclude ): (Killed).
- Backtrack to (Exclude ): .
- Try (Include ): .
- (Killed).
- Try (Exclude ): .
- Try (Include ): .
- Solution Found: .
Following this search path, the algorithm discovers two additional solutions:
- Solution 2:
- Solution 3:
Graph Coloring (-Colorability)
Problem Definition
Find all ways to color the vertices of an undirected graph using at most colors such that no two adjacent vertices share the same color.
Formulation
- Representation: Graph represented as adjacency matrix
G[1..n, 1..n]. - Solution Vector: An -tuple where is the color of vertex .
- Implicit Constraint: If
G[i, j] = 1(edge exists), then .
Algorithms
Algorithm NextValue(k, m)
// Assigns a valid color value to x[k]
{
repeat
{
x[k] := (x[k] + 1) mod (m + 1); // Try next color
if (x[k] = 0) then return; // All colors tried
// Check adjacency conflicts
conflict := false;
for j := 1 to k - 1 do
{
if (G[k, j] = 1 and x[k] = x[j]) then
{
conflict := true;
break;
}
}
if (not conflict) then return; // Valid color found
} until false;
}
Algorithm GraphColoring(k, m)
// Recursively color node k
{
repeat
{
NextValue(k, m); // Assign a valid color
if (x[k] = 0) then return; // No color possible
if (k = n) then
Print(x[1..n]);
else
GraphColoring(k + 1, m);
} until false;
}
Step-by-Step Trace of Graph Coloring
Let’s color a cycle graph ( vertices, edges: ) with colors.
graph TD
1((1)) --- 2((2))
1 --- 4((4))
2 --- 3((3))
4 --- 3
- Vertex 1: Assign color . ()
- Vertex 2: Cannot be color (adjacent to 1). Assign color . ()
- Vertex 3: Adjacent to 2.
- Try color . Valid (not connected to 1). ()
- Vertex 4: Adjacent to 1 and 3.
- Cannot be color (adjacent to 1 and 3). Try color . Valid!
- Solution Found:
- Try color . Valid. ()
- Vertex 4: Adjacent to 1 and 3.
- Cannot be color or . Try color . Valid!
- Solution Found:
- Vertex 4: Adjacent to 1 and 3.
Hamiltonian Cycles
Problem Definition
Find a closed loop in a graph that visits every vertex exactly once and returns to the starting vertex.
Formulation
- Solution Vector: An -tuple containing the path vertices.
- Starting Constraint: Fix to remove cyclic shifts.
- Implicit Constraints for ():
- Edge must exist:
G[x[k-1], x[k]] = 1. - Distinct vertices: for all .
- Final return (for ):
G[x[n], x[1]] = 1.
- Edge must exist:
Algorithms
Algorithm NextValueHC(k)
{
repeat
{
x[k] := (x[k] + 1) mod (n + 1); // Try next vertex
if (x[k] = 0) then return; // All vertices tried
if (G[x[k-1], x[k]] = 1) then
{
// Check if vertex was already visited
visited := false;
for j := 1 to k - 1 do
if (x[j] = x[k]) then visited := true;
if (not visited) then
{
// If last vertex, check return edge to start x[1]
if (k < n or (k = n and G[x[n], x[1]] = 1)) then
return; // Valid vertex
}
}
} until false;
}
Algorithm Hamiltonian(k)
{
repeat
{
NextValueHC(k);
if (x[k] = 0) then return;
if (k = n) then
Print(x[1..n] + " " + x[1]); // Cycle completed
else
Hamiltonian(k + 1);
} until false;
}
Step-by-Step Trace of Hamiltonian Cycles
Let’s find a Hamiltonian cycle in a 5-vertex graph:
graph TD
1((1)) --- 2((2))
1 --- 3((3))
1 --- 4((4))
1 --- 5((5))
2 --- 3
2 --- 5
3 --- 4
3 --- 5
4 --- 5
Let’s assume adjacency list for Node 1: . Node 2: , etc.
- Start at .
- Try .
- Try .
- Try .
- Try .
- Check if has edge back to . Yes, edge exists.
- Cycle Found: .
- If did not exist, the algorithm would backtrack, try , return to , and try other configurations.