THE SYLLABUS TASK
What you need to solve
Write a program to implement graph traversal using DFS.
Enter V E, then E edge pairs u v, then the start vertex. Vertices are numbered 0 to V−1; V is 1–20. This is a simple undirected, unweighted graph using an adjacency matrix. Self-loops, repeated edges and invalid vertex numbers are rejected. The second output traverses all components, starting with the requested component.
UNDERSTAND THE IDEA
Explanation
A graph can contain cycles and disconnected components. visited prevents processing a vertex again. This solution uses an adjacency matrix: graph[u][v] == 1 means u and v are connected; undirected edges set both symmetric entries.
DFS explores one neighbour deeply before returning to try another. It marks a vertex visited before recursion, then considers neighbours in ascending vertex-number order.
The first line visits only the vertices reachable from start. The second resets visited, traverses that component again, then begins a traversal at every remaining unvisited vertex in ascending order. Isolated vertices therefore appear in the second report.
PLAN BEFORE CODING
Algorithm
- Read and validate the vertex and edge counts.
- Add each edge in both directions in the matrix.
- Validate start and clear visited flags.
- Traverse the start component using recursion for DFS or a queue for BFS.
- Reset visited and traverse all components, starting with the requested one.
- Print the two separate reports.
SEE THE CONTROL FLOW
Flowcharts
Follow arrows from Start. Diamonds are decisions; Yes/No labels select a branch. Returning arrows repeat a loop. Function steps group the pointer/index updates explained in the code. Expand the operation charts for more detail. On a phone, scroll sideways within a diagram.
DFS of one component
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
/* Read a complete integer token; reject bad or oversized input safely. */
int readInt(int *value) {
char token[64];
int ch;
size_t used = 0;
while ((ch = getchar()) != EOF && isspace((unsigned char)ch)) { }
if (ch == EOF) return 0;
do {
if (used < sizeof token - 1) token[used++] = (char)ch;
else {
while ((ch = getchar()) != EOF && !isspace((unsigned char)ch)) { }
return 0;
}
ch = getchar();
} while (ch != EOF && !isspace((unsigned char)ch));
token[used] = '\0';
char *end;
errno = 0;
long result = strtol(token, &end, 10);
if (errno || *end != '\0' || result < -1000000 || result > 1000000) return 0;
*value = (int)result;
return 1;
}
#define MAX_VERTICES 20
int graph[MAX_VERTICES][MAX_VERTICES];
int visited[MAX_VERTICES];
int vertices;
void dfs(int vertex) {
visited[vertex] = 1; printf(" %d", vertex);
for (int neighbour = 0; neighbour < vertices; ++neighbour)
if (graph[vertex][neighbour] && !visited[neighbour]) dfs(neighbour);
}
/* Input: V E, E undirected edges (u v), then start vertex. */
int main(void) {
int edges, start;
if (!readInt(&vertices) || !readInt(&edges) || vertices < 1 ||
vertices > MAX_VERTICES || edges < 0 || edges > vertices * (vertices - 1) / 2) goto invalid;
for (int i = 0; i < edges; ++i) {
int u, v;
if (!readInt(&u) || !readInt(&v) || u < 0 || u >= vertices ||
v < 0 || v >= vertices || u == v || graph[u][v]) goto invalid;
graph[u][v] = graph[v][u] = 1;
}
if (!readInt(&start) || start < 0 || start >= vertices) goto invalid;
printf("DFS (start component):"); dfs(start); putchar('\n');
/* Optional second report includes disconnected and isolated vertices. */
memset(visited, 0, sizeof visited);
printf("DFS (all components):"); dfs(start);
for (int v = 0; v < vertices; ++v) if (!visited[v]) dfs(v);
putchar('\n'); return 0;
invalid:
puts("Invalid graph input."); return 1;
}
Code loads into your existing compiler. Enter the sample input there; sign-in and execution rules stay the same.
Compile and run locally
gcc -std=c17 graph-dfs.c -o lab
./labOn Windows, run .\lab.exe after compiling with GCC.
UNDERSTAND THE CODE
Line-by-line explanation
These steps follow the exact source above, including its input helper, operations, command handling and cleanup. Line numbers count blank lines; related statements are grouped. Start with main to follow execution, then open the functions it calls.
Start with main → · Data structure and operations →
Line 1Include terminal input and output
#include <stdio.h>stdio.h declares printf, puts, getchar and putchar. The input helper reads characters with getchar; the main program prints operation results using the output functions.
Line 2Include conversion and memory utilities
#include <stdlib.h>stdlib.h declares strtol for converting an integer token. It also declares malloc and free, which the linked structures and tree programs use to obtain and release node storage.
Line 3Include string and memory utilities
#include <string.h>string.h declares utilities such as memset. It is part of the shared header list; merely including a header does not call any function or change stored values.
Lines 4–5Include character tests and conversion error reporting
#include <ctype.h>
#include <errno.h>ctype.h supplies isspace for recognising whitespace. errno.h supplies errno so the program can detect a range error reported by strtol.
Lines 7–8Define a checked integer-input helper
/* Read a complete integer token; reject bad or oversized input safely. */
int readInt(int *value) {The comment states the purpose of readInt. Its int *value parameter receives the address of a caller variable. The helper returns 1 after storing a valid value there, or 0 on failure; these flags are separate from main’s process exit status.
Lines 9–11Reserve a token buffer and its reading state
char token[64];
int ch;
size_t used = 0;token holds up to 63 input characters and a null terminator. ch is int so it can distinguish character input from EOF. used starts at zero and counts the stored token characters.
Lines 12–13Skip leading whitespace and handle end-of-file
while ((ch = getchar()) != EOF && isspace((unsigned char)ch)) { }
if (ch == EOF) return 0;getchar reads one character at a time. The loop discards whitespace before the token. The unsigned-char cast supplies an allowed argument to isspace. If EOF is reached before a token starts, return 0 without changing the caller variable.
Lines 14–15Store token characters while space remains
do {
if (used < sizeof token - 1) token[used++] = (char)ch;The do-while body handles the first non-whitespace character already read. The size check leaves one buffer slot for a null terminator. used++ uses the old index for storage, then advances the count.
Lines 16–19Reject a token that is too long
else {
while ((ch = getchar()) != EOF && !isspace((unsigned char)ch)) { }
return 0;
}When the buffer has no character space left, consume the rest of that token through whitespace or EOF, then return failure. The program does not silently treat a truncated prefix as the full input number.
Lines 20–21Read the next character and decide whether the token continues
ch = getchar();
} while (ch != EOF && !isspace((unsigned char)ch));Keep collecting until whitespace or EOF. The terminating whitespace has been consumed by getchar; it is not stored as part of the token.
Lines 22–24Terminate the string and prepare a checked conversion
token[used] = '\0';
char *end;
errno = 0;Write the null terminator after the stored token. end will identify where conversion stopped. Reset errno before strtol so a previous library error cannot be mistaken for this conversion’s result.
Line 25Convert the complete token as a decimal integer
long result = strtol(token, &end, 10);strtol reads token in base 10 and returns a long. Passing &end lets it write the address of the first unconverted character. A sign such as - is allowed as part of a valid integer token.
Line 26Reject conversion errors, leftover characters and out-of-range values
if (errno || *end != '\0' || result < -1000000 || result > 1000000) return 0;A nonzero errno rejects a reported conversion error. *end must be the null terminator, so input such as 12x is rejected rather than accepted as 12. The allowed value range is -1,000,000 to 1,000,000.
Lines 27–29Store the accepted integer through its address
*value = (int)result;
return 1;
}After validation, cast result to int and assign it through *value. Return 1 for success. The brace ends readInt. The caller variable remains unchanged on the earlier failure returns.
Lines 31–34Reserve an adjacency matrix and visited flags
#define MAX_VERTICES 20
int graph[MAX_VERTICES][MAX_VERTICES];
int visited[MAX_VERTICES];
int vertices;MAX_VERTICES is 20. The file-scope arrays graph and visited have static storage and start at zero. graph[u][v] records an edge, visited[v] records discovery, and vertices is the number of active vertex indexes, 0 through vertices - 1.
Lines 35–36Mark and print a vertex before exploring its edges
void dfs(int vertex) {
visited[vertex] = 1; printf(" %d", vertex);dfs receives a vertex index. Mark it visited immediately, then print it. Early marking prevents a later edge in a cycle from recursively revisiting the same vertex.
Lines 37–39Explore one unvisited neighbour recursively
for (int neighbour = 0; neighbour < vertices; ++neighbour)
if (graph[vertex][neighbour] && !visited[neighbour]) dfs(neighbour);
}Scan possible neighbour indexes in ascending order. Recurse only if the matrix records an edge and the neighbour has not been visited. Each call explores its deeper branch before returning to continue the current row scan; this gives depth-first order.
Lines 40–41Document graph input and start main
/* Input: V E, E undirected edges (u v), then start vertex. */
int main(void) {Read the vertex count and edge count, then that many undirected endpoint pairs, then the start vertex. These graph inputs differ from the command-driven stack and list programs.
Lines 42–44Validate graph size and edge count
int edges, start;
if (!readInt(&vertices) || !readInt(&edges) || vertices < 1 ||
vertices > MAX_VERTICES || edges < 0 || edges > vertices * (vertices - 1) / 2) goto invalid;Declare edges and start, then read the vertex and edge counts. The vertex count must be 1–20 and edges must be nonnegative and no larger than V(V - 1)/2, the maximum for a simple undirected graph without self-loops. Short-circuit OR prevents later expressions from using a value whose read failed.
Lines 45–46Read one endpoint pair for every edge
for (int i = 0; i < edges; ++i) {
int u, v;Loop edges times. u and v are the endpoint indexes of the current undirected connection.
Lines 47–48Reject invalid endpoints and duplicate edges
if (!readInt(&u) || !readInt(&v) || u < 0 || u >= vertices ||
v < 0 || v >= vertices || u == v || graph[u][v]) goto invalid;Each endpoint must be between 0 and vertices - 1. u == v rejects self-loops, and graph[u][v] rejects a repeated edge. Earlier OR conditions check bounds before the matrix lookup, so invalid indexes are not used.
Lines 49–50Store both directions of an undirected edge
graph[u][v] = graph[v][u] = 1;
}Set graph[u][v] and graph[v][u] to 1. The equal entries let either endpoint discover the other. The brace ends edge input.
Lines 51–52Validate the start and report its reachable component
if (!readInt(&start) || start < 0 || start >= vertices) goto invalid;
printf("DFS (start component):"); dfs(start); putchar('\n');Check the start index, then call dfs and print a newline. The first report contains only vertices reachable from the supplied start; it does not invent edges to disconnected vertices.
Lines 53–55Reset discovery flags for a second complete report
/* Optional second report includes disconnected and isolated vertices. */
memset(visited, 0, sizeof visited);
printf("DFS (all components):"); dfs(start);The comment explains the optional all-components report. memset sets the visited array back to zero so the second traversal can visit the start component again. Then print its new label and traverse from start.
Lines 56–57Visit remaining components and finish successfully
for (int v = 0; v < vertices; ++v) if (!visited[v]) dfs(v);
putchar('\n'); return 0;Inspect vertex indexes in ascending order. Start another traversal only for an unvisited vertex, covering disconnected components and isolated vertices exactly once in this report. End the line and return success.
Lines 58–60Report a rejected graph through a shared error label
invalid:
puts("Invalid graph input."); return 1;
}The earlier goto invalid statements arrive at this label. Print the graph-input error and return failure. The graph arrays do not own separately allocated nodes requiring cleanup.
FOLLOW THE STATE
Dry run
| Step | Operation / state | Result |
|---|---|---|
| Start at 0 | Visit 0; choose neighbour 1 | 0 |
| At 1 | Choose unvisited neighbour 3 | 0 1 3 |
| Backtrack to 0 | Visit remaining neighbour 2 | 0 1 3 2 |
CHECK THE BEHAVIOUR
Sample input & output
These outputs were produced by compiling and running this exact source. Inputs are provided in the stated order; the programs do not print repeated menu prompts.
Sample 1
4 3
0 1
0 2
1 3
0
DFS (start component): 0 1 3 2
DFS (all components): 0 1 3 2
Sample 2
5 2
0 1
2 3
0
DFS (start component): 0 1
DFS (all components): 0 1 2 3 4
Sample 3
4 4
0 1
1 2
2 0
2 3
2
DFS (start component): 2 0 1 3
DFS (all components): 2 0 1 3
Common mistakes
- Mark visited before recursion/enqueue to prevent cycles or duplicates.
- Do not claim an adjacency-matrix traversal is O(V + E).
- Do not assume the start vertex reaches every component.
WHY THIS GROWTH RATE?
Time and space complexity
Full adjacency-matrix traversal O(V²); matrix storage O(V²); DFS call stack O(V).
For every visited vertex, the loop scans all V possible neighbours in its matrix row, including entries with no edge. If R vertices are reachable, start-component traversal takes O(R × V). Traversing all V vertices costs O(V × V) = O(V²), even for a sparse graph.
The program prints a start-component report and an all-components report. Adding their costs is O(RV + V²) = O(V²). Reading E edges adds O(E); a simple graph has at most V(V−1)/2 edges, so total work stays O(V²). An adjacency-list implementation would instead give O(V + E) traversal, but that is not the representation used here.
The matrix stores V² connection entries (conceptually O(V²)); the demonstration reserves a fixed 20 × 20 array. visited uses O(V). A deepest DFS path can contain V vertices, so recursion uses O(V) auxiliary stack space.
Big-O describes growth in the stated input quantity. Fixed factors and lower-order terms are omitted. Supported input bounds keep the educational examples practical; operation costs depend on the actual representation and functions shown.
PREPARE FOR YOUR LAB VIVA
Related viva questions & answers
Try answering aloud, then expand the answer to check your reasoning. These questions focus on the concepts used in this program.
Why is a vertex marked visited before DFS explores its neighbors?
An undirected neighbor can immediately point back to the current vertex, and cycles can revisit it through other routes. Marking first prevents repeated recursion and infinite traversal around those cycles.
What makes DFS explore one route deeply before trying another?
It recursively calls DFS on an unvisited neighbor before continuing the current vertex's neighbor loop. The call must return before the next neighbor is explored, creating depth-first behavior.
Why are both adjacency-matrix entries set for each accepted edge?
The input graph is undirected. Edge u-v therefore permits movement from u to v and from v to u, represented by adj[u][v] and adj[v][u].
Why might DFS from the requested start omit some vertices?
It can reach only vertices connected to that start by a path. In a disconnected graph, vertices in other components require separate DFS starts, which the complete-traversal phase supplies.
How does the ascending neighbor loop affect DFS output?
When several unvisited neighbors are available, the smallest numbered one is explored first. Another neighbor ordering could produce a different valid DFS sequence on the same graph.
Why is complete DFS O(V^2) for this adjacency-matrix implementation?
Each visited vertex scans an entire row of V possible neighbors. Visiting all V vertices gives V × V checks, even when the number of actual edges is small.