Daa Unit - 4 Notes
Daa Unit - 4 Notes
Example
Given an undirected graph:
There are 2 connected components:
1. {A, B, C, D}
2. {E, F}
• No vertex in component 1 is connected to
any vertex in component 2.
How to Find Connected Components
There are two popular graph traversal algorithms used to identify connected components:
1. Depth-First Search (DFS)
2. Breadth-First Search (BFS)
General Approach:
• Initialize all vertices as unvisited.
• Traverse the graph using DFS or BFS.
• Every time start a new traversal from an unvisited node and discover a new component.
• Count the number of such traversals — that’s the number of connected components.
Algorithm to find Connected Components
Input:
• An undirected graph G = (V, E)
o V: Set of vertices {0, 1, ..., n-1}
o E: Set of edges
• The graph is typically represented as an adjacency list (adj)
Output:
• Total number of connected components
• Optionally: List of nodes in each component
Depth First Search (DFS) Breadth First Search (BFS)
Step 1: Initialize a visited array of size n to false Step 1: Create a 'visited' array of size n,
visited[i] = false for all i from 0 to n-1 initialized to false
Step 2: Initialize a counter to 0 for connected
Step 2: Initialize a component counter to 0 components
Step 3: For each vertex v in 0 to n-1:
Step 3: For each vertex v in V: If visited[v] == false:
If visited[v] == false: Increment counter
// Start a new DFS from vertex v Create an empty queue
Call DFS(v) Enqueue v and mark visited[v] = true
Increment the component counter While queue is not empty:
Dequeue current vertex u
Step 4: Define DFS(v): For each neighbor of u:
Mark visited[v] = true If neighbor not visited:
For each neighbor u of v: Enqueue neighbor
If visited[u] == false: Mark it as visited
Call DFS(u) // Optional: Print the nodes visited in
this BFS (this is one component)
Step 5: Return the component counter (total Step 4: Return counter as number of connected
number of connected components) components
#include <iostream> #include <iostream>
using namespace std; #include <queue>
#define MAX 100 using namespace std;
int adj[MAX][MAX]; // Adjacency matrix #define MAX 100
bool visited[MAX]; // Visited array int adj[MAX][MAX];// Adjacency matrix
int n; // Number of nodes bool visited[MAX]; // Visited array
void dfs(int node) { int n; // Number of nodes
cout << node << " "; void bfs(int start) {
visited[node] = true; queue<int> q;
for (int i = 0; i < n; i++) { q.push(start);
if (adj[node][i] == 1 && !visited[i]) { visited[start] = true;
dfs(i); while (!q.empty()) {
} int node = q.front(); q.pop();
} cout << node << " ";
} for (int i = 0; i < n; i++) {
int main() { if (adj[node][i] == 1 && !visited[i]) {
int edges; q.push(i);
cout << "Enter number of nodes and edges: "; visited[i] = true;
cin >> n >> edges; }
}
// Initialize }
for (int i = 0; i < n; i++) { }
visited[i] = false; int main() {
for (int j = 0; j < n; j++) int edges;
adj[i][j] = 0; cout << "Enter number of nodes and edges:
} ";
cin >> n >> edges;
cout << "Enter edges (u v):\n"; // Initialize
for (int i = 0; i < edges; i++) { for (int i = 0; i < n; i++) {
int u, v; visited[i] = false;
cin >> u >> v; for (int j = 0; j < n; j++)
adj[u][v] = 1; adj[i][j] = 0;
adj[v][u] = 1; // Because undirected }
} cout << "Enter edges (u v):\n";
for (int i = 0; i < edges; i++) {
cout << "Connected component (DFS): "; int u, v;
dfs(0); // For connected graph, start from 0 cin >> u >> v;
adj[u][v] = 1;
return 0; adj[v][u] = 1; // Because undirected
} }
cout << "Connected component (BFS): ";
bfs(0); // For connected graph, start from 0
return 0;
}
Sample Input
Enter number of nodes and edges: 6 5
Enter edges (u v):
01
12
13
34
45
DFS Output:
Connected component (DFS): 0 1 2 3 4 5
BFS Output:
Connected component (BFS): 0 1 2 3 4 5
Biconnected Graph:
A connected undirected graph is biconnected if:
• It has no articulation points.
• There are at least two disjoint paths between every pair of
vertices.
Example
Consider the undirected graph:
The triangle formed by A-B-C-D is biconnected: removing any one
of A, B, or C still keeps the triangle connected.
• Vertex D is an articulation point: removing D disconnects E from the rest of the graph.
• So, the graph has two biconnected components:
o {A, B, C, D}
o {D, E}
How to Find Biconnected Components?
We use Depth-First Search (DFS) and track:
• Discovery time (first visit)
• Lowest reachable vertex (back edges)
This involves:
• A DFS tree
• A stack to keep track of edges
• Identification of articulation points
The Tarjan’s Algorithm is commonly used to find all BCCs in O(V + E) time.
Properties of Biconnected Components
Property Description
A biconnected component is as large as possible — no
Maximal additional edges or vertices from the graph can be included
without violating biconnectivity.
Removing any one vertex does not disconnect the
No articulation point inside
component.
Articulation points (cut vertices) are common to two or
Multiple components may
more biconnected components, but edges belong to only
share articulation points
one.
Each edge belongs to exactly
Unlike vertices, edges are not shared between components.
one biconnected component
DFS traversal can be used to decompose a graph into
DFS-based decomposition biconnected components using lowpoint values and
discovery times.
During DFS, we can push edges to a stack; when a
Identified using stack component ends, pop until the current edge — this gives one
biconnected component.
Graph with no articulation A connected graph without cut vertices is fully
point is itself biconnected biconnected.
Every cycle in the graph is completely contained within a
Cycle property
single biconnected component.
Component Breakdown:
Pink Component
• Nodes: 1, 2
• Articulation Point: 2
• Description: Node 1 connects to the rest of the graph only via node 2. If node 2 is
removed, node 1 becomes isolated.
Blue Component
• Nodes: 2, 4, 11
• Articulation Point: 2, 8
• Description: A small component bridging 2 to node 8
via node 11 and 4.
Green Component
• Nodes: 2, 3, 5, 6
• Articulation Point: 2
• Description: A tree-like component connected through
2. Removing node 2 separates this subgraph.
Key Observations
• Every edge belongs to only one biconnected component.
• Articulation points belong to multiple components.
• Removing articulation points would increase the number of connected components.
Example
Consider the graph with edges: (0-1), (0-2), (1-3), (2-3), (3-4), (4-5), (5-3)
Step 1: Start DFS at Node 0
• disc[0] = 1, low[0] = 1
• Move to neighbor 1
BCC2:
• When returning from 4 to 3, and low[4] ≥ disc[3], we pop:
Output: (3-4) → add to previous or new BCC
BCC3:
• When returning from 2 to 3, and low[2] ≥ disc[3], we pop:
Output: (2-3), (2-0) → new BCC
BCC4:
• When returning from 1 to 3, and low[3] ≥ disc[1], pop:
Output: (1-3) → new BCC
BCC5:
• Returning from 1 to 0, and low[1] ≥ disc[0], pop:
Output: (0-1) → new BCC
Step 7: Identify Articulation Points
• Node 3: removing it disconnects 4, 5
• Node 1: removing it disconnects 0, 3
So, articulation points: 1, 3
Program
#include <iostream> }
#include <cstring> }
using namespace std; void printArticulationPoints(int n) {
#define MAX 10 cout << "\nArticulation Points: ";
int graph[MAX][MAX]; // Adjacency matrix bool found = false;
bool visited[MAX]; for (int i = 0; i < n; i++) {
int disc[MAX], low[MAX], parent[MAX]; if (ap[i]) {
bool ap[MAX]; // Articulation points cout << i << " ";
int timeCounter = 0; found = true;
void initialize(int n) { }
memset(graph, 0, sizeof(graph)); }
memset(visited, false, sizeof(visited)); if (!found) cout << "None";
memset(disc, -1, sizeof(disc)); cout << endl;
memset(low, -1, sizeof(low)); }
memset(parent, -1, sizeof(parent)); bool isBiconnected(int n) {
memset(ap, false, sizeof(ap)); DFS(0, n);
timeCounter = 0; for (int i = 0; i < n; i++)
} if (!visited[i])
void addEdge(int u, int v) { return false;
graph[u][v] = 1; for (int i = 0; i < n; i++)
graph[v][u] = 1; if (ap[i])
} return false;
void DFS(int u, int n) { return true;
visited[u] = true; }
disc[u] = low[u] = ++timeCounter; int main() {
int children = 0; int n = 6;
for (int v = 0; v < n; v++) { initialize(n);
if (graph[u][v]) { // Add edges based on the given diagram
if (!visited[v]) { addEdge(0, 1);
children++; addEdge(0, 2);
parent[v] = u; addEdge(1, 3);
DFS(v, n); addEdge(2, 3);
low[u] = min(low[u], low[v]); addEdge(3, 4);
if (parent[u] == -1 && children > addEdge(4, 5); // dotted edge
1) cout << "Nodes: ";
ap[u] = true; for (int i = 0; i < n; i++) cout << i << " ";
if (parent[u] != -1 && low[v] >= cout << "\nEdges: (undirected)\n";
disc[u]) for (int i = 0; i < n; i++)
ap[u] = true; for (int j = i + 1; j < n; j++)
} if (graph[i][j])
else if (v != parent[u]) { cout << i << " - " << j << "\n";
low[u] = min(low[u], disc[v]); printAdjacencyMatrix(n);
}
} if (isBiconnected(n)) {
} cout << "\nThe graph is
} BICONNECTED.\n";
void printAdjacencyMatrix(int n) { } else {
cout << "\nAdjacency Matrix:\n"; cout << "\nThe graph is NOT
for (int i = 0; i < n; i++) { biconnected.\n";
cout << i << ": "; }
for (int j = 0; j < n; j++) { printArticulationPoints(n);
cout << graph[i][j] << " "; return 0;
} }
cout << endl;
Sample Output:
Nodes: 0 1 2 3 4 5
Edges: (undirected)
0-1
0-2
1-3
2-3
3-4
4-5
Adjacency Matrix:
0: 0 1 1 0 0 0
1: 1 0 0 1 0 0
2: 1 0 0 1 0 0
3: 0 1 1 0 1 0
4: 0 0 0 1 0 1
5: 0 0 0 0 1 0
DYNAMIC PROGRAMMING
Dynamic Programming (DP) is a method for solving complex problems by breaking them
down into simpler subproblems, solving each only once, and storing their results (usually in
a table or array) to avoid redundant computations.
"Don't solve the same subproblem more than once. Store its result and reuse it."
This is especially useful in problems with overlapping subproblems and optimal
substructure.
Think of memoization like remembering answers to questions you've already solved in a quiz.
Instead of solving them again, you just look them up in your notes.
BACKTRACKING - DEFINITION
Backtracking is a general algorithmic technique for solving recursive problems by trying
to build a solution incrementally, and removing solutions that fail to satisfy the constraints
of the problem at any point. Backtracking is a form of recursion where we try to solve a
problem by choosing an option at each step, and if it leads to a solution, continue; else,
backtrack (i.e., undo the choice) and try another.
It is used when:
• Need to explore all possible combinations.
• Need to undo or backtrack if a path leads to a dead end.
Working Principle
Backtracking uses a depth-first search (DFS) approach:
1. Start with an empty solution.
2. Try to add a value that meets constraints.
3. If adding the value leads to a solution, recurse.
4. If not, remove the value (backtrack) and try the next.
5. Repeat until all possibilities are explored.
Algorithm
1. Define a solution space or state space (usually recursive).
2. Choose a structure to represent the partial solution (e.g., array, string).
3. Start from an empty/initial state.
4. For each step or position in the problem:
a. Make a choice (try a candidate value).
b. Check if the current state is valid (satisfies constraints).
- If valid:
i. Recur to explore further decisions.
- If invalid:
i. Undo the current choice (backtrack).
5. If a complete solution is found, record or print it.
6. Return to the previous decision point and try next possible choice.
Pseudocode
procedure BACKTRACKING(state):
if is_solution(state):
process_solution(state)
return
for each candidate in candidates(state):
if is_valid(candidate, state):
make_move(candidate, state)
BACKTRACKING(state)
undo_move(candidate, state)
Components
Component Description
state The current partial solution or position in the solution space
is_solution() Checks whether the current state is a complete and valid solution
process_solution() Records, displays, or stores the complete solution
candidates() Generates possible next steps or values to try
is_valid() Prunes branches that violate constraints
make_move() Applies the decision/candidate
undo_move() Reverts the decision (important for backtracking)
The solution space is the set of all possible configurations that can be generated by the
algorithm while trying to solve the problem. Backtracking explores this space partially or fully
depending on the constraints and pruning.
Constraints in Backtracking
Constraints are rules or conditions that each partial solution must satisfy in order to continue
expanding into a valid final solution. There are two types of constraints:
Type Description
Explicit Constraints Must always be satisfied. Eg: no two queens in the same row.
Real-Life Analogy:
Suppose you're choosing a 4-digit PIN.
• Solution space: All 4-digit combinations (0000 to 9999).
• Constraints: PIN must not have repeating digits → prune options like 1123 or 8888.
Example: Sudoku
Solution Space:
• 9×9 grid → 81 cells
• Each cell can be 1-9
→ Total raw combinations: 9^81 (huge!)
Constraints:
• Row: Each digit appears once
• Column: Each digit appears once
• Block (3x3): Each digit appears once
→ Backtracking + constraints drastically cuts down this space.
Backtracking doesn't explore the entire solution space. It only goes deep into branches that are
promising (i.e., obey constraints).
Problem Statement
Place eight queens on a standard 8×8 chessboard such that no two queens attack each other.
Queens attack:
• Horizontally
• Vertically
• Diagonally (both left and right)
So, the goal is to place one queen in each row in such a way that:
• No two queens share the same column
• No two queens are on the same diagonal
Why Is It Important?
The 8-Queens problem:
• Introduces constraint-based problem solving
• Demonstrates backtracking – a brute-force technique for recursive search
• Lays foundation for solving N-Queens, Sudoku, graph coloring, path finding, etc.
Mathematical Background
• There are 92 possible valid solutions to the 8-queens problem.
• If solutions that are symmetric through rotation/reflection are considered the same, there
are 12 unique solutions.
Solution Approach
The most common approach is backtracking, which works like this:
1. Start placing queens row by row.
2. At each step, try placing a queen in all columns of the current row.
3. If a position is safe, place it and move to the next row.
4. If no column is safe in a row, backtrack to the previous row and try a different position.
5. Repeat until all queens are placed successfully.
Algorithm
Step 1:
Create a one-dimensional array board[8]
→ board[i] = j means a queen is placed at row i and column j.
Step 2:
Define a function isSafe(row, col) that checks:
• If any previously placed queen (in rows 0 to row - 1) is in:
o the same column
o or the same diagonal
for i from 0 to row - 1:
if board[i] == col → same column → return false
if abs(board[i] - col) == abs(i - row) → same diagonal → return false
return true
Step 3:
Define the recursive backtracking function placeQueen(row):
If row == 8:
print the board as a solution
return
For col = 0 to 7:
If isSafe(row, col):
board[row] = col // place queen at (row, col)
placeQueen(row + 1) // move to next row
// Implicit backtrack on returning
Step 4:
Call placeQueen(0) from the main() function to start placing queens from row 0.
Note
• Backtracking automatically removes the queen when returning from recursion.
• You may optionally reset board[row] = -1 during backtrack for clarity.
Problem Statement:
Given:
• A set of n positive integers: S = {s₁, s₂, ..., sₙ}
• A target sum: M
Find:
• One or more subsets of S such that the sum of elements in the subset equals M.
Applications
• It appears in resource allocation, cryptography, combinatorial optimization, and
decision-making.
• It forms the basis for subset-sum variations, including the Knapsack Problem.
Example:
Let S = {3, 34, 4, 12, 5, 2} and M = 9.
Subset {4, 5} sums to 9.
Subset {3, 12} sums to 15 (invalid).
Approaches to Solve:
1. Brute-force / Exhaustive Search:
o Check all 2^n subsets → slow for large n.
2. Backtracking:
o Systematically explore subsets.
o Abandon paths that exceed the target.
3. Dynamic Programming:
o Build a table dp[i][j] indicating if sum j is possible using the first i elements.
4. Bitmasking or Memoization (for optimization):
o Used for larger datasets in practice.
Constraints:
• Input values must be non-negative integers.
• The set size and target value should be within practical computation limits for brute-
force.
Applications:
• Subset sum cryptosystems (e.g., Merkle-Hellman)
• Partition problems
• Load balancing
• Budget planning
Algorithm
Problem Input:
• A set S[1...n] of positive integers
• A target sum M
Goal:
Find all subsets of S whose sum is exactly equal to M.
Preconditions:
• Sort the set S in ascending order (to optimize early pruning).
• Use a global array x[1...n] to store binary choices:
x[i] = 1 → include S[i],
x[i] = 0 → exclude S[i].
Recursive Steps:
1. If sum == M:
→ Print the current subset (x[1...n])
→ return
Visual Trace
Trace only the key decisions in a subset tree, representing each recursive call as a node:
(i, sum) where:
• i = index in the set
• sum = current sum accumulated
We show (include) and (exclude) paths.
Definition
Graph colouring is the process of assigning colours to the vertices (or edges) of a graph such
that no two adjacent elements share the same colour.
Vertex Colouring:
Assign colours to vertices such that no two
adjacent vertices (i.e., connected directly by an
edge) have the same colour.
Real-world Examples
Application Description
Courses (vertices) that share students (edges) need different
Timetabling/Scheduling
time slots
Map Colouring Countries sharing borders get different colours
In compilers, variables that interfere cannot share the same
Register Allocation
register
Wi-Fi Channel
Neighbouring routers should not use the same frequency
Assignment
Key Terms
Term Explanation
Proper colouring No adjacent vertices share the same colour
Chromatic number (χ) Minimum number of colours needed for a proper colouring
Greedy colouring Simple algorithm that assigns the first available colour
k-Colourable graph Can be coloured using ≤ k colours
1. Greedy Algorithm
Assign the lowest possible colour to each vertex one by one. Simple but not optimal.
2. Backtracking
Try assigning colours recursively, backtracking when a conflict occurs.
3. Welsh–Powell Algorithm
Colour higher degree nodes first — improves greedy method.