001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.text.similarity;
018
019/**
020 * A similarity algorithm indicating the length of the longest common subsequence between two strings.
021 *
022 * <p>
023 * The Longest common subsequence algorithm returns the length of the longest subsequence that two strings have in
024 * common. Two strings that are entirely different, return a value of 0, and two strings that return a value
025 * of the commonly shared length implies that the strings are completely the same in value and position.
026 * <i>Note.</i>  Generally this algorithm is fairly inefficient, as for length <i>m</i>, <i>n</i> of the input
027 * {@code CharSequence}'s {@code left} and {@code right} respectively, the runtime of the
028 * algorithm is <i>O(m*n)</i>.
029 * </p>
030 *
031 * <p>
032 * As of version 1.10, a more space-efficient of the algorithm is implemented. The new algorithm has linear space
033 * complexity instead of quadratic. However, time complexity is still quadratic in the size of input strings.
034 * </p>
035 *
036 * <p>
037 * The implementation is based on Hirschberg's Longest Commons Substring algorithm (cited below).
038 * </p>
039 *
040 * <p>For further reading see:</p>
041 * <ul>
042 * <li>
043 * Lothaire, M. <i>Applied combinatorics on words</i>. New York: Cambridge U Press, 2005. <b>12-13</b>
044 * </li>
045 * <li>
046 * D. S. Hirschberg, "A linear space algorithm for computing maximal common subsequences," CACM, 1975, pp. 341--343.
047 * </li>
048 * </ul>
049 *
050 * @since 1.0
051 */
052public class LongestCommonSubsequence implements SimilarityScore<Integer> {
053
054    /**
055     * Singleton instance.
056     */
057    static final LongestCommonSubsequence INSTANCE = new LongestCommonSubsequence();
058
059    /**
060     * An implementation of "ALG B" from Hirschberg's CACM '71 paper.
061     * Assuming the first input sequence is of size <code>m</code> and the second input sequence is of size
062     * <code>n</code>, this method returns the last row of the dynamic programming (DP) table when calculating
063     * the LCS of the two sequences in <i>O(m*n)</i> time and <i>O(n)</i> space.
064     * The last element of the returned array, is the size of the LCS of the two input sequences.
065     *
066     * @param left first input sequence.
067     * @param right second input sequence.
068     * @return last row of the dynamic-programming (DP) table for calculating the LCS of <code>left</code> and <code>right</code>
069     * @since 1.10.0
070     */
071    private static int[] algorithmB(final CharSequence left, final CharSequence right) {
072        final int m = left.length();
073        final int n = right.length();
074
075        // Creating an array for storing two rows of DP table
076        final int[][] dpRows = new int[2][1 + n];
077
078        for (int i = 1; i <= m; i++) {
079            // K(0, j) <- K(1, j) [j = 0...n], as per the paper:
080            // Since we have references in Java, we can swap references instead of literal copying.
081            // We could also use a "binary index" using modulus operator, but directly swapping the
082            // two rows helps readability and keeps the code consistent with the algorithm description
083            // in the paper.
084            final int[] temp = dpRows[0];
085            dpRows[0] = dpRows[1];
086            dpRows[1] = temp;
087
088            for (int j = 1; j <= n; j++) {
089                if (left.charAt(i - 1) == right.charAt(j - 1)) {
090                    dpRows[1][j] = dpRows[0][j - 1] + 1;
091                } else {
092                    dpRows[1][j] = Math.max(dpRows[1][j - 1], dpRows[0][j]);
093                }
094            }
095        }
096
097        // LL(j) <- K(1, j) [j=0...n], as per the paper:
098        // We don't need literal copying of the array, we can just return the reference
099        return dpRows[1];
100    }
101
102    /**
103     * An implementation of "ALG C" from Hirschberg's CACM '71 paper.
104     * Assuming the first input sequence is of size <code>m</code> and the second input sequence is of size
105     * <code>n</code>, this method returns the Longest Common Subsequence (LCS) of the two sequences in
106     * <i>O(m*n)</i> time and <i>O(m+n)</i> space.
107     *
108     * @param left first input sequence.
109     * @param right second input sequence.
110     * @return the LCS of <code>left</code> and <code>right</code>
111     * @since 1.10.0
112     */
113    private static String algorithmC(final CharSequence left, final CharSequence right) {
114        final int m = left.length();
115        final int n = right.length();
116
117        final StringBuilder out = new StringBuilder();
118
119        if (m == 1) { // Handle trivial cases, as per the paper
120            final char leftCh = left.charAt(0);
121            for (int j = 0; j < n; j++) {
122                if (leftCh == right.charAt(j)) {
123                    out.append(leftCh);
124                    break;
125                }
126            }
127        } else if (n > 0 && m > 1) {
128            final int mid = m / 2; // Find the middle point
129
130            final CharSequence leftFirstPart = left.subSequence(0, mid);
131            final CharSequence leftSecondPart = left.subSequence(mid, m);
132
133            // Step 3 of the algorithm: two calls to Algorithm B
134            final int[] l1 = algorithmB(leftFirstPart, right);
135            final int[] l2 = algorithmB(reverse(leftSecondPart), reverse(right));
136
137            // Find k, as per the Step 4 of the algorithm
138            int k = 0;
139            int t = 0;
140            for (int j = 0; j <= n; j++) {
141                final int s = l1[j] + l2[n - j];
142                if (t < s) {
143                    t = s;
144                    k = j;
145                }
146            }
147
148            // Step 5: solve simpler problems, recursively
149            out.append(algorithmC(leftFirstPart, right.subSequence(0, k)));
150            out.append(algorithmC(leftSecondPart, right.subSequence(k, n)));
151        }
152
153        return out.toString();
154    }
155
156    // An auxiliary method for CharSequence reversal
157    private static String reverse(final CharSequence s) {
158        return new StringBuilder(s).reverse().toString();
159    }
160
161    /**
162     * Calculates the longest common subsequence similarity score of two {@code CharSequence}'s passed as
163     * input.
164     *
165     * <p>
166     * This method implements a more efficient version of LCS algorithm which has quadratic time and
167     * linear space complexity.
168     * </p>
169     *
170     * <p>
171     * This method is based on newly implemented {@link #algorithmB(CharSequence, CharSequence)}.
172     * An evaluation using JMH revealed that this method is almost two times faster than its previous version.
173     * </p>
174     *
175     * @param left first character sequence
176     * @param right second character sequence
177     * @return length of the longest common subsequence of <code>left</code> and <code>right</code>
178     * @throws IllegalArgumentException if either String input {@code null}
179     */
180    @Override
181    public Integer apply(final CharSequence left, final CharSequence right) {
182        // Quick return for invalid inputs
183        if (left == null || right == null) {
184            throw new IllegalArgumentException("Inputs must not be null");
185        }
186        // Find lengths of two strings
187        final int leftSz = left.length();
188        final int rightSz = right.length();
189
190        // Check if we can avoid calling algorithmB which involves heap space allocation
191        if (leftSz == 0 || rightSz == 0) {
192            return 0;
193        }
194
195        // Check if we can save even more space
196        if (leftSz < rightSz) {
197            return algorithmB(right, left)[leftSz];
198        }
199        return algorithmB(left, right)[rightSz];
200    }
201
202    /**
203     * Computes the longest common subsequence between the two {@code CharSequence}'s passed as input.
204     *
205     * <p>
206     * Note, a substring and subsequence are not necessarily the same thing. Indeed, {@code abcxyzqrs} and
207     * {@code xyzghfm} have both the same common substring and subsequence, namely {@code xyz}. However,
208     * {@code axbyczqrs} and {@code abcxyzqtv} have the longest common subsequence {@code xyzq} because a
209     * subsequence need not have adjacent characters.
210     * </p>
211     *
212     * <p>
213     * For reference, we give the definition of a subsequence for the reader: a <i>subsequence</i> is a sequence that
214     * can be derived from another sequence by deleting some elements without changing the order of the remaining
215     * elements.
216     * </p>
217     *
218     * @param left first character sequence
219     * @param right second character sequence
220     * @return the longest common subsequence found
221     * @throws IllegalArgumentException if either String input {@code null}
222     * @deprecated Deprecated as of 1.2 due to a typo in the method name.
223     * Use {@link #longestCommonSubsequence(CharSequence, CharSequence)} instead.
224     * This method will be removed in 2.0.
225     */
226    @Deprecated
227    public CharSequence logestCommonSubsequence(final CharSequence left, final CharSequence right) {
228        return longestCommonSubsequence(left, right);
229    }
230
231    /**
232     * Computes the longest common subsequence between the two {@code CharSequence}'s passed as
233     * input.
234     *
235     * <p>
236     * This method implements a more efficient version of LCS algorithm which although has quadratic time, it
237     * has linear space complexity.
238     * </p>
239     *
240     *
241     * <p>
242     * Note, a substring and subsequence are not necessarily the same thing. Indeed, {@code abcxyzqrs} and
243     * {@code xyzghfm} have both the same common substring and subsequence, namely {@code xyz}. However,
244     * {@code axbyczqrs} and {@code abcxyzqtv} have the longest common subsequence {@code xyzq} because a
245     * subsequence need not have adjacent characters.
246     * </p>
247     *
248     * <p>
249     * For reference, we give the definition of a subsequence for the reader: a <i>subsequence</i> is a sequence that
250     * can be derived from another sequence by deleting some elements without changing the order of the remaining
251     * elements.
252     * </p>
253     *
254     * @param left first character sequence
255     * @param right second character sequence
256     * @return the longest common subsequence found
257     * @throws IllegalArgumentException if either String input {@code null}
258     * @since 1.2
259     */
260    public CharSequence longestCommonSubsequence(final CharSequence left, final CharSequence right) {
261        // Quick return
262        if (left == null || right == null) {
263            throw new IllegalArgumentException("Inputs must not be null");
264        }
265        // Find lengths of two strings
266        final int leftSz = left.length();
267        final int rightSz = right.length();
268
269        // Check if we can avoid calling algorithmC which involves heap space allocation
270        if (leftSz == 0 || rightSz == 0) {
271            return "";
272        }
273
274        // Check if we can save even more space
275        if (leftSz < rightSz) {
276            return algorithmC(right, left);
277        }
278        return algorithmC(left, right);
279    }
280
281    /**
282     * Computes the lcsLengthArray for the sake of doing the actual lcs calculation. This is the
283     * dynamic programming portion of the algorithm, and is the reason for the runtime complexity being
284     * O(m*n), where m=left.length() and n=right.length().
285     *
286     * @param left first character sequence
287     * @param right second character sequence
288     * @return lcsLengthArray
289     * @deprecated Deprecated as of 1.10. A more efficient implementation for calculating LCS is now available.
290     * Use {@link #longestCommonSubsequence(CharSequence, CharSequence)} instead to directly calculate the LCS.
291     * This method will be removed in 2.0.
292     */
293    @Deprecated
294    public int[][] longestCommonSubstringLengthArray(final CharSequence left, final CharSequence right) {
295        final int[][] lcsLengthArray = new int[left.length() + 1][right.length() + 1];
296        for (int i = 0; i < left.length(); i++) {
297            for (int j = 0; j < right.length(); j++) {
298                if (i == 0) {
299                    lcsLengthArray[i][j] = 0;
300                }
301                if (j == 0) {
302                    lcsLengthArray[i][j] = 0;
303                }
304                if (left.charAt(i) == right.charAt(j)) {
305                    lcsLengthArray[i + 1][j + 1] = lcsLengthArray[i][j] + 1;
306                } else {
307                    lcsLengthArray[i + 1][j + 1] = Math.max(lcsLengthArray[i + 1][j], lcsLengthArray[i][j + 1]);
308                }
309            }
310        }
311        return lcsLengthArray;
312    }
313
314}