Before attempting this problem, you should be comfortable with:
Dynamic Programming - Breaking problems into overlapping subproblems with optimal substructure
Memoization - Caching recursive results to avoid redundant computation
Prefix/Suffix Arrays - Precomputing cumulative counts for efficient range queries
1. Dynamic Programming (Top-Down)
Intuition
A monotone increasing binary string consists of some number of 0s followed by some number of 1s. At each position, we need to decide whether to keep the character as is or flip it. The key insight is that we can track whether we are still in the "all zeros" portion or have transitioned to the "all ones" portion.
If we are still allowed to have zeros (mono = true), we can either keep a 0 or flip a 1 to 0, or we can transition to the ones portion. Once we commit to having only 1s, any 0 we encounter must be flipped. This recursive structure with memoization efficiently explores all valid ways to partition the string.
Algorithm
Use a recursive function dfs(i, mono) where i is the current index and mono indicates whether we can still place zeros.
Base case: if i equals the string length, return 0 (no more flips needed).
If mono is true and the current character is '0', we can either keep it (continue with zeros) or flip it to 1 and switch to ones-only mode.
If mono is true and the current character is '1', we can either keep it (switch to ones-only) or flip it to 0 (continue with zeros).
If mono is false (ones-only mode), keep 1s as is and flip any 0s.
Memoize results to avoid redundant computation.
Return the minimum flips starting from index 0 with mono = true.
Instead of recursion, we can iterate through the string from right to left and build up the solution. For each position, we track the minimum flips needed if that position and everything after it should be all 1s versus if we are still in the flexible zone where 0s are allowed.
Processing from right to left lets us use already-computed results for positions ahead of the current one. The state transitions mirror the top-down approach but avoid recursion overhead.
Algorithm
Create a 2D DP array where dp[i][1] represents minimum flips from index i when we can still have zeros, and dp[i][0] when we must have all ones.
Initialize the base case: dp[n][0] = dp[n][1] = 0 (no characters left means no flips).
Iterate from index n-1 down to 0.
For each position, if the character is '0':
In flexible mode: either keep it or flip to 1 and switch modes.
In ones-only mode: must flip to 1.
If the character is '1':
In flexible mode: either flip to 0 or keep it and switch to ones-only.
publicclassSolution{publicintminFlipsMonoIncr(String s){int n = s.length();int[][] dp =newint[n +1][2];for(int i = n -1; i >=0; i--){if(s.charAt(i)=='0'){
dp[i][1]=Math.min(1+ dp[i +1][0], dp[i +1][1]);
dp[i][0]=1+ dp[i +1][0];}else{// s.charAt(i) == '1'
dp[i][1]=Math.min(1+ dp[i +1][1], dp[i +1][0]);
dp[i][0]= dp[i +1][0];}}return dp[0][1];}}
classSolution{public:intminFlipsMonoIncr(string s){int n = s.size();
vector<vector<int>>dp(n +1,vector<int>(2,0));for(int i = n -1; i >=0; i--){if(s[i]=='0'){
dp[i][1]=min(1+ dp[i +1][0], dp[i +1][1]);
dp[i][0]=1+ dp[i +1][0];}else{// s[i] == '1'
dp[i][1]=min(1+ dp[i +1][1], dp[i +1][0]);
dp[i][0]= dp[i +1][0];}}return dp[0][1];}};
classSolution{/**
* @param {string} s
* @return {number}
*/minFlipsMonoIncr(s){const n = s.length;const dp = Array.from({length: n +1},()=>[0,0]);for(let i = n -1; i >=0; i--){if(s[i]==='0'){
dp[i][1]= Math.min(1+ dp[i +1][0], dp[i +1][1]);
dp[i][0]=1+ dp[i +1][0];}else{// s[i] === '1'
dp[i][1]= Math.min(1+ dp[i +1][1], dp[i +1][0]);
dp[i][0]= dp[i +1][0];}}return dp[0][1];}}
publicclassSolution{publicintMinFlipsMonoIncr(string s){int n = s.Length;int[,] dp =newint[n +1,2];for(int i = n -1; i >=0; i--){if(s[i]=='0'){
dp[i,1]= Math.Min(1+ dp[i +1,0], dp[i +1,1]);
dp[i,0]=1+ dp[i +1,0];}else{
dp[i,1]= Math.Min(1+ dp[i +1,1], dp[i +1,0]);
dp[i,0]= dp[i +1,0];}}return dp[0,1];}}
funcminFlipsMonoIncr(s string)int{
n :=len(s)
dp :=make([][]int, n+1)for i :=range dp {
dp[i]=make([]int,2)}for i := n -1; i >=0; i--{if s[i]=='0'{
dp[i][1]=min(1+dp[i+1][0], dp[i+1][1])
dp[i][0]=1+ dp[i+1][0]}else{
dp[i][1]=min(1+dp[i+1][1], dp[i+1][0])
dp[i][0]= dp[i+1][0]}}return dp[0][1]}
class Solution {funminFlipsMonoIncr(s: String): Int {val n = s.length
val dp =Array(n +1){IntArray(2)}for(i in n -1 downTo 0){if(s[i]=='0'){
dp[i][1]=minOf(1+ dp[i +1][0], dp[i +1][1])
dp[i][0]=1+ dp[i +1][0]}else{
dp[i][1]=minOf(1+ dp[i +1][1], dp[i +1][0])
dp[i][0]= dp[i +1][0]}}return dp[0][1]}}
classSolution{funcminFlipsMonoIncr(_ s:String)->Int{let chars =Array(s)let n = chars.count
var dp =[[Int]](repeating:[0,0], count: n +1)for i instride(from: n -1, through:0, by:-1){if chars[i]=="0"{
dp[i][1]=min(1+ dp[i +1][0], dp[i +1][1])
dp[i][0]=1+ dp[i +1][0]}else{
dp[i][1]=min(1+ dp[i +1][1], dp[i +1][0])
dp[i][0]= dp[i +1][0]}}return dp[0][1]}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(n)
3. Dynamic Programming (Space Optimized)
Intuition
The bottom-up solution only needs the DP values from the next position to compute the current position. This means we do not need an entire 2D array; just two variables suffice.
By maintaining only the previous row's values, we reduce space from O(n) to O(1) while preserving the same logic.
Algorithm
Initialize dp[0] and dp[1] to 0 (representing the state after processing the entire string).
Iterate from right to left through the string.
For each character, compute newDp0 (ones-only mode) and newDp1 (flexible mode) based on the current dp values.
If the character is '0': flexible mode chooses the minimum between keeping it or flipping; ones-only mode must flip.
If the character is '1': flexible mode chooses between flipping to 0 or switching to ones-only; ones-only mode keeps it.
A monotone increasing string is all 0s followed by all 1s. We can think of the string as being split at some index: everything before is 0, everything after is 1. For each possible split point, we need to flip all 1s on the left to 0 and all 0s on the right to 1.
Precomputing prefix counts of 1s and suffix counts of 0s lets us evaluate each split point in O(1) time. The answer is the minimum sum across all split points.
Algorithm
Create a prefix array leftOnes where leftOnes[i] counts the number of 1s in indices 0 to i-1.
Create a suffix array rightZeros where rightZeros[i] counts the number of 0s in indices i to n-1.
For each split point i from 0 to n, the cost is leftOnes[i] + rightZeros[i].
classSolution:defminFlipsMonoIncr(self, s:str)->int:
n =len(s)
left_ones =[0]*(n +1)
right_zeros =[0]*(n +1)for i inrange(n):
left_ones[i +1]= left_ones[i]+(1if s[i]=='1'else0)for i inrange(n -1,-1,-1):
right_zeros[i]= right_zeros[i +1]+(1if s[i]=='0'else0)
res =float('inf')for i inrange(n +1):
res =min(res, left_ones[i]+ right_zeros[i])return res
publicclassSolution{publicintminFlipsMonoIncr(String s){int n = s.length();int[] leftOnes =newint[n +1];int[] rightZeros =newint[n +1];for(int i =0; i < n; i++){
leftOnes[i +1]= leftOnes[i]+(s.charAt(i)=='1'?1:0);}for(int i = n -1; i >=0; i--){
rightZeros[i]= rightZeros[i +1]+(s.charAt(i)=='0'?1:0);}int res =Integer.MAX_VALUE;for(int i =0; i <= n; i++){
res =Math.min(res, leftOnes[i]+ rightZeros[i]);}return res;}}
classSolution{public:intminFlipsMonoIncr(string s){int n = s.size();
vector<int>leftOnes(n +1,0),rightZeros(n +1,0);for(int i =0; i < n; i++){
leftOnes[i +1]= leftOnes[i]+(s[i]=='1'?1:0);}for(int i = n -1; i >=0; i--){
rightZeros[i]= rightZeros[i +1]+(s[i]=='0'?1:0);}int res = INT_MAX;for(int i =0; i <= n; i++){
res =min(res, leftOnes[i]+ rightZeros[i]);}return res;}};
classSolution{/**
* @param {string} s
* @return {number}
*/minFlipsMonoIncr(s){const n = s.length;const leftOnes =Array(n +1).fill(0);const rightZeros =Array(n +1).fill(0);for(let i =0; i < n; i++){
leftOnes[i +1]= leftOnes[i]+(s[i]==='1'?1:0);}for(let i = n -1; i >=0; i--){
rightZeros[i]= rightZeros[i +1]+(s[i]==='0'?1:0);}let res =Infinity;for(let i =0; i <= n; i++){
res = Math.min(res, leftOnes[i]+ rightZeros[i]);}return res;}}
publicclassSolution{publicintMinFlipsMonoIncr(string s){int n = s.Length;int[] leftOnes =newint[n +1];int[] rightZeros =newint[n +1];for(int i =0; i < n; i++){
leftOnes[i +1]= leftOnes[i]+(s[i]=='1'?1:0);}for(int i = n -1; i >=0; i--){
rightZeros[i]= rightZeros[i +1]+(s[i]=='0'?1:0);}int res =int.MaxValue;for(int i =0; i <= n; i++){
res = Math.Min(res, leftOnes[i]+ rightZeros[i]);}return res;}}
funcminFlipsMonoIncr(s string)int{
n :=len(s)
leftOnes :=make([]int, n+1)
rightZeros :=make([]int, n+1)for i :=0; i < n; i++{
leftOnes[i+1]= leftOnes[i]if s[i]=='1'{
leftOnes[i+1]++}}for i := n -1; i >=0; i--{
rightZeros[i]= rightZeros[i+1]if s[i]=='0'{
rightZeros[i]++}}
res :=1<<30for i :=0; i <= n; i++{
res =min(res, leftOnes[i]+rightZeros[i])}return res
}
class Solution {funminFlipsMonoIncr(s: String): Int {val n = s.length
val leftOnes =IntArray(n +1)val rightZeros =IntArray(n +1)for(i in0 until n){
leftOnes[i +1]= leftOnes[i]+if(s[i]=='1')1else0}for(i in n -1 downTo 0){
rightZeros[i]= rightZeros[i +1]+if(s[i]=='0')1else0}var res = Int.MAX_VALUE
for(i in0..n){
res =minOf(res, leftOnes[i]+ rightZeros[i])}return res
}}
classSolution{funcminFlipsMonoIncr(_ s:String)->Int{let chars =Array(s)let n = chars.count
var leftOnes =[Int](repeating:0, count: n +1)var rightZeros =[Int](repeating:0, count: n +1)for i in0..<n {
leftOnes[i +1]= leftOnes[i]+(chars[i]=="1"?1:0)}for i instride(from: n -1, through:0, by:-1){
rightZeros[i]= rightZeros[i +1]+(chars[i]=="0"?1:0)}var res =Int.max
for i in0...n {
res =min(res, leftOnes[i]+ rightZeros[i])}return res
}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(n)
5. Dynamic Programming (Optimal)
Intuition
We can solve this problem in a single pass by maintaining a running count of 1s seen so far and the minimum flips needed. When we see a 1, it might need to be flipped later if we decide that position should be 0. When we see a 0, we can either flip it to 1 (incrementing our flip count) or flip all previous 1s to 0.
The key insight is that the minimum flips at any position equals the minimum of: (1) flipping this 0 plus the previous minimum, or (2) flipping all 1s seen so far to 0s.
Algorithm
Initialize res (minimum flips) and cntOne (count of 1s seen) to 0.
Iterate through each character in the string.
If the character is '1', increment cntOne.
If the character is '0', update res = min(res + 1, cntOne):
classSolution:defminFlipsMonoIncr(self, s:str)->int:
res = cntOne =0for c in s:if c =='1':
cntOne +=1else:
res =min(res +1, cntOne)return res
publicclassSolution{publicintminFlipsMonoIncr(String s){int res =0, cntOne =0;for(int i =0; i < s.length(); i++){if(s.charAt(i)=='1'){
cntOne++;}else{
res =Math.min(res +1, cntOne);}}return res;}}
classSolution{public:intminFlipsMonoIncr(string s){int res =0, cntOne =0;for(char& c : s){if(c =='1'){
cntOne++;}else{
res =min(res +1, cntOne);}}return res;}};
classSolution{/**
* @param {string} s
* @return {number}
*/minFlipsMonoIncr(s){let res =0,
cntOne =0;for(let c of s){if(c ==='1'){
cntOne++;}else{
res = Math.min(res +1, cntOne);}}return res;}}
publicclassSolution{publicintMinFlipsMonoIncr(string s){int res =0, cntOne =0;foreach(char c in s){if(c =='1'){
cntOne++;}else{
res = Math.Min(res +1, cntOne);}}return res;}}
funcminFlipsMonoIncr(s string)int{
res, cntOne :=0,0for_, c :=range s {if c =='1'{
cntOne++}else{
res =min(res+1, cntOne)}}return res
}
class Solution {funminFlipsMonoIncr(s: String): Int {var res =0var cntOne =0for(c in s){if(c =='1'){
cntOne++}else{
res =minOf(res +1, cntOne)}}return res
}}
classSolution{funcminFlipsMonoIncr(_ s:String)->Int{var res =0var cntOne =0for c in s {if c =="1"{
cntOne +=1}else{
res =min(res +1, cntOne)}}return res
}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(1) extra space.
Common Pitfalls
Misunderstanding the Monotone Increasing Property
A monotone increasing binary string means all 0s come before all 1s. Some incorrectly interpret this as strictly increasing values or allow alternating patterns. The valid forms are only: all zeros, all ones, or zeros followed by ones with exactly one transition point.
Forgetting to Track the Count of Ones
The optimal solution relies on tracking how many 1s have been seen so far. When encountering a 0, you must decide whether to flip it to 1 or flip all previous 1s to 0. Forgetting to maintain this count leads to incorrect flip calculations.
Off-by-One Errors in Split Point Logic
When using the prefix-suffix approach, the split point represents where zeros end and ones begin. The valid split points range from 0 (all ones) to n (all zeros). Missing the boundary cases or incorrectly indexing the prefix/suffix arrays causes wrong answers.