Ruby List Comprehension in Ruby

liqiang · 2015年01月11日 · 最后由 luikore 回复于 2015年01月12日 · 3263 次阅读

Haskell 的网站上看到了一道数独的题目,和一系列不同的解法

其中最精简的一个解法是这样的:

Module Main where f x s@(h:y)=let(r,c)=divMod(length x)9;m#n=mdiv3==ndiv3;e=[0..8]in [a|z<-['1'..'9'],h==z||h=='.'&& notElem z(map((x++s)!!)[i*9+j|i<-e, j<-e,i==r||j==c||i#r&&j#c]),a<-f(x++[z])y] f x[]=[x] main=print$f[]"53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79"

原始链接在此

这份代码可以先不着急读,后面会详细讲解。

这个解法代码非常少,其实算法也是普通的回溯法。具体来说就是从前到后检查所有没填数字的位置,排除与这个数字同一行、同一列、及处在同一个块中的所有数字,然后从剩下的数字中挑选一个填上去,再对下一个没填数字的位置做相同的操作,直到把棋盘上所有的位置都填完。如果在某个位置上找不到可用的数字,则回溯到上一个位置,尝试下一个可能的数字。

使用 ruby 写出来的代码可以是这样的:

BLOCK_SIZE = 3
PANEL_SIZE = BLOCK_SIZE * BLOCK_SIZE

def nums_in_row(panel, index)
  row_start = (index / PANEL_SIZE) * PANEL_SIZE
  panel[row_start..row_start + PANEL_SIZE - 1]
end

def nums_in_column(panel, index)
  column = index % PANEL_SIZE
  (0..PANEL_SIZE - 1).map{|row| panel[row * PANEL_SIZE + column]}
end

def nums_in_block(panel, index)
  block_row = index / PANEL_SIZE / BLOCK_SIZE
  block_column = index % PANEL_SIZE / BLOCK_SIZE

  nums = []

  (0..BLOCK_SIZE - 1).each do |block_i|
    (0..BLOCK_SIZE - 1).each do |block_j|
      row = block_row * BLOCK_SIZE + block_i
      column = block_column * BLOCK_SIZE + block_j
      nums << panel[row * PANEL_SIZE + column]
    end
  end
  nums
end

def all_possible_next(state)
  target_index = state.index 0
  used_nums = (nums_in_row(state, target_index) +
                nums_in_column(state, target_index) +
                nums_in_block(state, target_index)).uniq
  ((1..PANEL_SIZE).to_a - used_nums - [0]).map do |num|
    cloned = state.clone
    cloned[target_index] = num
    cloned
  end
end

def all_filled?(state)
  state.none?{|num| num == 0}
end

def solve(original)
  stack = []
  stack.push original

  solutions = []

  until stack.empty?
    current = stack.pop
    all_possible_next(current).each do |state|
      if all_filled? state
        solutions << state
      else
        stack.push state
      end
    end
  end
  solutions
end

代码量上差距巨大!那为什么用 Haskell 可以写的这么简洁呢,其中一个很重要的原因已经写在这道题答案的 url 中了,它使用了 List Comprehension。

原始的代码变量名比较随意,不太好读,我把它稍微重构了一下,如下:

module Main where
f :: [Char] -> [Char] -> [[Char]]
f x s@(h:y)=let (row,column)=divMod (length x) 9
                inSameSegment m n = m`div`3==n`div`3
                availNums=[0..8]
                inSameBlock x1 y1 x2 y2 = inSameSegment x1 x2 && inSameSegment y1 y2
                usedNums = map((x++s)!!)[i*9+j|i<-availNums,j<-availNums,i==row||j==column||inSameBlock i j row column]
            in
              [a|z<-['1'..'9'], h /= '.' || h=='.' && notElem z (usedNums), a <- f (x++[z]) y]
f x []=[x]

main=print $ f [] "53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79"

其中这部分

[i*9+j|i<-availNums,j<-availNums,i==row||j==column||inSameBlock i j row column]

就是一个典型的 list comprehension。 这个表达式返回的是一个数组(Haskell 中其实叫列表) 它的计算过程是:i 和 j 分别可以是 availNums 这个数组中的任意元素,如果 i 和 j 的某个组合满足这个条件

i==row||j==column||inSameBlock i j row column

那么 i*9+j 就会成为最终返回的数组中的一个元素。 对于这个例子,可以简单的理解为一个两重循环,像这样:

result = []
availNums.each do |i|
  availNums.each do |j|
    if i==row||j==column||inSameBlock(i, j, row, column)
      result << i*9+j
    end
  end
end

直接写两重循环还可以忍受,但是如果再深就没法看了。但是使用 list comprehension 就还好,因为多余的元素足够少。 那么好吧,就让我们实现一个 ruby 的 list comprehension 吧。

