Skip to content

Commit 616d15a

Browse files
authored
Shortest coprime segment using sliding window technique (#6296)
* Shortest coprime segment using sliding window technique * mvn checkstyle passes * gcd function reformatted * fixed typo in ShortestCoprimeSegment * 1. shortestCoprimeSegment now returns not the length, but the shortest segment itself. 2. Testcases have been adapted, a few new ones added. * clang formatted ShortestCoprimeSegmentTest.java code
1 parent a21abe6 commit 616d15a

File tree

2 files changed

+200
-0
lines changed

2 files changed

+200
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.thealgorithms.slidingwindow;
2+
3+
import java.util.Arrays;
4+
import java.util.LinkedList;
5+
6+
/**
7+
* The Sliding Window technique together with 2-stack technique is used to find coprime segment of minimal size in an array.
8+
* Segment a[i],...,a[i+l] is coprime if gcd(a[i], a[i+1], ..., a[i+l]) = 1
9+
* <p>
10+
* Run-time complexity: O(n log n)
11+
* 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.
12+
* For 'remove' worst-case would be O(n) operation, but this happens rarely.
13+
* Main observation is that each element gets processed a constant amount of times, hence complexity will be:
14+
* O(n log n), where log n comes from complexity of gcd.
15+
* <p>
16+
* 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.
17+
* 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]).
18+
* 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).
19+
* <p>
20+
* One can change methods 'legalSegment' and function 'f' in DoubleStack to adapt this code to other sliding-window type problems.
21+
* I recommend this article for more explanations: "<a href="https://codeforces.com/edu/course/2/lesson/9/2">CF Article</a>">Article 1</a> or <a href="https://usaco.guide/gold/sliding-window?lang=cpp#method-2---two-stacks">USACO Article</a>
22+
* <p>
23+
* 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)
24+
*
25+
* @author DomTr (<a href="https://github.com/DomTr">Github</a>)
26+
*/
27+
public final class ShortestCoprimeSegment {
28+
// Prevent instantiation
29+
private ShortestCoprimeSegment() {
30+
}
31+
32+
/**
33+
* @param arr is the input array
34+
* @return shortest segment in the array which has gcd equal to 1. If no such segment exists or array is empty, returns empty array
35+
*/
36+
public static long[] shortestCoprimeSegment(long[] arr) {
37+
if (arr == null || arr.length == 0) {
38+
return new long[] {};
39+
}
40+
DoubleStack front = new DoubleStack();
41+
DoubleStack back = new DoubleStack();
42+
int n = arr.length;
43+
int l = 0;
44+
int shortestLength = n + 1;
45+
int beginsAt = -1; // beginning index of the shortest coprime segment
46+
for (int i = 0; i < n; i++) {
47+
back.push(arr[i]);
48+
while (legalSegment(front, back)) {
49+
remove(front, back);
50+
if (shortestLength > i - l + 1) {
51+
beginsAt = l;
52+
shortestLength = i - l + 1;
53+
}
54+
l++;
55+
}
56+
}
57+
if (shortestLength > n) {
58+
shortestLength = -1;
59+
}
60+
if (shortestLength == -1) {
61+
return new long[] {};
62+
}
63+
return Arrays.copyOfRange(arr, beginsAt, beginsAt + shortestLength);
64+
}
65+
66+
private static boolean legalSegment(DoubleStack front, DoubleStack back) {
67+
return gcd(front.top(), back.top()) == 1;
68+
}
69+
70+
private static long gcd(long a, long b) {
71+
if (a < b) {
72+
return gcd(b, a);
73+
} else if (b == 0) {
74+
return a;
75+
} else {
76+
return gcd(a % b, b);
77+
}
78+
}
79+
80+
/**
81+
* This solves the problem of removing elements quickly.
82+
* Even though the worst case of 'remove' method is O(n), it is a very pessimistic view.
83+
* We will need to empty out 'back', only when 'from' is empty.
84+
* Consider element x when it is added to stack 'back'.
85+
* 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'.
86+
* In other words, every element gets processed by a constant number of operations.
87+
* So 'remove' amortized runtime is actually O(n).
88+
*/
89+
private static void remove(DoubleStack front, DoubleStack back) {
90+
if (front.isEmpty()) {
91+
while (!back.isEmpty()) {
92+
front.push(back.pop());
93+
}
94+
}
95+
front.pop();
96+
}
97+
98+
/**
99+
* 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.
100+
*/
101+
private static class DoubleStack {
102+
LinkedList<Long> stack;
103+
LinkedList<Long> values;
104+
105+
DoubleStack() {
106+
values = new LinkedList<>();
107+
stack = new LinkedList<>();
108+
values.add(0L); // Initialise with 0 which is neutral element in terms of gcd, i.e. gcd(a,0) = a
109+
}
110+
111+
long f(long a, long b) { // Can be replaced with other function
112+
return gcd(a, b);
113+
}
114+
115+
public void push(long x) {
116+
stack.addLast(x);
117+
values.addLast(f(values.getLast(), x));
118+
}
119+
120+
public long top() {
121+
return values.getLast();
122+
}
123+
124+
public long pop() {
125+
long res = stack.getLast();
126+
stack.removeLast();
127+
values.removeLast();
128+
return res;
129+
}
130+
131+
public boolean isEmpty() {
132+
return stack.isEmpty();
133+
}
134+
}
135+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.thealgorithms.slidingwindow;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
5+
import java.util.Arrays;
6+
import org.junit.jupiter.api.Test;
7+
8+
/**
9+
* Unit tests for ShortestCoprimeSegment algorithm
10+
*
11+
* @author DomTr (<a href="https://github.com/DomTr">...</a>)
12+
*/
13+
public class ShortestCoprimeSegmentTest {
14+
@Test
15+
public void testShortestCoprimeSegment() {
16+
assertArrayEquals(new long[] {4, 6, 9}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {4, 6, 9, 3, 6}));
17+
assertArrayEquals(new long[] {4, 5}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {4, 5, 9, 3, 6}));
18+
assertArrayEquals(new long[] {3, 2}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {3, 2}));
19+
assertArrayEquals(new long[] {9, 10}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {3, 9, 9, 9, 10}));
20+
21+
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};
22+
long[] answer5 = Arrays.copyOfRange(test5, 0, test5.length - 1);
23+
assertArrayEquals(answer5, ShortestCoprimeSegment.shortestCoprimeSegment(test5));
24+
25+
// Test suite, when the entire array needs to be taken
26+
long[] test6 = new long[] {3 * 7, 7 * 5, 5 * 7 * 3, 3 * 5};
27+
assertArrayEquals(test6, ShortestCoprimeSegment.shortestCoprimeSegment(test6));
28+
29+
long[] test7 = new long[] {3 * 11, 11 * 7, 11 * 7 * 3, 3 * 7};
30+
assertArrayEquals(test7, ShortestCoprimeSegment.shortestCoprimeSegment(test7));
31+
32+
long[] test8 = new long[] {3 * 11, 11 * 7, 11 * 7 * 3, 11 * 7 * 3 * 5, 5 * 7};
33+
assertArrayEquals(test8, ShortestCoprimeSegment.shortestCoprimeSegment(test8));
34+
35+
long[] test9 = new long[] {3 * 11, 11 * 7, 11 * 7 * 3, 11 * 7 * 3 * 5, 11 * 7 * 3 * 5 * 13, 7 * 13};
36+
assertArrayEquals(test9, ShortestCoprimeSegment.shortestCoprimeSegment(test9));
37+
38+
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};
39+
assertArrayEquals(test10, ShortestCoprimeSegment.shortestCoprimeSegment(test10));
40+
41+
// Segment can consist of one element
42+
long[] test11 = new long[] {1};
43+
assertArrayEquals(test11, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {4, 6, 1, 3, 6}));
44+
long[] test12 = new long[] {1};
45+
assertArrayEquals(test12, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {1}));
46+
}
47+
@Test
48+
public void testShortestCoprimeSegment2() {
49+
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}));
50+
assertArrayEquals(new long[] {5 * 7, 2}, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {2 * 3, 2 * 3 * 5, 2 * 3 * 5 * 7, 5 * 7, 2}));
51+
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}));
52+
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}));
53+
}
54+
@Test
55+
public void testNoCoprimeSegment() {
56+
// There may not be a coprime segment
57+
long[] empty = new long[] {};
58+
assertArrayEquals(empty, ShortestCoprimeSegment.shortestCoprimeSegment(null));
59+
assertArrayEquals(empty, ShortestCoprimeSegment.shortestCoprimeSegment(empty));
60+
assertArrayEquals(empty, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {4, 6, 8, 12, 8}));
61+
assertArrayEquals(empty, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {4, 4, 4, 4, 10, 4, 6, 8, 12, 8}));
62+
assertArrayEquals(empty, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {100}));
63+
assertArrayEquals(empty, ShortestCoprimeSegment.shortestCoprimeSegment(new long[] {2, 2, 2}));
64+
}
65+
}

0 commit comments

Comments
 (0)