monotone_crescendo/lib.rs
1//! A library for calculating the least number of flips needed to make a binary string monotone increasing.
2//! Also contains utility functions for allocating WebAssembly linear memory and returning a pointer to it, as well
3//! as deallocating said memory.
4
5#[cfg(test)]
6mod tests {
7 use super::solution::*;
8 use super::wasm_memory::call_solution_with_input;
9
10 #[test]
11 fn test_call_solution_with_input() {
12 let input = String::from("Cumulative\011010\0");
13 let mut buf = input.into_bytes();
14 let ptr = buf.as_mut_ptr();
15
16 unsafe {
17 call_solution_with_input(ptr);
18 }
19 }
20
21 #[test]
22 fn test_with_prefix_sums() {
23 assert_eq!(monotone_crescendo_prefix_sums("11010"), 2);
24 }
25
26 #[test]
27 fn test_with_prefix_sums_without_redundant_zero() {
28 assert_eq!(
29 monotone_crescendo_prefix_sums_without_redundant_zero("11010"),
30 2
31 );
32 }
33
34 #[test]
35 fn test_cumulative() {
36 assert_eq!(monototone_crescendo_cumulative("11010"), 2);
37 }
38}
39
40pub mod wasm_memory {
41 //! # Utilities for interacting with WASM's linear memory
42
43 use super::solution::*;
44 use std::{ffi::CString, mem, os::raw::c_void};
45
46 /// # Allocate memory in WebAssembly's linear memory and return its offset
47 #[no_mangle]
48 pub extern "C" fn alloc() -> *mut c_void {
49 let mut buf = Vec::with_capacity(1024);
50 let ptr = buf.as_mut_ptr();
51
52 mem::forget(buf);
53
54 ptr
55 }
56
57 /// # Deallocate memory in WebAssembly's linear memory at the offset located at `ptr`
58 ///
59 /// No explicit calls to [std::mem::drop] are needed as [std::vec::Vec::from_raw_parts] takes ownership
60 /// of the memory pointed at by `ptr`.
61 #[no_mangle]
62 pub unsafe extern "C" fn dealloc(ptr: *mut c_void) {
63 let _ = Vec::from_raw_parts(ptr, 1024, 1024);
64 }
65
66 /// # Call the specified [solution][super::solution] method with the given input
67 ///
68 /// `ptr` should be a pointer containing UTF-8 encoded characters separated by a null character. The first block of characters
69 /// should be the name of solution to use, and the second block of characters should be the input to the solution itself.
70 #[no_mangle]
71 pub unsafe extern "C" fn call_solution_with_input(ptr: *mut u8) {
72 let mut iter = ptr;
73
74 let mut solution_name = String::new();
75 let mut input = String::new();
76
77 let mut null_count = 0;
78 while null_count < 2 {
79 let c = *iter as char;
80
81 iter = iter.add(1);
82
83 if c == '\0' {
84 null_count += 1;
85 continue;
86 }
87
88 match null_count {
89 0 => solution_name.push(c),
90 1 => input.push(c),
91 _ => panic!("Unexpected null_count value"),
92 }
93 }
94
95 let flips = match solution_name.as_str() {
96 "Prefix Sums" => monotone_crescendo_prefix_sums(&input),
97 "Cumulative" => monototone_crescendo_cumulative(&input),
98 "Prefix Sums w/o Redundant Zero" => {
99 monotone_crescendo_prefix_sums_without_redundant_zero(&input) as i32
100 }
101 _ => -1,
102 };
103
104 let answer = match flips {
105 -1 => format!("Solution name \"{}\" does not exist", solution_name),
106 _ => format!("The minimum number of flips needed is: {}", flips),
107 };
108
109 let c_str = CString::new(answer).unwrap();
110 let c_str_bytes = c_str.as_bytes_with_nul();
111
112 let return_bytes = std::slice::from_raw_parts_mut(ptr, 1024);
113 return_bytes[..c_str_bytes.len()].copy_from_slice(c_str_bytes);
114 }
115}
116
117pub mod solution {
118 //! # Implementations of the monotone increasing problem
119
120 use std::cmp::min;
121 /// # This is the [official solution][0] to [LeetCode problem #926][1]
122 ///
123 /// The solution was simply translated by me into rust from java.
124 ///
125 /// ## Explanation
126 ///
127 /// I find the explanation on LeetCode to this solution somewhat overcomplicated and hard to understand, so I will attempt
128 /// to explain it (hopefully) a bit simpler here.
129 ///
130 /// The first thing this solution does is calculate the [prefix sum][2] of every item in the string and store it in an array. So for example, if we had the string `11010`, the prefix sum array
131 /// for that string would look like: `[0,1,2,2,3,3]`. This prefix sum array represents the the number of 1's that are present before the corresponding character index in the string.
132 ///
133 /// This solution then works by looping through the given string and, for each character in the string, analyzing the string as two halves partioned
134 /// at that character. The number of 1's in the first half are added to the number of 0's in the second half to determine the total number of flips
135 /// needed to make the string monotone increasing for that location in the string.
136 ///
137 /// We get the number of 1's in the first half by looking up the corresponding index in the prefix sum array.
138 /// We get the number of 0's in the second half by subtracting the number of 1's in the second half from the length
139 /// of the second half.
140 ///
141 /// We then compare this number to the current minimum flips value and take the minimum of these two values again. The minimium
142 /// we have in the end is what we return.
143 ///
144 /// Note: the prefix sum array could also be optimized to remove the redundant element at index 0. The way the solution on LeetCode
145 /// calculates the prefix sum array, the first element will always be zero. For example, if we had the string `11010`, the prefix sum array
146 /// could be optimized to `[1,2,2,3,3]`. Apparently the official solution calculates the prefix sum array with the redundant zero at the beginning
147 /// to [prevent some sort of possible overflow condition][3], out of principle more than anything else. See [monotone_crescendo_prefix_sums_without_redundant_zero] for an example.
148 ///
149 /// [0]: <https://leetcode.com/problems/flip-string-to-monotone-increasing/solution/>
150 /// [1]: <https://leetcode.com/problems/flip-string-to-monotone-increasing/>
151 /// [2]: <https://en.wikipedia.org/wiki/Prefix_sum>
152 /// [3]: <https://leetcode.com/problems/flip-string-to-monotone-increasing/solution/1047706>
153 pub fn monotone_crescendo_prefix_sums(s: &str) -> i32 {
154 let str_size = s.chars().count() as i32;
155 let mut prefix_sums = vec![0; str_size as usize + 1];
156 let mut min_flips: i32 = i32::MAX;
157
158 for (i, char) in s.chars().enumerate() {
159 prefix_sums[i + 1] = prefix_sums[i] + (if char == '1' { 1 } else { 0 });
160 }
161
162 let mut j = 0;
163
164 while j <= str_size {
165 let ones_before_j = prefix_sums[j as usize];
166 let ones_after_j = prefix_sums[str_size as usize] - prefix_sums[j as usize];
167
168 let length_of_str_after_j = str_size - j;
169 let zeroes_after_j = length_of_str_after_j - ones_after_j;
170
171 min_flips = min(min_flips, ones_before_j + zeroes_after_j);
172
173 println!(
174 "min_flips: {:?}, j: {:?}, ones_before_j: {:?}, zeroes_after_j: {:?}",
175 min_flips, j, ones_before_j, zeroes_after_j
176 );
177
178 j += 1;
179 }
180
181 min_flips
182 }
183
184 /// # Essentially the same solution as [monotone_crescendo_prefix_sums] except without the redundant leading zero in the prefix sum array
185 ///
186 /// This function also utilizes [u8]'s instead of [i32]'s.
187 /// This could have also been done in [monotone_crescendo_prefix_sums] as our input will always lead to positive prefix sums and a positive
188 /// return value which should all be less than or equal to 255. The only reason [monotone_crescendo_prefix_sums] uses [i32]'s is that I made an
189 /// arbitrary choice to do so, and when removing the redundant zero I thought about it a bit more and realized [u8]'s would be fine.
190 pub fn monotone_crescendo_prefix_sums_without_redundant_zero(s: &str) -> u8 {
191 let str_size = s.chars().count();
192 let mut prefix_sums: Vec<u8> = Vec::with_capacity(str_size);
193 let mut min_flips: u8 = u8::MAX;
194
195 println!("min_flips: {:?}", min_flips);
196
197 for (i, char) in s.chars().enumerate() {
198 let num = match char {
199 '1' => 1,
200 _ => 0,
201 };
202 if i == 0 {
203 prefix_sums.push(num);
204 continue;
205 }
206
207 prefix_sums.push(prefix_sums.get(i - 1).unwrap() + num);
208 }
209
210 let mut j = 0;
211
212 while j < str_size {
213 let ones_before_j = match j {
214 0 => 0,
215 _ => prefix_sums[j],
216 };
217 let ones_after_j = prefix_sums[str_size - 1] - prefix_sums[j];
218
219 let length_of_str_after_j = (str_size - 1 - j) as u8;
220 let zeroes_after_j = length_of_str_after_j - ones_after_j;
221
222 min_flips = min(min_flips, ones_before_j + zeroes_after_j);
223
224 println!(
225 "min_flips: {:?}, j: {:?}, ones_before_j: {:?}, zeroes_after_j: {:?}",
226 min_flips, j, ones_before_j, zeroes_after_j
227 );
228
229 j += 1;
230 }
231
232 min_flips
233 }
234
235 /// # This is an ingenious solution which was [contributed][0] by LeetCode user [tarunbisht][1] and translated to Rust by me
236 ///
237 /// ## Explanation
238 ///
239 /// This solution works by keeping a running total of all ones and all zeroes, where the count of zeroes is initlally contained in the variable `flips`.
240 /// The variable `flips` is then set to the minimum value of the running total of zeroes or running total of ones.
241 ///
242 /// Overall this solution is not only simpler to understand, at least for me, but also requires only O(1) space as opposed to O(N) space.
243 ///
244 /// [0]: <https://leetcode.com/problems/flip-string-to-monotone-increasing/solution/725259>
245 /// [1]: <https://leetcode.com/tarunbisht>
246 pub fn monototone_crescendo_cumulative(s: &str) -> i32 {
247 let mut ones = 0;
248 let mut flips = 0;
249
250 for char in s.chars() {
251 match char {
252 '1' => ones += 1,
253 _ => flips += 1,
254 };
255
256 flips = min(ones, flips);
257 }
258
259 flips
260 }
261}