input = ARGF.readlines(chomp: true).map {|row| springs, groups = row.split(" ") [springs, groups.scan(/\d+/).map(&:to_i)] } p input # .map { [ _1.chars, _2 ] } .map { [Array.new(5, _1).join(??).chars, _2 * 5] } .map {|springs, groups| arrangement_counts = Hash.new {|h,k| count, springs, groups = k first, *rest = springs h[k] = case [count, first, groups] in count, _, [] if count.nonzero? 0 elsif springs.include?(?#) 0 else 1 end in count, _, groups if count > groups.fetch(0) 0 # early exit if the damaged group ever gets too large in count, nil, groups # no more springs if groups.size > 1 0 elsif count == groups.fetch(0) 1 else 0 end in count, ?#, groups # continue a damaged group h[[count + 1, rest, groups]] in 0, ?., groups h[[0, rest, groups]] in count, ?., groups # finish a damaged group if count == groups.fetch(0) h[[0, rest, groups[1..]]] else 0 end in count, ??, g if count == g.fetch(0) # unknown spring, matched current group h[[0, rest, g[1..]]] in count, ??, g unless count.zero? # unknown spring, ongoing group h[[count + 1, rest, g]] in count, ??, g # unknown spring [ h[[count, rest, g]], # undamaged h[[count + 1, rest, g]], # damaged ].sum else fail "#{{first:, springs: springs.join, groups:}}" end } arrangement_counts[[0, springs, groups]] }.sum # original part one # p input.map {|springs, groups| # unknowns = springs.count(??) # (0...2**unknowns).map {|arrangement| # arrangement = arrangement.to_s(2).rjust(unknowns, ?0).chars.map { _1 == ?0 ? ?. : ?# } # candidate = springs.gsub(??) { arrangement.shift } # candidate.chars # .slice_when { _1 != _2 } # .reject { _1[0] == ?. } # .map(&:size) == groups # }.count(&:itself) # }.sum