Day 3: Lobby

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL

FAQ

  • Kotlin

    First day this year where I am very happy with my solution. Just super simple, recursive string building.

    Solution
    class Day03 : Puzzle {
    
        val banks = mutableListOf<String>()
    
        override fun readFile() {
            val input = readInputFromFile("src/main/resources/a2025/day03.txt")
            banks.addAll(input.lines().filter(String::isNotBlank))
        }
    
        override fun solvePartOne(): String {
            val sum = banks.map { buildNumber(it, 2) }.sumOf { it.toLong() }
            return sum.toString()
        }
    
        override fun solvePartTwo(): String {
            val sum = banks.map { buildNumber(it, 12) }.sumOf { it.toLong() }
            return sum.toString()
        }
    
        private fun buildNumber(bank: String, remainingDigits: Int): String {
            if (remainingDigits <= 0) return ""
            val current = bank.dropLast(remainingDigits - 1)
            val max = current.max()
            return max + buildNumber(bank.split(max, limit = 2)[1], remainingDigits - 1)
        }
    }
    

    full code on Codeberg

    • chunkystyles ( chunkystyles@sopuli.xyz ) 
      link
      fedilink
      English
      arrow-up
      2
      ·
      10 months ago

      Today was interesting. My first thought was that part 2 would be a lot more complex at first glance. But I realized that my solution for part 1 worked almost out of the box for part 2.

      I was also pleased to see that the algorithm ran in 1ms, which was a good deal faster than just parsing the input.

      fun main() {
          val input = getInput(3)
          val banks = parseInput(input)
          var total = 0L
          banks.forEach { bank ->
              var location = 0
              var joltage = 0L
              for (power in 11 downTo 0) {
                  val multiplier = 10.toDouble().pow(power).toLong()
                  val batteryLocation = findBattery(bank, location, bank.size - power - 1)
                  val battery = bank[batteryLocation]
                  location = batteryLocation + 1
                  joltage += battery.toLong() * multiplier
              }
              total += joltage
          }
          println(total)
      }
      
      fun parseInput(input: String): List<List<Int>> = input
          .split("\n")
          .filter { it.isNotBlank() }
          .map { it.toCharArray() }
          .map { it.map { digit -> digit.digitToInt() } }
      
      fun findBattery(bank: List<Int>, start: Int, end: Int): Int {
          var max = 0
          var location = 0
          for (i in start..end) {
              val battery = bank[i]
              if (battery > max) {
                  max = battery
                  location = i
                  if (battery == 9) {
                      break
                  }
              }
          }
          return location
      }
      
        • chunkystyles ( chunkystyles@sopuli.xyz ) 
          link
          fedilink
          English
          arrow-up
          2
          ·
          10 months ago

          I was curious, so I ran yours and it is only like 3-4ms slower. I was honestly surprised it was that close.

          Just goes to show that we’re often wrong when estimating and the only real way to know is to benchmark.

          • Deebster ( Deebster@programming.dev ) 
            link
            fedilink
            arrow-up
            2
            ·
            10 months ago

            My version used strings as well, and I thought that as I was comparing small integers either way, it made sense to stay in ASCII as the strings were already easy to index, and it meant I could skip parsing input numbers, only needing to parse output numbers so they could be summed.

            I did start with numbers so I could convert it back to compare, but it’s so fast (the whole thing takes 1ms - and that’s reading/parsing the input twice) that it’s almost a micro benchmark.

          • Yeah, I vaguely remember reading something about how close string splitting is to all the logarithm math in splitting numbers, and since then I’ve just always used strings because that’s way more intuitive to me lol

  • Deebster ( Deebster@programming.dev ) 
    link
    fedilink
    English
    arrow-up
    4
    ·
    10 months ago

    Rust

    My first version worked with numbers, but since I was still sick of yesterday’s pow(10) calls, I changed it to use ascii for the second half - the nice thing is that means it can work with hex input with no modification!

    Clippy was complaining about “needless_range_loops”, but it’s way more readable this way.

    struct PowerSource(Vec<Bank>);
    
    impl FromStr for PowerSource {
        type Err = Report;
    
        fn from_str(s: &str) -> Result<Self> {
            let banks = s.lines().map(|l| Bank(l.to_owned())).collect();
            Ok(Self(banks))
        }
    }
    
    impl PowerSource {
        fn max_joltage(&self, num_digits: usize) -> usize {
            self.0.iter().map(|b| b.max_joltage(num_digits)).sum()
        }
    }
    
    struct Bank(String);
    
    impl Bank {
        fn max_joltage(&self, num_digits: usize) -> usize {
            let batts = self.0.as_bytes();
    
            let mut digits = vec![b'0'; num_digits];
            let mut start = 0;
            for d in 0..num_digits {
                for i in start..=batts.len() - num_digits + d {
                    if batts[i] > digits[d] {
                        digits[d] = batts[i];
                        start = i + 1;
                    }
                }
            }
    
            usize::from_str(str::from_utf8(&digits).unwrap()).unwrap()
        }
    }
    
  • Pyro ( Pyro@programming.dev ) 
    link
    fedilink
    arrow-up
    4
    ·
    edit-2
    10 months ago

    Python

    This was the easier one for me out of the first 3 days. Cleaned up my solution before posting for better readability:

    # get joltage of picked batteries
    def get_joltage(batteries_picked: list[int]):
        bank_joltage = 0
        for batt in batteries_picked:
            bank_joltage = bank_joltage * 10 + batt
        return bank_joltage
    
    # get maximum joltage of a bank
    def get_bank_joltage(bank: str, pick_limit = 2) -> int:
        # pick first <pick_limit> batteries
        batteries_picked = [int(bank[i]) for i in range(pick_limit)]
        max_joltage = get_joltage(batteries_picked)
    
        # iterate over remaining batteries
        for i in range(pick_limit, len(bank)):
            batt = int(bank[i])        
            # we add batt for selection consideration
            batteries_picked.append(batt)
            # If all batteries are in descending order and batt is the lowest, 
            #   we will eventually discard batt
            to_discard = pick_limit
    
            # However if not, we discard the leftmost MSB battery which has lower joltage than its successor
            #   and shift all batteries left with batt added at the end.
            # This guarantees that we keep the maximum lexicographical order of picked batteries
            #   regardless of batt's value.
            for i in range(pick_limit):
                if batteries_picked[i] < batteries_picked[i+1]:
                    to_discard = i
                    break
            batteries_picked.pop(to_discard)
    
            # update max_joltage, it may have increased
            max_joltage = max(max_joltage, get_joltage(batteries_picked))
    
        return max_joltage
    
    # part 1 asserts
    assert get_bank_joltage("987654321111111", pick_limit=2) == 98
    assert get_bank_joltage("811111111111119", pick_limit=2) == 89
    assert get_bank_joltage("234234234234278", pick_limit=2) == 78
    assert get_bank_joltage("818181911112111", pick_limit=2) == 92
    
    # part 2 asserts
    assert get_bank_joltage("987654321111111", pick_limit=12) == 987654321111
    assert get_bank_joltage("811111111111119", pick_limit=12) == 811111111119
    assert get_bank_joltage("234234234234278", pick_limit=12) == 434234234278
    assert get_bank_joltage("818181911112111", pick_limit=12) == 888911112111
    
    # get total joltage of a set of banks
    def solve(data: str, pick_limit = 2):
        total_joltage = 0
        for bank in data.splitlines():
            total_joltage += get_bank_joltage(bank, pick_limit)
        return total_joltage
    
    # asserts for sample data
    sample = """987654321111111
    811111111111119
    234234234234278
    818181911112111"""
    assert solve(sample, pick_limit=2) == 357               # part 1
    assert solve(sample, pick_limit=12) == 3121910778619    # part 2
    
    
  • Strlcpy@1 ( strlcpy@lemmy.sdf.org ) 
    link
    fedilink
    English
    arrow-up
    4
    ·
    10 months ago

    C

    Surprise, O(n^12) solutions don’t scale! But then it was delightful when the realization hit that the solution is actually very simple to implement - just keep removing the first digit that is followed by a higher one.

    static uint64_t joltage(char *s, int len, int target) {
    	int i;
    
    	for (; len > target; len--) {
    		for (i=0; i<len-1 && s[i] >= s[i+1]; i++) ;
    		memmove(s+i, s+i+1, len-i);
    	}
    
    	return strtoul(s, NULL, 10);
    }
    
    int main() {
    	char buf[1024];
    	uint64_t p1=0,p2=0;
    	int len;
    
    	while (fgets(buf, sizeof(buf), stdin)) {
    		for (len=0; isdigit(buf[len]); len++) ;
    		buf[len] = '\0';
    		p2 += joltage(buf, len, 12);
    		p1 += joltage(buf, 12, 2);
    	}
    
    	printf("03: %lu %lu\n", p1, p2);
    }
    

    Repo link

    I’m still having to finish yesterday’s x86-16 assembly implementation, for which I had to write some support code to deal with large numbers as strings. That will come in useful today, too!

  • Amy ( lwhjp@piefed.blahaj.zone ) 
    link
    fedilink
    English
    arrow-up
    3
    ·
    10 months ago

    Haskell

    Yay, dynamic programming!

    import Data.Map qualified as Map  
    
    maxJolt :: Int -> [Char] -> Int  
    maxJolt r xs = read $ maximize r 0  
      where  
        n = length xs  
        maximize =  
          (curry . (Map.!) . Map.fromList . (zip <*> map (uncurry go)))  
            [(k, o) | k <- [1 .. r], o <- [r - k .. n - k]]  
        go k o =  
          maximum $ do  
            (x, o') <- drop o $ zip xs [1 .. n - (k - 1)]  
            return . (x :) $ if k == 1 then [] else maximize (k - 1) o'  
    
    main = do  
      input <- lines <$> readFile "input03"  
      mapM_ (print . sum . (`map` input) . maxJolt) [2, 12]  
    
    • Amy ( lwhjp@piefed.blahaj.zone ) 
      link
      fedilink
      English
      arrow-up
      1
      ·
      10 months ago

      Version 2. I realized last night that my initial approach was way more complicated than it needed to be…

      import Data.List
      import Data.Semigroup
      
      maxJolt :: Int -> [Char] -> Int
      maxJolt r xs = read $ go r (length xs) xs
        where
          go r n xs =
            (\(Arg x xs) -> x : xs) . maximum $
              do
                (n', x : xs') <- zip (reverse [r .. n]) (tails xs)
                return . Arg x $ if r == 1 then [] else go (r - 1) (n' - 1) xs'
      
      main = do
        input <- lines <$> readFile "input03"
        mapM_ (print . sum . (`map` input) . maxJolt) [2, 12]
      
  • janAkali ( janAkali@lemmy.sdf.org ) 
    link
    fedilink
    arrow-up
    3
    ·
    10 months ago

    Nim

    type
      AOCSolution[T,U] = tuple[part1: T, part2: U]
    
    proc maxJoltage(bank: string, n: int): int =
      var index = 0
      for leftover in countDown(n-1, 0):
        var best = bank[index]
        for batteryInd in index+1 .. bank.high-leftover:
          let batt = bank[batteryInd]
          if batt > best: (best = batt; index = batteryInd)
          if best == '9': break # max for single battery
        result += (best.ord - '0'.ord) * 10^leftover
        inc index
    
    proc solve(input: string): AOCSolution[int, int] =
      for line in input.splitLines:
        result.part1 += line.maxJoltage 2
        result.part2 += line.maxJoltage 12
    

    Runtime: ~240 μs

    Day 3 was very straightforward, although I did wrestle a bit with the indexing.
    Honestly, I expected part 2 to require dynamic programming, but it turned out I only needed to tweak a few numbers in my part 1 code.

    Full solution at Codeberg: solution.nim

  • Avicenna ( Avicenna@programming.dev ) 
    link
    fedilink
    arrow-up
    3
    ·
    10 months ago
    import numpy as np
    
    def parse_input(file_path):
    
      with file_path.open("r") as fp:
        banks = map(str.strip, fp.readlines())
    
      return map(lambda x: list(map(int, list(x))), banks)
    
    def max_jolt(bank, length):
    
      if length==1:
        return max(bank)
    
      amax = np.argmax(bank[:-(length-1)])
    
      return 10**(length-1)*bank[amax] + max_jolt(bank[amax+1:], length-1)
    
    def solve_problem(file_name, length):
    
      banks = parse_input(Path(cwd, file_name))
      sumj = 0
    
      for bank in banks:
        sumj += max_jolt(bank, length)
    
      return sumj
    
  • Camille ( Camille@lemmy.ml ) 
    link
    fedilink
    arrow-up
    3
    ·
    10 months ago

    Go

    I usually write a little helper library to bootstrap the input reading process and sometimes even the downloading of the input file. So here it is:

    utils.go
    package utils
    
    import (
    	"bufio"
    	"os"
    	"strings"
    )
    
    type Input interface {
    	GetLineChannel() (chan string, error)
    }
    
    type FilePath string
    type InputText string
    
    func (path FilePath) GetLineChannel() (chan string, error) {
    	file, err := os.Open(string(path))
    	if err != nil {
    		return nil, err
    	}
    
    	scanner := bufio.NewScanner(file)
    
    	ch := make(chan string, 1024)
    	go (func() {
    		defer file.Close()
    
    		for scanner.Scan() {
    			ch <- scanner.Text()
    		}
    
    		close(ch)
    	})()
    
    	return ch, nil
    }
    
    func (inputText InputText) GetLineChannel() (chan string, error) {
    	lines := strings.Split(string(inputText), "\n")
    	ch := make(chan string, len(lines))
    
    	go (func() {
    		for _, line := range lines {
    			ch <- line
    		}
    
    		close(ch)
    	})()
    
    	return ch, nil
    }
    

    And here comes the solution to day 3:

    package main
    
    import (
    	"aoc/utils"
    	"errors"
    	"fmt"
    	"math"
    )
    
    const inputText = `987654321111111
    811111111111119
    234234234234278
    818181911112111`
    
    type bank []int
    
    func (bk bank) largestNDigitJoltage(n int) int {
    	digits := make([]int, n)
    	count := 0
    	idx := 0
    
    	lenbk := len(bk)
    
    	for range n {
    		for i := idx; i < lenbk-(n-count-1); i++ {
    			val := bk[i]
    			if val > digits[count] {
    				idx = i + 1
    				digits[count] = val
    			}
    		}
    		count++
    	}
    
    	sum := 0
    	for index, val := range digits {
    		sum += val * int(math.Pow10(n-index-1))
    	}
    
    	return sum
    }
    
    func readBank(line string) (bank, error) {
    	runes := []rune(line)
    	bk := make(bank, len(runes))
    	for idx, c := range runes {
    		switch c {
    		case '0':
    			bk[idx] = 0
    		case '1':
    			bk[idx] = 1
    		case '2':
    			bk[idx] = 2
    		case '3':
    			bk[idx] = 3
    		case '4':
    			bk[idx] = 4
    		case '5':
    			bk[idx] = 5
    		case '6':
    			bk[idx] = 6
    		case '7':
    			bk[idx] = 7
    		case '8':
    			bk[idx] = 8
    		case '9':
    			bk[idx] = 9
    		default:
    			msg := fmt.Sprintf("not a number: %c", c)
    			return bank{}, errors.New(msg)
    		}
    	}
    	return bk, nil
    }
    
    func getBankChannel(input chan string) chan bank {
    	ch := make(chan bank, cap(input))
    
    	go func() {
    		for line := range input {
    			bank, err := readBank(line)
    			if err != nil {
    				fmt.Errorf("error reading line %v: %v\n", line, err)
    				close(ch)
    				return
    			}
    			ch <- bank
    		}
    		close(ch)
    	}()
    
    	return ch
    }
    
    func stepOne(input chan string) (int, error) {
    	ch := getBankChannel(input)
    	sum := 0
    	for bank := range ch {
    		sum += bank.largestNDigitJoltage(2)
    	}
    
    	return sum, nil
    }
    
    func stepTwo(input chan string) (int, error) {
    	ch := getBankChannel(input)
    	sum := 0
    	for bank := range ch {
    		sum += bank.largestNDigitJoltage(12)
    	}
    
    	return sum, nil
    }
    
    func main() {
    	// input2 := utils.InputText(inputText)
    	input := utils.FilePath("day03.txt")
    
    	ch, err := input.GetLineChannel()
    	if err != nil {
    		fmt.Errorf("step one error: %v\n", err)
    		return
    	}
    
    	var one int
    	one, err = stepOne(ch)
    	if err != nil {
    		fmt.Errorf("step one error: %v\n", err)
    		return
    	}
    	fmt.Printf("Step one result: %v\n", one)
    
    	// input2 := utils.InputText(inputText)
    	input2 := utils.FilePath("day03.txt")
    
    	ch, err = input2.GetLineChannel()
    	if err != nil {
    		fmt.Errorf("step two error: %v\n", err)
    		return
    	}
    
    	var two int
    	two, err = stepTwo(ch)
    	if err != nil {
    		fmt.Errorf("step two error: %v\n", err)
    		return
    	}
    	fmt.Printf("Step two result: %v\n", two)
    }
    

    While I am quite an adaptable person and I learn to program quickly in about all the languages I’ve tried, I’m still at the beginning of my journey with Go. It does feel like the language is trying to resist me being clever at every corner. I understand the reasons, why not, but damn it does make the development a bit frustrating at times

  • h4x0r ( h4x0r@lemmy.dbzer0.com ) 
    link
    fedilink
    English
    arrow-up
    3
    ·
    edit-2
    10 months ago

    c

    #include "aoc.h"
    #include <stdio.h>
    #include <string.h>
    
    constexpr usize LINE_BUFSZ = (1 << 7);
    constexpr u64 TEN = 10;
    constexpr u64 TWELVE = 12;
    
    static void
    joltage(strc line, u64* total, usize on) {
      usize len = strlen(line);
      usize off = len - on;
      usize slen = 0;
      c8 stack[LINE_BUFSZ] = {};
      for (usize i = 0; i < len; i++) {
        while (slen > 0 && off > 0 && stack[slen - 1] < line[i]) {
          slen--;
          off--;
        }
        stack[slen++] = line[i];
      }
      u64 jltg = 0;
      for (usize i = 0; i < on; i++) {
        jltg = (jltg * TEN) + (u64)(stack[i] - '0');
      }
      *total += jltg;
    }
    
    static void
    solve(u64 on) {
      FILE* input = fopen("input", "r");
      c8 line[LINE_BUFSZ] = {};
      u64 total = 0;
      while (fgets(line, sizeof(line), input)) {
        line[strcspn(line, "\n")] = 0;
        joltage(line, &total, on);
      }
      fclose(input);
      printf("%lu\n", total);
    }
    
    i32
    main(void) {
      solve(2);
      solve(TWELVE);
    }
    
  • Jayjader ( Jayjader@jlai.lu ) 
    link
    fedilink
    arrow-up
    2
    ·
    10 months ago

    (Browser-based) Javascript

    For part 2, I eagerly wrote a nice, clean, generic, functional depth-first search, only to get an out of memory error 😭. Note the top-level code blocks: they scope the variables declared inside them, allowing me to run the whole script repeatedly in the console without getting “redeclared variable name” errors.

    function part1(inputText) {
      let totalOutputJoltage = 0;
      for (const batteryBankDef of inputText.split('\n')) {
        let bestBankJoltage = 0;
        const previousDigits = [];
        for (const character of batteryBankDef) {
          const currentDigit = Number.parseInt(character, 10);
          for (const previousDigit of previousDigits) {
            const possibleVoltage = 10 * previousDigit + currentDigit;
            if (possibleVoltage > bestBankJoltage) {
              bestBankJoltage = possibleVoltage;
            }
          }
          previousDigits.push(currentDigit);
        }
            totalOutputJoltage += bestBankJoltage;
      }
      return totalOutputJoltage;
    }
    {
        const start = performance.now();
        const result = part1(document.body.textContent)
        const end = performance.now();
        console.info({day: 3, part: 1, result, time: end - start})
    }
    
    function findNthDigitForSequence(bankDef, n, startIndex) {
      let digit = 9;
      while (digit > 0) {
        for (let i = startIndex; i < bankDef.length - 11 + n; i++) {
          if (bankDef[i] === digit.toString()) {
            return [digit, i]
          }
        }
        digit--;
      }
      return undefined;
    }
    function findBestJoltageForBank(bankDef) {
      const digits = [];
      let previousFoundDigitIndex = -1;
      for (let i = 0; i < 12; i++) {
        const digitFound = findNthDigitForSequence(bankDef, i, previousFoundDigitIndex + 1);
        if (digitFound === undefined) {
          debugger;
          return undefined;
        }
        const [digit, index] = digitFound;
        digits.push(digit);
        previousFoundDigitIndex = index;
      }
      return Number.parseInt(digits.join(''), 10);
    }
    function part2(inputText) {
      let totalOutputJoltage = 0;
      for (const batteryBankDef of inputText.trim().split('\n')) {
        totalOutputJoltage += findBestJoltageForBank(batteryBankDef) ?? 0;
      }
      return totalOutputJoltage;
    }
    
    {
      const start = performance.now();
      const result = part2(document.body.textContent);
      const end = performance.now();
      console.info({ day: 3, part: 2, time: end - start, result });
    }
  • Quant ( Quant@programming.dev ) 
    link
    fedilink
    arrow-up
    2
    ·
    edit-2
    10 months ago

    My original solution for part 1 was just removing the last digit, get the highest number, cut off everything up to and including that first number, get the highest number again.
    Once I did part 2 I realized I can just throw in a loop, cut off parts of the end so there’s enough numbers left for the subsequent iterations and keep the rest the same.
    Now it works for any number of batteries and all you’d need to change is the number after Total! :D

    Online pad: AoC-2025-D3

    You can even use your own input by uploading a file (make sure it’s using LF line endings only with a trailing one at the end) and replacing the example input with this: &rs inf &fo "input-file.txt"

    Code
    $ 987654321111111
    $ 811111111111119
    $ 234234234234278
    $ 818181911112111
    ⊜∘⊸≠@\n
    
    Max ← ⊢⊸⍖
    
    Jolt! ← (
      ¯^
      ""
      ⍥(⊙(⤚⊡Max◡↘+₁
          ⊙(⊙↘⤚⋅∘+₁))
        ⊂
      )^
      ⊙⋅◌
    )
    
    Total! ← (
      ≡(⋕Jolt!^)
      /+
    )
    
    PartOne ← Total!2
    PartTwo ← Total!12
    
    ⊸PartOne
    &pf "Part One: "
    &p
    PartTwo
    &pf "Part Two: "
    &p
    
  • GiantTree ( GiantTree@feddit.org ) 
    link
    fedilink
    English
    arrow-up
    2
    ·
    10 months ago

    Kotlin

    I’m late to the party but I hope some of you will still be inspired by my submisison. This is an iterative solution. I began with a recursive solution that worked but I noticed that it should really be rewritten in an iterative way. The solution is also pointlessly optimized, to some degree, but that’s just what I like to do. 🙂

    The logic follows a simple pattern of knowing which window of the battery bank to search in. Given the amount of batteries that remain to be turned on, if you were to turn on the last battery in the window, you’d need to turn on all the remaining batteries. So the window begins at one position past the prior battery and ends at the last battery you actually can choose to turn on. Once that has been turned on, all remaining ones need to be turned on. The window can only actually shrink to at least one position.

    Code inside
    class Day03 : AOCSolution {
        override val year = 2025
        override val day = 3
    
        override fun part1(inputFile: String): String {
            return readResourceBinary(inputFile).lineSequence().sumOf { batteryBank ->
                findHighestJoltage(batteryBank, 2)
            }.toString()
        }
    
        override fun part2(inputFile: String): String {
            return readResourceBinary(inputFile).lineSequence().sumOf { batteryBank ->
                findHighestJoltage(batteryBank, 12)
            }.toString()
        }
    
        private fun findHighestJoltage(
            bank: EightBitString,
            batteries: Int,
        ): Long {
            val digitsArray = ByteArray(batteries) { -1 }
    
            var lastDigitIndex = 0
            repeat(batteries) { currentDigit ->
                val remainingDigits = batteries - currentDigit
                val lastIndex = bank.length - remainingDigits + 1
    
                val maxIndex = bank.indexOfMax(lastDigitIndex, lastIndex)
                lastDigitIndex = maxIndex + 1
                digitsArray[batteries - remainingDigits] = bank[maxIndex].toDigit()
            }
    
            return digitsArray.fold(0L) { acc, i -> acc * 10L + i }
        }
    
    
        private companion object {
            private fun ByteArray.lineSequence(): Sequence<EightBitString> {
                val buffer = EightBitString(this)
                var currentOffset = 0
                return generateSequence {
                    for (characterIndex in currentOffset until buffer.limit()) {
                        if (buffer[characterIndex] == '\n') {
                            val slice = buffer.subSequence(currentOffset, characterIndex)
    
                            // Despite believing that `currentIndex` is not read,
                            // it is indeed read the next time this generator is called.
                            @Suppress("AssignedValueIsNeverRead")
                            currentOffset = characterIndex + 1
                            return@generateSequence slice
                        }
                    }
                    // A '\n' is always found, because the files end with a new line.
                    return@generateSequence null
                }
            }
    
            private fun EightBitString.indexOfMax(
                startIndex: Int,
                endIndex: Int,
            ): Int {
                if (startIndex >= endIndex) {
                    return -1
                }
                var maxIndex = startIndex
                var max = 0.toByte()
                for (i in startIndex until endIndex) {
                    val c = getByte(i)
                    if (c > max) {
                        maxIndex = i
                        max = c
                    }
                }
                return maxIndex
            }
    
            private fun Char.toDigit(): Byte = (this - '0').toByte()
        }
    }
    
    
  • CameronDev ( CameronDev@programming.dev ) OP
    link
    fedilink
    arrow-up
    1
    ·
    edit-2
    10 months ago
       fn calc_joltage(
            values: &[u32],
            count: usize,
            cache: &mut HashMap<(usize, usize), usize>,
        ) -> usize {
            if let Some(result) = cache.get(&(values.len(), count)) {
                return *result;
            }
            if count == 0 {
                return 0;
            }
            let mut highest = 0;
            let mut highest_base = 0;
            for (i, value) in values[0..values.len() - count + 1].iter().enumerate() {
                if *value < highest_base {
                    continue;
                }
                let base_joltage = (*value as usize) * 10_usize.pow(count as u32 - 1);
                let joltage = base_joltage + calc_joltage(&values[i + 1..], count - 1, cache);
                if joltage > highest {
                    highest = joltage;
                    highest_base = *value;
                }
            }
            cache.insert((values.len(), count), highest);
            highest
        }
    
        #[test]
        fn test_y2025_day3_part2() {
            let input = std::fs::read_to_string("input/2025/day_3.txt").unwrap();
            let mut total = 0;
            input.lines().for_each(|line| {
                let banks = line
                    .chars()
                    .map(|c| c.to_digit(10).unwrap())
                    .collect::<Vec<u32>>();
                let joltage = calc_joltage(&banks, 12, &mut HashMap::new());
                total += joltage;
            });
            println!("Total: {}", total);
        }
    

    Seems i missed the faster solutions, but i did get this down to a respectable 400ms. edit: 400ms was not respectable, mykl’s method took 1ms. Mine was close though, with a bit more brain and optimisation I got there.

    And the bot worked all by itself!

  • Rust

    Seeing some of the other solutions in this thread, there are definitely simpler (and probably still faster) solutions possible, but I first sorted the bank by the highest batteries (keeping the index information) and then used a recursive greedy algorithm to find the largest battery that still follows the index order.

    View on github

    fn part1(input: String) {
        let mut sum = 0;
        'banks: for l in input.lines() {
            let mut sorted: Vec<(usize, u32)> = l
                .chars()
                .map(|c| c.to_digit(10).unwrap())
                .enumerate()
                .collect();
            sorted.sort_by(|(_, a), (_, b)| a.cmp(b).reverse());
            for (idx, first) in &sorted {
                for (id2, second) in &sorted {
                    if id2 > idx {
                        sum += first * 10 + second;
                        continue 'banks;
                    }
                }
            }
        }
        println!("{sum}");
    }
    
    // Recursive implementation of greedy algorithm.
    // Returns Vec of length 12 if a result was found, guaranteed to be optimal.
    // If there is no solution with the input, a shorter Vec is returned.
    fn recursive(bank: &[(usize, u32)], mut cur: Vec<(usize, u32)>) -> Vec<(usize, u32)> {
        let pos = cur.last().unwrap().0;
        for &(idx, e) in bank.iter().filter(|(idx, _)| *idx > pos) {
            cur.push((idx, e));
            if cur.len() == 12 {
                // Recursion anchor: We have filled all 12 spots and therefore found
                // the best solution
                return cur;
            }
            // Recurse
            cur = recursive(bank, cur);
            if cur.len() == 12 {
                // Result found
                return cur;
            }
            // Nothing found, try next in this position
            cur.pop();
        }
        // Unsuccessful search with given inputs
        cur
    }
    
    fn part2(input: String) {
        let mut sum = 0;
        'banks: for l in input.lines() {
            let mut sorted: Vec<(usize, u32)> = l
                .chars()
                .map(|c| c.to_digit(10).unwrap())
                .enumerate()
                .collect();
            sorted.sort_by(|(_, a), (_, b)| a.cmp(b).reverse());
            let mut cur: Vec<(usize, u32)> = Vec::with_capacity(12);
            for &(idx, first) in &sorted {
                cur.push((idx, first));
                cur = recursive(&sorted, cur);
                if cur.len() == 12 {
                    let num = cur.iter().fold(0u64, |acc, e| acc * 10 + e.1 as u64);
                    sum += num;
                    continue 'banks;
                }
                cur.pop();
            }
        }
        println!("{sum}");
    }
    
    util::aoc_main!();