Ruby使用自己的输出迭代一个函数,而不会改变原始输入

时间:2017-06-02 13:22:49

标签: ruby iteration

目标:尝试优化alphametics problem on exercism的解决方案。

我的方法:我不想蛮力一次尝试所有字母的所有可能组合,而是先尝试与单位相对应的字母。如果猜测满足单位,则使用该猜测作为尝试十位单位的起点。对于问题所需的许多地方等等。

理由:通过不浪费时间来减少不能使用不能为字母子集工作的数字子集来减少迭代次数。

这是我的代码解决了谜题,但速度非常慢(8个字母的拼图大约需要5-6秒,10个字母的时间更长)。

class Alphametics
  class << self
    def solve(input)
      @input = input
      guess(@input)
    end

    def guess(input)
      letters_to_keys(input)
      left_most_digit_of_sum_max(input, @candidates)
      left_most_digit_is_not_0(first_letters(input), @candidates)
      generate_guess(@candidates)
    end

    def letters_to_keys(input) # create hash of letters and their possible values
      @candidates = {}
      letters(input).each {|ltr| @candidates[ltr] = (0..9).to_a}
      @candidates
    end

    def letters(input) # creates an array of unique letters from summands and sum
      (summands(input) + [sum(input)]).join.chars.uniq
    end

    def summands(input) #takes the input and separates summands from sum, removing the "+" character, returns array of "words" as strings
      input.split(" == ")[0].split(" + ")
    end

    def sum(input) # takes the input and separates sum from summands, returning a string of only letters
      input.split(" == ")[1]
    end

    def left_most_digit_of_sum_max(input, candidates) # the first digit of the sum has its value considerably constrained by the number summands when the sum has more digits than any of the individual summands
      if summands(input).all? {|word| word.length < sum(input).length}
        candidates[sum(input)[0]] = (0..summands(input).length-1).to_a
      end
    end

    def left_most_digit_is_not_0(letters, candidates) # the first digit of any number cannot be zero
      letters.each {|ltr| candidates[ltr] -= [0]}
    end

    def first_letters(input) # create an array of the first letter of each word -- for determining which letters cannot be zero
      a = []
      summands(input).each { |word| a << word[0]}
      a << sum(input)[0]
      a
    end

    def generate_guess(candidates)
      answer = {}
      potential_values = candidates.values
      letters = candidates.keys
      guess_index(potential_values).each do |i|
        dividend = i
        guess = []
        (0..potential_values.length-1).each do |ndx|
          divisor = potential_values[ndx].length
          if guess.include?(potential_values[ndx][dividend % divisor])
            next i
          else
            guess << potential_values[ndx][dividend % divisor]
            dividend = dividend / divisor
          end
        end
        if guess.length == potential_values.length && check_summation(letters, guess)
          answer = solution(guess, letters)
          return answer
        end
      end
      return answer
    end

    def guess_index(potential_values)
      index_length = 1
      potential_values.each { |vals| index_length*=vals.length }
      (0..index_length-1).to_a
    end

    def check_summation(letters, guess) # check that guess satisfies the summation
      nums = int_sub_for_letter(@input, letters, guess)
      summands(nums).collect{|x| x.to_i}.reduce(:+) == sum(nums).to_i
    end

    def int_sub_for_letter(input, letters, guess) # substitute numbers for letters in string
      string = input
      (0..guess.length-1).each do |i|
        string = string.gsub(letters[i], guess[i].to_s)
      end
      return string
    end

    def solution(guess, letters) # generate solution hash without mutating candidates hash
      letters.zip(guess).to_h
    end


  end
end
t1 = Time.now
p Alphametics.solve("SEND + MORE == MONEY")
t2 = Time.now
p t2-t1

我试图通过减少所需的迭代次数来提高效率。我已经通过顺序执行每个地方(如果垂直写入的总和的列)的计算来完成此操作。因此,在其他地方重复的地方的字母将具有较少的潜在价值,从而导致总体猜测的排列更少,因此总和更少的检查。

从本质上说,我遇到的问题是: 鉴于&#34; hash&#34;使用拼图术语中的字符串作为值,如何在每个&#34;地点&#34;上迭代一组值,根据需要展开集合以适应下一个&#34;地点&#中的新字母34 ;? 为了说明,对于拼图&#34; SEND + MORE == MONEY&#34;你有

