1573. Number of Ways to Split a String
Problem Description
You are given a binary string s
(containing only '0's and '1's). Your task is to split this string into exactly 3 non-empty parts: s1
, s2
, and s3
, where concatenating them gives back the original string (s1 + s2 + s3 = s
).
The requirement is that each of the three parts must contain the same number of '1's. You need to count how many different ways you can perform such a split.
For example, if s = "10101"
, one valid split could be s1 = "1"
, s2 = "01"
, s3 = "01"
, where each part contains exactly one '1'.
Since the answer can be very large, return the result modulo 10^9 + 7
.
Key points to understand:
- The string must be split into exactly 3 non-empty parts
- Each part must have an equal number of '1's
- The order of characters within each part must be preserved (you're just choosing where to make the cuts)
- If it's impossible to split the string such that each part has the same number of '1's (for example, if the total number of '1's is not divisible by 3), return 0
Intuition
Let's think about what determines a valid split. First, we need to count the total number of '1's in the string. If this count isn't divisible by 3, it's impossible to split the string into three parts with equal '1's, so we return 0.
If the total count is divisible by 3, each part must contain exactly count/3
ones. Now, the key insight is that we don't need to try all possible splits - we just need to find the valid ranges where we can place our two cut points.
Consider the special case where there are no '1's at all. In this case, any split works! We just need to choose 2 positions out of n-1
possible cutting positions, which gives us C(n-1, 2) = (n-1) * (n-2) / 2
ways.
For the general case with '1's present, let's say each part needs exactly k
ones. We need to find:
- Where can we place the first cut? It must be after exactly
k
ones. - Where can we place the second cut? It must be after exactly
2k
ones.
The trick is recognizing that there might be '0's between the k-th and (k+1)-th one, giving us flexibility in where to cut. For example, if we have "...1000001..." and we need to cut after the first '1', we can cut at any of the positions where the '0's are.
So we find:
i1
: the position of the k-th '1' (minimum position for first cut)i2
: the position of the (k+1)-th '1' (maximum position for first cut, exclusive)j1
: the position of the 2k-th '1' (minimum position for second cut)j2
: the position of the (2k+1)-th '1' (maximum position for second cut, exclusive)
The number of valid ways to split is then (i2 - i1) * (j2 - j1)
because we can independently choose any valid position for the first cut and any valid position for the second cut.
Learn more about Math patterns.
Solution Approach
The implementation follows the intuition we developed by systematically finding the valid cut positions.
Step 1: Count the total number of '1's
We first traverse the string and count all '1's. If this count cannot be divided by 3 (i.e., count % 3 != 0
), we immediately return 0 as it's impossible to split equally.
Step 2: Handle the special case of no '1's
If count = 0
, all characters are '0's. We can place our two cuts at any positions among the n-1
possible positions between characters. The number of ways is C(n-1, 2) = (n-1) * (n-2) / 2
.
Step 3: Find the boundary positions
For the general case, we calculate cnt = count / 3
, which is the number of '1's each part must have.
We implement a helper function find(x)
that returns the index of the x-th '1' in the string. This function iterates through the string, counting '1's until it reaches the target count.
Using this function, we find four critical positions:
i1 = find(cnt)
: Position of the cnt-th '1' (earliest possible position for first cut)i2 = find(cnt + 1)
: Position of the (cnt+1)-th '1' (latest possible position for first cut + 1)j1 = find(cnt * 2)
: Position of the (2*cnt)-th '1' (earliest possible position for second cut)j2 = find(cnt * 2 + 1)
: Position of the (2*cnt+1)-th '1' (latest possible position for second cut + 1)
Step 4: Calculate the number of ways
The first cut can be placed at any position from i1
to i2-1
(inclusive), giving us (i2 - i1)
choices.
The second cut can be placed at any position from j1
to j2-1
(inclusive), giving us (j2 - j1)
choices.
Since these choices are independent, the total number of ways is (i2 - i1) * (j2 - j1)
.
Step 5: Return the result modulo 10^9 + 7
Since the answer can be very large, we return the result modulo 10^9 + 7
.
The time complexity is O(n)
as we traverse the string a constant number of times, and the space complexity is O(1)
as we only use a few variables to store positions and counts.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the string s = "10101"
.
Step 1: Count total '1's
- Traverse the string: '1' (count=1), '0', '1' (count=2), '0', '1' (count=3)
- Total count = 3, which is divisible by 3 ✓
- Each part needs: cnt = 3/3 = 1 one
Step 2: Check for special case
- Since count ≠ 0, we proceed to the general case
Step 3: Find boundary positions
- Find the 1st '1' (cnt=1): at index 0 → i1 = 0
- Find the 2nd '1' (cnt+1=2): at index 2 → i2 = 2
- Find the 2nd '1' (cnt*2=2): at index 2 → j1 = 2
- Find the 3rd '1' (cnt*2+1=3): at index 4 → j2 = 4
Step 4: Calculate valid splits
- First cut can be placed at positions: [0, 1] (from i1=0 to i2-1=1)
- Position 0: splits to "1" | "0101"
- Position 1: splits to "10" | "101"
- Second cut can be placed at positions: [2, 3] (from j1=2 to j2-1=3)
- Position 2: splits the remainder to "1" | "01" or "10" | "1"
- Position 3: splits the remainder to "101" | "" or "1010" | "1"
Let's verify all combinations:
- First cut at 0, second cut at 2: "1" | "01" | "01" ✓ (each has 1 one)
- First cut at 0, second cut at 3: "1" | "010" | "1" ✓ (each has 1 one)
- First cut at 1, second cut at 2: "10" | "1" | "01" ✓ (each has 1 one)
- First cut at 1, second cut at 3: "10" | "10" | "1" ✓ (each has 1 one)
Number of ways = (i2 - i1) × (j2 - j1) = (2 - 0) × (4 - 2) = 2 × 2 = 4
Step 5: Return result
- Return 4 % (10^9 + 7) = 4
Solution Implementation
1class Solution:
2 def numWays(self, s: str) -> int:
3 """
4 Count the number of ways to split binary string s into 3 parts
5 with equal number of 1s in each part.
6 """
7
8 def find_position_of_nth_one(target_count: int) -> int:
9 """
10 Find the index position of the nth '1' in the string.
11
12 Args:
13 target_count: Which occurrence of '1' to find (1-indexed)
14
15 Returns:
16 Index position of the target_count-th '1'
17 """
18 ones_seen = 0
19 for index, char in enumerate(s):
20 ones_seen += int(char == '1')
21 if ones_seen == target_count:
22 return index
23 return -1 # Should not reach here if target_count is valid
24
25 # Count total number of 1s and check if divisible by 3
26 total_ones = sum(char == '1' for char in s)
27 ones_per_part, remainder = divmod(total_ones, 3)
28
29 # If not divisible by 3, impossible to split equally
30 if remainder != 0:
31 return 0
32
33 string_length = len(s)
34 MOD = 10**9 + 7
35
36 # Special case: no 1s in the string (all zeros)
37 if ones_per_part == 0:
38 # Can place 2 dividers among (n-1) positions
39 # Number of ways = C(n-1, 2) = (n-1)*(n-2)/2
40 return ((string_length - 1) * (string_length - 2) // 2) % MOD
41
42 # Find boundaries for valid split positions
43 # First split can be placed after ones_per_part 1s until before (ones_per_part+1)th 1
44 first_part_end = find_position_of_nth_one(ones_per_part)
45 second_part_start = find_position_of_nth_one(ones_per_part + 1)
46
47 # Second split can be placed after 2*ones_per_part 1s until before (2*ones_per_part+1)th 1
48 second_part_end = find_position_of_nth_one(ones_per_part * 2)
49 third_part_start = find_position_of_nth_one(ones_per_part * 2 + 1)
50
51 # Count valid positions for each split
52 first_split_options = second_part_start - first_part_end
53 second_split_options = third_part_start - second_part_end
54
55 # Total ways is product of options for each split
56 return (first_split_options * second_split_options) % MOD
57
1class Solution {
2 private String binaryString;
3 private static final int MOD = (int) 1e9 + 7;
4
5 public int numWays(String s) {
6 this.binaryString = s;
7 int stringLength = s.length();
8
9 // Count total number of 1s in the string
10 int totalOnes = 0;
11 for (int i = 0; i < stringLength; i++) {
12 if (s.charAt(i) == '1') {
13 totalOnes++;
14 }
15 }
16
17 // Check if total number of 1s is divisible by 3
18 if (totalOnes % 3 != 0) {
19 return 0; // Cannot split into 3 parts with equal 1s
20 }
21
22 // Special case: if there are no 1s, we can place two cuts anywhere
23 if (totalOnes == 0) {
24 // Number of ways to choose 2 positions from (n-1) possible cut positions
25 long ways = ((long)(stringLength - 1) * (stringLength - 2) / 2) % MOD;
26 return (int) ways;
27 }
28
29 // Calculate how many 1s each part should have
30 int onesPerPart = totalOnes / 3;
31
32 // Find the indices after which we can place the first cut
33 // firstCutStart: index of the last character of first part (with onesPerPart 1s)
34 // firstCutEnd: index of the first character of second part (with onesPerPart + 1 ones total from start)
35 int firstCutStart = findIndexWithKOnes(onesPerPart);
36 int firstCutEnd = findIndexWithKOnes(onesPerPart + 1);
37
38 // Find the indices after which we can place the second cut
39 // secondCutStart: index of the last character of second part (with 2 * onesPerPart 1s)
40 // secondCutEnd: index of the first character of third part (with 2 * onesPerPart + 1 ones total from start)
41 int secondCutStart = findIndexWithKOnes(onesPerPart * 2);
42 int secondCutEnd = findIndexWithKOnes(onesPerPart * 2 + 1);
43
44 // Calculate number of ways
45 // First cut can be placed at any position between firstCutStart and firstCutEnd (exclusive)
46 // Second cut can be placed at any position between secondCutStart and secondCutEnd (exclusive)
47 long firstCutOptions = firstCutEnd - firstCutStart;
48 long secondCutOptions = secondCutEnd - secondCutStart;
49
50 return (int) ((firstCutOptions * secondCutOptions) % MOD);
51 }
52
53 /**
54 * Find the index of the character when we have exactly targetCount 1s
55 * counting from the beginning of the string
56 * @param targetCount the target number of 1s to find
57 * @return the index where we reach exactly targetCount 1s
58 */
59 private int findIndexWithKOnes(int targetCount) {
60 int onesCount = 0;
61 for (int i = 0; i < binaryString.length(); i++) {
62 if (binaryString.charAt(i) == '1') {
63 onesCount++;
64 }
65 if (onesCount == targetCount) {
66 return i;
67 }
68 }
69 // This line should never be reached if the input is valid
70 return binaryString.length() - 1;
71 }
72}
73
1class Solution {
2public:
3 int numWays(string s) {
4 // Count total number of 1s in the string
5 int totalOnes = 0;
6 for (char& c : s) {
7 totalOnes += (c == '1');
8 }
9
10 // Check if total ones can be divided into 3 equal parts
11 if (totalOnes % 3 != 0) {
12 return 0;
13 }
14
15 const int MOD = 1e9 + 7;
16 int n = s.size();
17
18 // Special case: if there are no 1s, we can place two dividers anywhere
19 // Number of ways = C(n-1, 2) = (n-1) * (n-2) / 2
20 if (totalOnes == 0) {
21 return (1LL * (n - 1) * (n - 2) / 2) % MOD;
22 }
23
24 // Each part should have this many 1s
25 int onesPerPart = totalOnes / 3;
26
27 // Lambda function to find the index of the k-th occurrence of 1
28 auto findKthOneIndex = [&](int k) {
29 int onesCount = 0;
30 for (int i = 0; i < n; ++i) {
31 onesCount += (s[i] == '1');
32 if (onesCount == k) {
33 return i;
34 }
35 }
36 return -1; // Should never reach here if input is valid
37 };
38
39 // Find indices for first and second part boundaries
40 // firstPartEnd: index of the last 1 in first part
41 // secondPartStart: index of the first 1 in second part
42 int firstPartEnd = findKthOneIndex(onesPerPart);
43 int secondPartStart = findKthOneIndex(onesPerPart + 1);
44
45 // secondPartEnd: index of the last 1 in second part
46 // thirdPartStart: index of the first 1 in third part
47 int secondPartEnd = findKthOneIndex(onesPerPart * 2);
48 int thirdPartStart = findKthOneIndex(onesPerPart * 2 + 1);
49
50 // Calculate number of ways to place the two dividers
51 // First divider can be placed after firstPartEnd and before secondPartStart
52 // Second divider can be placed after secondPartEnd and before thirdPartStart
53 int waysForFirstDivider = secondPartStart - firstPartEnd;
54 int waysForSecondDivider = thirdPartStart - secondPartEnd;
55
56 return (1LL * waysForFirstDivider * waysForSecondDivider) % MOD;
57 }
58};
59
1function numWays(s: string): number {
2 // Count total number of 1s in the string
3 let totalOnes = 0;
4 for (const c of s) {
5 totalOnes += (c === '1' ? 1 : 0);
6 }
7
8 // Check if total ones can be divided into 3 equal parts
9 if (totalOnes % 3 !== 0) {
10 return 0;
11 }
12
13 const MOD = 1e9 + 7;
14 const n = s.length;
15
16 // Special case: if there are no 1s, we can place two dividers anywhere
17 // Number of ways = C(n-1, 2) = (n-1) * (n-2) / 2
18 if (totalOnes === 0) {
19 return Math.floor((n - 1) * (n - 2) / 2) % MOD;
20 }
21
22 // Each part should have this many 1s
23 const onesPerPart = totalOnes / 3;
24
25 // Helper function to find the index of the k-th occurrence of 1
26 const findKthOneIndex = (k: number): number => {
27 let onesCount = 0;
28 for (let i = 0; i < n; i++) {
29 onesCount += (s[i] === '1' ? 1 : 0);
30 if (onesCount === k) {
31 return i;
32 }
33 }
34 return -1; // Should never reach here if input is valid
35 };
36
37 // Find indices for first and second part boundaries
38 // firstPartEnd: index of the last 1 in first part
39 // secondPartStart: index of the first 1 in second part
40 const firstPartEnd = findKthOneIndex(onesPerPart);
41 const secondPartStart = findKthOneIndex(onesPerPart + 1);
42
43 // secondPartEnd: index of the last 1 in second part
44 // thirdPartStart: index of the first 1 in third part
45 const secondPartEnd = findKthOneIndex(onesPerPart * 2);
46 const thirdPartStart = findKthOneIndex(onesPerPart * 2 + 1);
47
48 // Calculate number of ways to place the two dividers
49 // First divider can be placed after firstPartEnd and before secondPartStart
50 // Second divider can be placed after secondPartEnd and before thirdPartStart
51 const waysForFirstDivider = secondPartStart - firstPartEnd;
52 const waysForSecondDivider = thirdPartStart - secondPartEnd;
53
54 // Return the product of ways modulo MOD
55 return (waysForFirstDivider * waysForSecondDivider) % MOD;
56}
57
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the string s
.
- The initial calculation
sum(c == '1' for c in s)
iterates through the entire string once:O(n)
- The
find()
function is called at most 4 times (for findingi1
,i2
,j1
,j2
), and each call iterates through the string in the worst case:4 * O(n) = O(n)
- When
cnt == 0
, the calculation((n - 1) * (n - 2) // 2) % mod
is done in constant time:O(1)
- All other operations (modulo, multiplication, subtraction) are constant time:
O(1)
Overall, the algorithm makes a constant number of passes through the string, resulting in O(n)
time complexity.
Space Complexity: O(1)
- The algorithm uses only a fixed number of variables:
cnt
,m
,n
,mod
,i1
,i2
,j1
,j2
, and the local variables in thefind()
function (t
,i
,c
) - The
find()
function doesn't create any additional data structures - The generator expression
sum(c == '1' for c in s)
doesn't create an intermediate list, it processes elements one at a time - No recursive calls are made, so there's no additional stack space usage
The space usage doesn't scale with the input size, making it O(1)
constant space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Error in Split Position Calculation
A common mistake is misunderstanding what the split positions represent. When we find positions like first_part_end
and second_part_start
, developers often confuse whether these are inclusive or exclusive boundaries.
The Pitfall:
# Incorrect interpretation might lead to: first_split_options = second_part_start - first_part_end - 1 # Wrong! # or first_split_options = second_part_start - first_part_end + 1 # Wrong!
Why it happens: The confusion arises because first_part_end
is the index of the last '1' in the first part, and second_part_start
is the index of the first '1' in the second part. The valid cut positions are all indices from first_part_end
(inclusive) to second_part_start - 1
(inclusive).
The Correct Approach:
# The cut can be placed right after first_part_end up to right before second_part_start # This gives us (second_part_start - first_part_end) valid positions first_split_options = second_part_start - first_part_end
2. Integer Overflow in the Special Case
When all characters are '0's, we calculate C(n-1, 2) = (n-1) * (n-2) / 2
. For large strings, this multiplication can cause integer overflow before applying the modulo operation.
The Pitfall:
# For very large n, this might overflow in some languages return (string_length - 1) * (string_length - 2) // 2 % MOD
The Solution:
# Apply modulo during the calculation to prevent overflow result = ((string_length - 1) * (string_length - 2) // 2) % MOD # Or even safer: result = (((string_length - 1) % MOD) * ((string_length - 2) % MOD) // 2) % MOD
3. Incorrect Handling of Edge Cases
Another pitfall is not properly handling the case where find_position_of_nth_one
might not find enough '1's, especially when calculating third_part_start
.
The Pitfall:
# If total_ones equals 2*ones_per_part exactly, there's no (2*ones_per_part + 1)th '1' third_part_start = find_position_of_nth_one(ones_per_part * 2 + 1) # Returns -1 or crashes
The Solution:
# Handle the boundary case explicitly if ones_per_part * 2 + 1 > total_ones: third_part_start = string_length # Use string length as the boundary else: third_part_start = find_position_of_nth_one(ones_per_part * 2 + 1)
4. Misunderstanding the Problem Requirements
Some developers might think they need to find all possible combinations of three substrings with equal '1's, rather than understanding that the three parts must be contiguous segments of the original string.
The Pitfall: Trying to use combinations or permutations to select characters from different positions, which would break the string's original order.
The Correct Understanding: The problem asks for contiguous splits only. We're choosing two cut positions in the string, not rearranging characters. The string "10101" can only be split by choosing where to place two cuts, maintaining the original character sequence in each part.
Given a sorted array of integers and an integer called target, find the element that
equals to the target and return its index. Select the correct code that fills the
___
in the given code snippet.
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left ___ right:
4 mid = (left + right) // 2
5 if arr[mid] == target:
6 return mid
7 if arr[mid] < target:
8 ___ = mid + 1
9 else:
10 ___ = mid - 1
11 return -1
12
1public static int binarySearch(int[] arr, int target) {
2 int left = 0;
3 int right = arr.length - 1;
4
5 while (left ___ right) {
6 int mid = left + (right - left) / 2;
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
1function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left ___ right) {
6 let mid = left + Math.trunc((right - left) / 2);
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!