def lc_internal(lists, params, condition_proc, result_proc)
  l = lists.first
  if l.nil?
    case params.size
      when 1
        if condition_proc.call params[0]
          [(result_proc.call params[0])]
        end
      when 2
        if condition_proc.call params[0], params[1]
          [(result_proc.call params[0], params[1])]
        end
      else
        raise 'only support at most 2 list'
    end
  else
    l.map do |ele|
      vals = lc_internal lists[1..-1], params + [ele], condition_proc, result_proc
      vals.nil? ? [] : vals
    end.reduce(:+)
  end
end

def list_comprehension(*lists, condition_proc, result_proc)
  lists.first.map do |ele|
    lc_internal lists[1..-1], [ele], condition_proc, result_proc
  end.reduce(:+)
end

其中向外暴漏的是 list_comprehension,它包含了三个参数,第一个参数可以接受多个数组;第二个参数是个 Proc,用来判断某个组合是否满足指定的条件;第三个参数也是个 Proc,使用这个组合来计算出相应的值,这个值,如前面提到的,会成为最终返回数组中的一个元素。 关于“如何把一个数组传递给多个参数的函数这件事情”,不知道怎么做,所以写了一个丑陋的 case 语句,目前只能支持两个列表,多了就报错了!

然后再回到第一个版本的 ruby 代码,使用 list_comprehension 之后,主要变化的地方就是 all_possible_next 这个函数,现在这个函数变成这样了:

def all_possible_next(state)
  avail_nums = (0..PANEL_SIZE - 1).to_a

  target_index = state.index 0
  row = target_index / PANEL_SIZE
  column = target_index % PANEL_SIZE
  used_nums = list_comprehension(avail_nums, avail_nums,
                               Proc.new{|i, j| i == row or j == column || (i / 3 == row / 3 && j / 3 == column / 3)},
                               Proc.new{|i, j| state[i*9+j]}).uniq
  ((1..PANEL_SIZE).to_a - used_nums).map do |num|
    cloned = state.clone
    cloned[target_index] = num
    cloned
  end
end

当然了,原来版本中的前三个函数也都不需要了。原来的 63 行代码变成现在 43 行了,少了 1/3。 这份 ruby 代码可以继续优化,不过本文就到此为止了。

函数式编程中真是有不少好东西,可以借鉴到其他语言中用。

不错,顶一下

list comprehension 就是 filtermap 而已,而且 ruby 也有 divmod 啊...

def row n
  n...(n+9)
end

def col n
  (0...9).map{|x| x*9 + n }
end

def box m, n
  [m, m+1, m+2].product([n, n+1, n+2]).map{|i, j| i*9 + j}
end

def f x, a
  x -= 1
  return a if x < 0
  return f(x, a) if a[x] != '.'
  a = a.dup
  q, r = x.divmod 9
  ds = ('1'..'9').to_a - a.values_at(*row(q*9), *col(r), *box(q/3*3, r/3*3))
  ds.find {|d| a[x] = d; return r if (r = f x, a) }
end

r = f 81, "53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79".chars
puts r.each_slice(9).map(&:join)

还有一行流解数独不过太魔法

程序写简单了,会不会增加理解难度呢

#2 楼 @luikore 偶像,你来啦!你在上班吗?

#4 楼 @kikyous 今天成人节...

#2 楼 @luikore 你这个解有错误,box 这个函数只包含了一个块内对角线三个元素的下标。

另外这行代码

ds.find {|d| a[x] = d; return r if (r = f x, a) }

的逻辑有问题吧,if (r = f x, a) 永远都是真,那么程序并不会尝试 ds 里面所有的值,每次都是尝试了第一个就返回了。 而且在 block 中加 return 语句的写法也太诡异了。。。

关于 list comprehension,这里例子可能还不是很典型,之前见过这样一个例子挺好的: 在每条边都小于 100 的三角形中,找出所有直角三角形,并列出三条边的长度

[(a,b,c) | a<-[1..100],b<-[1..a],c<[1..b],c^2+b^2==a^2]

map 加 filter 搞不定 list comprehension 哦,起码要先来个笛卡尔积。

#6 楼 @liqiang 你说的对... 应该是 product 而不是 zip, 已经改正

f x, a 在找不到匹配值的时候会返回 nil, 不是永远为真的 (find 在空 ds 或者所有尝试都失败后返回 nil)

block 里 return 很正常啊,写多了你就习惯了

#7 楼 @luikore 恩,看懂了,这个解法是得到一个合法解就返回了,不会求出所有的解。

@liqiang 三角形这个例子不错,但是你实现的 list_comprehension 也不支持动态范围的 b, c, 那么这么写

list_comprehension [*1..100], [*1..100], [*1..100],
  proc{|a, b, c| c**2 + b**2 == a**2 },
  :itself.to_proc

就相当于这么写而已:

[*1..100].product([*1..100], [*1..100]).select{|a, b, c| c**2 + b**2 == a**2}.map &:itself
需要 登录 后方可回复, 如果你还没有账号请 注册新账号