h = { 
      0 => ['D', 'E', 'Y'], 
      1 => ['ND', 'RE', 'EY'], 
      2 => ['END', 'ORE', 'NEY', 
      3 => ['SEND', 'MORE', 'ONEY'], 
      4 => ['MONEY'] 
    }

根据位置和加数的数量约束每个字母的可能值后,您得到:

candidate_values = { 
      M => [1], 
      S => [1,2,3,4,5,6,7,8,9], 
      E => [0,1,2,3,4,5,6,7,8,9], 
      N => [0,1,2,3,4,5,6,7,8,9], 
      D => [0,1,2,3,4,5,6,7,8,9], 
      O => [0,1,2,3,4,5,6,7,8,9], 
      R => [0,1,2,3,4,5,6,7,8,9], 
      Y => [0,1,2,3,4,5,6,7,8,9] 
    }

我知道如何为每个地方生成所有可能的猜测,但我想要做的是为那些地方生成所有猜测,迭代这组猜测并检查猜测是否满足那些地方的添加。如果确实如此,我想采取这种猜测,用它来为十位产生一组可能的猜测,并以相同的方式迭代这些猜测。但如果找不到十位的解,那么它应该回到猜测的迭代。

FWIW,这些是我尝试用来设置迭代的方法:

def check_guess(guess, candidates, current_segments, current_letters, exp)
  nums_to_chk = current_segments
  current_letters.each do |ltr|
    nums_to_chk = int_sub_for_letter(nums_to_chk, current_letters, ltr, guess)
  end
  nums_to_chk.collect {|i| i.reverse.to_i}[0..-2].reduce(:+).to_s[-(exp+1)..-1] == nums_to_chk[-1].reverse
end
def take_a_guess(guesses, candidates, segments, current_segments, current_letters, exp)
  guesses.each do |guess|
    if check_guess(guess, candidates, current_segments, current_letters, exp)
      partial_solution = current_letters.zip(guess.each_slice(1).to_a).to_h
      exp+=1
      next_segments = segments[exp]
      new_letters = letters_to_chk(next_segments) - current_letters
      new_letters.each{|ltr| partial_solution[ltr] = candidates[ltr]}
      next_letters = partial_solution.keys
      next_values = get_potential_values(partial_solution, next_letters)
      next_guesses = generate_guesses(next_values)
      take_a_guess(next_guesses, partial_solution, segments, next_segments, next_letters, exp)

    end
  end
end

1 个答案:

答案 0 :(得分:0)

这不是一个真正的答案,而是一个扩展的评论。我只是想提出一些解决问题的建议。 (我会使用递归,顺便说一句。)我花了相当多的时间在这上面,但我所提供的并不是非常令人满意,在整个地方游荡,并且不完整。我已经写了很多单词和方程式,以及一些不完整的代码,但已经将所有这些都删除了。

我开始讨厌这个问题了。这是一个问题的类型,人们可以在短短几分钟内找到解决它的一般方法,但是有很多特殊情况和曲折处理代码来解决它是非常耗时的像完成根管一样有趣。

问题

该链接提供了正在解决的问题的示例。我们给出了字符串或大写字母数组,可以按如下方式查看。

  S E N D
  M O R E +
-----------
M O N E Y

这个想法是为每个字母分配一个数字,以便得到的结果是正确的。这就是:

{ "S"=>9, "E"=>5, "N"=>6, "D"=>7, "M"=>1, "O"=>0, "R"=>8, "Y"=>2 }

给出了以下内容。

  9 5 6 7
  1 0 8 5 +
-----------
1 0 6 5 2

<强>观察

为方便起见,我将按如下方式表达此问题。

arr = [["*", "S", "E", "N", "D"],
       ["*", "M", "O", "R", "E"],
       ["M", "O", "N", "E", "Y"]]

我们可以假设&#34; M&#34;必须等于1,因为不会在该位置写0。因此,我们的已知值的开始哈希是

known = { "*"=>0, "M"=>1 }

使用以下数组更方便。

arr.transpose.reverse
  #=> [["D", "E", "Y"],
  #    ["N", "R", "E"],
  #    ["E", "O", "N"],
  #    ["S", "M", "O"],
  #    ["*", "*", "M"]] 

第一行代表1位的列,第二行代表10位的列,依此类推。我们的搜索将逐行进行,直到我们对所有行进行有效分配。

第三行(例如)代表两个等式:

v("E") + v("O") + c = v("N")
v("E") + v("O") + c = v("N") + 10

其中v给出每个字母的值(数字0-9),c是前一列(01的值)。这两个方程分别对应于01向前传送到左侧的列(下一个最高位数)。

我们的想法是将数字0-9依次分配给&#34; D&#34;然后为每个分配数字0-9分配给&#34; E&#34;,然后计算关联值&#34; Y&#34;,v("Y")。如果v("Y") < 10,则到下一列的结转是0;其他v("Y")替换为v("Y") % 10,结转到下一列是1

假设&#34; D&#34;被分配了6。然后搜索树中当前位置的哈希值为

known = { "*"=>0, "M"=>1, "D"=>6 }

如果&#34; E&#34;被分配7,哈希将成为

known = { "*"=>0, "M"=>1, "D"=>6, "E"=>7 }

&#34; Y&#34;的价值然后将计算为

v("Y") = 6 + 7 + 0 #> 13

(请注意,1&#39s列的结转为零。)由于v("Y") > 9v("Y") = v("Y") % 10 #=> 31会转移到下一列,known

known = { "*"=>0, "M"=>1, "D"=>6, "E"=>7, "Y"=> 3 }

当处理10列时,方程

v("N") + v("R") + 1 = v("E")
v("N") + v("R") + 1 = v("E") + 10

但目前已知v("E"),这些方程式减少到

v("N") + v("R") = 2   (carryover 0)
v("N") + v("R") = 12  (carryover 1)

我们现在可以为v("R")分配不同的数字(比方说),并为v("N")求解。

在第3行,我们只需求解v("R")。如果是19之间,我们会继续下一行;否则我们放弃搜索树的那一部分并返回到10列。当第一列(上面数组的最后一行)的计算成功时,算法以解决方案终止;否则就得出结论,没有解决方案。

假设某列包含字母xyz,并且c(0或1)已从前一列和v中提取是一个将这些字母中的每一个映射到数字的函数。

需要考虑的案例

以下是需要考虑的各种情况。

x == y == z

这需要v(x) = 9c #=> 1

x == y, z != x, v(z)已知

这里的等式是

2*v(x) + c = v(z)

2*v(x) + c = v(z) + 10

如果v(z)已知,我们可以像v(x)那样解决。

v(x) = (v(z) - c)/2

v(x) = (v(z) - c + 10)/2

假设v(z) #=> 5c #=> 1。上面的两个表达式评估为v(x) #=> 2v(x) #=> 7。我们需要探索这两种可能性。如果c #=> 0v(x)在两种情况下都采用小数值,那么我们可以得出结论,在搜索树v(z)的此分支处不能等于5

x == y, z != xv(x)已知

我们在这里解决

v(z) = 2*v(z) + c

v(z) = 2*v(z) + c - 10

请注意,0 <= v(z) <= 45 <= v(z) <= 9分别代表两个表达式,无论c是否等于01。因此,只有一个解决方案。假设v(z) #=> 6。然后,表达式评估v(z) #=> 14 + cv(z) #=> 4 + c . Therefore, we need only evaluate the first expression. If v(x)&lt; 10 we set v(x)to that value and carry 0 to the next column; otherwise we set v(x)to that number less 10 and carry 1`到下一栏。

假设大写和小写字母分别代表具有已知值和未知值的字母。等于从前一列(或或1)带来的数量。

解决一个未知值

x + Y +  = Z      #=> x = Z - Y -        if Z > Y  (carry 0)
x + Y +  = Z + 10 #=> x = Z + 10 - Y -   if Z <= Y (carry 1)

占据其他两个可能位置的未知值的计算是相似的。

解决两个未知值

假设有两个未知值由两个字母表示。

x + y +  = Z

要使x为正数,y <= Z - ,除了yZ = 0之外,它会为 = 1提供一系列值,x + y + = Z + 10 x除外哪里没有解决方案。另一种情况是

0 <= y <= [Z -  + 10, 9].min = 9

y为正数,0-9,表示x + x + = Z x + x + = Z + 10 可以分配任意数字。

同样,当已知值占据其他两个可能位置中的一个时,计算类似。

解决三个未知值

如果所有三个值都未知,我们可以按顺序将数字Z - 分配给第一行中的字母,将问题减少为具有两个未知值的值,我刚才讨论过。

特殊情况

x

如果x = (Z - )/2 x = (Z - )/2 + 5 为奇数或-1,则这些方程没有积分解(对于0 <= x <= 9)。如果该数额是非负的甚至是

x + Y +  = x
对于这两个数字,

X + y + = y,因此必须在搜索树中探索这两项任务。

如果x + Y + = x + 10 #=> Y #=> 10 - X + y + = y + 10 #=> X #=> 10 - Y没有完整的解决方案。然而,

Z

如果已知值(10 - x)等于y,则未知值(x + x + = x #=> x = - )可以等于任何数字。

最后一种情况是有三个未知数,都是相同的。如果

0

x = 1必须等于x + x + = x + 10 #=> x = 10 - `` ,在这种情况下,而如果<{p}}

1

x = 9必须等于{{1}},在这种情况下{{1}}。