Skip to content

Commit 71dd8f7

Browse files
committed
Shortest coprime segment using sliding window technique
1 parent a21abe6 commit 71dd8f7

File tree

2 files changed

+156
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)