Count the Number of Good Partitions
HardPrompt
You are given a 0-indexed array nums consisting of positive integers.
A partition of an array into one or more contiguous subarrays is called good if no two subarrays contain the same number.
Return the total number of good partitions of nums.
Since the answer may be large, return it modulo 109 + 7.
Example 1:
Input: nums = [1,2,3,4]
Output: 8
Explanation: The 8 possible good partitions are: ([1], [2], [3], [4]), ([1], [2], [3,4]), ([1], [2,3], [4]), ([1], [2,3,4]), ([1,2], [3], [4]), ([1,2], [3,4]), ([1,2,3], [4]), and ([1,2,3,4]).Example 2:
Input: nums = [1,1,1,1]
Output: 1
Explanation: The only possible good partition is: ([1,1,1,1]).Example 3:
Input: nums = [1,2,1,3]
Output: 2
Explanation: The 2 possible good partitions are: ([1,2,1], [3]) and ([1,2,1,3]).
Constraints:
1 <= nums.length <= 1051 <= nums[i] <= 109
Approaches
2 approaches with complexity analysis and trade-offs.
A brute-force approach can be devised by considering every possible split point in the array. A partition is defined by a set of split points. A partition is 'good' if for any two of its subarrays, the sets of numbers they contain are disjoint. This property implies that any valid split point i must separate the array into two parts, nums[0...i] and nums[i+1...n-1], that have no numbers in common.
This approach iterates through all n-1 potential split points. For each point i, it explicitly constructs the set of numbers in the prefix and checks for any overlap with the numbers in the suffix. If c such valid split points are found, they define c+1 minimal blocks that cannot be internally divided. The problem then becomes counting the ways to partition these blocks. With c locations between blocks where a split can occur, and two choices at each (split or not split), the total number of good partitions is 2^c.
Algorithm
- Initialize a variable
split_pointsto 0. - Iterate through each possible split index
ifrom0ton-2, wherenis the length ofnums. - For each
i, check if a split is valid. A split is valid if the set of numbers innums[0...i]is disjoint from the set of numbers innums[i+1...n-1]. - To perform the check:
- Create a hash set for the prefix
nums[0...i]. - Iterate through the suffix
nums[i+1...n-1]and check if any of its elements are present in the prefix's hash set.
- Create a hash set for the prefix
- If the sets are disjoint, increment
split_points. - After checking all possible split indices, the total number of good partitions is
2^split_points. - Calculate this value modulo
10^9 + 7using modular exponentiation.
Walkthrough
This approach directly implements the definition of a valid split point. We iterate through every index i from 0 to n-2 and test if we can make a cut after nums[i]. To test this, we need to verify that no number appearing in nums[0...i] also appears in nums[i+1...n-1]. A straightforward way to do this is to use hash sets. For each i, we build a hash set of numbers in the prefix nums[0...i]. Then, we iterate through the suffix nums[i+1...n-1] and check if any of its elements exist in our hash set. If we find no common elements, we've identified a valid split point and we increment a counter. After checking all n-1 potential points, if we have found c valid split points, the total number of good partitions will be 2^c.
import java.util.HashSet;import java.util.Set; class Solution { public int countGoodPartitions(int[] nums) { long MOD = 1_000_000_007; int n = nums.length; if (n == 1) { return 1; } int splitPoints = 0; for (int i = 0; i < n - 1; i++) { Set<Integer> prefixNums = new HashSet<>(); for (int j = 0; j <= i; j++) { prefixNums.add(nums[j]); } boolean isDisjoint = true; for (int j = i + 1; j < n; j++) { if (prefixNums.contains(nums[j])) { isDisjoint = false; break; } } if (isDisjoint) { splitPoints++; } } return (int) power(2, splitPoints, MOD); } private long power(long base, int exp, long mod) { long res = 1; base %= mod; while (exp > 0) { if (exp % 2 == 1) { res = (res * base) % mod; } base = (base * base) % mod; exp /= 2; } return res; }}Complexity
Time
O(N^2), where N is the length of the array. The outer loop runs N-1 times, and for each iteration, the inner loops for building the set and checking for disjointness take O(N) time in total.
Space
O(N) in the worst case, as the hash set can store up to N unique elements within the loop.
Trade-offs
Pros
Simple to understand and implement based on the problem definition.
Cons
The time complexity of O(N^2) is too slow for the given constraints (N up to 10^5) and will result in a Time Limit Exceeded error.
A more efficient approach uses a greedy strategy. The core insight is that for a partition to be 'good', all occurrences of any given number must belong to the same subarray. This allows us to group elements into minimal contiguous blocks that cannot be split internally.
A split is possible after an index i if and only if every number that has appeared in nums[0...i] does not appear again in nums[i+1...n-1]. This condition is met if the last occurrence of every number in nums[0...i] is at an index less than or equal to i.
We can find these minimal blocks in a single pass. We iterate through the array while keeping track of the maximum last occurrence index (max_reach) of all numbers encountered so far. When our current index i equals max_reach, it means we have found the end of a valid block. The segment from the start of the current block to i contains all occurrences of its numbers.
If we decompose the array into k such blocks, there are k-1 boundaries between them. At each boundary, we can either place a split or not. This gives 2^(k-1) possible good partitions.
Algorithm
- First, pre-compute the last occurrence index for each number in
nums. This can be done by iterating throughnumsonce and storing the results in a hash maplast_occurrence. - Initialize a counter for minimal blocks,
blocks = 0, and a variablemax_reach = 0. - Iterate through the array
numsfromi = 0ton-1:- For each element
nums[i], updatemax_reachto be the maximum of its current value andlast_occurrence.get(nums[i]). - If
max_reachis equal to the current indexi, it signifies the end of a minimal, self-contained block. All numbers seen so far have their last occurrences within the current segment. Incrementblocks.
- For each element
- After the loop,
blockswill hold the total number of such minimal blocks. - The number of good partitions is the number of ways to partition these
blocksitems, which is2^(blocks - 1). - Compute this value modulo
10^9 + 7using modular exponentiation.
Walkthrough
This optimal approach is based on a greedy algorithm. First, we iterate through the input array nums to populate a hash map, last, which maps each number to its last seen index. This takes O(N) time.
Next, we iterate through nums again from left to right. We maintain a variable max_reach that tracks the farthest last-occurrence index of any number we have seen in the current potential block. For each index i, we update max_reach = max(max_reach, last.get(nums[i])). If at any point i == max_reach, it means we have found the boundary of a minimal block. All numbers within this block (from the end of the previous block to i) have their last occurrences within this range, so this block is guaranteed to be disjoint from the rest of the array. We count how many such blocks, k, we can form.
The total number of good partitions is then 2^(k-1), as we have k-1 potential places to make a cut (between the k blocks). We calculate this using modular exponentiation to handle large results.
import java.util.HashMap;import java.util.Map; class Solution { public int countGoodPartitions(int[] nums) { long MOD = 1_000_000_007; int n = nums.length; Map<Integer, Integer> last = new HashMap<>(); for (int i = 0; i < n; i++) { last.put(nums[i], i); } int blocks = 0; int maxReach = 0; for (int i = 0; i < n; i++) { maxReach = Math.max(maxReach, last.get(nums[i])); if (maxReach == i) { blocks++; } } // If there are 'blocks' minimal segments, there are 'blocks - 1' places to cut. // Each place has 2 choices: cut or not cut. So 2^(blocks - 1) ways. return (int) power(2, blocks - 1, MOD); } private long power(long base, int exp, long mod) { if (exp < 0) return 0; // Should not happen as blocks >= 1 long res = 1; base %= mod; while (exp > 0) { if (exp % 2 == 1) { res = (res * base) % mod; } base = (base * base) % mod; exp /= 2; } return res; }}Complexity
Time
O(N), where N is the length of `nums`. Populating the hash map takes O(N), the main loop takes O(N), and modular exponentiation takes O(log N). The total complexity is dominated by the linear scans.
Space
O(U), where U is the number of unique elements in `nums`. This is for the hash map storing the last occurrences. In the worst case, U can be N.
Trade-offs
Pros
Highly efficient with a linear time complexity, which passes the given constraints.
Solves the problem with a single pass after pre-computation, making it very fast.
Cons
Requires extra space for the hash map, which can be up to O(N) if all elements are unique.
Solutions
Solution
class Solution {public int numberOfGoodPartitions(int[] nums) { Map<Integer, Integer> last = new HashMap<>(); int n = nums.length; for (int i = 0; i < n; ++i) { last.put(nums[i], i); } final int mod = (int)1 e9 + 7; int j = -1; int k = 0; for (int i = 0; i < n; ++i) { j = Math.max(j, last.get(nums[i])); k += i == j ? 1 : 0; } return qpow(2, k - 1, mod); }private int qpow(long a, int n, int mod) { long ans = 1; for (; n > 0; n >>= 1) { if ((n & 1) == 1) { ans = ans * a % mod; } a = a * a % mod; } return (int)ans; }}class Solution {public: int numberOfGoodPartitions(vector<int> &nums) { unordered_map<int, int> last; int n = nums.size(); for (int i = 0; i < n; ++i) { last[nums[i]] = i; } const int mod = 1e9 + 7; int j = -1, k = 0; for (int i = 0; i < n; ++i) { j = max(j, last[nums[i]]); k += i == j; } auto qpow = [&](long long a, int n, int mod) { long long ans = 1; for (; n; n >>= 1) { if (n & 1) { ans = ans * a % mod; } a = a * a % mod; } return (int)ans; }; return qpow(2, k - 1, mod); }};class Solution: def numberOfGoodPartitions(self, nums: List[int]) -> int: last = {x: i for i, x in enumerate(nums)} mod = 10 ** 9 + 7 j, k = - 1, 0 for i, x in enumerate(nums): j = max(j, last[x]) k += i == j return pow(2, k - 1, mod)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.