diff --git a/src/main/java/com/thealgorithms/slidingwindow/ShortestCoprimeSegment.java b/src/main/java/com/thealgorithms/slidingwindow/ShortestCoprimeSegment.java new file mode 100644 index 000000000000..b99f7ca7d62f --- /dev/null +++ b/src/main/java/com/thealgorithms/slidingwindow/ShortestCoprimeSegment.java @@ -0,0 +1,135 @@ +package com.thealgorithms.slidingwindow; + +import java.util.Arrays; +import java.util.LinkedList; + +/** + * The Sliding Window technique together with 2-stack technique is used to find coprime segment of minimal size in an array. + * Segment a[i],...,a[i+l] is coprime if gcd(a[i], a[i+1], ..., a[i+l]) = 1 + *

+ * Run-time complexity: O(n log n) + * What is special about this 2-stack technique is that it enables us to remove element a[i] and find gcd(a[i+1],...,a[i+l]) in amortized O(1) time. + * For 'remove' worst-case would be O(n) operation, but this happens rarely. + * Main observation is that each element gets processed a constant amount of times, hence complexity will be: + * O(n log n), where log n comes from complexity of gcd. + *

+ * More generally, the 2-stack technique enables us to 'remove' an element fast if it is known how to 'add' an element fast to the set. + * In our case 'adding' is calculating d' = gcd(a[i],...,a[i+l+1]), when d = gcd(a[i],...a[i]) with d' = gcd(d, a[i+l+1]). + * and removing is find gcd(a[i+1],...,a[i+l]). We don't calculate it explicitly, but it is pushed in the stack which we can pop in O(1). + *

+ * One can change methods 'legalSegment' and function 'f' in DoubleStack to adapt this code to other sliding-window type problems. + * I recommend this article for more explanations: "CF Article">Article 1 or USACO Article + *

+ * Another method to solve this problem is through segment trees. Then query operation would have O(log n), not O(1) time, but runtime complexity would still be O(n log n) + * + * @author DomTr (Github) + */ +public final class ShortestCoprimeSegment { + // Prevent instantiation + private ShortestCoprimeSegment() { + } + + /** + * @param arr is the input array + * @return shortest segment in the array which has gcd equal to 1. If no such segment exists or array is empty, returns empty array + */ + public static long[] shortestCoprimeSegment(long[] arr) { + if (arr == null || arr.length == 0) { + return new long[] {}; + } + DoubleStack front = new DoubleStack(); + DoubleStack back = new DoubleStack(); + int n = arr.length; + int l = 0; + int shortestLength = n + 1; + int beginsAt = -1; // beginning index of the shortest coprime segment + for (int i = 0; i < n; i++) { + back.push(arr[i]); + while (legalSegment(front, back)) { + remove(front, back); + if (shortestLength > i - l + 1) { + beginsAt = l; + shortestLength = i - l + 1; + } + l++; + } + } + if (shortestLength > n) { + shortestLength = -1; + } + if (shortestLength == -1) { + return new long[] {}; + } + return Arrays.copyOfRange(arr, beginsAt, beginsAt + shortestLength); + } + + private static boolean legalSegment(DoubleStack front, DoubleStack back) { + return gcd(front.top(), back.top()) == 1; + } + + private static long gcd(long a, long b) { + if (a < b) { + return gcd(b, a); + } else if (b == 0) { + return a; + } else { + return gcd(a % b, b); + } + } + + /** + * This solves the problem of removing elements quickly. + * Even though the worst case of 'remove' method is O(n), it is a very pessimistic view. + * We will need to empty out 'back', only when 'from' is empty. + * Consider element x when it is added to stack 'back'. + * After some time 'front' becomes empty and x goes to 'front'. Notice that in the for-loop we proceed further and x will never come back to any stacks 'back' or 'front'. + * In other words, every element gets processed by a constant number of operations. + * So 'remove' amortized runtime is actually O(n). + */ + private static void remove(DoubleStack front, DoubleStack back) { + if (front.isEmpty()) { + while (!back.isEmpty()) { + front.push(back.pop()); + } + } + front.pop(); + } + + /** + * DoubleStack serves as a collection of two stacks. One is a normal stack called 'stack', the other 'values' stores gcd-s up until some index. + */ + private static class DoubleStack { + LinkedList stack; + LinkedList values; + + DoubleStack() { + values = new LinkedList<>(); + stack = new LinkedList<>(); + values.add(0L); // Initialise with 0 which is neutral element in terms of gcd, i.e. gcd(a,0) = a + } + + long f(long a, long b) { // Can be replaced with other function + return gcd(a, b); + } + + public void push(long x) { + stack.addLast(x); + values.addLast(f(values.getLast(), x)); + } + + public long top() { + return values.getLast(); + } + + public long pop() { + long res = stack.getLast(); + stack.removeLast(); + values.removeLast(); + return res; + } + + public boolean isEmpty() { + return stack.isEmpty(); + } + } +} diff --git a/src/test/java/com/thealgorithms/slidingwindow/ShortestCoprimeSegmentTest.java b/src/test/java/com/thealgorithms/slidingwindow/ShortestCoprimeSegmentTest.java new file mode 100644 index 000000000000..acb9e1e30ac7 --- /dev/null +++ b/src/test/java/com/thealgorithms/slidingwindow/ShortestCoprimeSegmentTest.java @@ -0,0 +1,65 @@ +package com.thealgorithms.slidingwindow; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for ShortestCoprimeSegment algorithm + * + * @author DomTr (...) + */ +public class ShortestCoprimeSegmentTest { + @Test + public void testShortestCoprimeSegment() { + assertArrayEquals(new long[] {4, 6, 9}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {4, 6, 9, 3, 6})); + assertArrayEquals(new long[] {4, 5}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {4, 5, 9, 3, 6})); + assertArrayEquals(new long[] {3, 2}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {3, 2})); + assertArrayEquals(new long[] {9, 10}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {3, 9, 9, 9, 10})); + + long[] test5 = new long[] {3 * 11, 11 * 7, 11 * 7 * 3, 11 * 7 * 3 * 5, 11 * 7 * 3 * 5 * 13, 7 * 13, 11 * 7 * 3 * 5 * 13}; + long[] answer5 = Arrays.copyOfRange(test5, 0, test5.length - 1); + assertArrayEquals(answer5, ShortestCoprimeSegment.shortestCoprimeSegment(test5)); + + // Test suite, when the entire array needs to be taken + long[] test6 = new long[] {3 * 7, 7 * 5, 5 * 7 * 3, 3 * 5}; + assertArrayEquals(test6, ShortestCoprimeSegment.shortestCoprimeSegment(test6)); + + long[] test7 = new long[] {3 * 11, 11 * 7, 11 * 7 * 3, 3 * 7}; + assertArrayEquals(test7, ShortestCoprimeSegment.shortestCoprimeSegment(test7)); + + long[] test8 = new long[] {3 * 11, 11 * 7, 11 * 7 * 3, 11 * 7 * 3 * 5, 5 * 7}; + assertArrayEquals(test8, ShortestCoprimeSegment.shortestCoprimeSegment(test8)); + + long[] test9 = new long[] {3 * 11, 11 * 7, 11 * 7 * 3, 11 * 7 * 3 * 5, 11 * 7 * 3 * 5 * 13, 7 * 13}; + assertArrayEquals(test9, ShortestCoprimeSegment.shortestCoprimeSegment(test9)); + + long[] test10 = new long[] {3 * 11, 7 * 11, 3 * 7 * 11, 3 * 5 * 7 * 11, 3 * 5 * 7 * 11 * 13, 2 * 3 * 5 * 7 * 11 * 13, 2 * 3 * 5 * 7 * 11 * 13 * 17, 2 * 3 * 5 * 7 * 11 * 13 * 17 * 19, 2 * 3 * 5 * 7 * 11 * 13 * 17 * 19 * 23, 7 * 13}; + assertArrayEquals(test10, ShortestCoprimeSegment.shortestCoprimeSegment(test10)); + + // Segment can consist of one element + long[] test11 = new long[] {1}; + assertArrayEquals(test11, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {4, 6, 1, 3, 6})); + long[] test12 = new long[] {1}; + assertArrayEquals(test12, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {1})); + } + @Test + public void testShortestCoprimeSegment2() { + assertArrayEquals(new long[] {2 * 3, 2 * 3 * 5, 2 * 3 * 5 * 7, 5 * 7}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {2 * 3, 2 * 3 * 5, 2 * 3 * 5 * 7, 5 * 7, 2 * 3 * 5 * 7})); + assertArrayEquals(new long[] {5 * 7, 2}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {2 * 3, 2 * 3 * 5, 2 * 3 * 5 * 7, 5 * 7, 2})); + assertArrayEquals(new long[] {5 * 7, 2 * 5 * 7, 2 * 11}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {2 * 3, 2 * 3 * 5, 2 * 3 * 5 * 7, 5 * 7, 2 * 5 * 7, 2 * 11})); + assertArrayEquals(new long[] {3 * 5 * 7, 2 * 3, 2}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {2, 2 * 3, 2 * 3 * 5, 3 * 5 * 7, 2 * 3, 2})); + } + @Test + public void testNoCoprimeSegment() { + // There may not be a coprime segment + long[] empty = new long[] {}; + assertArrayEquals(empty, ShortestCoprimeSegment.shortestCoprimeSegment(null)); + assertArrayEquals(empty, ShortestCoprimeSegment.shortestCoprimeSegment(empty)); + assertArrayEquals(empty, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {4, 6, 8, 12, 8})); + assertArrayEquals(empty, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {4, 4, 4, 4, 10, 4, 6, 8, 12, 8})); + assertArrayEquals(empty, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {100})); + assertArrayEquals(empty, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {2, 2, 2})); + } +}