- প্রকাশিত
সি-তে গ্রাফ ইমপ্লিমেন্টেশন এবং অ্যালগরিদম ইন্টারমিডিয়েট সি কনসেপ্টস পার্ট ৫
- লেখক
- লেখক
- নাম
- মো: নাসিম শেখ
- টুইটার
- টুইটার
- @nasimStg
আমরা linked list এর মতো linear structure এর মাধ্যমে যাত্রা করেছি, stacks and queues এর মতো ADT explore করেছি, এবং hierarchical trees নেভিগেট করেছি। এখন, আমরা সংযোগগুলি মডেলিং করার জন্য সম্ভবত সবচেয়ে general এবং শক্তিশালী ডেটা স্ট্রাকচারে পৌঁছেছি: Graphs। social network এবং road map থেকে computer network এবং project dependencies পর্যন্ত, graph everywhere! এই পোস্টটি graph fundamentals, C তে common representation method, এবং essential traversal algorithm গুলি introduce করে। বিন্দুগুলি সংযুক্ত করার জন্য প্রস্তুত হন!
সুচিপত্র
গ্রাফ কি? মৌলিক পরিভাষা
একটি Graph G আনুষ্ঠানিকভাবে G = (V, E) হিসাবে সংজ্ঞায়িত হয়, যেখানে:
- V হলো Vertices এর একটি set (যাকে Nodes ও বলা হয়)। এগুলি entity বা item represent করে।
- E হলো Edges এর একটি set। এগুলি vertices এর জোড়া এর মধ্যে সংযোগ বা সম্পর্ক represent করে।
Trees এর থেকে ভিন্ন, graph এর জন্মগতভাবে root বা strict parent-child hierarchy নেই। সেগুলিতে Cycles ও থাকতে পারে, যেখানে আপনি edge এর একটি path follow করতে পারেন এবং আপনার starting vertex এ ফিরে আসতে পারেন।
Graph এর মূল পরিভাষা:
- Vertex (Node): Set V এর একটি element।
- Edge: Set E তে দুটি vertices এর মধ্যে একটি connection।
- Undirected Graph: Edges এর কোনো direction নেই। যদি A এবং B এর মধ্যে একটি edge থাকে, তবে আপনি A থেকে B এবং B থেকে A traverse করতে পারেন। (Facebook friendships ভাবুন)।
- Directed Graph (Digraph): Edges এর একটি direction আছে। A থেকে B পর্যন্ত একটি edge B থেকে A পর্যন্ত একটি edge বোঝায় না। (Twitter follows বা one-way streets ভাবুন)।
- Weighted Graph: প্রতিটি edge এর একটি associated numerical value (weight বা cost) আছে। (edge গুলির distance আছে এমন road map, বা latency সহ network connection ভাবুন)।
- Unweighted Graph: Edges কেবল একটি connection represent করে, একটি associated weight ছাড়াই।
- Adjacent Vertices: দুটি vertices adjacent যদি তাদের সংযোগকারী একটি edge থাকে।
- Degree (একটি Vertex এর - Undirected): একটি vertex এর সাথে connected edges এর সংখ্যা।
- In-degree (Directed): একটি vertex এর ভিতরে pointing করা edges এর সংখ্যা।
- Out-degree (Directed): একটি vertex থেকে দূরে pointing করা edges এর সংখ্যা।
- Path: vertices এর একটি sequence যেখানে প্রতিটি adjacent pair একটি edge দ্বারা connected।
- Cycle: একটি path যা একই vertex এ start এবং end হয় এবং কমপক্ষে একটি edge ধারণ করে। Trees বিশেষত acyclic graphs।
- Connected Graph (Undirected): graph এর যেকোনো দুটি vertices এর মধ্যে একটি path আছে।
- Disconnected Graph: multiple "subgraph" (component) দ্বারা গঠিত একটি graph যা একে অপরের সাথে connected নয়।
সি-তে গ্রাফ Representation
আমরা memory তে vertices এবং edges কীভাবে store করব? দুটি প্রাথমিক পদ্ধতি রয়েছে:
1. Adjacency Matrix
- Concept: একটি 2D array
adjMatrix[V][V]
, যেখানেV
হলো vertices এর সংখ্যা (প্রায়শই 0 থেকে V-1 ইনডেক্স করা হয়)। _ unweighted graph এর জন্য:adjMatrix[i][j] = 1
যদি vertexi
থেকেj
পর্যন্ত একটি edge থাকে, অন্যথায়0
। _ weighted graph এর জন্য:adjMatrix[i][j] = weight
যদি একটি edge থাকে, অন্যথায়0
,infinity
, বা কিছু special marker। * undirected graph এর জন্য, matrix টি symmetric হয় (adjMatrix[i][j] == adjMatrix[j][i]
)। - Structure (Unweighted এর উদাহরণ):
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int numVertices;
int** adjMatrix; // dynamically allocated 2D array এর Pointer
} GraphMatrix;
// adjacency matrix দ্বারা represented একটি graph তৈরি করার Function
GraphMatrix* createGraphMatrix(int vertices) {
GraphMatrix* graph = (GraphMatrix*)malloc(sizeof(GraphMatrix));
if (!graph) return NULL;
graph->numVertices = vertices;
// row allocate করুন (pointer এর array)
graph->adjMatrix = (int**)malloc(vertices * sizeof(int*));
if (!graph->adjMatrix) { free(graph); return NULL; }
// প্রতিটি row এর জন্য column allocate করুন এবং 0 তে initialize করুন
for (int i = 0; i < vertices; i++) {
graph->adjMatrix[i] = (int*)calloc(vertices, sizeof(int)); // calloc 0 তে initialize করে
if (!graph->adjMatrix[i]) {
// ব্যর্থ হলে allocated memory cleanup করুন
while (--i >= 0) free(graph->adjMatrix[i]);
free(graph->adjMatrix);
free(graph);
return NULL;
}
}
return graph;
}
// একটি edge যোগ করার Function (undirected, unweighted)
void addEdgeMatrix(GraphMatrix* graph, int src, int dest) {
if (src >= 0 && src < graph->numVertices && dest >= 0 && dest < graph->numVertices) {
graph->adjMatrix[src][dest] = 1;
graph->adjMatrix[dest][src] = 1; // undirected graph এর জন্য
} else {
fprintf(stderr, "Invalid vertex index for edge (%d, %d)\n", src, dest);
}
}
// graph memory free করার Function
void freeGraphMatrix(GraphMatrix* graph) {
if (!graph) return;
for (int i = 0; i < graph->numVertices; i++) {
free(graph->adjMatrix[i]); // column free করুন
}
free(graph->adjMatrix); // row free করুন
free(graph); // graph structure free করুন
}
- Pros: দুটি নির্দিষ্ট vertices এর মধ্যে একটি edge exists কিনা তা check করতে খুব fast O(1) lookup। dense graph (যেখানে |E| |V|^2 এর কাছাকাছি) এর জন্য simple concept।
- Cons: edges এর সংখ্যা নির্বিশেষে O(V^2) space প্রয়োজন। sparse graph (বেশিরভাগ real-world graph) এর জন্য খুব inefficient। vertices যোগ/অপসারণ ব্যয়বহুল। একটি vertex এর সমস্ত neighbor এর উপর iterate করতে O(V) time লাগে।
2. Adjacency List
- Concept: linked list এর একটি array (বা dynamic array/vector)। array index একটি vertex এর সাথে correspond করে (0 থেকে V-1)।
adjList[i]
vertexi
এর সমস্ত adjacent vertices ধারণকারী linked list এর head point করে। - Structure (Unweighted এর উদাহরণ):
#include <stdio.h>
#include <stdlib.h>
// adjacency list এর জন্য Node structure
typedef struct AdjListNode {
int dest; // Destination vertex index
// weighted graph এর জন্য, add: int weight;
struct AdjListNode* next; // next adjacent vertex এর Pointer
} AdjListNode;
// graph itself represent করার Structure
typedef struct {
int numVertices;
AdjListNode** adjLists; // AdjListNode এর pointer এর একটি array (list এর head)
// প্রয়োজনে bool is_directed; add করুন
} GraphList;
// একটি নতুন adjacency list node তৈরি করার Function
AdjListNode* newAdjListNode(int dest) {
AdjListNode* newNode = (AdjListNode*)malloc(sizeof(AdjListNode));
if (!newNode) { perror("Node alloc failed"); return NULL; }
newNode->dest = dest;
newNode->next = NULL;
// weighted graph ব্যবহার করলে weight initialize করুন
return newNode;
}
// V vertices সহ একটি graph তৈরি করার Function
GraphList* createGraphList(int vertices) {
GraphList* graph = (GraphList*)malloc(sizeof(GraphList));
if (!graph) { perror("Graph alloc failed"); return NULL; }
graph->numVertices = vertices;
// adjacency list এর একটি array তৈরি করুন (pointer)
graph->adjLists = (AdjListNode**)malloc(vertices * sizeof(AdjListNode*));
if (!graph->adjLists) { perror("AdjList array alloc failed"); free(graph); return NULL; }
// প্রতিটি adjacency list empty (NULL) হিসাবে initialize করুন
for (int i = 0; i < vertices; ++i) {
graph->adjLists[i] = NULL;
}
return graph;
}
// একটি undirected graph এ একটি edge যোগ করার Function
void addEdgeList(GraphList* graph, int src, int dest) {
if (src < 0 || src >= graph->numVertices || dest < 0 || dest >= graph->numVertices) {
fprintf(stderr, "Invalid vertex index for edge (%d, %d)\n", src, dest);
return;
}
// src থেকে dest পর্যন্ত একটি edge যোগ করুন। New node list এর front এ যোগ করা হয়।
AdjListNode* newNodeDest = newAdjListNode(dest);
if (!newNodeDest) return; // Allocation check
newNodeDest->next = graph->adjLists[src]; // new node কে current head point করুন
graph->adjLists[src] = newNodeDest; // head update করুন
// graph undirected হওয়ায়, dest থেকে src পর্যন্ত একটি edge ও যোগ করুন
AdjListNode* newNodeSrc = newAdjListNode(src);
if (!newNodeSrc) return; // Allocation check (tricky if first alloc succeeded!)
// আরও robust error handling strategy বিবেচনা করুন
newNodeSrc->next = graph->adjLists[dest];
graph->adjLists[dest] = newNodeSrc;
// directed graph এর জন্য, শুধুমাত্র প্রথম অংশ (src to dest) যোগ করুন
}
// graph memory free করার Function
void freeGraphList(GraphList* graph) {
if (!graph) return;
for (int i = 0; i < graph->numVertices; ++i) {
AdjListNode* current = graph->adjLists[i];
AdjListNode* nextNode;
while (current != NULL) {
nextNode = current->next;
free(current);
current = nextNode;
}
}
free(graph->adjLists); // pointer এর array free করুন
free(graph); // graph structure free করুন
}
// graph print করার Helper (debugging এর জন্য)
void printGraphList(GraphList* graph) {
printf("Graph Adjacency Lists:\n");
for (int v = 0; v < graph->numVertices; ++v) {
AdjListNode* temp = graph->adjLists[v];
printf("Vertex %d: ", v);
while (temp) {
printf("-> %d ", temp->dest);
temp = temp->next;
}
printf("-> NULL\n");
}
}
- Pros: sparse graph এর জন্য space efficient O(V + E)। edges যোগ করা efficient। vertex
i
এর neighbor দের উপর iterate করা efficient (O(degree(i)))। বেশিরভাগ application এর জন্য generally preferred। - Cons:
u
এবংv
এর মধ্যে একটি specific edge check করতেu
এর degree এর সমানুপাতিক time লাগতে পারে।
কোনটি choose করবেন? সাধারণত, Adjacency List preferred যদি না graph খুব dense হয় বা আপনার ঘন ঘন constant-time edge existence check এর প্রয়োজন হয়। আমরা নিচের traversal algorithm গুলোর জন্য Adjacency List ব্যবহার করব।
গ্রাফ Traversal Algorithm
Traversal মানে একটি starting vertex থেকে সমস্ত reachable vertices systematically visiting করা। Graph এ cycles থাকতে পারে, তাই infinite loop এড়াতে আমাদের visited vertices ট্র্যাক রাখা অবশ্যই প্রয়োজন।
visited
Array: আমরা সাধারণত একটি boolean array visited[V]
ব্যবহার করি, যা false
তে initialize করা হয়। একটি vertex visiting করার আগে, আমরা check করি visited[vertex]
true কিনা। যদি না হয়, আমরা এটিকে true
mark করি এবং proceed করি।
1. Depth-First Search (DFS)
- Concept: backtracking এর আগে একটি path এ যতদূর সম্ভব explore করুন। যদি আপনি একটি dead end বা ইতিমধ্যেই visited node hit করেন, আপনি backtrack করে অন্য path চেষ্টা করবেন। implicitly একটি stack ব্যবহার করে (recursion এর মাধ্যমে) বা explicitly।
- Implementation (Recursive, Adjacency List):
#include <stdbool.h> // For bool type
// Recursive DFS helper function
void DFSUtil(GraphList* graph, int vertex, bool visited[]) {
// current node কে visited mark করুন এবং এটিকে print করুন
visited[vertex] = true;
printf("%d ", vertex);
// সমস্ত adjacent vertices এর জন্য Recur করুন
AdjListNode* temp = graph->adjLists[vertex];
while (temp != NULL) {
int adjacentVertex = temp->dest;
if (!visited[adjacentVertex]) {
DFSUtil(graph, adjacentVertex, visited); // Recursive call
}
temp = temp->next;
}
}
// Main DFS function - disconnected graph handle করে
void DFS(GraphList* graph, int startVertex) {
if (!graph || startVertex < 0 || startVertex >= graph->numVertices) {
fprintf(stderr, "Invalid input for DFS\n");
return;
}
printf("DFS starting from vertex %d: ", startVertex);
// visited array allocate এবং initialize করুন
bool* visited = (bool*)calloc(graph->numVertices, sizeof(bool)); // calloc false তে initialize করে
if (!visited) { perror("Visited array alloc failed"); return; }
// starting vertex এর জন্য recursive helper function call করুন
DFSUtil(graph, startVertex, visited);
// ঐচ্ছিক: graph disconnected হতে পারে, সমস্ত components visited হয় কিনা তা নিশ্চিত করতে
// সমস্ত vertices এর উপর iterate করুন।
// printf("\nChecking other components (if any):\n");
// for (int i = 0; i < graph->numVertices; i++) {
// if (!visited[i]) {
// printf("Component starting at %d: ", i);
// DFSUtil(graph, i, visited);
// printf("\n");
// }
// }
printf("\n");
free(visited); // visited array cleanup করুন
}
- Application: Paths খুঁজে বের করা, cycle detect করা, topological sort করা (Directed Acyclic Graph - DAGs এর জন্য), connected component খুঁজে বের করা।
2. Breadth-First Search (BFS)
- Concept: level by level vertex neighbor explore করুন। starting node visit করুন, তারপর তার direct neighbor দের সব, তারপর তাদের neighbor দের সব, এবং so on। একটি Queue ব্যবহার করে।
- Implementation (Iterative using Queue, Adjacency List): Queue implementation প্রয়োজন (Trees post এ ব্যবহৃত similar, integer vertex indices store করে)।
#include <stdbool.h>
// --- BFS এর জন্য Minimal Queue Implementation (int vertex indices store করে) ---
// Available থাকলে previous post থেকে আপনার actual Queue code দিয়ে replace করুন
typedef struct IntQueueNode {
int data;
struct IntQueueNode* next;
} IntQueueNode;
typedef struct {
IntQueueNode *front, *rear;
} IntQueue;
IntQueue* createIntQueue() { /* Implementation needed */ }
void enqueueInt(IntQueue* q, int item) { /* Implementation needed */ }
int dequeueInt(IntQueue* q) { /* Implementation needed - return -1 or error on empty */ }
bool isIntQueueEmpty(IntQueue* q) { /* Implementation needed */ }
void freeIntQueue(IntQueue* q) { /* Implementation needed */ }
// --- End Minimal Queue ---
// --- Placeholder Queue Implementation ---
IntQueue* createIntQueue() {
IntQueue* q = (IntQueue*)malloc(sizeof(IntQueue));
if (!q) return NULL;
q->front = q->rear = NULL;
return q;
}
bool isIntQueueEmpty(IntQueue* q) {
return q->front == NULL;
}
void enqueueInt(IntQueue* q, int item) {
IntQueueNode* temp = (IntQueueNode*)malloc(sizeof(IntQueueNode));
if (!temp) return;
temp->data = item;
temp->next = NULL;
if (q->rear == NULL) { q->front = q->rear = temp; return; }
q->rear->next = temp;
q->rear = temp;
}
int dequeueInt(IntQueue* q) {
if (isIntQueueEmpty(q)) return -1; // Error indicator
IntQueueNode* temp = q->front;
int item = temp->data;
q->front = q->front->next;
if (q->front == NULL) q->rear = NULL;
free(temp);
return item;
}
void freeIntQueue(IntQueue* q) {
while (!isIntQueueEmpty(q)) { dequeueInt(q); }
free(q);
}
// --- End Placeholder Queue Implementation ---
// Main BFS function
void BFS(GraphList* graph, int startVertex) {
if (!graph || startVertex < 0 || startVertex >= graph->numVertices) {
fprintf(stderr, "Invalid input for BFS\n");
return;
}
printf("BFS starting from vertex %d: ", startVertex);
// visited array allocate এবং initialize করুন
bool* visited = (bool*)calloc(graph->numVertices, sizeof(bool));
if (!visited) { perror("Visited array alloc failed"); return; }
// BFS এর জন্য একটি queue তৈরি করুন
IntQueue* queue = createIntQueue();
if (!queue) { free(visited); perror("Queue creation failed"); return; }
// start node কে visited mark করুন এবং এটিকে enqueue করুন
visited[startVertex] = true;
enqueueInt(queue, startVertex);
while (!isIntQueueEmpty(queue)) {
// একটি vertex dequeue করুন এবং এটিকে print করুন
int currentVertex = dequeueInt(queue);
if (currentVertex == -1) break; // logic correct হলে এটি হওয়া উচিত নয়
printf("%d ", currentVertex);
// dequeued vertex এর সমস্ত adjacent vertices পান
AdjListNode* temp = graph->adjLists[currentVertex];
while (temp != NULL) {
int adjVertex = temp->dest;
// একটি adjacent visited না হলে, এটিকে visited mark করুন এবং এটিকে enqueue করুন
if (!visited[adjVertex]) {
visited[adjVertex] = true;
enqueueInt(queue, adjVertex);
}
temp = temp->next;
}
}
printf("\n");
freeIntQueue(queue); // queue cleanup করুন
free(visited); // visited array cleanup করুন
// BFS inherently startVertex এর connected component explore করে।
// disconnected graph explore করতে হলে, unvisited node থেকে শুরু করে BFS multiple time call করুন।
}
- Application: unweighted graph এ shortest path খুঁজে বের করা, connected components খুঁজে বের করা, network broadcasting algorithm (যেমন reachable node খুঁজে বের করা), web crawler।
Traversal এর বাইরে: অন্যান্য Graph Algorithm
DFS এবং BFS অনেক আরও advanced graph algorithm এর building block, যার মধ্যে রয়েছে:
- Shortest Path Algorithms: _ Dijkstra's Algorithm: weighted graph (non-negative weight সহ) এ node গুলির মধ্যে shortest path খুঁজে বের করে। _ Bellman-Ford Algorithm: negative edge weight সহ graph handle করে (এবং negative cycle detect করে)।
- Minimum Spanning Tree (MST) Algorithms: minimum possible total edge weight সহ সমস্ত vertices connect করে এমন একটি subgraph খুঁজে বের করে। _ Prim's Algorithm _ Kruskal's Algorithm
- Topological Sort: Directed Acyclic Graph (DAG) এ vertices এর linear ordering এমন যে vertex
u
থেকে vertexv
পর্যন্ত প্রতিটি directed edge এর জন্য, ordering এv
এর আগেu
আসে। dependencies সহ task scheduling এর জন্য ব্যবহৃত হয়।
এগুলি সাধারণত আরও advanced algorithm course গুলিতে cover করা হয় তবে এখানে আলোচনা করা graph representation এবং traversal concept গুলির উপর rely করে।
main
)
ব্যবহারের উদাহরণ (// (graph structure, create/add/free function, DFS, BFS,
// এবং প্রয়োজনীয় Queue function উপরে অন্তর্ভুক্ত করুন)
int main() {
int V = 6; // vertices এর সংখ্যা
GraphList* graph = createGraphList(V);
if (!graph) return 1;
printf("Creating an undirected graph with %d vertices.\n", V);
addEdgeList(graph, 0, 1);
addEdgeList(graph, 0, 2);
addEdgeList(graph, 1, 3);
addEdgeList(graph, 1, 4);
addEdgeList(graph, 2, 4);
addEdgeList(graph, 3, 4);
addEdgeList(graph, 3, 5);
addEdgeList(graph, 4, 5);
// adjacency list representation print করুন
printGraphList(graph);
printf("\n");
// vertex 0 থেকে শুরু করে DFS perform করুন
DFS(graph, 0);
// vertex 0 থেকে শুরু করে BFS perform করুন
BFS(graph, 0);
// vertex 3 থেকে শুরু করে DFS perform করুন
DFS(graph, 3);
// vertex 3 থেকে শুরু করে BFS perform করুন
BFS(graph, 3);
printf("\nFreeing the graph...\n");
freeGraphList(graph);
graph = NULL;
printf("Graph freed.\n");
return 0;
}
উপসংহার
Graphs সি-তে complex relationship এবং network model করার জন্য একটি শক্তিশালী এবং flexible উপায় সরবরাহ করে। আমরা দেখেছি Adjacency Matrix এবং আরও commonly used Adjacency List ব্যবহার করে সেগুলি কীভাবে represent করতে হয়। fundamental traversal algorithm, Depth-First Search (DFS) এবং Breadth-First Search (BFS) আয়ত্ত করা, একটি crucial visited
marker ব্যবহার করে, graph explore করার এবং অনেক problem solve করার ability unlock করে। এই traversal গুলি shortest path, minimum spanning tree, এবং topological sort এর মতো আরও complex algorithm তৈরি হওয়ার foundation।
আপনার সি toolkit এ graph যোগ হওয়ার সাথে সাথে, আপনি wide range of interconnected problem tackling করার জন্য equipped! আমাদের intermediate journey তে এরপর কি? সম্ভবত low-level Bitwise Operations, Function Pointers এর শক্তি, বা সি-তে robust Error Handling technique গুলির একটি closer look।
আরও পড়া এবং সম্পর্কিত পোস্ট
_ ইন্টারমিডিয়েট সি কনসেপ্টস পার্ট ৪: সি-তে Trees সম্পর্কে সবকিছু _ ইন্টারমিডিয়েট সি কনসেপ্টস পার্ট ৩: সি-তে Stack এবং Queue Implement করা (DFS এর জন্য Stack, BFS এর জন্য Queue) _ ইন্টারমিডিয়েট সি কনসেপ্টস পার্ট ১: সি-তে ডাইনামিক মেমরি অ্যালোকেশন _ সি-তে শুরু করা পার্ট ৬: সি-তে পয়েন্টার * সি-তে শুরু করা পার্ট ৭: স্ট্রাকচার এবং ইউনিয়ন