Given an array of positive integers target
and an array initial
of same size with all zeros.
Return the minimum number of operations to form a target
array from initial
if you are allowed to do the following operation:
- Choose any subarray from
initial
and increment each value by one.
The answer is guaranteed to fit within the range of a 32-bit signed integer.
Example 1:
Input: target = [1,2,3,2,1]
Output: 3
Explanation: We need at least 3 operations to form the target array from the initial array.
[0,0,0,0,0] increment 1 from index 0 to 4 (inclusive).
[1,1,1,1,1] increment 1 from index 1 to 3 (inclusive).
[1,2,2,2,1] increment 1 at index 2.
[1,2,3,2,1] target array is formed.
Example 2:
Input: target = [3,1,1,2]
Output: 4
Explanation: (initial)[0,0,0,0] -> [1,1,1,1] -> [1,1,1,2] -> [2,1,1,2] -> [3,1,1,2] (target).
Example 3:
Input: target = [3,1,5,4,2]
Output: 7
Explanation: (initial)[0,0,0,0,0] -> [1,1,1,1,1] -> [2,1,1,1,1] -> [3,1,1,1,1]
-> [3,1,2,2,2] -> [3,1,3,3,2] -> [3,1,4,4,2] -> [3,1,5,4,2] (target).
Example 4:
Input: target = [1,1,1,1]
Output: 1
Constraints:
1 <= target.length <= 10^5
1 <= target[i] <= 10^5
Solution 1. O(N^2) TLE
We'll start to choose the entire array and increase each by 1 until we reach some minimum values. At this point, we can not do operations on them anymore. These minimum values essentially split the original problem into multiple smaller sub-problems. So we can solve this problem recursively.
1. start from base value 0, and the entire array range, find minV and add minV - base operations.
2. recursively solve each smaller subproblems with new base value minV.
The following implementation does a linear scan to find all minV. This makes the overall runtime to be O(N^2).
class Solution { public int minNumberOperations(int[] target) { return helper(target, 0, target.length - 1, 0); } private int helper(int[] target, int left, int right, int base) { if(left > right) { return 0; } int op = 0; int min = Integer.MAX_VALUE; List<Integer> minIdx = new ArrayList<>(); for(int i = left; i <= right; i++) { if(target[i] < min) { min = target[i]; minIdx = new ArrayList<>(); minIdx.add(i); } else if(target[i] == min) { minIdx.add(i); } } op += (min - base); op += helper(target, left, minIdx.get(0) - 1, min); for(int i = 1; i < minIdx.size(); i++) { op += helper(target, minIdx.get(i - 1) + 1, minIdx.get(i) - 1, min); } op += helper(target, minIdx.get(minIdx.size() - 1) + 1, right, min); return op; } }
Solution 2. O(N * logN) with segment tree
The bottleneck in solution 1 is that it takes O(N) time to find a minimum value in a given range. We can use segment tree to acheive O(logN) minimum range query at the cost of O(N) preprocessing the target array into a segment tree.
Q: But segment tree range minimum query only returns a minimum value. We need to find all the indices of such minimum value within a subproblem's range in order to recur on smaller subproblems. How do we achieve this without using linear scan?
A: We can preprocess the target array to save each unique value's indices in sorted lists. The in each subproblem with range [left, right], we first do a binary search to find the smallest minimum value index L such that L >= left; Then do another binary search to find the biggest minimum value index R such that R <= right. Now we have the valid index range for splitting into smaller subproblems.
Each subproblem is independent with other subproblems and only solved once. So we have O(N) subproblems. Each subproblem takes O(log N) to find a range minimum and do 2 binary search. So it takes O(N * logN) time. Preprocessing of segment tree and index mapping takes O(N) time.
class Solution { private Map<Integer, List<Integer>> idxMap = new HashMap<>(); private SegmentTree st = null; public int minNumberOperations(int[] target) { for(int i = 0; i < target.length; i++) { idxMap.computeIfAbsent(target[i], k -> new ArrayList<>()).add(i); } st = new SegmentTree(target); return helper(0, target.length - 1, 0); } private int helper(int left, int right, int base) { if(left > right) { return 0; } int op = 0; int min = st.getMinimumInRange(left, right); op += (min - base); List<Integer> minIdx = idxMap.get(min); int l = bs1(minIdx, left); int r = bs2(minIdx, right); if(l >= 0 && r >= 0) { op += helper(left, minIdx.get(l) - 1, min); for(int i = l + 1; i <= r; i++) { op += helper(minIdx.get(i - 1) + 1, minIdx.get(i) - 1, min); } op += helper(minIdx.get(r) + 1, right, min); } return op; } private int bs1(List<Integer> list, int t) { int l = 0, r = list.size() - 1; while(l < r - 1) { int mid = l + (r - l) / 2; if(list.get(mid) < t) { l = mid + 1; } else { r = mid; } } if(list.get(l) >= t) { return l; } else if(list.get(r) >= t) { return r; } return -1; } private int bs2(List<Integer> list, int t) { int l = 0, r = list.size() - 1; while(l < r - 1) { int mid = l + (r - l) / 2; if(list.get(mid) > t) { r = mid - 1; } else { l = mid; } } if(list.get(r) <= t) { return r; } else if(list.get(l) <= t) { return l; } return -1; } } class SegmentTree { private int[] st; private int leafNodeNum; private int expandedSize; private int height; public SegmentTree(int[] nums) { leafNodeNum = nums.length; height = (int)(Math.ceil(Math.log(nums.length) / Math.log(2))); expandedSize = (int)Math.pow(2, height); int max_size = expandedSize * 2 - 1; st = new int[max_size]; for(int i = 0; i < expandedSize; i++) { st[expandedSize - 1 + i] = i >= leafNodeNum ? Integer.MAX_VALUE : nums[i]; } for(int i = expandedSize - 2; i >= 0; i--) { st[i] = Math.min(st[i * 2 + 1], st[i * 2 + 2]); } } /* @params: [left, right] is a range in the original input array nums */ public int getMinimumInRange(int left, int right) { // convert range of the original array to segment tree range left += (expandedSize - 1); right += (expandedSize - 1); int min = Integer.MAX_VALUE; while(left <= right) { if(left % 2 == 0) { min = Math.min(min, st[left]); left++; } if(right % 2 != 0) { min = Math.min(min, st[right]); right--; } left = (left - 1) / 2; right = (right - 2) /2; } return min; } }
Solution 3. O(N) solution that is unbelivably simple!
Well, it turns out the optimal solution is very very simple. The key observation is that for two adjacent values in target array prev and curr, we have the following 2 cases.
1. prev >= curr, this means to reach prev, we've already past curr, so when we add the operations needed for prev, we've already taken the operations needed for curr into account;
2. prev < curr, it takes at least curr - prev to go from prev to curr, so when visiting curr, we need to add curr - prev operations.
Basically, if we are seeing an increasing sequence, we need to add the adjacent difference; otherwise previous operations already take care of all the <= values in a non-increasing sequence.
class Solution { public int minNumberOperations(int[] target) { int op = 0, prev = 0; for(int v : target) { op += Math.max(v - prev, 0); prev = v; } return op; } }