ARTS_003

"week 03"

Posted by Xion on November 17, 2018      views:

这是耗子叔发起的一个活动,每周为一个周期,需要完成以下内容

  • Algrothm: leetcode算法题目
  • Review: 阅读并且点评一篇英文技术文章
  • Tip/Techni: 学习一个技术技巧
  • Share: 分享一篇有观点和思考的技术文章

[Algrothm] knight-dialer

problem

https://leetcode.com/problems/knight-dialer/

A chess knight can move as indicated in the chess diagram below:

 .           

图片可以点击原链接查看 

This time, we place our chess knight on any numbered key of a phone pad (indicated above), and the knight makes N-1 hops.  Each hop must be from one key to another numbered key.

Each time it lands on a key (including the initial placement of the knight), it presses the number of that key, pressing N digits total.

How many distinct numbers can you dial in this manner?

Since the answer may be large, output the answer modulo 10^9 + 7.

 

Example 1:

Input: 1
Output: 10
Example 2:

Input: 2
Output: 20
Example 3:

Input: 3
Output: 46
 

Note:

1 <= N <= 5000

思路

  • 获取到状态转移的关系
  • 这是一个典型的动态规划的问题,可以按照三个步骤的思路来处理(https://xionchen.github.io/2018/11/03/arts-001/#dp%E7%90%86%E8%A7%A3)
    • 递归
    • 存储
    • 改为从头开始

我的解法

  • 状态转移关系,用字典表示
          {1:[6,7],
           2:[7,9],
           3:[4,8],
           4:[3,9,0],
           5:[],
           6:[1,7,0],
           7:[2,6],
           8:[1,3],
           9:[2,4],
           0:[4,6]}
    

    递归

       def knightDialer(self, N):
          """
          :type N: int
          :rtype: int
          """
          def move_to(now_number, n):
              key = '{}|{}'.format(n, now_number)
    
              if n == 1:
                  if move_to_cache.get(key):
                      return move_to_cache.get(key)
                  move_to_cache[key] = len(preview_numbers_of[now_number])
                  return len(preview_numbers_of[now_number])
              else:
                  if move_to_cache.get(key):
                      return move_to_cache.get(key)
                  now = 0
                  for number in preview_numbers_of[now_number]:
                      now += move_to(number, n-1)
    
                  move_to_cache[key] = now
                  return now
    
          preview_numbers_of = {1: [6, 8],
                                2: [7, 9],
                                3: [4, 8],
                                4: [3, 9, 0],
                                6: [1, 7, 0],
                                7: [2, 6],
                                8: [1, 3],
                                9: [2, 4],
                                0: [4, 6]}
          move_to_cache = {}
    
          if N == 1:
              return 10
          else:
              sum_num = 0
              for k, v in preview_numbers_of.items():
                  sum_num += move_to(k, N-1)
              return sum_num   
    

显然这样效率非常低,可以profile以下看看,求N=10的情况下,move to被调用了6576次,显然其中有很多重复求解的情况

move_to	11643	3	3
<method 'items' of 'dict' objects>	1	0	0
<built-in method builtins.__build_class__>	1	0	0
<built-in method builtins.len>	6576	0	0
<built-in method builtins.print>	1	0	0
<module>	1	3	0
Solution	1	0	0
knightDialer	1	3	0

存储

  class Solution(object):
    def knightDialer(self, N):
        """
        :type N: int
        :rtype: int
        """
        def move_to(now_number, n):
            key = '{}|{}'.format(now_number,n)

            if n == 1:
                if move_to_cache.get(key):
                    return move_to_cache.get(key)
                move_to_cache[key] = len(preview_numbers_of[now_number])
                return len(preview_numbers_of[now_number])
            else:
                if move_to_cache.get(key):
                    return move_to_cache.get(key)
                now = 0
                for number in preview_numbers_of[now_number]:
                    now += move_to(number, n-1)

                move_to_cache[key] = now
                return now

        preview_numbers_of = {1: [6, 8],
                              2: [7, 9],
                              3: [4, 8],
                              4: [3, 9, 0],
                              6: [1, 7, 0],
                              7: [2, 6],
                              8: [1, 3],
                              9: [2, 4],
                              0: [4, 6]}
        move_to_cache = {}
        if N == 1:
            return 10
        else:
            sum_num = 0
            for k, v in preview_numbers_of.items():
                sum_num += move_to(k, N-1)
            return sum_num


if __name__ == '__main__':
    s = Solution()
    print(s.knightDialer(N=10))  

再profle一下看看

<method 'format' of 'str' objects>	169	0	0
<method 'get' of 'dict' objects>	257	0	0
<method 'items' of 'dict' objects>	1	0	0
<built-in method builtins.__build_class__>	1	0	0
<built-in method builtins.len>	18	0	0
<built-in method builtins.print>	1	0	0
<module>	1	0	0
move_to	169	0	0
Solution	1	0	0
knightDialer	1	0	0

结果显然好了不少

改为非递归,即动态规划的方式

    def knightDialer(self, N):
        preview_numbers_of = [ [4, 6],  [6, 8],  [7, 9],  [4, 8], [0, 3, 9], [], [0, 1, 7], [2, 6], [1, 3],
              [2, 4]]
        dp = [[0] * 10 for _ in range(N)]
        mod = 10 ** 9 + 7
        for i in range(10): dp[0][i] = 1

        for i in range(1, N):
            for j in range(10):
                for number in preview_numbers_of[j]:
                    dp[i][j] += dp[i - 1][number]
                dp[i][j] %= mod

        return sum(dp[N - 1]) % mod

其实改为非递归之后还是存在超时问题,后来修改了不少地方:

  • 状态转移字典改为数组,减少了hash
  • 列表推导改为了循环,使用inner操作
  • 之前没注意还有mod 10 ** 9 +7 ,加上了
  • 一样的代码要多跑几遍,leetcode的服务器有时候比较慢

最终才没有超时

solution

官反提供的solution基本与自己实现的一致,有一些小细节不一样。

class Solution(object):
    def knightDialer(self, N):
        MOD = 10**9 + 7
        moves = [[4,6],[6,8],[7,9],[4,8],[3,9,0],[],
                     [1,7,0],[2,6],[1,3],[2,4]]

        dp = [1] * 10
        for hops in xrange(N-1):
            dp2 = [0] * 10
            for node, count in enumerate(dp):
                for nei in moves[node]:
                    dp2[nei] += count
                    dp2[nei] %= MOD
            dp = dp2
        return sum(dp) % MOD
  • dp不是一次性生成的,是append出来的,这个看了python里面list的实现,基本与java一致,是数组,所以一次性生存可以防止频繁的resize。
  • 使用了xrange,这个在python3中就没去别了,py2里面xrange比较快。
  • 每一步都mod,参考了python里面加法的实现,这样确实能够减少运算。

[Review]

还是强化学习有关内容 https://github.com/wwxFromTju/awesome-reinforcement-learning-zh

内容比较多,并且独立完整,所有详细的内容记录在了这里 RL Course by David Silver

[Tip] python type hint

what

https://www.python.org/dev/peps/pep-0484/

这个特性我非常喜欢,正好最经用到了。其实就是python的类型提示,它有什么用呢?

why

python的优点在于灵活,缺点也在于灵活,所以这个语言的下限其实非常低,可以写出非常难以阅读的代码。类型提示在某种程度上可以有效的改善这种情况。

正如我在一篇博客里面提到,阅读源码的时间是你写代码时间的十倍以上。任何写起来爽的同时降低的代码可读性的操作都是一时之爽。

这点上有点类似mongodb一样,mongodb在易用性下了大功夫,但是在事务处理,安全方面却做的不是很好,维护成本非常高。这就导致了一个现象,大家一开始做poc或者创业公司很喜欢用mongodb。但是随着业务压力的上升,渐渐的都只能离开它(这种情况近几年有所改善)。

所以生孩子的效率固然重要,但是绝对不能忽视养孩子的成本。

说了这么多就是为了说明一件事,type hint用好了,真的很有用。它能有效的帮助代码最初的设计结构不被破坏,有效的提高代码可读性,而且还有一点,自动提升能更加准确和智能。

how

首先是官方的文档 如果有一点python基础的话应该很快就能掌握。这里我只简单的写个例子。

from type import List
class Book:
    def __init__(self):
        ...

class BookService:
    def __init__(self):
       ...
       ...
    
    def get_books_by_type(type: str) -> List(Book):
       ...

这样就很明确,type是一个str类型,它会返回一个Book的list。

这样避免的type这个变量的模糊和返回类型的模糊,如果没有标注,有的人可能试图传入一个别的类型作为type,或者在修改代码的时候直接返回一个Book obj而不是Book 的list。

当然,这些都能写道函数的注释里,但是我的观点的代码的自解释性优先与注释。所以更推荐这种方式。

[Share] 美团技术leader:写给工程师的十条精进原则

美团技术leader:写给工程师的十条精进原

这些都是进入职场的工程师必备的素质:

  • Owner意识
  • 时间观念
  • 以始为终
  • 保持敬畏
  • 事不过二
  • 设计优先
  • P/PC平衡
  • 善于提问
  • 空杯心态