ARTS 第4周| LeetCode 1143 最长上升子序列| 21天能学会编程吗| Go defer 的用法

ARTS

ARTS 是陈浩(网名左耳朵耗子)在极客时间专栏里发起的一个活动,目的是通过分享的方式来坚持学习。

每人每周写一个 ARTS:Algorithm 是一道算法题,Review 是读一篇英文文章,Technique/Tips 是分享一个小技术,Share 是分享一个观点。

本周内容

这周的 ARTS 你将看到:

  1. 动态规划典型中的典型,最长上升子序列。
  2. 你对编程的热爱都用十年吗?
  3. 这些 defer 的坑让你大呼卧槽。
  4. 本周没有灵光一闪。

Algorithm

本周的算法题是经典题目:最长上升子序列。

LeetCode 1143. Longest Increasing Subsequence

这道题即是经典的动态规划题,也是经典面试题。但是因为前一段时间做了太多回溯相关的题,所以这道题我偏要用 DFS 来 AC,但没想到这真的挺坑。话不多说,直接贴 DFS 代码。

//  这是一次 DFS 的超低空飞行,危险刺激但是能 AC
func lengthOfLIS_DFS(nums []int) int {
    var dfs func(s, prev, count int) int
    // 直接递归会超时,必须加记忆
    // key 表示 nums 字符起始位置
    // value 表示从 key 代表的位置开始到 nums 结尾 LIS 的长度增量
    mem := make(map[int]int, 0)
    // s 正在处理的子串起始下标
    // prev 递归树中上一个层级已构成的上升序列最大值(最后一个元素值)
    // count nums[s] 加入到递归分支的 LIS 之后 LIS 的长度
    // 这样之所以可以记忆中间结果是因为
    // 不同的递归求解 LIS 的过程可能会经过相同的 nums[s] 
    // 只要当前到达的 nums[s] 相同的话,后续能将上升序列长度增加多少
    // 完全取取决于 nums[s] 之后的子数组构成
    // 这种记忆方式已经无限接近 DP 了
    dfs = func(s, prev, count int) int {
        if _, ok := mem[s]; ok {
            return count+mem[s]
        }
        var ret int
        flag := false
        for i := s; i < len(nums); i++ {
            if nums[i] > prev {
                flag = true
                ret = max(dfs(i+1, nums[i], count+1), ret)
            }
        }
        if !flag {
            ret = count-1
        }
        mem[s] = ret - count
        return ret
    }
    return dfs(0, -1<<31, 1)
}

func max(a, b int) int {
    if a < b {
        return b
    }
    return a
}

DFS 代码没有变量 mem 来记忆中间结果的话,肯定会超时,但是考虑如果做这个记忆的确花了很多时间。思考过程都在注释里了,下面是枯燥无味的 DP 解法。

// DP 就是这么枯燥且无味,毫无优化就 faster than 71.14%
func lengthOfLIS(nums []int) int {
    // dp[i] 表示截止到 nums[i] 时 LIS 的长度
    // 需要遍历一遍 dp 找到最大值
    dp := make([]int, len(nums))
    // base case 看起来很像废话,但 LIS 的长度确实最少是 1
    for i := 0; i < len(dp); i++ {
        dp[i] = 1
    }    
    var ans int
    for i := 0; i < len(nums); i++ {
        for j := 0; j < i; j++ {
            if nums[i] > nums[j] {
                dp[i] = max(dp[i], dp[j]+1)
            }
        }
        ans = max(ans, dp[i])
    }
    return ans
}

这道题算是 DP 典型题中的典型题,所以无论如何也要理解呦。

Review 文章推荐

这周回一下来好像没有看任何一篇值得拿出来 Review 的好的纯技术英文文章,但是因为最近看了不少技术人的成长和学习的实践方式和方法论相关的文章,这周就回顾一下这个话题相关的几篇文章吧。

首先,是这一篇Teach Yourself Programming in Ten Years,直译过来就是“花十年时间自学编程”。作者对这些年“21天学会XX”这种书籍的流程发表了自己的看法。作者强调21天只能让你了解一个技术领域,但是如果精通或者成为专家,可能是要十年才行。

然后,是这一篇The Greatest Developer Fallacy Or The Wisest Words You’ll Ever Hear?. 文章的标题中所说的可能是至理名言也可能是谬论的话,其实就是大家平常所说的「我会在真正用到的时候在学习这项技能」。这句话初看起来闪烁着实用主义的光芒,但实际上并不完全可行。因为很多问题并不是优雅地出现,经常是通过各种故障瓶颈被发现,跟你用来处理问题的时间并不多。亦或是在平时的开发中,如果你「并不知道」有某项技术工具或者算法能够快速解决一个让你焦头烂额问题,那么你很可能会事倍功半。作者在文中有个比方很恰当:“等我有了钱我再学理财”,所以到底先有鸡还是先有蛋?我想应该是先学会理财才能提升财富增长的速度,先了解技术领域才能在需要的时候知道该用什么技术解决问题。在陈浩的这篇中文翻译中有一个读者留言写的非常好。

我觉得真正的问题不是“Learn it when I need it”,而是做不到“Do one thing and do it well”:当需要的时候,没有深度地学习,而是浅尝辄止。… 从Google到SO,惊喜地发现一段看似合适的代码,侥幸地试一试,竟然成功了,再看看IE,效果还过得去,那就这样吧。这更算是“Copy it when I need it”.

所以说,十年或者说十万小时是某种客观存在的积累过程,不存在任何绝对意义上的捷径,如果有的话那只可能是让你一直坚持下去的热爱了。这样说来兴趣可能不仅仅带你走进一个领域,更是决定了你能走多远。务虚说了这么多,下面是几篇务实的文章。说归说,最后践行起来还是任重道远的。
陈浩的程序员练级攻略-2018版 以及曹春晖的文章工程师应该怎么学习 推荐给你。

Tip 编程技巧

这周的技巧延续了上周的阅读内容,搜索 defer 原理的时候找到了一些挺有意思的总结,比这篇一些让你惊呼“卧槽”的 defer 技巧。主要是一些你可能很难想到的 defer 隐形坑位,可能平时开发不会有人这么写(拐弯抹角给自己挖坑),但作为一些思维方式训练还是很好的,可以更深刻的理解 defer. 嗷对了,这是作者的一个列文章,但都很简短,花一个小时就可以看完。

Share 灵光一闪

本周没有灵光一闪,希望下周能闪一下。以上。


发表评论

电子邮件地址不会被公开。 必填项已用*标注