# encoding: utf-8
#
# The problems other than the first are cited from;
# www.nikoli.com/ja/puzzles/numberlink
#
module Solver
def solve
previous_state, present_state = nil, dump
while !solved? && present_state != previous_state
previous_state = present_state
step
present_state = dump
end
self
end
def solved?
all?(&:fixed?)
end
def step
each(&:solve)
end
def reductio_ad_absurdum
each(&:check_cell_type_error)
while !solve.solved?
previous_state = dump
cell = reject(&:directed?)[0]
candidate = cell.connectable_cells[0]
begin
cell.disconnect(candidate)
reductio_ad_absurdum
rescue NumberLink::Cell::CellTypeError
restore(previous_state)
cell_at(*cell.pos).connect(cell_at(*candidate.pos))
end
end
self
end
end
module CellInteraction
CellTypeError = Class.new(StandardError)
def cell_at(x, y)
return unless (0...@width) === x && (0...@height) === y
@field[y][x]
end
def disjoint?(other)
(@marks & other.marks).empty?
end
# Returns nil if and only if other is not next to self.
def relative_position(other)
if pos_x == other.pos_x
case pos_y - other.pos_y
when -1 then [:down, :up]
when 1 then [:up, :down]
end
elsif pos_y == other.pos_y
case pos_x - other.pos_x
when -1 then [:right, :left]
when 1 then [:left, :right]
end
end
end
def connectable?(other)
return false if fixed? || other.fixed?
rp, * = relative_position(other)
rp && !@disconnection.include?(rp) && !@connection.include?(rp)
end
def connected?(other)
other, * = relative_position(other) if self.class == other
other && @connection.include?(other)
end
def disconnected?(other)
other, * = relative_position(other) if self.class === other
other && @disconnection.include?(other)
end
def connect(other)
return if fixed? || other.fixed?
rp = relative_position(other)
@connection |= [rp[0]]
other.connection |= [rp[1]]
chain = chained_cells
common_marks = @marks & other.marks
other.marks.replace(common_marks)
chained_cells.each do |cell|
next if cell.fixed?
cell.marks = other.marks
cell.fix if cell.fixable?
end
content_connection
other.content_connection
end
def disconnect(other)
return if fixed? || other.fixed?
rp = relative_position(other)
@disconnection |= [rp[0]]
other.disconnection |= [rp[1]]
@marks.delete(other.marks[0]) if other.identified?
other.marks.delete(@marks[0]) if identified?
content_connection
other.content_connection
end
def content_connection
return fix if fixed? || fixable?
param = edge? ? 1 : 0
if @connection.size == 2 - param
rest_directions.each{|dir| disconnect(adjacent_cell(dir))}
elsif @disconnection.size == 2 + param
rest_directions.each{|dir| connect(adjacent_cell(dir))}
end
fix if fixable?
end
def connectable_cells
x, y = pos
[cell_at(x - 1, y), cell_at(x, y - 1),
cell_at(x + 1, y), cell_at(x, y + 1)].select do |other|
other && connectable?(other)
end
end
def adjacent?(other)
!!relative_position(other)
end
def adjacent_cell(*directions)
x, y = pos
x -= 1 if directions.include?(:left)
y -= 1 if directions.include?(:up)
x += 1 if directions.include?(:right)
y += 1 if directions.include?(:down)
cell_at(x, y)
end
def chained_cells(direction = nil)
[self] + (@connection - [direction]).flat_map do |dir|
ac = adjacent_cell(dir)
ac.check_cell_type_error
ac.chained_cells(opposite_direction(dir))
end
end
def adjacent_chains?(chain1, chain2)
chain1.any? do |cell1|
chain2.any? do |cell2|
cell1.adjacent?(cell2) && cell1.disconnected?(cell2)
end
end
end
def check_cell_type_error
broken = @connection.size + @disconnection.size > 4 ||
edge? && @connection.size > 1 ||
!edge? && @disconnection.size > 2
raise CellTypeError, 'Connection or Disconnection is broken' if broken
raise CellTypeError, 'No Marks in a cell' if @marks.empty?
detouring = bended? &&
@connection.any? do |dir|
ac = adjacent_cell(dir)
ac.bended? &&
!(@connection & ac.connection).empty?
end
raise CellTypeError, 'Detouring' if detouring
end
end
module CellSolver
def solve
disconnect_with_inside_of_corner ||
disconnect_with_disjoint_cells ||
connect_with_same_marked_cells ||
connect_when_no_alternatives ||
disconnect_with_adjacent_chains ||
reduce_marks ||
deduce_from_slanting_cells ||
false
end
def disconnect_with_disjoint_cells
connectable_cells.each{|other| disconnect(other) if disjoint?(other)}
directed?
end
def connect_with_same_marked_cells
return unless identified?
connectable_cells.each{|other| connect(other) if @marks == other.marks}
directed?
end
def connect_when_no_alternatives
connectables = connectable_cells
param = edge? ? 1 : 2
connectables.each{|cell| connect(cell)} if @connection.size + connectables.size == param
directed?
end
def disconnect_with_inside_of_corner
return unless bended?
inside_cell = adjacent_cell(*@connection)
@connection.each{|dir| inside_cell.disconnect(adjacent_cell(dir))}
end
def disconnect_with_adjacent_chains
chain = chained_cells
connectable_cells.each do |other|
disconnect(other) if adjacent_chains?(chain, other.chained_cells)
end
directed?
end
def reduce_marks
return if edge? || !@connection.empty?
connectables = connectable_cells
identified = connectables.select(&:identified?)
identified_marks = identified.map(&:marks)
major_mark = identified_marks.find{|m| identified_marks.count(m) == 2}
cs, is = connectables.size, identified.size
if major_mark && (cs == 3 || is == 4 && identified_marks.uniq.size == 3)
identified.each{|cell| connect(cell) if cell.marks == major_mark}
elsif !major_mark && (cs == 4 && is == 3 || cs == 3 && is == 2)
@marks.replace(@marks & identified_marks.reduce(:|))
check_cell_type_error
connect((connectables - identified)[0])
end
directed?
end
def deduce_from_slanting_cells
return if edge?
slanting_ways = [[:left, :up], [:left, :down], [:right, :up], [:right, :down]]
slanting_ways.each{|dir| deduce_one_slanting_way(dir)}
directed?
end
def deduce_one_slanting_way(directions)
[directions, directions.reverse].each do |dir1, dir2|
next if !disconnected?(opposite_direction(dir1)) ||
connected?(opposite_direction(dir2))
sc = self
deduction = true
while sc = sc.adjacent_cell(dir1, dir2)
break(deduction = false) if sc.edge?
break if sc.disconnected?(dir2)
end
connect(adjacent_cell(opposite_direction(dir2))) if deduction
end
end
def opposite_direction(direction)
case direction
when :left then :right
when :up then :down
when :right then :left
when :down then :up
end
end
end
class NumberLink
include Solver
include Enumerable
NULL_MARK = '*'
TypeError = Class.new(StandardError)
def initialize(puzzle_sheet)
puzzle_ary = puzzle_sheet.split($/).map(&:split)
@height = puzzle_ary.size
@width = puzzle_ary[0].size
all_marks = puzzle_sheet.split.uniq.sort - [NULL_MARK]
@field = []
@field.concat(
puzzle_ary.map.with_index do |ary, y|
ary.map.with_index do |mark, x|
default_cell(x, y, mark, all_marks)
end
end
)
check_type_error
self
end
def default_cell(x, y, mark, all_marks)
disconnection = []
disconnection << :left if x == 0
disconnection << :right if x == @width - 1
disconnection << :up if y == 0
disconnection << :down if y == @height - 1
default_marks = mark == NULL_MARK ? Array.new(all_marks) : [mark]
Cell.new([x, y], default_marks, mark != NULL_MARK,
disconnection, @field, @width, @height)
end
def check_type_error
raise TypeError, 'Field is not rectangle' if @field.any?{|ary| ary.size != @width}
all_marks = each_with_object([]){|cell, m| m << cell.marks[0] if cell.edge?}
not_paired_marks = all_marks.uniq.select{|m| all_marks.count(m) != 2}
return if not_paired_marks.empty?
msg = not_paired_marks.size == 1 ?
'There is a not paired mark: %p' :
'There are not paired marks: %p'
raise TypeError, msg % [not_paired_marks]
end
def each
@field.each{|ary| ary.each{|cell| yield cell}}
end
def to_s
@field.map(&:join).join($/)
end
def cell_at(x, y)
return unless (0...@width) === x && (0...@height) === y
@field[y][x]
end
def dump
Marshal.dump(@field)
end
def restore(port)
@field.replace(Marshal.load(port))
end
end
class NumberLink::Cell
include CellInteraction
include CellSolver
attr_reader :pos, :field
attr_accessor :marks, :connection, :disconnection
def initialize(position, marks, edge, disconnection, field, width, height)
@pos = position
@marks = marks
@edge = edge
@connection = []
@disconnection = disconnection
@fixed = false
@field = field
@width = width
@height = height
end
def to_s
return ' ' if @marks.empty?
edge_str = ((@connection == [:left] ? '─%s' : '%2s') % @marks)[-2, 2]
return edge_str if @edge
case @connection.sort
when [:left, :right] then '──'
when [:left, :up] then '─┘'
when [:down, :left] then '─┐'
when [:right, :up] then ' └'
when [:down, :right] then ' ┌'
when [:down, :up] then ' │'
when [:left] then '- '
when [:right] then ' -'
when [:up] then ' \''
when [:down] then ' ,'
else ' ?'
end
end
def fix
return if fixed?
@fixed = true
freeze
end
def fixed?
@fixed
end
def fixable?
!fixed? && identified? && directed?
end
def directed?
@connection.size + @disconnection.size == 4
end
def edge?
@edge
end
def pos_x
@pos[0]
end
def pos_y
@pos[1]
end
def identified?
@marks.size == 1
end
def straight?
@connection.sort!
@connection == [:down, :up] || @connection == [:left, :right]
end
def bended?
@connection.size == 2 && !straight?
end
def rest_directions
[:left, :up, :right, :down] - (@connection + @disconnection)
end
end
$stdin.readlines($/ * 2).each do |str|
begin
puts NumberLink.new(str.chomp.gsub(/\*|\w/, ' \\& ')).reductio_ad_absurdum
rescue NumberLink::Cell::CellTypeError
puts 'No Solution'
rescue NumberLink::TypeError => ex
puts ex
end
puts '-' * 48
end