Skip to content

Commit fabb3c0

Browse files
Merge pull request #41 from atulya-srivastava/levenshtein-distance-algo
Levenshtein distance algorithm implementation
2 parents 79503b2 + a72d595 commit fabb3c0

File tree

6 files changed

+830
-3
lines changed

6 files changed

+830
-3
lines changed

src/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
44
import SortingPage from "./pages/sorting/SortingPage";
55
import GraphPage from "./pages/graph/GraphPage";
66
import Homepage from "./pages/Homepage.jsx";
7+
import DynamicProgrammingPage from "./pages/dynamic-programming/DyanmicProgrammingPage.jsx";
78

89
function App() {
910
return (
@@ -13,6 +14,7 @@ function App() {
1314
{/* <Route path="/graph/union-find" element={<UnionFindPage />} /> */}
1415
<Route path="/sorting" element={<SortingPage />} />
1516
<Route path="/graph" element={<GraphPage />} />
17+
<Route path="/dynamic-programming" element={<DynamicProgrammingPage />} />
1618
</Routes>
1719
</Router>
1820
);
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
const calculateLevenshtein = (s1, s2, costs) => {
2+
const m = s1.length;
3+
const n = s2.length;
4+
const dp = Array(m + 1)
5+
.fill(null)
6+
.map(() => Array(n + 1).fill(null));
7+
8+
for (let i = 0; i <= m; i++) dp[i][0] = i * costs.delete;
9+
for (let j = 0; j <= n; j++) dp[0][j] = j * costs.insert;
10+
11+
for (let i = 1; i <= m; i++) {
12+
for (let j = 1; j <= n; j++) {
13+
if (s1[i - 1] === s2[j - 1]) {
14+
dp[i][j] = dp[i - 1][j - 1];
15+
} else {
16+
dp[i][j] = Math.min(
17+
dp[i - 1][j] + costs.delete,
18+
dp[i][j - 1] + costs.insert,
19+
dp[i - 1][j - 1] + costs.replace
20+
);
21+
}
22+
}
23+
}
24+
return dp;
25+
};
26+
27+
const getExplanation = (s1, s2, i, j, dp, costs) => {
28+
if (i === 0) {
29+
return {
30+
title: `Base case: Converting empty string to "${s2.substring(0, j)}"`,
31+
text: `We need ${j} insertions. Cost = ${j} × ${costs.insert} = ${
32+
j * costs.insert
33+
}`,
34+
};
35+
}
36+
if (j === 0) {
37+
return {
38+
title: `Base case: Converting "${s1.substring(0, i)}" to empty string`,
39+
text: `We need ${i} deletions. Cost = ${i} × ${costs.delete} = ${
40+
i * costs.delete
41+
}`,
42+
};
43+
}
44+
45+
const char1 = s1[i - 1];
46+
const char2 = s2[j - 1];
47+
const match = char1 === char2;
48+
const deletion = dp[i - 1][j] + costs.delete;
49+
const insertion = dp[i][j - 1] + costs.insert;
50+
const substitution = dp[i - 1][j - 1] + (match ? 0 : costs.replace);
51+
52+
let title = `Cell [${i}][${j}]: Comparing '${char1}' with '${char2}'`;
53+
let text = "";
54+
55+
if (match) {
56+
text = `Characters match! We can skip (no operation).\n• Skip (diagonal): ${
57+
dp[i - 1][j - 1]
58+
} + 0 = ${substitution}\n• Delete '${char1}' (down): ${dp[i - 1][j]} + ${
59+
costs.delete
60+
} = ${deletion}\n• Insert '${char2}' (right): ${dp[i][j - 1]} + ${
61+
costs.insert
62+
} = ${insertion}\n✓ Best choice: Skip with cost = ${dp[i][j]}`;
63+
} else {
64+
text = `Characters don't match. We have three options:\n• Replace '${char1}' with '${char2}' (diagonal): ${
65+
dp[i - 1][j - 1]
66+
} + ${costs.replace} = ${substitution}\n• Delete '${char1}' (down): ${
67+
dp[i - 1][j]
68+
} + ${costs.delete} = ${deletion}\n• Insert '${char2}' (right): ${
69+
dp[i][j - 1]
70+
} + ${costs.insert} = ${insertion}\n`;
71+
const minCost = dp[i][j];
72+
let chosenOp = "";
73+
if (
74+
substitution === minCost &&
75+
substitution <= deletion &&
76+
substitution <= insertion
77+
) {
78+
chosenOp = "Replace (diagonal)";
79+
} else if (insertion === minCost && insertion < deletion) {
80+
chosenOp = "Insert (right)";
81+
} else if (deletion === minCost) {
82+
chosenOp = "Delete (down)";
83+
}
84+
text += `✓ Best choice: ${chosenOp} with cost = ${minCost}`;
85+
}
86+
return { title, text };
87+
};
88+
89+
const backtrackOperations = (s1, s2, dp, costs) => {
90+
const operations = [];
91+
const pathCells = [];
92+
let i = s1.length;
93+
let j = s2.length;
94+
95+
pathCells.push([i, j]);
96+
97+
while (i > 0 || j > 0) {
98+
let op;
99+
if (i === 0) {
100+
op = {
101+
op: "insert",
102+
char: s2[j - 1],
103+
pos: i,
104+
cells: [
105+
[i, j],
106+
[i, j - 1],
107+
],
108+
};
109+
operations.unshift(op);
110+
j--;
111+
} else if (j === 0) {
112+
op = {
113+
op: "delete",
114+
char: s1[i - 1],
115+
pos: i,
116+
cells: [
117+
[i, j],
118+
[i - 1, j],
119+
],
120+
};
121+
operations.unshift(op);
122+
i--;
123+
} else if (s1[i - 1] === s2[j - 1]) {
124+
op = {
125+
op: "skip",
126+
char: s1[i - 1],
127+
pos: i,
128+
cells: [
129+
[i, j],
130+
[i - 1, j - 1],
131+
],
132+
};
133+
operations.unshift(op);
134+
i--;
135+
j--;
136+
} else {
137+
const repCost = dp[i - 1][j - 1] + costs.replace;
138+
const insCost = dp[i][j - 1] + costs.insert;
139+
const delCost = dp[i - 1][j] + costs.delete;
140+
141+
if (repCost <= delCost && repCost <= insCost) {
142+
op = {
143+
op: "replace",
144+
from: s1[i - 1],
145+
to: s2[j - 1],
146+
pos: i,
147+
cells: [
148+
[i, j],
149+
[i - 1, j - 1],
150+
],
151+
};
152+
operations.unshift(op);
153+
i--;
154+
j--;
155+
} else if (insCost < delCost) {
156+
op = {
157+
op: "insert",
158+
char: s2[j - 1],
159+
pos: i,
160+
cells: [
161+
[i, j],
162+
[i, j - 1],
163+
],
164+
};
165+
operations.unshift(op);
166+
j--;
167+
} else {
168+
op = {
169+
op: "delete",
170+
char: s1[i - 1],
171+
pos: i,
172+
cells: [
173+
[i, j],
174+
[i - 1, j],
175+
],
176+
};
177+
operations.unshift(op);
178+
i--;
179+
}
180+
}
181+
pathCells.push([i, j]);
182+
}
183+
184+
return { operations, path: pathCells };
185+
};
186+
187+
const processOperations = (ops, s1) => {
188+
let currentString = s1;
189+
return ops
190+
.map((op) => {
191+
const displayString = currentString;
192+
const actualPos = op.pos - (s1.length - currentString.length);
193+
194+
let displayText = "";
195+
if (op.op === "replace") {
196+
displayText = `${displayString}: replace ${
197+
currentString[actualPos - 1]
198+
} with ${op.to} at position ${actualPos}`;
199+
currentString =
200+
currentString.substring(0, actualPos - 1) +
201+
op.to +
202+
currentString.substring(actualPos);
203+
} else if (op.op === "delete") {
204+
displayText = `${displayString}: delete ${
205+
currentString[actualPos - 1]
206+
} at position ${actualPos}`;
207+
currentString =
208+
currentString.substring(0, actualPos - 1) +
209+
currentString.substring(actualPos);
210+
} else if (op.op === "insert") {
211+
displayText = `${displayString}: insert ${op.char} at position ${actualPos}`;
212+
currentString =
213+
currentString.substring(0, actualPos) +
214+
op.char +
215+
currentString.substring(actualPos);
216+
} else if (op.op === "skip") {
217+
displayText = `${displayString}: don't change ${op.char} at position ${actualPos}`;
218+
}
219+
220+
return { ...op, displayText, isSkip: op.op === "skip" };
221+
})
222+
.concat([
223+
{
224+
displayText: `${currentString}: (final result)`,
225+
isFinal: true,
226+
cells: [],
227+
},
228+
]);
229+
};
230+
231+
export {
232+
calculateLevenshtein,
233+
getExplanation,
234+
backtrackOperations,
235+
processOperations,
236+
};

0 commit comments

Comments
 (0)