1 #!/usr/bin/env rdmd-dev
2 
3 /** Longest Common Subsequence, typically used as a base for writing diff/compare algorithms.
4 
5     Copyright: Per Nordlöw 2014-.
6     License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
7     Authors: $(WEB Per Nordlöw)
8 
9     See_Also: https://en.wikipedia.org/wiki/Longest_common_subsequence_problem
10     See_Also: https://en.wikipedia.org/wiki/Diff
11 */
12 
13 module nxt.lcs;
14 
15 /** Longest Common Subsequence (LCS).
16     See_Also: http://rosettacode.org/wiki/Longest_common_subsequence#Recursive_version
17 */
18 T[] lcsR(T)(in T[] a,
19             in T[] b) pure nothrow
20 {
21     import std.range.primitives : empty;
22     if (a.empty || b.empty)
23     {
24         return null;            // undefined
25     }
26     if (a[0] == b[0])
27     {
28         return a[0] ~ lcsR(a[1 .. $],
29                            b[1 .. $]);
30     }
31     const longest = (T[] x, T[] y) => x.length > y.length ? x : y;
32     return longest(lcsR(a, b[1 .. $]),
33                    lcsR(a[1 .. $], b));
34 }
35 
36 /** Longest Common Subsequence (LCS).
37     Faster Dynamic Programming Version.
38     See_Also: http://rosettacode.org/wiki/Longest_common_subsequence#Faster_dynamic_programming_version
39 */
40 T[] lcsDP(T)(in T[] a,
41              in T[] b) pure /* nothrow */
42 {
43     import std.algorithm.comparison : max;
44 
45     auto L = new uint[][](a.length + 1, b.length + 1);
46 
47     foreach (immutable i; 0 .. a.length)
48     {
49         foreach (immutable j; 0 .. b.length)
50         {
51             L[i + 1][j + 1] = (a[i] == b[j]) ? (1 + L[i][j]) :
52                 max(L[i + 1][j], L[i][j + 1]);
53         }
54     }
55 
56     import core.internal.traits : Unqual;
57     Unqual!T[] result;
58 
59     for (auto i = a.length, j = b.length; i > 0 && j > 0; )
60     {
61         if (a[i - 1] == b[j - 1])
62         {
63             result ~= a[i - 1];
64             i--;
65             j--;
66         }
67         else
68         {
69             if (L[i][j - 1] < L[i - 1][j])
70                 i--;
71             else
72                 j--;
73         }
74     }
75 
76     import std.algorithm : reverse;
77     result.reverse();           // not nothrow
78     return result;
79 }
80 
81 /** Get LCS Lengths. */
82 uint[] lcsLengths(R)(R xs,
83                      R ys) pure nothrow
84 {
85     import std.algorithm.comparison : max;
86 
87     auto prev = new typeof(return)(1 + ys.length);
88     auto curr = new typeof(return)(1 + ys.length);
89 
90     foreach (immutable x; xs)
91     {
92         import std.algorithm: swap;
93         swap(curr, prev);
94         size_t i = 0;
95         foreach (immutable y; ys)
96         {
97             curr[i + 1] = (x == y) ? prev[i] + 1 : max(curr[i], prev[i + 1]);
98             i++;
99         }
100     }
101 
102     return curr;
103 }
104 
105 void lcsDo(T)(in T[] xs,
106               in T[] ys,
107               bool[] xsInLCS,
108               in size_t idx = 0) pure nothrow
109 {
110     immutable nx = xs.length;
111     immutable ny = ys.length;
112 
113     if (nx == 0)
114         return;
115 
116     if (nx == 1)
117     {
118         import std.algorithm: canFind;
119         if (ys.canFind(xs[0]))
120             xsInLCS[idx] = true;
121     }
122     else
123     {
124         immutable mid = nx / 2;
125         const xb = xs[0.. mid];
126         const xe = xs[mid .. $];
127         immutable ll_b = lcsLengths(xb, ys);
128 
129         import std.range: retro;
130         const ll_e = lcsLengths(xe.retro,
131                                 ys.retro); // retro is slow with DMD
132 
133         // import std.algorithm.comparison : max;
134         //immutable k = iota(ny + 1)
135         //              .reduce!(max!(j => ll_b[j] + ll_e[ny - j]));
136         import std.range: iota;
137         import std.algorithm: minPos;
138         import std.typecons: tuple;
139         immutable k = iota(ny + 1).minPos!((i, j) => tuple(ll_b[i] + ll_e[ny - i]) >
140                                            tuple(ll_b[j] + ll_e[ny - j]))[0];
141 
142         lcsDo(xb, ys[0 .. k], xsInLCS, idx);
143         lcsDo(xe, ys[k .. $], xsInLCS, idx + mid);
144     }
145 }
146 
147 /** Longest Common Subsequence (LCS) using Hirschberg.
148     Linear-Space Faster Dynamic Programming Version.
149 
150     Time Complexity: O(m*n)
151     Space Complexity: O(min(m,n))
152 
153     To speed up this code on DMD remove the memory allocations from $(D lcsLengths), and
154     do not use the $(D retro) range (replace it with $(D foreach_reverse))
155 
156     See_Also: https://en.wikipedia.org/wiki/Hirschberg%27s_algorithm
157     See_Also: http://rosettacode.org/wiki/Longest_common_subsequence#Hirschberg_algorithm_version
158 */
159 const(T)[] lcs(T)(in T[] xs,
160                   in T[] ys) pure /*nothrow*/
161 {
162     auto xsInLCS = new bool[xs.length];
163     lcsDo(xs, ys, xsInLCS);
164     import std.range : zip;
165     import std.algorithm.iteration : filter, map;
166     import std.array : array;
167     return zip(xs, xsInLCS).filter!q{ a[1] }.map!q{ a[0] }.array; // Not nothrow.
168 }
169 
170 string lcs(in string a,
171            in string b) pure /*nothrow*/
172 {
173     import std.string : representation, assumeUTF;
174     return lcs(a.representation,
175                b.representation).assumeUTF;
176 }
177 
178 unittest
179 {
180     immutable x = "thisisatest";
181     immutable y = "testing123testing";
182     immutable z = "tsitest";
183     assert(z == lcsR(x, y));
184     assert(z == lcsDP(x, y));
185     assert(z == lcs(x, y));
186     assert("" == lcs("", ""));
187     assert("" == lcs(null, null));
188 }
189 
190 unittest
191 {
192     immutable x = [1, 2, 3];
193     immutable y = [4, 5, 6];
194     immutable z = [];
195     assert(z == lcsR(x, y));
196     assert(z == lcsDP(x, y));
197     assert(z == lcs(x, y));
198 }
199 
200 unittest
201 {
202     immutable x = [1, 2, 3];
203     immutable y = [2, 3, 4];
204     immutable z = [2, 3];
205     assert(z == lcsR(x, y));
206     assert(z == lcsDP(x, y));
207     assert(z == lcs(x, y));
208 }
209 
210 unittest
211 {
212     alias T = int;
213     size_t n = 300;
214     auto x = new T[n];
215     auto y = new T[n];
216     assert(lcs(x, y).length == n);
217 }
218 
219 version(benchmark) unittest
220 {
221     import std.algorithm : levenshteinDistance, levenshteinDistanceAndPath;
222     import nxt.random_ex: randInPlaceBlockwise;
223 
224     alias T = int;
225     size_t n = 300;
226     auto x = new T[n];
227     auto y = new T[n];
228 
229     x.randInPlaceBlockwise;
230     y.randInPlaceBlockwise;
231 
232     void bLCS() { const d = lcs(x, y); }
233     void bLD() { const d = levenshteinDistance(x, y); }
234     void bLDAP() { const dp = levenshteinDistanceAndPath(x, y); }
235 
236     import std.datetime: benchmark;
237     import std.algorithm: map;
238     import std.array: array;
239 
240     auto r = benchmark!(bLCS, bLD, bLDAP)(1);
241     auto q = r.array.map!(a => a.msecs);
242 
243     import std.stdio: writeln;
244     writeln(q, " milliseconds");
245 }