diff options
| author | Andrew Schneider <atschneider@temple.edu> | 2024-06-23 15:23:43 -0400 |
|---|---|---|
| committer | Andrew Schneider <atschneider@temple.edu> | 2024-06-23 15:23:43 -0400 |
| commit | 946a1a013c6e78f164357175f4662f2286fe60d5 (patch) | |
| tree | e1676124614f994ca6d5f1c80e4c8c204627eb79 | |
| parent | ffc47a8850ee877978e371d11a01a028862a3f9d (diff) | |
| download | perlweeklychallenge-club-946a1a013c6e78f164357175f4662f2286fe60d5.tar.gz perlweeklychallenge-club-946a1a013c6e78f164357175f4662f2286fe60d5.tar.bz2 perlweeklychallenge-club-946a1a013c6e78f164357175f4662f2286fe60d5.zip | |
Initial commit of PWC 274
| -rw-r--r-- | challenge-274/atschneid/README.md | 562 | ||||
| -rw-r--r-- | challenge-274/atschneid/c/ch-1.c | 89 | ||||
| -rw-r--r-- | challenge-274/atschneid/c/ch-2.c | 104 | ||||
| -rw-r--r-- | challenge-274/atschneid/c/makefile | 10 | ||||
| -rw-r--r-- | challenge-274/atschneid/haskell/ch-1.hs | 32 | ||||
| -rw-r--r-- | challenge-274/atschneid/haskell/ch-2.hs | 51 | ||||
| -rw-r--r-- | challenge-274/atschneid/haskell/makefile | 10 | ||||
| -rw-r--r-- | challenge-274/atschneid/haskell/unused-extras.hs | 17 | ||||
| -rw-r--r-- | challenge-274/atschneid/julia/ch-1.jl | 52 | ||||
| -rw-r--r-- | challenge-274/atschneid/julia/ch-2.jl | 49 | ||||
| -rw-r--r-- | challenge-274/atschneid/perl/ch-1.pl | 25 | ||||
| -rw-r--r-- | challenge-274/atschneid/perl/ch-2.pl | 79 |
12 files changed, 885 insertions, 195 deletions
diff --git a/challenge-274/atschneid/README.md b/challenge-274/atschneid/README.md index a5fa9cb2ed..fba2c19c3f 100644 --- a/challenge-274/atschneid/README.md +++ b/challenge-274/atschneid/README.md @@ -1,280 +1,452 @@ # Trying to Be X Percent More Interesting -**Challenge 273 solutions by Andrew Schneider** +**Challenge 274 solutions by Andrew Schneider** -Being a stream of consciousness write up of some details of my code for [PWC 273](https://theweeklychallenge.org/blog/perl-weekly-challenge-273/) +Being a stream of consciousness write up of some details of my code for [PWC 274](https://theweeklychallenge.org/blog/perl-weekly-challenge-274/) -The challenges this week seemed pretty straightforward. Both of them have pretty direct solutions based on an algorithm of walking along a string, character by character, setting some flag variables or counts. In both cases it was simple to come up solutions that go through each string exactly one time, clearly in $\mathcal{O}(n)$ for $n = |\text{string}|$. +Great challenges this week! Challenge 1 seemed easy, but depending on how you want to manage the unspecified details, it can get as complicated as you want. Perl really crushes it on this one. Challenge 2 took some thought. I iterated through a bunch of solutions. I first worked out a solution in Python (not shown) to work out the details, then translated that solution into Perl, then C, then Haskell, then Julia, then back again, with improvements along the way. I went from using 3 loops to 2, to none-ish. -So it is basically impossible to find a better algorithm. But I needed to remind myself that that is *boring!* For one, I try to make my solutions in different languages as different as possible, and just transcribing the same code into different syntax would not be that. And this is supposed to be a chance to do something fun, learn something new. +This is the first week that I used my random selection script to pick which lannguages to use, and abided by the results. This week the wheel landed on Haskell, Julia, and C. I figure I'll keep Perl as a perennial selection, since this is the *Perl* Weekly Challenge after all. -So let big-O be darned, I'll throw caution to the wind and solve these problems in whatever wacky way I want, and I'll try to not be such a buzzkill. Strong caveat: there is still a lot of similarity between certain pairs of solutions, because I only have a limited amount of imagination, but I'm trying, or I'm growing, or I'm trying to grow, or something. +## Challenge 1: Goat Latin -## Challenge 1: Percentage of Character - -> Task 1: Percentage of Character</br> +> Task 1: Goat Latin</br> > Submitted by: Mohammad Sajid Anwar</br> -> You are given a string, $str and a character $char.</br> +> You are given a sentence, $sentance.</br> +> </br> +> Write a script to convert the given sentence to Goat Latin, a made up language similar to Pig Latin.</br> +> </br> +> Rules for Goat Latin:</br> > </br> -> Write a script to return the percentage, nearest whole, of given character in the given string.</br> +> 1) If a word begins with a vowel ("a", "e", "i", "o", "u"), append</br> +> "ma" to the end of the word.</br> +> 2) If a word begins with consonant i.e. not a vowel, remove first</br> +> letter and append it to the end then add "ma".</br> +> 3) Add letter "a" to the end of first word in the sentence, "aa" to</br> +> the second word, etc etc.</br> > </br> > Example 1</br> -> Input: $str = "perl", $char = "e"</br> -> Output: 25</br> +> Input: $sentence = "I love Perl"</br> +> Output: "Imaa ovelmaaa erlPmaaaa"</br> +> </br> > Example 2</br> -> Input: $str = "java", $char = "a"</br> -> Output: 50</br> +> Input: $sentence = "Perl and Raku are friends"</br> +> Output: "erlPmaa andmaaa akuRmaaaa aremaaaaa riendsfmaaaaaa"</br> +> </br> > Example 3</br> -> Input: $str = "python", $char = "m"</br> -> Output: 0</br> -> Example 4</br> -> Input: $str = "ada", $char = "a"</br> -> Output: 67</br> -> Example 5</br> -> Input: $str = "ballerina", $char = "l"</br> -> Output: 22</br> -> Example 6</br> -> Input: $str = "analitik", $char = "k"</br> -> Output: 13 - -To start, I think this Julia snippet best shows my idea for the *canonical* solution to this problem +> Input: $sentence = "The Weekly Challenge"</br> +> Output: "heTmaa eeklyWmaaa hallengeCmaaaa"</br> -```julia -function percent_char_v1(string, char) - # the simplest and cheapest algorithm - # also the most boring - # - # step through each char of the string - # count the matches, count all chars - # one pass through the string - # - # divide matches by total chars and convert to integer percent - count_c = 0 - count_all = 0 - for c in string - count_c += c == char ? 1 : 0 - count_all += 1 - end - Int( round( count_c / count_all * 100 ) ) -end -``` +What the instructions don't specify is what constitues a "word". A simple implementation might be to consider all non-whitespace characters to be words. I feel like this would lead to undesired behavior around punctuation, which typically does not have whitespace between its preceding character. Another thing to consider is whether we disturb the non-word portion of the string. For example, if we split on whitespace we may end up elliding consecutive spaces in our goatified string. I decided to treat any consecutive spans of alpha characters as words and everything else as non-words. -We need to look at every character in the string to see if it matches. There's no way around that, so we at least need to iterate over the string one time. This algorithm iterates over the string one time. In fact, since we are already doing the iteration, we can count the number of total characters along the way, so there's no extra call to `size` or `length` or anything. I could probably do some work around the last line, we do floating point division, multiply by 100, round, then cast back to an int, not strictly necessary that last part but why not. +I tried to have my output be consistent across implementations. In my design a string like "hey, you!" will get goatified to "eyhmaa, ouymaaa!" (and not like "ey,hmaa ou!ymaaa"). One detail I'm not completely happy with is how apostrophes are handled. A string like "don't look!" gets goatified to "ondmaa'tmaaa ooklmaaaa!", but my heart tells me it should really come out like "on'tdmaa ooklmaaa!". I didn't feel little working too hard on this little detail, so I stuck with the former. ### Perl -When I first read this problem description I thought, "great! a chance to use `use integer`!" Only the specification of going to the *nearest whole* threw me off that plan. So I glumly typed out a Perlish version of the above algorithm until I heard a voice whisper "buzzkill" inside my head, and I thought, there's gotta be a way to do this using `use integer`, and voila, I cooked us this gem +Perl really handles this one quite nicely. At first I split on whitespace and performed multiple rounds of regex substitution, but doing global substitution is such a neater solution, and preserves non-word characters. ```perl -sub percent_char_string( $char, $string ) { - # do all arithmetic in integer domain - use integer; - - # get the length of $string - my $length = length( $string ); - - # count how many characters match $char - my $sum_chars = $string =~ s"$char""g; - - # find the integer division of $sum_chars / $length - # then figure out if we would round up or down and add 1 or 0 respectively - my $result = $sum_chars * 100 / $length + (($sum_chars * 100 % $length) * 2 >= $length); - - return $result; +sub make_to_goat_latin( $string ) { + # if non-vowel initial then move to end + $string =~ s/\b([BCDFGHJKLMNPQRSTVWXYZ])(\w+)\b/$2$1/ig; + + # suffix each (proper) word with maa, maaa, maaaa, etc. + my $count = 0; + $string =~ s/\b(\w+)\b/$1 . 'ma' . 'a' x ++$count/eg; + return $string; } ``` -What's the runtime? I really don't know. `length( $string )` should be $\mathcal{O}(|\text{string}|)$ either when called or at string creation. `$string =~ s"$char""g` is the big question mark for me. Would it be cheaper to do `$string =~ s"$char"\1"g`? Does this even reliably work? I suppose `$char` could have a special regex meaning... bah. Anyway, we're not worrying about all that! we're here to have fun. +I first handle moving a non-vowel initial to the end of each word. Then append the appropriate length suffix to each word. I probably could have handled this all in one monster regex, but I think this is a good readability tradeoff. I don't need to worry about splitting it out because the first substitution will not create a word from a non-word. Using the `e` flag in the second substitution to calculate the length of the "maa...a" suffix really makes things easy here. In my other solutions I don't rely on regex nearly as much if at all, because I didn't have anything as powerful as this. -Now to what I think is the craziest bit here, figuring out if we round up. Integer division, easy, it truncates. This part `($sum_chars * 100 % $length) * 2 >= $length` made me a little squeamish at first and in my first implementation I made a subtle bug here. What we're asking is: is the remainder (`$sum_chars * 100 % $length`) greater than or equal to half the string length. If yes then we would round up if we had been using floats, so we add 1. We could even do the `* 2` part as a bitshift, so really this has to be at least as efficient as converting to float and back. +### C -The bug I mentioned, I had written `($sum_chars * 100 % $length) >= $length / 2` at first, but since we're doing `use integer` then `$length / 2` gets truncated, which is not what we want. It didn't affect any of the examples, but a simple case to illustrate the problem, suppose `$sum_chars` was 1 and `$length` was 3 (as if our string was "ada" and our character was 'd'). This way we get $100 \mod 3 = 1 \ge 3 // 2 = 1$ using integer division, ie, $1 \ge 1$ and we *round up* when we shouldn't. In the correct formulation this comes out to $2 \ge 3$ (work it out for yourself) and we *round down* which is correct. The trouble with integer division! Sheesh. But worth it to get to use `use integer`. +In C, and the others, the strategy I used is to first split the string into an alternating word, non-word list, next operate on the words in the list, and finally stitch the list back together (or ... not quite "together"). In C I really got into the weeds in character manipulations. I did't easily find existing functions to interact between chars and strings (C style) the way I needed to, so this ended up looking like an "Intro to Programming in C" homework assignment. -### Julia +As laid out above, first I split the string into an alternating word, non-word list. I make it a linked list of `word_ll` type. -For my *fun* version of the function in Julia I ended up leaning on mostly builtin functionality. It yields a nice one-liner plus comments +```c +typedef struct word_ll { + char *word; + struct word_ll *next; +} word_ll ; -```julia -percent_char_v2(string, char) = - # size( findall( char, string ), 1 ) gives the count of all char in string - # divide by length of string, times 100 and convert to Int - Int( round( size( findall( char, string ), 1 ) / length( string ) * 100 ) ) +word_ll build_word_chaff_ll( char *str ) { + word_ll head = { .word=malloc( sizeof(char) * 100), .next=NULL }; + word_ll *next = &head; + + int status = 0; + size_t word_ctr = 0; + for (char *c = str; *c != '\0'; c++) { + + if ( + (status != 1 && ( (*c >= 65 && *c <= 90) || (*c >= 97 && *c <= 122) ) ) || + (status != 0 && !( (*c >= 65 && *c <= 90) || (*c >= 97 && *c <= 122) )) + ) { + next = next->next = malloc( sizeof(word_ll) ); + next->word = malloc( sizeof(char) * 100); + word_ctr = 0; + status = (status + 1) % 2; + } + next->word[word_ctr++] = *c; + } + + return head; +} ``` -Here I have all the inefficiencies, separately counting the matches and total characters, converting to float and back to int, but it's short! Also I got to play around with the Julia REPL help mode to find the names of functions I wanted. +I am doing a lot of `malloc`s in there, and returning an object containing a pointer, so it would be up to the calling function to handle `free`ing the memory. I don't explicitly do that since the program ends soon after this function returns. -### C++ +The logic here is I start building a string for the first non-word. If the next char is a non-word char, I append it to the current word. Otherwise I finish that word, start building a word string, and append to that while we see word characters. I alternate back and forth between non-word and word until I get through all chars of the string. -Oh how I have resisted learning any C++ for so long. To be fair, there is a *lot* there, a lot of code, a lot of functions, a lot of directions, all kind of bolted on a core C frame. But there is some cool stuff bubbling up too. What turned me around on C++ was starting to learn some Rust and realizing a lot of the cool stuff I was learning about in Rust you can do in C++ too, like RAII. +Below is the function that takes a word and the index of the word in the sentence (to figure out how many 'a's are needed) and does the goat word level changes. This would blow up if our words and their buffers were longer than 100 chars long, so I'm playing pretty fast and loose here, but it works for the examples. -```cpp -int percent_string_char( std::string s, char c ) { - // this is the boring algorithm that walks through each char in s - int sum_char = 0; - int sum_all = 0; - for (char sc : s ) { - sum_char += (sc == c); - sum_all++; +```c +void goatify_word( char *word, size_t alen ) { + char temp[100] = "\0"; + int i=1; + // starts with vowel? + switch (*word) { + case 'A':case 'E':case 'I':case 'O':case 'U':case 'a':case 'e':case 'i':case 'o':case 'u': + break; + default: + for (i=1; word[i] != '\0'; i++) { + temp[i-1] = word[i]; + } + temp[i-1] = word[0]; + strcpy( word, temp ); + break; + } + + i = strlen( word ); + word[i++] = 'm'; + for (int j=0; j <= alen; j++) { + word[i++] = 'a'; } - return std::round( sum_char * 100.0 / sum_all ); + word[i] = '\0'; } ``` -Well, here is the boring old algorithm after all. The compiler does seem to be letting me play fast and loose with any kind of numeric type, implicitly casting, which is convenient but also, seems like there could be some bugs from this. The most unique thing in my C++ solution is that I made a struct to hold the input data. How very object oriented of me +Finally here's that function that ties it all together -```cpp -struct char_and_string { - // a struct to hold a char, string for inputs - char character; - std::string string; -}; +```c +word_ll make_to_goat_latin( char *str ) { + word_ll list_head = build_word_chaff_ll( str ); + int word_ctr = 0; -std::ostream& operator<<( std::ostream& os, char_and_string const& cas ){ - // specifies how to pipe our struct into iostream - return os << "{ char='" << cas.character << "', string=\"" << cas.string << "\" }"; + for (word_ll *next = &list_head; next != NULL; next = next->next) { + if (word_ctr++ % 2) { + goatify_word(next->word, word_ctr/2); + } + } + return list_head; } ``` -I thought about making the core function a method on `char_and_string` but I didn't get there. Next time C++ comes around in my solutions I'll go all-in OO Programming, methods everywhere! +A better design would have not required `malloc`s inside the functions that are expected to get freed elsewhere. But this is a pretty small script, I opted for the mediocre solution. -### Rust +### Haskell -If it compiles it runs. Actually I had some logic bugs I nearly missed because I get into the compilation means success mindset. Is it partly out of frustration with how many tries it takes to get it to compile...? Anyway, I know Rust ain't gonna let me go around cast numerics implicitly, because it complained about using a `usize` as a `u32`. Safety! +I had a pretty clear vision of what I wanted to do in Haskell with this, which I stayed pretty true to. It is basically the same idea as with the C solution: split into word, non-word alternating list, run the goat mod on the words in order, stitch everything back together. As I was first working on this I built a tremendous amount of supporting infrastructure, including a `mapAdjacent` operator which takes a binary function and applies to every consecutive pair of values in a list. I even further modified that into `mapAdjacentR` and `mapAdjacentL` differing in which value is the first argument. Some days after, I was continuing to read Learn You a Haskell and realized I didn't need half of what I had written. What a shame! It was a good exercise, but being a diligent student, I cut out half my code and used the existing module functions. -My Rust function is most like my Perl solution. I keep it in the integer domain. +Here `groupBy` does the work of splitting our string into the alternating word, non-word list -```rust -fn percent_of_string_char_v2(string: &str, character: &char) -> usize { - let match_count = string.chars().filter(|c| c == character).count(); - let string_length = string.chars().count(); - let mc_100 = 100 * match_count; - let (div, remainder) = (mc_100 / string_length, mc_100 % string_length); - if remainder * 2 >= string_length { - div + 1 - } else { - div - } -} +```haskell +goatifyString string = + let words_list = groupBy ((==) `on` isAlpha) string + words_index = zip words_list [2 ..] + goated_words = map (\x -> goatifyWord (fst x) (div (snd x) 2)) words_index + in concat goated_words ``` -Not much to add to my Perl commentary here. Rust implicitly returns the last value so it's either `div + 1` or `div`. I could have left out the third `let` line, and baked that into the first one, but I thought it was a little clearer this way. +And then `goatifyWord` is defined as + +```haskell +goatifyWord :: String -> Int -> String +goatifyWord word count + | isVowel $ word !! 0 = + word ++ "ma" ++ (concat $ take count $ repeat "a") + | isAlpha $ word !! 0 = + (tail word) ++ [word !! 0] ++ "ma" ++ (concat $ take count $ repeat "a") + | otherwise = word +``` -## Challenge 2: B After A +That's basically it. -> Task 2: B After A</br> -> Submitted by: Mohammad Sajid Anwar</br> -> You are given a string, $str.</br> +### Julia + +In Julia I made some small use of regex to handle the words that start with consonants, but I still had to do something to handle the increasing length 'a' suffix. Here I have kind of combined the logic of both C functions (or Haskell functions) that splits the list, operates on the words, then stitches them back together. + +```julia +function make_to_goat_latin( str_in ) + word_chars = ['A':'Z';'a':'z'] + + current_word = [] + out_vec = [] + suffix = "ma" + + for char in str_in + if char in word_chars + push!( current_word, char ) + else + if size( current_word, 1 ) > 0 + suffix *= 'a' + word = join( current_word, "" ) + current_word = [] + word = replace( word, r"([^AEIOU])(.*)"i => s"\2\1" ) + push!( out_vec, word * suffix ) + end + push!( out_vec, char ) + end + end + join( out_vec, "" ) +end +``` + +Kind of a hacky mess, but it works and I'm learning + +## Challenge 2: Bus Route + +> Task 2: Bus Route</br> +> Submitted by: Peter Campbell Smith</br> +> Several bus routes start from a bus stop near my home, and go to the same stop in town. They each run to a set timetable, but they take different times to get into town.</br> > </br> -> Write a script to return true if there is at least one b, and no a appears after the first b.</br> +> Write a script to find the times - if any - I should let one bus leave and catch a strictly later one in order to get into town strictly sooner.</br> +> </br> +> An input timetable consists of the service interval, the offset within the hour, and the duration of the trip.</br> > </br> > Example 1</br> -> Input: $str = "aabb"</br> -> Output: true</br> +> Input: [ [12, 11, 41], [15, 5, 35] ]</br> +> Output: [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]</br> +> </br> +> Route 1 leaves every 12 minutes, starting at 11 minutes past the hour (so 11, 23, ...) and takes 41 minutes. Route 2 leaves every 15 minutes, starting at 5 minutes past (5, 20, ...) and takes 35 minutes.</br> +> </br> +> At 45 minutes past the hour I could take the route 1 bus at 47 past the hour, arriving at 28 minutes past the following hour, but if I wait for the route 2 bus at 50 past I will get to town sooner, at 25 minutes past the next hour.</br> +> </br> > Example 2</br> -> Input: $str = "abab"</br> -> Output: false</br> -> Example 3</br> -> Input: $str = "aaa"</br> -> Output: false</br> -> Example 4</br> -> Input: $str = "bbb"</br> -> Output: true - -Again, there's a simple algorithm that looks at every character in the string, we can even short circuit it if we fail early by seeing an 'a' after a 'b'. Let's look at the boring algorithm, this time in C++ - -```cpp -int validate_a_b_string_v1( std::string s ) { - // this is the boring algorithm that walks through each char in s - bool first_b = false; - for ( char sc : s ) { - // update if we've seen a 'b' yet - first_b |= ( sc == 'b' ); - if (first_b && sc == 'a') { - // if we see an 'a' after a 'b' then false - return false; - } - } - // if we haven't seen any 'b's then first_b is still false - return first_b; -} -``` - -Let's just say we're in C++ section already because this is all I did for C++. Oops! Like I said, next C++ time I'll go all in OO and make up for it. Also, this week I got distracted by some new insights into [PWC 270](https://theweeklychallenge.org/blog/perl-weekly-challenge-270/) which I'll mention below. - -### C++ +> Input: [ [12, 3, 41], [15, 9, 35], [30, 5, 25] ]</br> +> Output: [ 0, 1, 2, 3, 25, 26, 27, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 55, 56, 57, 58, 59 ]</br> -First we create a variable `first_b` to remember if we've seen any 'b' yet, because that determines how we proceed. Now for each char, if it's a 'b' `first_b` gets set to true. Next, if `first_b` is true, if we have seen a 'b' already, and the current character is 'a' then we can fail, return false. Otherwise, doesn't matter what character we see. Finally, if we make it through the string loop, if we have ever seen a 'b' then `first_b` is true, and success! otherwise `first_b` is false and failure, so we can just return `first_b`. +This was a problem that took some thought at first. As mentioned above, basically every time I implemented a solution I cut out a loop. ### Perl -The algorithm I came up with here is possibly as efficient as the C++ one, but in much more idomatic Perl (I think) so it's a win-win for my goals this week. +This is the ugliest of all my solutions, and closest to what I originally came up with. By the time of writing I know enough to make a better algorithm design, but out of sheer laziness I did not do that. Let's say this one is an artifact. + +The logic all happens in this monster subroutine ```perl -sub check_a_b_string( $string ) { - # find the first 'b' index - # if there's none (ie returns -1) then false - return 0 if 0 > ( my $first_b_idx = index $string, 'b' ); +sub find_switch_minutes( @schedules ) { + my $num_minutes = 60; + + my %next_depart; + my %next_arrive; + + for my $s (@schedules) { + my ($step, $start, $duration) = @$s; + + my @temp_vals = (-1) x ($num_minutes + $start + 1); + for (my $i = $start; $i < $num_minutes + $start + 1; $i+=$step) { + $temp_vals[$i] = [$i + $duration, $i]; + } + + # this is to backfill for the next arrival + my $fill_val = -1; + for (my $i = $#temp_vals; $i > -1; --$i) { + if ($temp_vals[$i] != -1) { + $fill_val = $temp_vals[$i]; + } else { + $temp_vals[$i] = $fill_val; + } + } + + # now find the minimum for arrival time and minimum for departure time + for my $i (0..$#temp_vals) { + my $val = $temp_vals[$i]; - # if there are any 'a's after the first 'b' then false - return 0 if ( substr $string, $first_b_idx ) =~ tr/a//; + if ($val == -1) { + last; + } - # otherwise true! - return 1; + if (not exists $next_depart{$i} or $val->[1] < $next_depart{$i}->[1]) { + $next_depart{$i} = $val; + } elsif ($val->[1] == $next_depart{$i}->[1] and $val->[0] < $next_arrive{$i}->[0]) { + $next_depart{$i} = $val; + } + + if (not exists $next_arrive{$i} or $val->[0] < $next_arrive{$i}->[0]) { + $next_arrive{$i} = $val; + } + } + } + + # now check if any earliest leave time isn't an earliest arrive time + my @indices; + for my $i (0..$num_minutes-1) { + if (not exists $next_arrive{$i} or not exists $next_depart{$i}) { + next; + } + if ($next_arrive{$i}->[0] < $next_depart{$i}->[0]) { + push @indices, $i; + } + } + + return @indices; } ``` -Using builtin functions there's some uncertainty about runtimes, but I'll assume index will stop searching the string once it finds the first match. If there's no match of a 'b' then we return false. Otherwise we use that index as a starting point for a substring, and check if there are any 'a's in there. Assuming that is linear in the size of the searched string, it essentially only needs to pass over the chars of the string one time. Efficient and sleek! It kind of flips the logic of the C++ version, there we check last if we ever saw a 'b', here we check that first. +First for every bus, I build an array mapping every minute to the next departing bus and its arrival time. Next I check against all bus schedules I have calculated, I keep a list of the next departing bus and its minimum arrival time (in case two buses depart at the same time), and the minimum arrival time. Finally I check if the minimum arrival time is strictly less than the minimum arrival time of the next departing bus(es). When I put it like that is sounds pretty simple. -### Rust +### C -In Rust for this challenge, I'm basically using the C++ algorithm, but I'm using match, and what is more idiomatic Rust than match?! Really Rust has a very powerful match operator, and you see it used often when unwrapping a function that returns a Result, which can be a value or an error, each wrapped inside of its own datatype. Here I'm just matching on the character value. +In C I was able to tighten things up a little -```rust -fn good_a_b_string(string: &str) -> bool { - let mut first_b = false; - for c in string.chars() { - match c { - // on 'b' then set first_b to true - 'b' => first_b |= true, - // on 'a', if we've seen a 'b' then false - 'a' => { - if first_b { - return false; - } - } - // otherwise no action, keep iterating - _ => continue, - } +```c +int find_switch_minutes( const input_tuple inputs[], const int num_inputs, int output[], const int num_minutes ) { + + time_tuple next_depart[num_minutes]; + time_tuple next_arrive[num_minutes]; + for (int i=0; i < num_minutes; i++) { + next_depart[i].arrive = next_arrive[i].arrive = 10000; + next_depart[i].depart = next_arrive[i].depart = 10000; + } + + for (int i=0; i < num_inputs; i++) { + const int step = inputs[i].step; + const int start = inputs[i].start; + const int duration = inputs[i].duration; + + time_tuple fill_val; + // now find the minimum for arrival time and minimum for departure time + for (int i = num_minutes + start; i > -1; --i) { + if ( (i - start) % step == 0 ) { + fill_val.arrive = i + duration; + fill_val.depart = i; + } + + if (i >= num_minutes) { + continue; + } + + if ( + (fill_val.depart < next_depart[i].depart) || + (fill_val.depart == next_depart[i].depart && fill_val.arrive < next_arrive[i].arrive) + ) { + next_depart[i].arrive = fill_val.arrive; + next_depart[i].depart = fill_val.depart; + } + + if (fill_val.arrive < next_arrive[i].arrive) { + next_arrive[i].arrive = fill_val.arrive; + next_arrive[i].depart = fill_val.depart; + } + } + } + + // now check if any earliest leave time isn't an earliest arrive time + int count_skips = 0; + for (int i=0; i < num_minutes; i++) { + if (next_arrive[i].arrive < next_depart[i].arrive) { + output[count_skips++] = i; } - // if we've seen a 'b' this is true - first_b + } + + return count_skips; } ``` -Match operates on a variable, here `c`. Each leg of the match represents a possible value, here just a simple value, and `_` is the else match, if it hasn't matched anything prior then it will match the on the `_` leg. +Instead of filling in the depart arrival schedule in one loop, and then find the minimu depart time and arrival in a separate loop, here I do them both in one pass. I go backward from the end time. If the minute is a departure minute I can calculate the new valid depart and arrival times, otherwise I just use the previous values. Also this way I don't need to build schedule arrays for each bus schedule, since I can use the depart, arrive values right away. Also here I pre-allocate the buffer for the return value and pass it to the function so I don't have to worry about hidden `malloc`s like I mentioned above. See. I learn as I go. The value I return from this function is the number of skip bus minutes I found, so I know how far into the pre-allocated array to read to get the results. + +### Haskell + +Here is where I really hit my stride. Usually when I am writing something in Haskell I have the feeling like, this is good enough to get the job done, but a real Haskeller would be sickened by this. But on this one, I feel like maybe I've done ok. I make heavy use of lazy evaluation, and I flipped the order of how I was thinking about the problem previously. + +To start I create some datatypes, to keep things neater + +```haskell +data Schedule = Schedule + { start :: Int, + step :: Int, + duration :: Int + } + deriving (Show) + +data TimeInfo = TimeInfo + { arrive :: Int, + depart :: Int + } + deriving (Show) +``` + +Then for a given schedule I can find is arrive, depart values for every minute + +```haskell +timeToArriveDepart :: Schedule -> Int -> TimeInfo +timeToArriveDepart ti t = + let factor = 1 + div (t - (start ti) - 1) (step ti) + depart = (start ti) + factor * (step ti) + in TimeInfo {depart = depart, arrive = depart + (duration ti)} + +getArriveDepartMinutes :: Schedule -> [TimeInfo] +getArriveDepartMinutes t = map (timeToArriveDepart t) [0 ..] +``` + +The logic here is that at time `t` I can figure out directly when the next bus will leave and arrive. There are two potential gotchas in this formula which I dodged without realizing, although one of them caused great confusion when I reimplemented this logic in Julia. But the formula: we can subtract the start time from the current time, then integer divide by the inter-bus-arrival time to find out when the next bus will leave. Multiply that plus one by the inter-arrival time and add back the start time. The first gotcha, that didn't come up in the test cases, is that this won't work if the start time is greater than the step time. I would have to put some different logic to handle that correctly. The second involves how integer division handles negatives. I have seen articles about this, but never thought it would affect me... See the Julia section for more. + +```haskell +checkSkipOne :: [TimeInfo] -> Bool +checkSkipOne tis = + let next_depart = minimum $ map (depart) tis + min_by_depart = minimum $ map (arrive) $ filter (\t -> (depart t) == next_depart) tis + min_by_arrive = minimum $ map (arrive) tis + in min_by_arrive < min_by_depart + +skipBuses :: [Schedule] -> [Int] +skipBuses tis = + let minute_infos = map (getArriveDepartMinutes) tis + skip_bools = take 60 $ map (checkSkipOne) $ transpose minute_infos + indexed_bools = zip skip_bools [0 ..] + in map (snd) $ filter (fst) indexed_bools +``` + +Next we write function to check if we should skip the next bus. We find the minimum depart time, then filter on all buses leaving at that time, taking the minimum arrival time. Next we take the minimum overall arrival time for the minute. If the former is greater than the latter, we've got a skip bus. Zip that map together with an index, filter on the true skip vals, and we have our answer. ### Julia -In Julia I use a couple of builtin functions. +I wrote my Julia solution last, after learning much from working out my Haskell solution. It has a similar breakdown ```julia -function validate_a_b( string ) - first_b = findfirst( 'b', string ) - last_a = findlast( 'a', string ) - # there is a 'b' - # and ( there are no 'a's or the last 'a' is before the first 'b' ) - first_b != nothing && ( last_a == nothing || last_a < first_b ) +function find_next_bus_info( t::Int, sched::Schedule ) + factor = 1 + div( ( t - sched.start - 1 ), sched.step, RoundDown ) + depart = factor * sched.step + sched.start + depart, depart + sched.duration end ``` -`findfirst` and `findlast` each return either the index of the found match or the value `nothing` if there was no match. We combine these into a long boolean expression that checks, from left to right, we found a 'b' and we found no 'a' or the last 'a' is before the first 'b'. Simple! +Here I basically copied over my function from Haskell, but I was getting odd results in the low index values. It took me a while to think about negative value integer division, and then I even got thrown off that track because of the way Haskell wants you to enter negative numbers (`(-1)` not just `-1`). But the bug came down to this. Haskell was doing integer division rounding down to minus infinity, while Julia by default was rounding to zero. So for example Haskell would take `div (-3) 4` and give `-1` while Julia would take `div(-3, 4)` and give `0`. Aiyahh! After much head scratching and doc reading I realized the issue, and Julia `div` takes an optional third arg telling it how to round negative values, and so I pass it the `RoundDown` arg above. -## Revisiting a 270 Problem - -Mohammad linked to a great discussion about Challenge 2 from [PWC 270](https://theweeklychallenge.org/blog/perl-weekly-challenge-270/) by [E. Choroba](https://blogs.perl.org/users/e_choroba/2024/06/equalise-an-array.html) which expanded my mind on the problem. If you haven't read it yet you should. On reading the problem description I didn't think about the possibility of all values ending up larger than they came in, and it sounds like nobody else among the official submissions did either, but he gives a case where the optimal solution requires this. +```julia +function find_switch_times( schedules ) + num_minutes = 60 + output = Vector{Int}() + + for t = 0:num_minutes - 1 + depart_arrive_vals = Vector{Tuple{Int, Int}}() + + for sched = schedules + push!( depart_arrive_vals, find_next_bus_info( t, sched ) ) + end + + if sort( depart_arrive_vals )[1][2] != sort( map( (x) -> x[2], depart_arrive_vals ) )[1] + push!( output, t ) + end + end + output +end +``` -I have been working on my own solution to the elucidated problem and some discussion thereof, but as of press time it is still in revision. I may try to finish it up this week, but definitely check out his blog post. +The rest of the code steps through the minutes and checks if it is a skip bus minute. Once I have the negative division bug worked out, the rest was no problem. ## Conclusion -Great work this week! I can't wait for the next problem set. +Well, here we are again. Another great set of challenges, some lackluster coding work on my part, and some sampling of my inner monologue as I look some code samples. Also, another week where I have come close, but not actually submitted any ideas for future PWCs. diff --git a/challenge-274/atschneid/c/ch-1.c b/challenge-274/atschneid/c/ch-1.c new file mode 100644 index 0000000000..941fa49369 --- /dev/null +++ b/challenge-274/atschneid/c/ch-1.c @@ -0,0 +1,89 @@ +# include <stdio.h> +# include <stdlib.h> +# include <string.h> + +typedef struct word_ll { + char *word; + struct word_ll *next; +} word_ll ; + +word_ll build_word_chaff_ll( char *str ) { + word_ll head = { .word=malloc( sizeof(char) * 100), .next=NULL }; + word_ll *next = &head; + + int status = 0; + size_t word_ctr = 0; |
