[LeetCode 406] Queue Reconstruction by Height
Suppose you have a random list of people standing in a queue. Each person is described by a pair of integers (h, k)
, where h
is the height of the person and k
is the number of people in front of this person who have a height greater than or equal to h
. Write an algorithm to reconstruct the queue.
Note:
The number of people is less than 1,100.
Example
Input: [[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]] Output: [[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]
The input size is relatively small, so O(N^2) solution should solve this problem in a contest setting.
Solution 1. Insert each person to its correct position. There are 2 variations of this straightforward O(N^2) solution.
Variation 1: Sort all people by desceding height then ascending previous count. i.e, we insert bigger heights first as they count as smaller heights' previous >= heights. For people p1, p2 with the same height p1[0] == p2[0] and p1[1] < p2[1], we first insert p1 because it will be used as p2's previous count. p2[1] > p1[1] gurantees that p1's position is before p2's. Insert one by one to a linked list. All people in linked list have >= height than the current people.
class Solution { public int[][] reconstructQueue(int[][] p) { Arrays.sort(p, (p1,p2) -> { if(p1[0] != p2[0]) { return p2[0] - p1[0]; } return p1[1] - p2[1]; }); LinkedList<int[]> ansList = new LinkedList<>(); for(int[] pp : p) { ansList.add(pp[1], pp); } int[][] ans = new int[ansList.size()][2]; for(int i = 0; i < ansList.size(); i++) { ans[i] = ansList.get(i); } return ans; } }
Variation 2: We know that if we process all people in ascending height order, then after processing a certain height H, we can ignore all processed people when processing bigger height H' because H has no contribution to people whose heights are > H. So we can image that we have N empty slot to start with, and we try to fill in the remaining slots. For p[i], we try to insert it to the p[i][1]th empty slot(0-index based and ignoring occupied slots). For p1[0] == p2[0], if p1[1] < p2[1], then we insert bigger count p2 first. Why? because to p1 contributes to p2's count, so in order to get the correct empty slot for p2, p1's correct position must be unfilled. Only after filling p2 we fill p1.
One implementation pitfall here is that we can not specify the answer 2D array's second dimension size to 2. If we do so, the null check logic is always false because Java auto fills 0.
class Solution { public int[][] reconstructQueue(int[][] people) { Arrays.sort(people, (p1, p2) -> { if(p1[0] != p2[0]) { return p1[0] - p2[0]; } return p2[1] - p1[1]; }); int[][] ans = new int[people.length][]; for(int i = 0; i < people.length; i++) { int cnt = 0, j = 0; for(; j < ans.length; j++) { if(ans[j] == null) { if(cnt == people[i][1]) { break; } cnt++; } } ans[j] = people[i]; } return ans; } }
Now comes the money question, can we do better? If we allow the input size up to 10^5, our O(N^2) certainly will TLE. The bottleneck is that it takes O(N) time to insert a person to his right position. Let's try to improve on the variation 2 solution. We can try to use a static array call it A to track the empty slots count up till a certain position. A[i] is the number of empty slots in [0, i]. If we have this information, it takes O(1) time to check a position's preceeding empty slots. However, each time we insert a new person, we must update all the entries that are after this new person, this again takes O(N) time. To get both efficient look up and update operations, we should use a dynamic data structure like Binary Indexed Tree. A range sum in [0, r] of a BIT tells us how many slots in [0, r] have been filled, we can then use this info to determine the available empty slots. The next question is which position we should do look up on? At any point, the range sums is always in non-decreasing sorted order. This is true because each time we insert a new person to position p, for all r < p, the range sums stay the same; for all r >= p, the range sums get bigger. As a result, we can combine BIT and binary search technique to achieve O(log N * log N) time for processing a new person, which is significantly better than O(N), especially when N gets large.
With the above analysis, we derive the following algorithm.
1. Sort people: height ascending order, count descending order.
2. Create a BIT that supports on range sum query for the number of filled positions in given range [0, r].
3. For each new person, binary search on BIT using the following decision tree.
Denote people[i][1] + 1 as K. It is the correct empty slot for this person, need to + 1 because input is 0-indexed and BIT is 1-indexed.
a. if MID - RS[MID] == K and position K - 1 in the answer is empty, this means we have K empty slots in the answer range [0, MID - 1] and the last empty slot of these K slots is at MID - 1, which is exactly where this new person should be.
b. if MID - RS[MID] < K, this means we do not have enough empty slots on the left half of search space, keep searching on the right half.
c. otherwise, either MID - RS[MID] > K, we have too many empty slots on left half, search there; or MID - RS[MID] == K but MID - 1 is filled. This means the correct position is on the left half.
The runtime of this optimized solution is O(N * log N + N * log N * log N), with O(N) extra space.
class Solution { class BinaryIndexedTree { int[] ft; BinaryIndexedTree(int n) { ft = new int[n]; } int rangeSum(int r) { int sum = 0; for(; r > 0; r -= (r & (-r))) { sum += ft[r]; } return sum; } void update(int k, int v) { for(; k < ft.length; k += (k & (-k))) { ft[k] += v; } } } public int[][] reconstructQueue(int[][] people) { int n = people.length; Arrays.sort(people, (p1, p2) -> { if(p1[0] != p2[0]) { return p1[0] - p2[0]; } return p2[1] - p1[1]; }); BinaryIndexedTree bit = new BinaryIndexedTree(n + 1); int[][] ans = new int[n][]; for(int i = 0; i < n; i++) { int l = 1, r = n; while(l < r) { int mid = l + (r - l) / 2; int filled = bit.rangeSum(mid); if(mid - filled == people[i][1] + 1 && ans[mid - 1] == null) { break; } else if(mid - filled < people[i][1] + 1) { l = mid + 1; } else { r = mid - 1; } } ans[l + (r - l) / 2 - 1] = people[i]; bit.update(l + (r - l) / 2, 1); } return ans; } }