A clear explanation of solving Flip String to Monotone Increasing with a one-pass dynamic programming approach.
Problem Restatement
We are given a binary string s.
Each character is either '0' or '1'.
A binary string is monotone increasing when all 0s come before all 1s. It may contain only 0s, only 1s, or both.
Valid examples:
"000"
"111"
"000111"
"001111"Invalid examples:
"10"
"101"
"010"We may flip any character:
0 -> 1
1 -> 0Return the minimum number of flips needed to make s monotone increasing. The official statement gives examples such as "00110" -> 1, "010110" -> 2, and "00011000" -> 2.
Input and Output
| Item | Meaning |
|---|---|
| Input | A binary string s |
| Output | Minimum number of flips |
| Constraint | 1 <= s.length <= 10^5 |
| Characters | Each character is '0' or '1' |
Function shape:
class Solution:
def minFlipsMonoIncr(self, s: str) -> int:
...Examples
Example 1:
s = "00110"The string is almost monotone increasing.
We can flip the last character:
00110 -> 00111That takes 1 flip.
So the answer is:
1Example 2:
s = "010110"One valid result is:
010110 -> 011111Another valid result is:
010110 -> 000111Both take 2 flips.
So the answer is:
2Example 3:
s = "00011000"We can flip the two 1s into 0s:
00011000 -> 00000000That takes 2 flips.
So the answer is:
2First Thought: Try Every Split
A monotone increasing binary string has one split point:
000...000111...111Everything before the split should be 0.
Everything after the split should be 1.
So one direct solution is to try every possible split.
For each split:
- Count how many
1s appear on the left. - Count how many
0s appear on the right. - Flip those characters.
For example:
s = "010110"If we choose the split after the third character:
010 | 110The left side should be all 0s, so the 1 on the left must be flipped.
The right side should be all 1s, so the 0 on the right must be flipped.
Total flips:
1 + 1 = 2This idea works, but scanning both sides for every split would be too slow.
Key Insight
We can process the string from left to right and keep only two values:
| Variable | Meaning |
|---|---|
ones | Number of 1s seen so far |
flips | Minimum flips needed to make the prefix monotone increasing |
When we see a 1, it already fits the increasing order. We can keep it and increment ones.
When we see a 0, there is a conflict if previous 1s exist.
For this 0, we have two choices:
- Flip this
0to1. - Keep this
0, but flip all previous1s to0.
So the recurrence is:
flips = min(flips + 1, ones)Meaning:
| Choice | Cost |
|---|---|
Flip current 0 to 1 | flips + 1 |
Keep current 0, flip previous 1s | ones |
Algorithm
Initialize:
ones = 0
flips = 0Then scan each character c in s.
If c == '1':
ones += 1If c == '0':
flips = min(flips + 1, ones)Finally return:
flipsWalkthrough
Use:
s = "010110"Start:
ones = 0
flips = 0| Character | Meaning | ones | flips |
|---|---|---|---|
0 | Already valid as a leading zero | 0 | 0 |
1 | Keep it | 1 | 0 |
0 | Flip this 0, or flip previous 1 | 1 | 1 |
1 | Keep it | 2 | 1 |
1 | Keep it | 3 | 1 |
0 | Flip this 0, or flip previous 1s | 3 | 2 |
The answer is:
2Correctness
At every position, flips stores the minimum number of flips needed to make the prefix ending at that position monotone increasing.
When the current character is 1, appending it to a monotone increasing prefix keeps the prefix valid, because 1 may appear after 0s or after other 1s. So flips does not change.
When the current character is 0, appending it after previous 1s may break monotone order. There are exactly two valid ways to repair this:
- Flip the current
0into1, which costsflips + 1. - Keep the current
0, which means every previous1must become0, costingones.
The algorithm takes the smaller of these two choices.
These two choices cover all valid monotone forms for the current prefix. Therefore, after each character, flips remains the minimum possible cost for that prefix.
After the final character, the prefix is the whole string, so flips is the minimum number of flips needed for the whole string.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | We scan the string once |
| Space | O(1) | We store only two counters |
Implementation
class Solution:
def minFlipsMonoIncr(self, s: str) -> int:
ones = 0
flips = 0
for c in s:
if c == "1":
ones += 1
else:
flips = min(flips + 1, ones)
return flipsCode Explanation
ones counts how many 1s we have seen so far.
ones = 0flips stores the best answer for the prefix processed so far.
flips = 0When we see a 1, it can stay where it is.
if c == "1":
ones += 1When we see a 0, it may violate the monotone increasing order if earlier 1s exist.
flips = min(flips + 1, ones)flips + 1 means we flip the current 0 into 1.
ones means we keep the current 0, but flip all previous 1s into 0.
The smaller value gives the best result for the current prefix.
Testing
def run_tests():
s = Solution()
assert s.minFlipsMonoIncr("00110") == 1
assert s.minFlipsMonoIncr("010110") == 2
assert s.minFlipsMonoIncr("00011000") == 2
assert s.minFlipsMonoIncr("0") == 0
assert s.minFlipsMonoIncr("1") == 0
assert s.minFlipsMonoIncr("00000") == 0
assert s.minFlipsMonoIncr("11111") == 0
assert s.minFlipsMonoIncr("111000") == 3
assert s.minFlipsMonoIncr("101010") == 3
print("all tests passed")
run_tests()| Test | Why |
|---|---|
"00110" | Official basic case |
"010110" | Has several possible optimal results |
"00011000" | Best answer flips 1s to 0s |
"0" | Minimum length |
"1" | Minimum length |
"00000" | Already monotone |
"11111" | Already monotone |
"111000" | Fully decreasing blocks |
"101010" | Alternating pattern |