Given an array arr
that represents a permutation of numbers from 1
to n
. You have a binary string of size n
that initially has all its bits set to zero.
At each step i
(assuming both the binary string and arr
are 1-indexed) from 1
to n
, the bit at position arr[i]
is set to 1
. You are given an integer m
and you need to find the latest step at which there exists a group of ones of length m
. A group of ones is a contiguous substring of 1s such that it cannot be extended in either direction.
Return the latest step at which there exists a group of ones of length exactly m
. If no such group exists, return -1
.
Example 1:
Input: arr = [3,5,1,2,4], m = 1
Output: 4
Explanation:
Step 1: "00100", groups: ["1"]
Step 2: "00101", groups: ["1", "1"]
Step 3: "10101", groups: ["1", "1", "1"]
Step 4: "11101", groups: ["111", "1"]
Step 5: "11111", groups: ["11111"]
The latest step at which there exists a group of size 1 is step 4.
Example 2:
Input: arr = [3,1,5,4,2], m = 2
Output: -1
Explanation:
Step 1: "00100", groups: ["1"]
Step 2: "10100", groups: ["1", "1"]
Step 3: "10101", groups: ["1", "1", "1"]
Step 4: "10111", groups: ["1", "111"]
Step 5: "11111", groups: ["11111"]
No group of size 2 exists during any step.
Example 3:
Input: arr = [1], m = 1
Output: 1
Example 4:
Input: arr = [2,1], m = 2
Output: 2
Constraints:
n == arr.length
1 <= n <= 10^5
1 <= arr[i] <= n
- All integers in
arr
are distinct. 1 <= m <= arr.length
The key to solve this problem is how to efficiently keep and update the current groups of 1 dynamically. Notice that we can use an interval [start, end] to represent a group as there will be no over-lapping intervals. If we can find an update an interval in O(1) or O(logN) time, we can solve this problem within the time limit.
Solution 1. TreeMap + HashMap
During contest, I came up with this solution: using a tree map to maintain and update intervals dynamically. A map entry's key is the start index and value is the end index. Then I maintain another map to track the count of all different lengths of groups. This way, if I loop the input array forward and do the following I'll get the right answer:
1. for a given position p, find the interval prev that is right before p and the interval next that is right after p.
2. if prev exists and its end index is adjacent with p, connect prev with p; if next exists and its start index is adjacent with p, connect next with p.
3. insert the new interval and update cnt map.
4. if cnt of length m > 0, update answer.
The runtime is O(N * logN), space is O(N).
class Solution { public int findLatestStep(int[] arr, int m) { TreeMap<Integer, Integer> tm = new TreeMap<>(); int[] cnt = new int[arr.length + 1]; int ans = -1; for(int i = 0; i < arr.length; i++) { Map.Entry<Integer, Integer> prev = tm.lowerEntry(arr[i]); Map.Entry<Integer, Integer> next = tm.higherEntry(arr[i]); int start = arr[i], end = arr[i]; if(prev != null && prev.getValue() + 1 == arr[i]) { start = prev.getKey(); int len = prev.getValue() - prev.getKey() + 1; cnt[len]--; tm.remove(prev.getKey()); } if(next != null && next.getKey() - 1 == arr[i]) { end = next.getValue(); int len = next.getValue() - next.getKey() + 1; cnt[len]--; tm.remove(next.getKey()); } cnt[end - start + 1]++; tm.put(start, end); if(cnt[m] > 0) { ans = i + 1; } } return ans; } }
Alternatively, since we are looking for the latest step that generates group of size m, we can loop backward using the same idea above. This way, we do not need to maintain cnt map. As soon as we find a group of length m, we return that step. At each step, we try to split a bigger length group into two smaller length groups. If at least one of these two smaller groups has length m, it means that right after the previous step, we had length m groups! But all steps afterwards provides no such groups, hence the previous step is the correct answer.
class Solution { public int findLatestStep(int[] arr, int m) { if(m == arr.length) { return m; } TreeMap<Integer, Integer> tm = new TreeMap<>(); tm.put(1, arr.length); int ans = -1; for(int i = arr.length - 1; i >= 0; i--) { Map.Entry<Integer, Integer> curr = tm.floorEntry(arr[i]); int ll = curr.getKey(), lr = arr[i] - 1; int rl = arr[i] + 1, rr = curr.getValue(); if(lr - ll + 1 == m || rr - rl + 1 == m) { ans = i; break; } tm.remove(ll); if(ll <= lr) { tm.put(ll, lr); } if(rl <= rr) { tm.put(rl, rr); } } return ans; } }
Solution 2. O(1) update on intervals using only array
For any position p that we are about to change from 0 to 1, we only care if p - 1 has 1 and p + 1 has 1, meaning we only care if the previous interval's end index and the next interval's start index. And because there are no over-lapping intervals at any time, so we never care about what happens inside an interval as long as we correctly update the boundaries of each interval every time.
So we can just maintain an array called intervals[i] to achieve the same goal with solution 1. Each entry represents the length of a group that starts at or ends at position i. Looping forward for each position p and do the following:
1. get the length of the group that ends at p - 1 and that starts at p + 1.
2. if the previous group that ends at p - 1 already has length m, then the current step is going to make it longer. Same for the next group that ends at p + 1.
3. connect previous with the current 1 at p then with next. if the new group has length m, update counter. Then update this new interval's start position p - prev's length and end position p + next's length to have the new length.
4. if after the current step we still have at least 1 group of length m, update answer.
Why does overwriting intervals[i]'s start and end index value work? It works because we do not have overlapping intervals. And after creating a new interval [left, right], we'll never have to process positions inside [left, right] again. Only positions that are before left or after right will show up. Every positions inside have been processed before we create this new interval. For positions that are before left, we only need the updated next interval length; For positions that are after right, we only need the updated previous interval length.
Both the runtime and space is O(N)
class Solution { public int findLatestStep(int[] arr, int m) { int n = arr.length, cnt = 0, ans = -1; int[] intervals = new int[n + 2]; for(int i = 0; i < n; i++) { int prev = intervals[arr[i] - 1]; int next = intervals[arr[i] + 1]; int len = prev + 1 + next; if(prev == m) { cnt--; } if(next == m) { cnt--; } if(len == m) { cnt++; } intervals[arr[i] - prev] = len; intervals[arr[i] + next] = len; if(cnt > 0) { ans = i + 1; } } return ans; } }
UnionFind solution practice.
There were some contestants that solved this problem using UnionFind, which is good practice.
The idea is simliar with the above 2 solutions and pretty simple: for each position p, check if it can connect with the component that includes position p - 1, then check if it can connect with the component that includes position p + 1. If it can connect with either, do it and update the cnt map for group lengths accordingly.
class Solution { class UnionFind { int[] root; int[] sz; UnionFind(int n) { root = new int[n]; sz = new int[n]; for(int i = 0; i < n; i++) { root[i] = i; sz[i] = 1; } } int findRoot(int p) { if(root[p] != p) { root[p] = findRoot(root[p]); } return root[p]; } void union(int p, int q) { int rp = findRoot(p); int rq = findRoot(q); if(rp != rq) { if(sz[rp] >= sz[rq]) { root[rq] = rp; sz[rp] += sz[rq]; } else { root[rp] = rq; sz[rq] += sz[rp]; } } } } public int findLatestStep(int[] arr, int m) { int n = arr.length, ans = -1; UnionFind uf = new UnionFind(n + 1); boolean[] set = new boolean[n + 1]; int[] cnt = new int[n + 1]; for(int i = 0; i < n; i++) { if(arr[i] > 0 && set[arr[i] - 1]) { int prevRoot = uf.findRoot(arr[i] - 1); cnt[uf.sz[prevRoot]]--; uf.union(arr[i], prevRoot); } if(arr[i] < n && set[arr[i] + 1]) { int nextRoot = uf.findRoot(arr[i] + 1); cnt[uf.sz[nextRoot]]--; uf.union(arr[i], nextRoot); } int currRoot = uf.findRoot(arr[i]); cnt[uf.sz[currRoot]]++; if(cnt[m] > 0) { ans = i + 1; } set[arr[i]] = true; } return ans; } }