13 votes

Day 7: Camel Cards

Today's problem description: https://adventofcode.com/2023/day/7

Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python with any of the "short names" listed in this page of supported languages):

<details>
<summary>Part 1</summary>

```python
Your code here.
```

</details>

21 comments

  1. RheingoldRiver
    Link
    oof, I did terrible on this one, I read too quickly and used traditional poker rules first; then I realized I had a typo in my values dict where I gave Q 11 and then J 12 and then T 10, and THEN I...

    oof, I did terrible on this one, I read too quickly and used traditional poker rules first; then I realized I had a typo in my values dict where I gave Q 11 and then J 12 and then T 10, and THEN I had another mistake where I checked for 2 distinct values if there's 1 pair instead of 4. Took me an hour and a half for just part 1. This one was the first one where the example being so short really hurt me too, because that last mistake didn't affect the test result at all.

    Part 2 was easy afterwards, but I tried to be clever about it & then when I gave up and did it in a less-clever way I'd wasted 15 minutes already. Today took me longer than day 5 did.

    My final code is too long and bad with huge chunks commented & several unused functions to post here, but it's in my repo

    3 votes
  2. Hvv
    (edited )
    Link
    C++ I've been doing these with another group of folks, but since we're here I might as well try to share the stuff I write until it becomes too time consuming or I forget. Going back and cleaning...

    C++

    I've been doing these with another group of folks, but since we're here I might as well try to share the stuff I write until it becomes too time consuming or I forget.

    Going back and cleaning up the code takes quite a bit of effort (if you program fast enough your code becomes hieroglyphics), but I think it's now legible enough to stick here.

    Part 1 Explanation I saw the hand descriptions and started worrying that I'd have to write Poker rules, but luckily these rules aren't that bad.

    The first important thing we see is that you can distinguish almost all the hands using the most frequent card in that hand (5 of a kind > 4 of a kind > (full house/3 of a kind) > (two-pair/pair) > high card).
    Then, (full house/3 of a kind) and (two-pair/pair) can be distinguished using a variety of methods (e.g. second most frequent card). I used "number of distinct card ranks in hand" which sounds ridiculous but is surprisingly simple when expressed in code.

    Then, the tiebreaker is just comparing the hands in lexicographic order according to card strength (but without initially sorting by card strength, since I think poker does do that).

    Once we have a way of getting hand strength, the rest of the problem is just sorting the list of hands according to (hand strength, the actual cards in the hand) and then multiplying each hand's bet by the index of that hand (+1 if your arrays start at 0, or reversed if you sorted by descending rank). In this case I just used a C++ custom comparator (wrapped in a custom struct) since the standard sort function will happily take your < function if you provide one.

    Part 1 Code
    #include <bits/stdc++.h>
    using namespace std;
    unordered_map<char,int> ranks = {
    	{'A',0},
    	{'K',1},
    	{'Q',2},
    	{'J',3},
    	{'T',4},
    	{'9',5},
    	{'8',6},
    	{'7',7},
    	{'6',8},
    	{'5',9},
    	{'4',10},
    	{'3',11},
    	{'2',12},
    };
    
    // Assigns a numeric value to each hand, where lower = better
    // This does NOT account for tiebreakers.
    int evaluate_hand(const vector<int>& hand) {
    	map<int,int> card_count;
    	for(const auto& c: hand) {
    		card_count[c]++;
    	}
    	vector<int> rank_freqs;
    	for(const auto& it: card_count) {
    		rank_freqs.push_back(it.second);
    	}
    	sort(rank_freqs.begin(),rank_freqs.end());
    
    	if(rank_freqs.back() == 5) {
    		return 0;
    	} else if(rank_freqs.back() == 4) {
    		return 1;
    	} else if(rank_freqs.back() == 3) {
    		// There are only 2 partitions of 2:
    		// 1+1, 2
    		// so we only need to look at the size to determine if
    		// the hand is a full house (3+2) or a three-of-a-kind (3+1+1)
    		if(rank_freqs.size() == 2) {
    			return 2;
    		} else {
    			return 3;
    		}
    	} else if(rank_freqs.back() == 2) {
    		// Similarly there are only 3 partitions of 3:
    		// 1+1+1, 1+2, 3
    		// The last one is "impossible" by our construction so again we 
    		// only need size to distinguish between two-pair (1+2+2) and 
    		// pair (1+1+1+2)
    		if(rank_freqs.size() == 3) {
    			return 4;
    		} else {
    			return 5;
    		}
    	} else {
    		return 6;
    	}
    }
    
    // A Camel hand, annotated with its value.
    struct CamelHand {
    	int quality,val;
    	string hand;
    	CamelHand(const vector<int>& ranked_hand, int val): quality(evaluate_hand(ranked_hand)), val(val) {
    		for(const auto& card: ranked_hand) {
    			hand.push_back(card);
    		}
    	}
    	bool operator<(const CamelHand& ot) const {
    		if(quality != ot.quality) {return quality < ot.quality;}
    		// Why write a comparator for arrays in lexicographic order when
    		// you can convert it to a string and get that for free?
    		return hand < ot.hand;
    	}
    
    };
    int main() {
    	vector<CamelHand> hands;
    	string string_hand;
    	int hand_value;
    	while(cin >> string_hand >> hand_value) {
    		vector<int> ranked_hand;
    		for(const auto& c: string_hand) {
    			ranked_hand.push_back(ranks[c]);
    		}
    		hands.emplace_back(ranked_hand,hand_value);
    	}
    	sort(hands.begin(),hands.end());
    	long long ans = 0;
    	for(int i=0;i<hands.size();i++) {
    		ans += hands[i].val*(hands.size()-i);
    	}
    	cout << ans << '\n';
    }
    
    Part 2 Explanation Aside from changing the strength order a bit, the main question here is how to deal with the Jokers, which will change in a way to give the strongest hand.

    Funnily enough, you can brute force the transformation of the Jokers into every possible card (despite how awful that sounds, assuming each hand is distinct you can prove that you'll only have to evaluate at most 245 = 7962624 hands, which is merely kind of bad and runs in <1 second on my machine).

    But we can be smarter than that. Stealing our observation of distinguishing hands by their most frequent cards from before, we realize that a Joker always does best by converting to the most frequent card in the hand. Since my implementation already converts a hand into a list of card frequencies, this is almost* as simple as adding the number of jokers to the most frequent hand.

    *almost, of course, because the problem writers knew you would think of this and likely threw a hand of all Jokers at you.

    Part 2 Code
    #include <bits/stdc++.h>
    using namespace std;
    unordered_map<char,int> ranks = {
    	{'A',0},
    	{'K',1},
    	{'Q',2},
    	{'T',3},
    	{'9',4},
    	{'8',5},
    	{'7',6},
    	{'6',7},
    	{'5',8},
    	{'4',9},
    	{'3',10},
    	{'2',11},
    	{'J',12}
    };
    const int JOKER = 12;
    
    int evaluate_hand(const vector<int>& hand) {
    	map<int,int> card_count;
    	int joker_count = 0;
    	for(const auto& c: hand) {
    		if(c == JOKER) {
    			joker_count++;
    		} else {
    			card_count[c]++;
    		}
    	}
    	vector<int> rank_freqs;
    	for(const auto& it: card_count) {
    		rank_freqs.push_back(it.second);
    	}
    	sort(rank_freqs.begin(),rank_freqs.end());
    
    	// We love those all-joker hands!
    	if(rank_freqs.empty()) {
    		rank_freqs.push_back(joker_count);
    	} else {
    		rank_freqs.back() += joker_count;
    	}
    
    	if(rank_freqs.back() == 5) {
    		return 0;
    	} else if(rank_freqs.back() == 4) {
    		return 1;
    	} else if(rank_freqs.back() == 3) {
    		if(rank_freqs.size() == 2) {
    			return 2;
    		} else {
    			return 3;
    		}
    	} else if(rank_freqs.back() == 2) {
    		if(rank_freqs.size() == 3) {
    			return 4;
    		} else {
    			return 5;
    		}
    	} else {
    		return 6;
    	}
    }
    
    struct CamelHand {
    	int quality,val;
    	string hand;
    	CamelHand(const vector<int>& ranked_hand, int val): quality(evaluate_hand(ranked_hand)), val(val) {
    		for(const auto& card: ranked_hand) {
    			hand.push_back(card);
    		}
    	}
    	bool operator<(const CamelHand& ot) const {
    		if(quality != ot.quality) {return quality < ot.quality;}
    		return hand < ot.hand;
    	}
    
    };
    int main() {
    	vector<CamelHand> hands;
    	string string_hand;
    	int hand_value;
    	while(cin >> string_hand >> hand_value) {
    		vector<int> ranked_hand;
    		for(const auto& c: string_hand) {
    			ranked_hand.push_back(ranks[c]);
    		}
    		hands.emplace_back(ranked_hand,hand_value);
    	}
    	sort(hands.begin(),hands.end());
    	long long ans = 0;
    	for(int i=0;i<hands.size();i++) {
    		ans += hands[i].val*(hands.size()-i);
    	}
    	cout << ans << '\n';
    }
    
    2 votes
  3. lily
    Link
    This was a fun problem, though I think I might have overcomplicated things a bit. I always like to try to write a single program that does both part 1 and 2 at once if possible, and that was a...

    This was a fun problem, though I think I might have overcomplicated things a bit. I always like to try to write a single program that does both part 1 and 2 at once if possible, and that was a little trickier than usual today. Nevertheless, I'm glad the problems are more interesting now. Today's still wasn't too difficult (especially compared to day 5), but it wasn't just parsing the input like the first few. Part 2 probably could have changed things up a bit more, though.

    Solution
    # Advent of Code 2023
    # Day 7: Camel Cards
    
    from copy import deepcopy
    from functools import cmp_to_key
    
    special_cards = {
        "T": 10,
        "J": 11,
        "Q": 12,
        "K": 13,
        "A": 14
    }
    
    hands_part_1 = []
    
    with open("inputs/day_7.txt") as file:
        for line in file:
            halves = line.split(" ")
            hands_part_1.append({
                "cards": [
                    int(card) if card in "123456789" else special_cards[card]
                    for card in halves[0]
                ],
                "bid": int(halves[1])
            })
    
    hands_part_2 = deepcopy(hands_part_1)
    
    for hand in hands_part_2:
        hand["cards"] = [0 if card == 11 else card for card in hand["cards"]]
    
    def find_type(cards):
        set_ = set(cards)
        unique_cards = len(set_)
    
        if unique_cards == 1:
            return 0
        elif unique_cards == 2:
            if cards.count(cards[0]) in (1, 4):
                return 1
            else:
                return 2
        elif unique_cards == 3:
            if any(cards.count(card) == 3 for card in set_):
                return 3
            elif any(cards.count(card) == 2 for card in set_):
                return 4
        elif unique_cards == 4:
            return 5
        else:
            return 6
    
    for hand in hands_part_1:
        hand["type"] = find_type(hand["cards"])
    
    for hand in hands_part_2:
        if 0 in hand["cards"]:
            hand["type"] = None
    
            for new_card in range(15):
                if new_card != 0:
                    modified_cards = [
                        new_card if card == 0 else card for card in hand["cards"]
                    ]
    
                    type_ = find_type(modified_cards)
    
                    if hand["type"] is None or type_ < hand["type"]:
                        hand["type"] = type_
        else:
            hand["type"] = find_type(hand["cards"])
    
    def compare_hands(hand_1, hand_2):
        if hand_1["type"] < hand_2["type"]:
            return 1
        elif hand_2["type"] < hand_1["type"]:
            return -1
        else:
            for card_1, card_2 in zip(hand_1["cards"], hand_2["cards"]):
                if card_1 > card_2:
                    return 1
                elif card_2 > card_1:
                    return -1
    
            return 0
    
    key = cmp_to_key(compare_hands)
    hands_part_1.sort(key=key)
    hands_part_2.sort(key=key)
    
    print(f"""Part 1: {
        sum(hand['bid'] * (i + 1) for i, hand in enumerate(hands_part_1))
    }""")
    
    print(f"""Part 2: {
        sum(hand['bid'] * (i + 1) for i, hand in enumerate(hands_part_2))
    }""")
    
    2 votes
  4. [2]
    pnutzh4x0r
    Link
    Language: Python This was fun. More enjoyable than I initially thought (though I've done card sorting code before). Part 1 This was pretty straightforward: create a histogram of the cards in each...

    Language: Python

    This was fun. More enjoyable than I initially thought (though I've done card sorting code before).

    Part 1

    This was pretty straightforward: create a histogram of the cards in each hand to determine their type, and if there is a tie-breaker, compare each card pairwise. I use the Counter class from collections to do the counting, and then had a dictionary/table to convert labels to numeric values for comparison. I used a very OOP approach and wrote a magic method for comparing hands and used that with Python's builtin sort. I even got to use Enum!

    LABELS = {l: v for v, l in enumerate('23456789TJQKA', 2)}
    
    class HandType(IntEnum):
        FIVE_OF_A_KIND  = 6
        FOUR_OF_A_KIND  = 5
        FULL_HOUSE      = 4
        THREE_OF_A_KIND = 3
        TWO_PAIR        = 2
        ONE_PAIR        = 1
        HIGH_CARD       = 0
    
    class Hand:
        def __init__(self, cards=str, bid=str):
            self.cards  = cards
            self.bid    = int(bid)
            counts      = Counter(self.cards)
            self.type   = (
                HandType.FIVE_OF_A_KIND  if len(counts) == 1 else
                HandType.FOUR_OF_A_KIND  if len(counts) == 2 and any(l for l, count in counts.items() if count == 4) else
                HandType.FULL_HOUSE      if len(counts) == 2 and any(l for l, count in counts.items() if count == 3) else
                HandType.THREE_OF_A_KIND if len(counts) == 3 and any(l for l, count in counts.items() if count == 3) else
                HandType.TWO_PAIR        if len(counts) == 3 and any(l for l, count in counts.items() if count == 2) else
                HandType.ONE_PAIR        if len(counts) == 4 and any(l for l, count in counts.items() if count == 2) else
                HandType.HIGH_CARD
            )
    
        def __lt__(self, other):
            if self.type == other.type:
                for s_label, o_label in zip(self.cards, other.cards):
                    if LABELS[s_label] == LABELS[o_label]:
                        continue
                    return LABELS[s_label] &lt; LABELS[o_label]
                return False
            return self.type &lt; other.type
    
        def __repr__(self):
            return f'Hand(cards={self.cards},bid={self.bid},type={self.type})'
    
    def read_hands(stream=sys.stdin) -> list[Hand]:
        return [Hand(*line.split()) for line in stream]
    
    def main(stream=sys.stdin) -> None:
        hands    = sorted(read_hands(stream))
        winnings = sum(rank * hand.bid for rank, hand in enumerate(hands, 1))
        print(winnings)
    
    Part 2

    For the second part, I just had to add some post-processing code to convert the jokers into actual cards. The key insight is to find the highest and most numerous non-Joker card and convert all the Jokers to that card label.

    This had two edge cases that tripped me up:

    1. 'JJJJJ': There is no other non-Joker here, so I messed up and ranked this the lowest because I ended up removing all counts.

    2. 'JJJ12': This also messed me up b/c the Joker was the most numerous card, and I didn't handle that properly.

    Once I fixed the post-processing code though, everything else remained the same. Below, I only show the parts that changed from Part A.

    LABELS = {l: v for v, l in enumerate('J23456789TQKA', 1)}
    
    ...
    
    class Hand:
        def __init__(self, cards=str, bid=str):
            self.cards  = cards
            self.bid    = int(bid)
            counts      = Counter(self.cards)
    
            if 'J' in counts and len(counts) > 1:
                max_label = max(set(counts) - {'J'}, key=lambda l: (counts[l], LABELS[l]))
                counts[max_label] += counts['J']
                del counts['J']
    
            self.type   = (...)
    

    GitHub Repo

    2 votes
    1. tjf
      (edited )
      Link Parent
      Very OOP I love it! I chose to write a cmp-style comparison function instead of operator overloading. I see we both thought to use collections.Counter.

      Very OOP I love it! I chose to write a cmp-style comparison function instead of operator overloading. I see we both thought to use collections.Counter.

      2 votes
  5. [4]
    DataWraith
    Link
    I started even later than usual, but this one was fun! In fact, it felt like the easiest out of all the puzzles so far. (Rust) Data structures use std::collections::HashMap; #[derive(PartialEq,...

    I started even later than usual, but this one was fun! In fact, it felt like the easiest out of all the puzzles so far.

    (Rust)

    Data structures
    use std::collections::HashMap;
    
    #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash)]
    pub enum Card {
        Ace = 14,
        King = 13,
        Queen = 12,
        JackOrJoker = 11,
        Ten = 10,
        Nine = 9,
        Eight = 8,
        Seven = 7,
        Six = 6,
        Five = 5,
        Four = 4,
        Three = 3,
        Two = 2,
    }
    
    impl From<char> for Card {
        fn from(c: char) -> Self {
            match c {
                'A' => Card::Ace,
                'K' => Card::King,
                'Q' => Card::Queen,
                'J' => Card::JackOrJoker,
                'T' => Card::Ten,
                '9' => Card::Nine,
                '8' => Card::Eight,
                '7' => Card::Seven,
                '6' => Card::Six,
                '5' => Card::Five,
                '4' => Card::Four,
                '3' => Card::Three,
                '2' => Card::Two,
                _ => panic!("Invalid card"),
            }
        }
    }
    
    #[derive(PartialEq, Eq, Clone, Debug)]
    pub struct Hand {
        pub cards: [Card; 5],
        pub bid: usize,
    }
    
    impl Ord for Hand {
        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
            (self.classify(), self.cards).cmp(&(other.classify(), other.cards))
        }
    }
    
    impl PartialOrd for Hand {
        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
            Some(self.cmp(other))
        }
    }
    
    #[derive(PartialEq, Eq, Clone, Copy, Debug, PartialOrd, Ord)]
    pub enum HandType {
        FiveOfAKind = 6,
        FourOfAKind = 5,
        FullHouse = 4,
        ThreeOfAKind = 3,
        TwoPair = 2,
        OnePair = 1,
        HighCard = 0,
    }
    
    impl Hand {
        pub fn classify(&self) -> HandType {
            let c = &self.cards;
            let mut s = HashMap::new();
    
            for card in c.iter() {
                let count = s.entry(card).or_insert(0);
                *count += 1;
            }
    
            match s.len() {
                // AAAAA
                1 => HandType::FiveOfAKind,
    
                2 => {
                    // AAAAB
                    if s.values().any(|&x| x == 4) {
                        return HandType::FourOfAKind;
                    }
    
                    // AAABB
                    HandType::FullHouse
                }
    
                3 => {
                    // AAABC
                    if s.values().any(|&x| x == 3) {
                        return HandType::ThreeOfAKind;
                    }
    
                    // AABBC
                    HandType::TwoPair
                }
    
                // AABCD
                4 => HandType::OnePair,
                // ABCDE
                5 => HandType::HighCard,
    
                _ => unreachable!("Only five cards to a hand."),
            }
        }
    }
    
    Parser
    use nom::{
        character::complete::{newline, space1},
        IResult,
    };
    
    use super::ds::{Card, Hand};
    
    pub fn parse(input: &str) -> IResult<&str, Vec<Hand>> {
        let (input, hands) = nom::multi::many1(parse_line)(input)?;
    
        Ok((input, hands))
    }
    
    fn parse_line(input: &str) -> IResult<&str, Hand> {
        let (input, cards) = nom::multi::count(parse_card, 5)(input)?;
        let (input, _) = space1(input)?;
        let (input, bid) = parse_usize(input)?;
        let (input, _) = newline(input)?;
    
        let cards = cards.clone();
    
        Ok((
            input,
            Hand {
                cards: [cards[0], cards[1], cards[2], cards[3], cards[4]],
                bid,
            },
        ))
    }
    
    fn parse_card(input: &str) -> IResult<&str, Card> {
        let (input, card) = nom::character::complete::one_of("AKQJT98765432")(input)?;
        let card = Card::from(card);
    
        Ok((input, card))
    }
    
    fn parse_usize(input: &str) -> IResult<&str, usize> {
        let (input, num) = nom::character::complete::digit1(input)?;
        let num = num.parse::<usize>().unwrap();
    
        Ok((input, num))
    }
    
    Part 1
    use super::parser;
    
    pub fn part1(input: &str) -> usize {
        let mut hands = parser::parse(input).unwrap().1;
    
        hands.sort();
    
        hands
            .iter()
            .enumerate()
            .fold(0, |acc, (i, hand)| acc + (i + 1) * hand.bid)
    }
    
    Part 2

    I should probably have used a brute-force solution here, because going through
    the rules multiple-times to figure out what to classify each hand took a bit of time.

    use std::collections::HashMap;
    
    use crate::{ds::HandType, parser};
    
    use super::ds::{Card, Hand};
    
    pub fn part2(input: &str) -> usize {
        let hands = parser::parse(input).unwrap().1;
    
        let mut hands2 = hands.into_iter().map(Part2Hand).collect::<Vec<_>>();
    
        hands2.sort();
    
        hands2
            .iter()
            .enumerate()
            .fold(0, |acc, (i, hand)| acc + (i + 1) * hand.0.bid)
    }
    
    #[derive(Eq, PartialEq, Clone, Copy, Debug)]
    pub struct Part2Card(Card);
    
    impl Ord for Part2Card {
        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
            match (self.0, other.0) {
                (Card::JackOrJoker, Card::JackOrJoker) => std::cmp::Ordering::Equal,
                (Card::JackOrJoker, _) => std::cmp::Ordering::Less,
                (_, Card::JackOrJoker) => std::cmp::Ordering::Greater,
                _ => self.0.cmp(&other.0),
            }
        }
    }
    
    impl PartialOrd for Part2Card {
        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
            Some(self.cmp(other))
        }
    }
    
    #[derive(Eq, PartialEq, Clone, Debug)]
    pub struct Part2Hand(Hand);
    
    impl Part2Hand {
        pub fn classify(&self) -> HandType {
            let c = &self.0.cards;
            let mut s = HashMap::new();
    
            for card in c.iter() {
                let count = s.entry(card).or_insert(0);
                *count += 1;
            }
    
            let num_jokers = *s.get(&Card::JackOrJoker).unwrap_or(&0);
    
            if num_jokers == 0 {
                return self.0.classify();
            }
    
            match s.len() {
                // AAAAA
                1 => HandType::FiveOfAKind,
                2 => HandType::FiveOfAKind,
                3 => {
                    // AAABC
                    if s.values().any(|&x| x == 3) {
                        return HandType::FourOfAKind;
                    }
    
                    // AABBC
                    if num_jokers == 1 {
                        return HandType::FullHouse;
                    }
    
                    HandType::FourOfAKind
                }
                4 => {
                    // AABCD
                    if s.values().filter(|&x| *x == 2).count() == 1 {
                        if num_jokers == 1 || num_jokers == 2 {
                            return HandType::ThreeOfAKind;
                        }
    
                        return HandType::TwoPair;
                    }
    
                    // AABBC
                    if num_jokers == 1 {
                        return HandType::FullHouse;
                    }
    
                    HandType::FourOfAKind
                }
                5 => HandType::OnePair,
                _ => unreachable!("Only five cards to a hand."),
            }
        }
    }
    
    impl Ord for Part2Hand {
        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
            (
                self.classify(),
                self.0
                    .cards
                    .iter()
                    .map(|c| Part2Card(*c))
                    .collect::<Vec<_>>(),
            )
                .cmp(&(
                    other.classify(),
                    other
                        .0
                        .cards
                        .iter()
                        .map(|c| Part2Card(*c))
                        .collect::<Vec<_>>(),
                ))
        }
    }
    
    impl PartialOrd for Part2Hand {
        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
            Some(self.cmp(other))
        }
    }
    
    Performance
    Benchmark 1: target/release/day-07
      Time (mean ± σ):      11.3 ms ±   1.7 ms    [User: 10.7 ms, System: 1.3 ms]
      Range (min … max):     6.4 ms …  16.8 ms    288 runs
    
    2 votes
    1. [3]
      wycy
      Link Parent
      I like your solution. One thing I'm confused about: how does this part work? My custom sorting was much clunkier.

      impl Ord for Hand {
      fn cmp(&self, other: &Self) -> std::cmp::Ordering {
      (self.classify(), self.cards).cmp(&(other.classify(), other.cards))
      }
      }

      I like your solution. One thing I'm confused about: how does this part work? My custom sorting was much clunkier.

      2 votes
      1. [2]
        DataWraith
        (edited )
        Link Parent
        This is simply a comparison of two 2-tuples, because Rust automatically compares these from left to right when using cmpon them. The first element of the tuple is the HandType returned by...

        This is simply a comparison of two 2-tuples, because Rust automatically compares these from left to right when using cmpon them. The first element of the tuple is the HandType returned by self.classify(), and since that itself is Ord, it gets sorted in the right order, from low to high. self.cards is the [Card; 5]array. Same as with the tuple, Rust automatically compares the array entries from left to right, and as with HandType, Card implements Ord, so it automatically gets sorted lexicographically. In Part 2 I just wrap the Hand struct in a newtype and override the Ord implementation to account for the new rules, though it is very clunky to convert the individual cards to Part2Card.

        I later realized that I probably didn't even need to implement Ord because you can just do something like hands.sort_by_cached_key(|k| (k.classify(), k.cards)).

        2 votes
        1. wycy
          Link Parent
          I see, I didn’t know about the automatic sorting left to right. Very handy for this.

          I see, I didn’t know about the automatic sorting left to right. Very handy for this.

          1 vote
  6. csos95
    Link
    Part one took me a while because I assumed something that wasn't in the wording of the problem. Once I realized that, it was a quick fix and part two didn't take long. What I assumed I assumed...

    Part one took me a while because I assumed something that wasn't in the wording of the problem.
    Once I realized that, it was a quick fix and part two didn't take long.

    What I assumed I assumed that the cards in the hand needed to be sorted, not left in the order they appear in.
    Rust
    use std::collections::HashMap;
    
    #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    enum Card1 {
        Two,
        Three,
        Four,
        Five,
        Six,
        Seven,
        Eight,
        Nine,
        Ten,
        Jack,
        Queen,
        King,
        Ace,
    }
    
    #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
    enum Hand {
        High,
        OnePair,
        TwoPair,
        Three,
        Full,
        Four,
        Five,
    }
    
    fn hand_type_part1(cards: &[Card1]) -> Hand {
        use Hand::*;
        let mut hand: HashMap<Card1, usize> = HashMap::new();
        for card in cards {
            *hand.entry(*card).or_default() += 1;
        }
        match hand.len() {
            5 => High,
            4 => OnePair,
            3 if *hand.values().max().unwrap() == 2 => TwoPair,
            3 if *hand.values().max().unwrap() == 3 => Three,
            2 if *hand.values().max().unwrap() == 3 => Full,
            2 if *hand.values().max().unwrap() == 4 => Four,
            1 => Five,
            _ => unreachable!(),
        }
    }
    
    #[aoc(day7, part1)]
    fn part1(input: &str) -> usize {
        let mut hands: Vec<(Hand, Vec<Card1>, usize)> = input
            .lines()
            .map(|line| {
                let mut parts = line.split(" ");
                let cards: Vec<Card1> = parts
                    .next()
                    .unwrap()
                    .chars()
                    .map(|c| {
                        use Card1::*;
                        match c {
                            '2' => Two,
                            '3' => Three,
                            '4' => Four,
                            '5' => Five,
                            '6' => Six,
                            '7' => Seven,
                            '8' => Eight,
                            '9' => Nine,
                            'T' => Ten,
                            'J' => Jack,
                            'Q' => Queen,
                            'K' => King,
                            'A' => Ace,
                            _ => unreachable!(),
                        }
                    })
                    .collect();
    
                let hand = hand_type_part1(&cards);
                let bid: usize = parts.next().unwrap().parse().unwrap();
                (hand, cards, bid)
            })
            .collect();
    
        hands.sort();
    
        hands
            .into_iter()
            .enumerate()
            .map(|(i, (_, _, bid))| (i + 1) * bid)
            .sum()
    }
    
    #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    enum Card2 {
        Joker,
        Two,
        Three,
        Four,
        Five,
        Six,
        Seven,
        Eight,
        Nine,
        Ten,
        Queen,
        King,
        Ace,
    }
    
    fn hand_type_part2(cards: &[Card2]) -> Hand {
        use Hand::*;
        let mut hand: HashMap<Card2, usize> = HashMap::new();
        for card in cards {
            *hand.entry(*card).or_default() += 1;
        }
    
        let jokers = *hand.get(&Card2::Joker).unwrap_or(&0);
    
        let max = *hand.values().max().unwrap();
    
        match hand.len() {
            // 0, 1
            5 if jokers == 0 => High,
            5 => OnePair,
            // 0, 1, 2
            4 if jokers == 0 => OnePair,
            4 if jokers == 1 => Three,
            4 if jokers == 2 => Three,
            // 0, 1, 2
            3 if max == 2 && jokers == 0 => TwoPair,
            3 if max == 2 && jokers == 1 => Full,
            3 if max == 2 && jokers == 2 => Four,
            // 0, 1, 3
            3 if max == 3 && jokers == 0 => Three,
            3 if max == 3 && jokers == 1 => Four,
            3 if max == 3 && jokers == 3 => Four,
            // 0, 2, 3
            2 if max == 3 && jokers == 0 => Full,
            2 if max == 3 && jokers == 2 => Five,
            2 if max == 3 && jokers == 3 => Five,
            // 0, 1, 4
            2 if max == 4 && jokers == 0 => Four,
            2 if max == 4 && jokers == 1 => Five,
            2 if max == 4 && jokers == 4 => Five,
            // 0, 5
            1 => Five,
            _ => unreachable!(),
        }
    }
    
    #[aoc(day7, part2)]
    fn part2(input: &str) -> usize {
        let mut hands: Vec<(Hand, Vec<Card2>, usize)> = input
            .lines()
            .map(|line| {
                let mut parts = line.split(" ");
                let cards: Vec<Card2> = parts
                    .next()
                    .unwrap()
                    .chars()
                    .map(|c| {
                        use Card2::*;
                        match c {
                            'J' => Joker,
                            '2' => Two,
                            '3' => Three,
                            '4' => Four,
                            '5' => Five,
                            '6' => Six,
                            '7' => Seven,
                            '8' => Eight,
                            '9' => Nine,
                            'T' => Ten,
                            'Q' => Queen,
                            'K' => King,
                            'A' => Ace,
                            _ => unreachable!(),
                        }
                    })
                    .collect();
    
                let hand = hand_type_part2(&cards);
                let bid: usize = parts.next().unwrap().parse().unwrap();
                (hand, cards, bid)
            })
            .collect();
    
        hands.sort();
    
        hands
            .into_iter()
            .enumerate()
            .map(|(i, (_, _, bid))| (i + 1) * bid)
            .sum()
    }
    
    2 votes
  7. Toric
    Link
    Took a bit of thinking in order to classify the hand types elegantly, but I think I came up with a pretty elegant solution based on the number of types of cards in the hand and the number of the...

    Took a bit of thinking in order to classify the hand types elegantly, but I think I came up with a pretty elegant solution based on the number of types of cards in the hand and the number of the most common card.

    The joker was pretty straightforward once I realized it was always advantageous to add the joker to the card you already have the most of.

    https://git.venberg.xyz/Gabe/advent_of_code_2023/src/branch/main/days/day07

    2 votes
  8. wycy
    (edited )
    Link
    Rust Discussion (spoilers) I implemented a custom sorting for my struct, so I couldn't think of a way to have both parts cleanly in 1, so part 2 is a copy/paste of part 1 with slight modifications...

    Rust

    Discussion (spoilers) I implemented a custom sorting for my struct, so I couldn't think of a way to have both parts cleanly in 1, so part 2 is a copy/paste of part 1 with slight modifications to the custom sorting.
    Part 1
    use std::env;
    use std::io::{self, prelude::*, BufReader};
    use std::fs::File;
    use std::collections::HashMap;
    use std::cmp::Ordering;
    
    #[derive(PartialEq,Eq)]
    struct Hand {
        cards: Vec<usize>,
        bid: usize,
    }
    impl From<&String> for Hand {
        fn from(s: &String) -> Self {
            let parts: Vec<_> = s.split_whitespace().collect();
            Self {
                cards: card_values(parts[0]),
                bid: parts[1].parse().unwrap(),
            }
        }
    }
    impl Hand {
        pub fn hand_type(&self) -> HandType {
            // Generate card counts
            let mut card_counts: HashMap<usize,usize> = HashMap::new();
            for card in &self.cards {
                *card_counts.entry(*card).or_insert(0) += 1;
            }
    
            // Determine type
            let mut counts: Vec<_> = card_counts.iter().map(|(_,ct)| ct).collect();
            counts.sort_by(|a,b| b.cmp(a));
            let max_kind = counts[0];
    
            if *max_kind  == 5usize { return HandType::FiveOfAKind; }
            if *max_kind  == 4usize { return HandType::FourOfAKind; }
            if *max_kind  == 3usize
            && *counts[1] == 2usize { return HandType::FullHouse; }
            if *max_kind  == 3usize { return HandType::ThreeOfAKind; }
            if *max_kind  == 2usize
            && *counts[1] == 2usize { return HandType::TwoPair; }
            if *max_kind  == 2usize { return HandType::OnePair; }
            return HandType::HighCard;
        }
    }
    impl Ord for Hand {
        fn cmp(&self, other: &Self) -> Ordering {
            if self.hand_type() != other.hand_type() {
                return (self.hand_type() as usize).cmp(&(other.hand_type() as usize));
            }
            // Hand kinds are equal
            for (card_self,card_other) in self.cards.iter().zip(other.cards.iter()) {
                if card_self != card_other { return card_self.cmp(card_other); }
            }
            panic!("Unable to sort cards");
        }
    }
    impl PartialOrd for Hand {
        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
            Some(self.cmp(other))
        }
    }
    
    fn value_from_char(c: char) -> usize {
        match c.to_digit(10) {
            Some(num) => num as usize,
            None => match c {
                'T' => 10,
                'J' => 11,
                'Q' => 12,
                'K' => 13,
                'A' => 14,
                other => panic!("Unexpected card: {other}"),
            }
        }
    }
    
    fn card_values(cards: &str) -> Vec<usize> {
        cards.chars().map(|c| value_from_char(c)).collect()
    }
    
    #[derive(Debug,Eq,PartialEq)]
    enum HandType {
        FiveOfAKind = 7,
        FourOfAKind = 6,
        FullHouse = 5,
        ThreeOfAKind = 4,
        TwoPair = 3,
        OnePair = 2,
        HighCard = 1,
    }
    
    fn solve(input: &str) -> io::Result<()> {
        let file = File::open(input).expect("Input file not found.");
        let reader = BufReader::new(file);
    
        // Input
        let input: Vec<String> = match reader.lines().collect() {
            Err(err) => panic!("Unknown error reading input: {}", err),
            Ok(result) => result,
        };
        let mut hands: Vec<_> = input.iter().map(Hand::from).collect();
        hands.sort();
    
        // Part 1
        let part1: usize = hands.iter().enumerate().map(|(rank,hand)| (rank+1) * hand.bid).sum();
        println!("Part 1: {part1}"); // 251136060
    
        Ok(())
    }
    
    fn main() {
        let args: Vec<String> = env::args().collect();
        let filename = &args[1];
        solve(&filename).unwrap();
    }
    
    Part 2
    use std::env;
    use std::io::{self, prelude::*, BufReader};
    use std::fs::File;
    use std::collections::HashMap;
    use std::cmp::Ordering;
    
    #[derive(PartialEq,Eq)]
    struct Hand {
        cards: Vec<usize>,
        bid: usize,
        card_str: String,
    }
    impl From<&String> for Hand {
        fn from(s: &String) -> Self {
            let parts: Vec<_> = s.split_whitespace().collect();
            Self {
                cards: card_values(parts[0]),
                bid: parts[1].parse().unwrap(),
                card_str: s.to_string(),
            }
        }
    }
    impl Hand {
        pub fn hand_type(&self) -> HandType {
            // Generate card counts
            let mut card_counts: HashMap<usize,usize> = HashMap::new();
            for card in &self.cards {
                *card_counts.entry(*card).or_insert(0) += 1;
            }
    
            // Determine type
            let mut counts: Vec<_> = card_counts.iter().filter(|&(c,_)| *c != 0usize).map(|(_,ct)| ct).collect();
            let jokers = card_counts.get(&0).unwrap_or(&0);
            counts.sort_by(|a,b| b.cmp(a));
            let max_kind = if counts.len() != 0 { counts[0] + jokers } else { *jokers };
    
            if  max_kind  == 5usize { return HandType::FiveOfAKind; }
            if  max_kind  == 4usize { return HandType::FourOfAKind; }
            if  max_kind  == 3usize
            && *counts[1] == 2usize { return HandType::FullHouse; }
            if  max_kind  == 3usize { return HandType::ThreeOfAKind; }
            if  max_kind  == 2usize
            && *counts[1] == 2usize { return HandType::TwoPair; }
            if  max_kind  == 2usize { return HandType::OnePair; }
            return HandType::HighCard;
        }
    }
    impl Ord for Hand {
        fn cmp(&self, other: &Self) -> Ordering {
            if self.hand_type() != other.hand_type() {
                return (self.hand_type() as usize).cmp(&(other.hand_type() as usize));
            }
            // Hand kinds are equal
            for (card_self,card_other) in self.cards.iter().zip(other.cards.iter()) {
                if card_self != card_other { return card_self.cmp(card_other); }
            }
            panic!("Unable to sort cards");
        }
    }
    impl PartialOrd for Hand {
        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
            Some(self.cmp(other))
        }
    }
    
    fn value_from_char(c: char) -> usize {
        match c.to_digit(10) {
            Some(num) => num as usize,
            None => match c {
                'T' => 10,
                'J' =>  0,
                'Q' => 12,
                'K' => 13,
                'A' => 14,
                other => panic!("Unexpected card: {other}"),
            }
        }
    }
    
    fn card_values(cards: &str) -> Vec<usize> {
        cards.chars().map(|c| value_from_char(c)).collect()
    }
    
    #[derive(Debug,Eq,PartialEq)]
    enum HandType {
        FiveOfAKind = 7,
        FourOfAKind = 6,
        FullHouse = 5,
        ThreeOfAKind = 4,
        TwoPair = 3,
        OnePair = 2,
        HighCard = 1,
    }
    
    fn solve(input: &str) -> io::Result<()> {
        let file = File::open(input).expect("Input file not found.");
        let reader = BufReader::new(file);
    
        // Input
        let input: Vec<String> = match reader.lines().collect() {
            Err(err) => panic!("Unknown error reading input: {}", err),
            Ok(result) => result,
        };
        let mut hands: Vec<_> = input.iter().map(Hand::from).collect();
        hands.sort();
    
        // Part 2
        let part2: usize = hands.iter().enumerate().map(|(rank,hand)| (rank+1) * hand.bid).sum();
        println!("Part 2: {part2}"); // 249400220
    
        Ok(())
    }
    
    fn main() {
        let args: Vec<String> = env::args().collect();
        let filename = &args[1];
        solve(&filename).unwrap();
    }
    
    2 votes
  9. tjf
    Link
    Posting this a day late, but here are my Python solutions. Got to use some stdlib goodies like functools.cmp_to_key and collections.Counter! Part 1 #!/usr/bin/env pypy3 from collections import...

    Posting this a day late, but here are my Python solutions. Got to use some stdlib goodies like functools.cmp_to_key and collections.Counter!

    Part 1
    #!/usr/bin/env pypy3
    
    from collections import Counter
    from functools import cmp_to_key
    
    CARDS = '23456789TJQKA'
    HAND_TYPES = ([1, 1, 1, 1, 1], [1, 1, 1, 2], [1, 2, 2],
                  [1, 1, 3], [2, 3], [1, 4], [5])
    
    def hand_type(h):
        c = Counter(h)
        return HAND_TYPES.index(sorted(c.values()))
    
    def cmp_hands(h1, h2):
        if hand_type(h1) < hand_type(h2):
            return -1
        if hand_type(h1) > hand_type(h2):
            return 1
        for c1, c2 in zip(h1, h2):
            v1 = CARDS.index(c1)
            v2 = CARDS.index(c2)
            if v1 < v2:
                return -1
            if v1 > v2:
                return 1
        return 0
    
    def cmp_handbids(hb1, hb2):
        return cmp_hands(hb1[0], hb2[0])
    
    ranked = sorted(map(str.split, open(0)), key=cmp_to_key(cmp_handbids))
    winnings = sum(rank * int(bid) for rank, (_, bid) in enumerate(ranked, 1))
    print(winnings)
    
    Pretty straightforward tweaks from part one:
    Part 2
    #!/usr/bin/env pypy3
    
    from collections import Counter
    from functools import cmp_to_key
    
    CARDS = 'J23456789TQKA'
    HAND_TYPES = ([1, 1, 1, 1, 1], [1, 1, 1, 2], [1, 2, 2],
                  [1, 1, 3], [2, 3], [1, 4], [5])
    
    def hand_type(h):
        c = Counter(h)
        jcount = c['J']
        del c['J']
        c[max(c, key=c.get) if c else 'J'] += jcount
        return HAND_TYPES.index(sorted(c.values()))
    
    def cmp_hands(h1, h2):
        if hand_type(h1) < hand_type(h2):
            return -1
        if hand_type(h1) > hand_type(h2):
            return 1
        for c1, c2 in zip(h1, h2):
            v1 = CARDS.index(c1)
            v2 = CARDS.index(c2)
            if v1 < v2:
                return -1
            if v1 > v2:
                return 1
        return 0
    
    def cmp_handbids(hb1, hb2):
        return cmp_hands(hb1[0], hb2[0])
    
    ranked = sorted(map(str.split, open(0)), key=cmp_to_key(cmp_handbids))
    winnings = sum(rank * int(bid) for rank, (_, bid) in enumerate(ranked, 1))
    print(winnings)
    
    2 votes
  10. first-must-burn
    Link
    My solution in golang A pretty interesting problem today. I was a bit lucky with my solution, it was pretty easy to use my part1 with a few minor tweaks, but if part2 had been different, I would...

    My solution in golang

    A pretty interesting problem today. I was a bit lucky with my solution, it was pretty easy to use my part1 with a few minor tweaks, but if part2 had been different, I would have had more work to do in part 2.

    I was slowed down by this Golang behavior that I always forget about: the object you get in a for range loop is a copy of the original object, so if you want to modify the elements as you loop over it, you have to use an integer index loop and access the slice element by index. My mental model for the for each type loop is rooted in Python, so the difference is hard to keep track of. Not saying one is right or wrong, per se. I like Go over all, but there are some warts on their loop syntax 1 2.

    Additional thoughts:

    Part 1 Spoilers

    I think the key insight for part1 is that for determining the hand type, the rank of the cards doesn't matter, you just care about the size of the groups.

    Part 2 Spoilers

    The insight here is that to get the best hand, you add the jokers to the largest group made by the non-joker cards. I have a proof of this in the code comments if anyone is interested.

    1 vote
  11. zazowoo
    Link
    I really had fun with this one. It felt like just the right balance of challenging without being overwhelming. These AoC exercises are making me fall even more in love with Elixir, which I started...

    I really had fun with this one. It felt like just the right balance of challenging without being overwhelming. These AoC exercises are making me fall even more in love with Elixir, which I started learning earlier this year.

    Part 2 Solution (Elixir)
    defmodule Solution do
      def run(filename) do
        File.stream!(filename, [:read])
        |> Stream.map(&String.trim/1)
        |> Stream.map(&parse_line/1)
        |> Enum.sort_by(fn {hand, _bid} -> hand end, &right_better_than_left?/2)
        |> Enum.with_index(1)
        |> Enum.reduce(0, fn {{_hand, bid}, rank}, total ->
          total + (bid * rank)
          end)
        |> IO.inspect()
      end
    
      defp parse_line(input) do
        [hand, bid] = String.split(input)
    
        {hand, String.to_integer(bid)}
      end
    
      defp right_better_than_left?(left, right) do
        left_hand_type = hand_rank(to_charlist(left))
        right_hand_type = hand_rank(to_charlist(right))
    
        cond do
          left_hand_type < right_hand_type ->
            true
          left_hand_type > right_hand_type ->
            false
          true ->
            tie_break(to_charlist(left), to_charlist(right))
        end
      end
    
      defp tie_break([], []), do: raise("same hand")
      defp tie_break([left_head | left_rest], [right_head | right_rest]) do
        cond do
          card_rank(left_head) < card_rank(right_head) ->
            true
          card_rank(left_head) > card_rank(right_head) ->
            false
          true ->
            tie_break(left_rest, right_rest)
        end
      end
    
      @card_ranks %{
        ?A => 14,
        ?K => 13, 
        ?Q => 12, 
        ?J => 1, 
        ?T => 10 
      }
    
      defp card_rank(card) do 
        Map.get(@card_ranks, card, card - 48)
      end
    
      defp hand_rank(hand) do
        cond do
          five_of_a_kind?(hand) ->
            7
          four_of_a_kind?(hand) ->
            6
          full_house?(hand) ->
            5
          three_of_a_kind?(hand) ->
            4
          two_pair?(hand) ->
            3
          one_pair?(hand) ->
            2
          high_card?(hand) ->
            1
        end
      end
    
      defp replace_jack_with_most_common_card(hand) do
        frequencies = 
          Enum.frequencies(hand)
          |> Map.delete(?J)
          |> Enum.sort_by(fn {_card, freq} -> freq end, :desc)
    
        hand
        |> Enum.map(fn card -> 
          hd(frequencies)
          |> elem(0)
          |> replace_wildcard(card)
        end)
      end
    
      defp replace_wildcard(replacement, ?J), do: replacement
      defp replace_wildcard(_replacement, orig), do: orig
    
      defp five_of_a_kind?(~c"JJJJJ"), do: true
      defp five_of_a_kind?(hand) do
        replace_jack_with_most_common_card(hand)
          |> Enum.frequencies()
          |> Map.values()
          == [5]
      end
    
      defp four_of_a_kind?(hand) do
        replace_jack_with_most_common_card(hand)
        |> Enum.frequencies()
        |> Map.values()
        |> Enum.member?(4)
      end
    
      defp full_house?(hand) do
        if Enum.member?(hand, ?J) do
          two_pair?(hand |> Enum.reject(fn card -> card == ?J end))
        else
          Enum.frequencies(hand)
            |> Map.values()
            |> Enum.sort()
            == [2, 3]
        end
      end
    
      defp three_of_a_kind?(hand) do
        replace_jack_with_most_common_card(hand)
          |> Enum.frequencies()
          |> Map.values()
          |> Enum.sort()
          == [1, 1, 3]
      end
    
      defp two_pair?(hand) do
        freqs = 
        Enum.frequencies(hand)
          |> Map.values()
          |> Enum.sort(:desc)
    
        hd(freqs) == 2 && hd(tl(freqs)) == 2
      end
    
      defp one_pair?(hand) do
        replace_jack_with_most_common_card(hand)
          |> Enum.frequencies()
          |> Map.values()
          |> Enum.sort()
        == [1, 1, 1, 2]
      end
    
      defp high_card?(hand) do
        Enum.frequencies(hand)
        |> Map.values()
        |> Enum.sort()
        == [1, 1, 1, 1, 1]
      end
    end
    
    System.argv()
    |> then(&(Enum.at(&1, 0) || "input"))
    |> then(&(Solution.run(&1)))
    
    1 vote
  12. infinitesimal
    Link
    Today felt straightforward enough. I initially thought I'd have to handwrite the branches in part 2, but I just tried adding up jokers and then copypasting part 1 logic and it worked so shrug....

    Today felt straightforward enough. I initially thought I'd have to handwrite the branches in part 2, but I just tried adding up jokers and then copypasting part 1 logic and it worked so shrug.

    Kotlin
    package aoc2023
    
    object Day7 {
        enum class Card { Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King, Ace }
        enum class Type { HighCard, OnePair, TwoPair, ThreeKind, FullHouse, FourKind, FiveKind }
    
        fun p1TypeOf(cards: List<Card>): Type {
            val sizes = cards.groupBy { it }.map { it.value.size }.sortedDescending()
            return when (sizes[0]) {
                5 -> Type.FiveKind
                4 -> Type.FourKind
                3 -> if (sizes[1] == 2) Type.FullHouse else Type.ThreeKind
                2 -> if (sizes[1] == 2) Type.TwoPair else Type.OnePair
                1 -> Type.HighCard
                else -> error(Unit)
            }
        }
    
        fun p2TypeOf(cards: List<Card>): Type {
            val numJacks = cards.groupBy { it }[Card.Jack]?.size ?: 0
            if (numJacks == 5) {
                return Type.FiveKind
            }
            val sizes = cards.groupBy { it }.filter { it.key != Card.Jack }.map { it.value.size }.toMutableList()
            sizes.sortDescending()
            sizes[0] += numJacks
            return when (sizes[0]) {
                5 -> Type.FiveKind
                4 -> Type.FourKind
                3 -> if (sizes[1] == 2) Type.FullHouse else Type.ThreeKind
                2 -> if (sizes[1] == 2) Type.TwoPair else Type.OnePair
                1 -> Type.HighCard
                else -> error(Unit)
            }
        }
    
        fun parse(input: String): List<Pair<List<Card>, Int>> {
            return input.trim().lines().map { line ->
                val (hand0, bid0) = line.split(" ")
                val bid = bid0.trim().toInt()
                val hand = hand0.trim().map { card0 ->
                    when (card0) {
                        '2' -> Card.Two
                        '3' -> Card.Three
                        '4' -> Card.Four
                        '5' -> Card.Five
                        '6' -> Card.Six
                        '7' -> Card.Seven
                        '8' -> Card.Eight
                        '9' -> Card.Nine
                        'T' -> Card.Ten
                        'J' -> Card.Jack
                        'Q' -> Card.Queen
                        'K' -> Card.King
                        'A' -> Card.Ace
                        else -> error(Unit)
                    }
                }
                Pair(hand, bid)
            }
        }
    
        fun part1(input: String): Int {
            val sorted = parse(input).sortedWith { x0, y0 ->
                val x = x0.first
                val y = y0.first
                x.indices.fold(p1TypeOf(x).compareTo(p1TypeOf(y))) { acc, i -> if (acc != 0) acc else x[i].compareTo(y[i]) }
            }
            return sorted.mapIndexed { i, (_, bid) -> (i + 1) * bid }.sum()
        }
    
        fun part2(input: String): Int {
            val sorted = parse(input).sortedWith { x0, y0 ->
                val x = x0.first
                val y = y0.first
                x.indices.fold(p2TypeOf(x).compareTo(p2TypeOf(y))) { acc, i ->
                    if (acc != 0) acc else {
                        val xiOrd = if (x[i] == Card.Jack) -1 else x[i].ordinal
                        val yiOrd = if (y[i] == Card.Jack) -1 else y[i].ordinal
                        xiOrd.compareTo(yiOrd)
                    }
                }
            }
            return sorted.mapIndexed { i, (_, bid) -> (i + 1) * bid }.sum()
        }
    }
    
    1 vote
  13. whs
    Link
    I spent like two hours because I didn't realize J is already part of the part 1 card list and it was moved, rather than added, in part 2. Then I refactored a lot of the code in part 2 as the...

    I spent like two hours because I didn't realize J is already part of the part 1 card list and it was moved, rather than added, in part 2. Then I refactored a lot of the code in part 2 as the counting algorithm start rearing its various flaws.

    Today's language is C++23. I like all the modern stuff in C++, like span, array, constexpr. Still, it's too easy to not knowing what to use in modern C++ and drop down to unsafe C, so I wouldn't write my production code in C++. With C++ crossed off the list, that should be every single "hard" languages I've ever written an application in, that I hopefully can solve these problems.

    #include <algorithm>
    #include <cstdint>
    #include <iostream>
    #include <vector>
    #include <string>
    #include <array>
    #include <span>
    #include <utility>
    
    #define PART2 1
    
    using namespace std;
    
    constexpr char cardIndex[] = {
    #if PART2
        'A', 'K', 'Q', 'T', '9', '8', '7', '6', '5', '4', '3', '2', 'J',
    #else
        'A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2',
    #endif
    };
    
    constexpr int cardPower(const char card) {
        for (int i = 0; i < sizeof(cardIndex); i++) {
            if (cardIndex[i] == card) {
                return i;
            }
        }
        unreachable();
    }
    
    class Hand {
        void updateCards() {
            for (int i = 0; i < sizeof(cardIndex); i++) {
                for (const char c : this->hand) {
                    if (c == cardIndex[i]) {
                        this->cards[i]++;
                    }
                }
            }
    #if PART2
            int maxIndex = 0;
            uint8_t maxValue = 0;
            auto cardsNoJoker = this->cardsButJoker();
            for (int i = 0; i < cardsNoJoker.size(); i++) {
                uint8_t value = cardsNoJoker[i];
                if(value > maxValue) {
                    maxIndex = i;
                    maxValue = value;
                }
            }
            this->cards[maxIndex] += this->cards[12];
    #endif
        }
    
        span<
            const uint8_t,
    #if PART2
        12
    #else
        13
    #endif
        > cardsButJoker() const {
    #if PART2
            return span(this->cards).first<12>();
    #else
            return span(this->cards);
    #endif
        }
    
        int8_t uniqueCardCount() const {
            int8_t uniqueCards = 0;
            for (uint8_t count : this->cardsButJoker()) {
                if (count > 0) {
                    uniqueCards++;
                }
            }
            return uniqueCards;
        }
    
    public:
        string hand;
        uint16_t bid;
        array<uint8_t, 13> cards = {0};
    
        void read() {
            getline(cin, this->hand, ' ');
            if (this->hand.empty()) {
                throw std::length_error("eol");
            }
            this->updateCards();
    
            string bid;
            getline(cin, bid);
            this->bid = static_cast<uint16_t>(stoul(bid));
        }
    
        int handType() const {
            if (this->isFiveOfAKind()) {
                return 7;
            } else if (this->isFourOfAKind()) {
                return 6;
            } else if (this->isFullHouse()) {
                return 5;
            } else if (this->isThreeOfAKind()) {
                return 4;
            } else if (this->isTwoPair()) {
                return 3;
            } else if (this->isOnePair()) {
                return 2;
            } else if (this->isHighCard()) {
                return 1;
            } else {
                throw runtime_error("WTF hand " + hand);
            }
        }
    
        bool isFiveOfAKind() const {
            for (uint8_t count : this->cardsButJoker()) {
                if (count == 5) {
                    return true;
                }
            }
            return false;
        }
    
        bool isFourOfAKind() const {
            // AAAAB
            // AAAJB
            for (uint8_t count : this->cardsButJoker()) {
                if (count == 4) {
                    return true;
                }
            }
            return false;
        }
    
        bool isFullHouse() const {
            // AABB1
            // AABBB
            int twoPairCount = 0;
            int threePairCount = 0;
            for(uint8_t count : this->cardsButJoker()) {
                if(count == 2) {
                    twoPairCount++;
                }else if(count == 3) {
                    threePairCount++;
                }
            }
            return threePairCount == 1 && twoPairCount == 1;
        }
    
        bool isThreeOfAKind() const {
            bool found3 = false;
            // TTT98
            // TTJ98
            for (uint8_t count : this->cardsButJoker()) {
                if (count == 3) {
                    found3 = true;
                } else if (count > 1) {
                    return false;
                }
            }
            return found3;
        }
    
        bool isTwoPair() const {
            // AABB1
            int pairCount = 0;
            for (uint8_t count : this->cards) {
                if (count == 2) {
                    pairCount++;
                }
            }
            return pairCount == 2;
        }
    
        bool isOnePair() const {
            // AA123
            // AJ123
            if (this->uniqueCardCount() != 4) {
                return false;
            }
            for (uint8_t count : this->cardsButJoker()) {
                if (count == 2) {
                    return true;
                }
            }
            return false;
        }
    
        bool isHighCard() const {
            return this->uniqueCardCount() == 5;
        }
    
        // Compare hand of the same type. Returning true if this hand is weaker
        bool compareWith(const Hand& other) const {
            for (int i = 0; i < this->hand.size(); i++) {
                if (this->hand[i] == other.hand[i]) {
                    continue;
                }
                int aIndex = cardPower(this->hand[i]);
                int bIndex = cardPower(other.hand[i]);
                return aIndex > bIndex;
            }
            // must be equal...
            return true;
        }
    };
    
    int main() {
        vector<Hand> hands;
        for (;;) {
            Hand hand;
            try {
                hand.read();
            }
            catch (std::length_error e) {
                break;
            }
            hands.push_back(hand);
        }
    
        // Sort hand by score ascendingly
        sort(hands.begin(), hands.end(), [](const Hand& a, const Hand& b) {
            int aScore = a.handType();
            int bScore = b.handType();
            if (aScore == bScore) {
                return a.compareWith(b);
            }
            return aScore < bScore;
        });
    
        int winning = 0;
        for (int i = 0; i < hands.size(); i++) {
            cout << "Hand " << i << " " << hands[i].hand << " type " << hands[i].handType() << " bid " << hands[i].bid <<
                endl;
            winning += hands[i].bid * (i + 1);
        }
        cout << winning << endl;
        // 248626506 too low part 2
        // 248314070 is wrong
        // 248302507 is wrong
        // 248255840 is wrong
    }
    
    1 vote
  14. [3]
    Johz
    Link
    Quite happy with this one today. It's not particularly complicated, but my initial naive implementation was quite slow (>100us each for both parts, IIRC), so I had to really figure out how to...

    Quite happy with this one today. It's not particularly complicated, but my initial naive implementation was quite slow (>100us each for both parts, IIRC), so I had to really figure out how to bring that down to get under my allotted per-day 42us. I've not benchmarked the final code properly, it might still be slightly over, but it's come down a lot!

    Parsing/evaluating hands
    fn parse_hand<const JOKERS: bool>(input: &[u8]) -> Hand {
        let mut cards = [0_u8; 5];
        let mut counts = [0_u8; 13];
        let mut largest = (0, 0);
        let mut second = (0, 0);
        let mut jokers = 0;
        for entry in cards.iter_mut().enumerate() {
            *entry.1 = match input[entry.0] {
                c @ b'2'..=b'9' => c - b'1',
                b'T' => 9,
                b'J' => {
                    if JOKERS {
                        0
                    } else {
                        10
                    }
                }
                b'Q' => 11,
                b'K' => 12,
                b'A' => 13,
                _ => unreachable!("Invalid card {:?}", input[entry.0] as char),
            };
            if JOKERS && *entry.1 == 0 {
                jokers += 1;
            } else {
                let count = counts.get_mut(*entry.1 as usize - 1).unwrap();
                *count += 1;
                if *entry.1 == largest.0 && *count > largest.1 {
                    largest.1 = *count;
                } else if *count > largest.1 {
                    second = largest;
                    largest = (*entry.1, *count);
                } else if *count > second.1 {
                    second = (*entry.1, *count);
                }
            }
        }
    
        let kind = match (largest.1 + jokers, second.1) {
            (1, _) => HandKind::HighCard,
            (2, 1) => HandKind::OnePair,
            (2, 2) => HandKind::TwoPairs,
            (3, 1) => HandKind::ThreeOfAKind,
            (3, 2) => HandKind::FullHouse,
            (4, _) => HandKind::FourOfAKind,
            (5, _) => HandKind::FiveOfAKind,
            _ => unreachable!("Invalid hand"),
        };
    
        Hand { cards, kind }
    }
    

    The rest of the code is on GitHub.

    Optimisation thoughts

    I wrote a fairly naive version initially, which had two problems. The first was that I didn't think hard enough about how jokers worked, and created a really complicated switch statement to check the hand type. Then I looked online at other people's solutions and realised that jokers will always work best if they add to the most common number, which made things a lot easier — cleaner, too!

    The second and more complicated one was that I was doing a lot of sorting, which was slooow. I don't think there's a way to avoid sorting all the elements at the end, so that's fair enough, but I was also doing a sort while figuring out the hand type — I counted up how many of each card type was present, and then sorted that result, so I could check how many of the most common card there were, how many of the second most common card, etc. The result was O(n + k · log(k)) — n for the number of cards in the hand (5), and k for the number of possible card types (13).

    At first, I tried running everything in parallel (hey, it's Rust, fearless concurrency &c), on the basis that if the problem is the parsing, I could split the parsing and run that in parallel. In practice, that was just slower because of the overheads of threads.

    In the end, I figured that I didn't need to properly sort the items, I just needed to largest and second-largest numbers, which I can do in roughly O(n + k) time (once through the hand to count up how many of each card type there is, once through each card type to find the highest/second highest). After that, I realised that I don't even need the k iteration — I can calculate which card has appeared most often during one loop of the cards. So now parsing a card is pretty much O(n), where n is the number of cards in the hand.

    1 vote
    1. [2]
      DataWraith
      Link Parent
      I really haven't done much performance work, but I wonder if you could use a Radix sort to speed up the final sorting, since we're basically sorting length six 'strings' (hand type + 5 cards)....

      I don't think there's a way to avoid sorting all the elements at the end, so that's fair enough,

      I really haven't done much performance work, but I wonder if you could use a Radix sort to speed up the final sorting, since we're basically sorting length six 'strings' (hand type + 5 cards). Since it is not based on comparisons, Radix sort can break the O(n*log(n)) barrier IIRC, but one would have to try it in order to see whether or not it ends up being faster than Rust's fairly optimized built-in sort on the puzzle input.

      3 votes
      1. Johz
        Link Parent
        Good shout, I can have a look into that. Especially given that the hand type and card options are limited (less than four bits per value) that could be pretty effective. Thanks for the suggestion!

        Good shout, I can have a look into that. Especially given that the hand type and card options are limited (less than four bits per value) that could be pretty effective. Thanks for the suggestion!

        2 votes
  15. jzimbel
    Link
    Elixir I split my part 1 and 2 solutions into separate modules, mostly just to namespace the functions so I didn't have to have a bunch of parse_p1 parse_p2 etc names. Common logic The sort key, a...

    Elixir

    I split my part 1 and 2 solutions into separate modules, mostly just to namespace the functions so I didn't have to have a bunch of parse_p1 parse_p2 etc names.

    Common logic

    The sort key, a tuple of the form {type_score :: integer, hand :: list(integer)} takes advantage of elixir/erlang's default term ordering. Unlike a lot of C-family languages, comparison operators like <, ==, etc do a deep comparison by default. Tuples are compared left to right, tie-breaker style. The second elements are checked only if the first elements are equal, and so on. The same goes for lists. So, using this value as a sort key lets me sort the list of plays exactly as the puzzle describes.

    defmodule AdventOfCode.Solution.Year2023.Day07 do
      def part1(input), do: solve(input, __MODULE__.Part1)
      def part2(input), do: solve(input, __MODULE__.Part2)
    
      def solve(input, mod) do
        input
        |> mod.parse()
        |> Enum.sort_by(fn {hand, _bid} -> {type_score(mod.card_groupings(hand)), hand} end)
        |> Enum.with_index(1)
        |> Enum.map(&winnings/1)
        |> Enum.sum()
      end
    
      defp type_score([5]), do: 6
      defp type_score([1, 4]), do: 5
      defp type_score([2, 3]), do: 4
      defp type_score([1, 1, 3]), do: 3
      defp type_score([1, 2, 2]), do: 2
      defp type_score([1, 1, 1, 2]), do: 1
      defp type_score([1, 1, 1, 1, 1]), do: 0
    
      defp winnings({{_hand, bid}, rank}), do: bid * rank
    end
    
    Logic specific to part 1
    defmodule AdventOfCode.Solution.Year2023.Day07.Part1 do
      def parse(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(&parse_play/1)
      end
    
      def card_groupings(hand) do
        hand |> Enum.frequencies() |> Map.values() |> Enum.sort()
      end
    
      defp parse_play(line) do
        [hand, bid] = String.split(line)
        hand = for <<char <- hand>>, do: parse_card_value(char)
        bid = String.to_integer(bid)
    
        {hand, bid}
      end
    
      for {char, value} <- Enum.zip(~c[TJQKA], 10..14) do
        defp parse_card_value(unquote(char)), do: unquote(value)
      end
    
      defp parse_card_value(digit_char), do: digit_char - ?0
    end
    
    Logic specific to part 2
    defmodule AdventOfCode.Solution.Year2023.Day07.Part2 do
      @j_value 1
    
      def parse(input) do
        input
        |> String.split("\n", trim: true)
        |> Enum.map(&parse_play/1)
      end
    
      def card_groupings(hand) do
        {jokers, hand} = Enum.split_with(hand, &(&1 == @j_value))
        j_count = length(jokers)
    
        hand
        |> AdventOfCode.Solution.Year2023.Day07.Part1.card_groupings()
        |> strengthen(j_count)
      end
    
      defp strengthen(groupings, 0), do: groupings
      defp strengthen([], 5), do: [5]
    
      defp strengthen(groupings, j_count) do
        {smaller, [biggest]} = Enum.split(groupings, -1)
        smaller ++ [biggest + j_count]
      end
    
      defp parse_play(line) do
        [hand, bid] = String.split(line)
        hand = for <<char <- hand>>, do: parse_card_value(char)
        bid = String.to_integer(bid)
    
        {hand, bid}
      end
    
      defp parse_card_value(?T), do: 10
      defp parse_card_value(?J), do: @j_value
    
      for {char, value} <- Enum.zip(~c[QKA], 12..14) do
        defp parse_card_value(unquote(char)), do: unquote(value)
      end
    
      defp parse_card_value(digit_char), do: digit_char - ?0
    end
    
    1 vote