مقدمة
تُستخدم خوارزمية Kosaraju لاكتشاف Strongly Connected Components (SCCs) في الرسوم البيانية الموجهة.
بعد فهم الفكرة النظرية، سننتقل هنا إلى تنفيذ عملي باستخدام JavaScript وTypeScript.
الخوارزمية تعتمد على ثلاث مراحل رئيسية:
DFS على الرسم الأصلي
↓
ترتيب انتهاء العقد
↓
عكس جميع الحواف
↓
DFS على الرسم المعكوس
↓
SCCsتمثيل الرسم البياني
يمكن استخدام Adjacency List بواسطة Map:
const graph = new Map([
[0, [1]],
[1, [2, 3]],
[2, [0]],
[3, [4]],
[4, [3]]
]);يمثل هذا:
0 → 1
1 → 2, 3
2 → 0
3 → 4
4 → 3DFS الأول وترتيب الانتهاء
يجب إضافة العقدة إلى Stack بعد معالجة جميع جيرانها:
function fillOrder(node, graph, visited, stack) {
visited.add(node);
for (const neighbor of graph.get(node) ?? []) {
if (!visited.has(neighbor)) {
fillOrder(neighbor, graph, visited, stack);
}
}
stack.push(node);
}مكان stack.push(node) مهم جداً لأن Kosaraju يحتاج إلى Finish Order وليس Discovery Order.
إنشاء Transpose Graph
نعكس كل حافة:
A → Bتصبح:
B → Afunction transposeGraph(graph) {
const transpose = new Map();
for (const node of graph.keys()) {
transpose.set(node, []);
}
for (const [node, neighbors] of graph.entries()) {
for (const neighbor of neighbors) {
if (!transpose.has(neighbor)) {
transpose.set(neighbor, []);
}
transpose.get(neighbor).push(node);
}
}
return transpose;
}DFS الثاني
بعد إنشاء الرسم المعكوس نمسح حالة الزيارة:
visited.clear();ثم نأخذ العقد من Stack واحدة تلو الأخرى وننفذ DFS على الرسم المعكوس.
كل DFS جديد يمثل SCC واحدة.
التنفيذ الكامل بـ JavaScript
function kosaraju(graph) {
const visited = new Set();
const stack = [];
function fillOrder(node) {
visited.add(node);
for (const neighbor of graph.get(node) ?? []) {
if (!visited.has(neighbor)) {
fillOrder(neighbor);
}
}
stack.push(node);
}
for (const node of graph.keys()) {
if (!visited.has(node)) {
fillOrder(node);
}
}
const transpose = new Map();
for (const node of graph.keys()) {
transpose.set(node, []);
}
for (const [node, neighbors] of graph.entries()) {
for (const neighbor of neighbors) {
if (!transpose.has(neighbor)) {
transpose.set(neighbor, []);
}
transpose.get(neighbor).push(node);
}
}
visited.clear();
const components = [];
function collect(node, component) {
visited.add(node);
component.push(node);
for (const neighbor of transpose.get(node) ?? []) {
if (!visited.has(neighbor)) {
collect(neighbor, component);
}
}
}
while (stack.length > 0) {
const node = stack.pop();
if (!visited.has(node)) {
const component = [];
collect(node, component);
components.push(component);
}
}
return components;
}TypeScript Generic
يمكن تعريف الرسم البياني كالتالي:
type Graph<T> = Map<T, T[]>;وبذلك يمكن استخدام أرقام أو أسماء خدمات أو أي معرفات أخرى كعقد.
function kosaraju<T>(graph: Graph<T>): T[][] {
const visited = new Set<T>();
const stack: T[] = [];
const fillOrder = (node: T): void => {
visited.add(node);
for (const neighbor of graph.get(node) ?? []) {
if (!visited.has(neighbor)) {
fillOrder(neighbor);
}
}
stack.push(node);
};
for (const node of graph.keys()) {
if (!visited.has(node)) {
fillOrder(node);
}
}
const transpose: Graph<T> = new Map();
for (const node of graph.keys()) {
transpose.set(node, []);
}
for (const [node, neighbors] of graph.entries()) {
for (const neighbor of neighbors) {
if (!transpose.has(neighbor)) {
transpose.set(neighbor, []);
}
transpose.get(neighbor)!.push(node);
}
}
visited.clear();
const components: T[][] = [];
const collect = (node: T, component: T[]): void => {
visited.add(node);
component.push(node);
for (const neighbor of transpose.get(node) ?? []) {
if (!visited.has(neighbor)) {
collect(neighbor, component);
}
}
};
while (stack.length > 0) {
const node = stack.pop()!;
if (!visited.has(node)) {
const component: T[] = [];
collect(node, component);
components.push(component);
}
}
return components;
}مثال Microservices
const services: Graph<string> = new Map([
["user", ["payment"]],
["payment", ["notification"]],
["notification", ["user"]],
["report", ["analytics"]],
["analytics", ["report"]],
["admin", ["user"]]
]);يمكن للخوارزمية اكتشاف مجموعات مثل:
{user, payment, notification}
{report, analytics}
{admin}وتشير أول مجموعتين إلى اعتماديات دائرية.
التعقيد
DFS الأول:
O(V + E)إنشاء الرسم المعكوس:
O(V + E)DFS الثاني:
O(V + E)إذن التعقيد الكلي:
O(V + E)والذاكرة أيضاً من رتبة:
O(V + E)مشكلة Recursion في JavaScript
في الرسوم العميقة جداً قد يؤدي DFS المتكرر إلى:
RangeError: Maximum call stack size exceededلذلك يُفضل استخدام DFS iterative عند التعامل مع رسوم ضخمة في بيئة Production.
أخطاء شائعة
من أهم الأخطاء:
- وضع العقدة في Stack قبل إنهاء جيرانها.
- عدم تنفيذ
visited.clear()قبل DFS الثاني. - تنفيذ DFS الثاني على الرسم الأصلي بدلاً من Transpose Graph.
- نسيان العقد التي لا تمتلك حواف خارجة.
- الاعتماد على ترتيب عناصر SCC عند كتابة الاختبارات.
الخلاصة
تنفيذ Kosaraju يعتمد على فكرة بسيطة ولكن ترتيب الخطوات مهم جداً:
DFS الأول
↓
Finish Order
↓
Transpose
↓
DFS الثاني
↓
SCCsباستخدام Adjacency List تعمل الخوارزمية في O(V + E).
نسخة TypeScript مناسبة بشكل خاص لتطبيقات حقيقية مثل تحليل Circular Dependencies بين Modules أو Packages أو Microservices بفضل Generic Types وType Safety.
الفكرة الأساسية التي يجب تذكرها هي:
DFS الأول يحدد ترتيب المعالجة، والرسم المعكوس يقلب اتجاه الوصول بين المكونات، ثم يكشف DFS الثاني حدود كل SCC.