最近刚开始算法学习,对递归思想非常着迷,做project euler时遇到题目就想用递归处理一下,但总的来说吃力不讨好。倒是今天给我找到一个绝佳的例子,相比于暴力破解,用递归重写后的代码轻巧了许多,可读性也增强了。

题目

project euler 31题:

In England the currency is made up of pound, £, and pence, p, and there are eight coins in general circulation:

1p, 2p, 5p, 10p, 20p, 50p, £1 (100p) and £2 (200p).

It is possible to make £2 in the following way: 1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p

How many different ways can £2 be made using any number of coins?

暴力破解1

看到题目最自然的想法是穷举一下所有的组合,检测和是不是200。

这是非常原始的暴力破解方法,好处是不需要进一步的思考,非常容易写,且保证能得到正确结果。这几点好处——尤其是保证能得到正确结果是后面优化的基石——使得这种粗陋的代码也有写出来的价值。

当然,缺点是效率不理想,一般不会作为最终版本来使用。这里还好,只用了三四分钟,有时则可能要几个月,那就不得不立刻开始优化了。

代码:

def coin():
    counter = 0
    for i100 in range(3):
        item100 = 100 * i100
        print "100:", i100
        for i50 in range(5):
            item50 = 50 * i50
            print "50:", i50
            for i20 in range(11):
                item20 = 20 * i20
                for i10 in range(21):
                    item10 = 10 * i10
                    for i5 in range(41):
                        item5 = 5 * i5
                        for i2 in range(101):
                            item2 = 2 * i2
                            for i in range(201):
                                if item100 + item50 + item20 + item10 + item5 + item2 + i == 200:
                                    counter += 1
    return counter

上面的代码没有考虑只取200分的情况,所以得到的结果加上1才是正确答案。

暴力破解2

project euler前面的题目都比较简单,根据经验,算法得当的话运算时间都可以轻松压在毫秒级,上面的代码算了几分钟是不可忍受的。

问题显然在于计算了太多本不需要的计算的可能,所谓优化,不过是不需要算的部分直接不算了,具体到这个问题上,上面是先列举可能,再测试是否符合要求,其实可以反过来处理,你不是要200吗,那就从里面往外减,什么时候减没了算完。

所以代码可以写成:

def coin2():
    counter = 0
    item200 = 200
    while item200 >= 0:
        item100 = item200
        while item100 >= 0:
            item50 = item100
            while item50 >= 0:
                item20 = item50
                while item20 >= 0:
                    item10 = item20
                    while item10 >= 0:
                        item5 = item10
                        while item5 >= 0:
                            item2 = item5
                            while item2 >= 0:
                                counter += 1
                                item2 -= 2
                            item5 -= 5
                        item10 -= 10
                    item20 -= 20
                item50 -= 50
            item100 -= 100
        item200 -= 200
    return counter

好消息是效率提升不少,现在只需要16ms就可以搞定。

但依旧是8曾循环嵌套,烧脑子可读性差容易出错就不说了,这里问题简单些只要8层就能解决问题,可如果遇到更多的呢?你能写出来解释器也吃不消吧。

递归

这种问题很适合用递归来处理,先不多说,看代码:

def coin3(l=[200, 100, 50, 20, 10, 5, 2, 1], target=200):
    counter = 0
    if len(l) == 1:
        return 1
    while target >= 0:
        counter += coin3(l[1:], target)
        target -= l[0]
    return counter

刚写出来的时候我也不相信,之前繁琐的代码竟然可以变的如此漂亮,但算出来的答案是一样的,不骗你。唯一的缺点是用递归慢一些。

那这是怎么正常工作的?

设想我们有一个一般化的处理方案,函数coin(l, target),吃进去一个包含了所有可取的硬币额度和一个最终需要的总数,吐出来的就是所有可能的组合方案,没错这就是我们要的。

但这个函数怎么写呢?下面一步很关键。

主要用到了分而治之的战略思想。先考虑一个简单的例子,coin([5, 2, 1], 10),很容易发现有10种可能,问题在于使用了怎样的解题思路。

一个保证不漏掉可能的思路是先考虑到底有取了多少个5分,显然只能是0或1或2,如果是0的话,问题被简化为求coin([2, 1], 10),同理,如果是1的话问题简化为求coin([2, 1], 5);最后只需要把几种情况下的可能加起来就是最终结果了。

这就是递归处理的思路,每次只处理一个候选项,并将简化了的问题通过自己调用自己的方式解决,一路下来正确答案就自己跳出来了。

唯一需要留心的是这种递归调用终结在什么地方,稍微思考一下就会发现,是整个问题最基础最简单的情形,这样解决起来也不费什么功夫了。

再看一眼

其实递归的思路和第二版的暴力破解是一样的,但得益于函数可以自我调用这一特点,代码被大大简化了。

就好比没有循环之前你想求1到100的求和只能老老实实一步一步去写,用上循环则只需要三行。

用递归的好处在于,问题被一般化了,8曾循环是这样写,100层还是这样写(只要不爆栈的话...)。代码思路变得更加清晰,可读性更好,出错的机会就小了。

但递归这种思想和人脑的工作方式有悖,所以理解也好、使用也罢并不是那么自然,需要训练。

不管如何这都是一种强大的处理问题的思路,值得好好斟酌一番。

一点补充

上面的代码其实还取了一个巧,因为最后一个选项是1,所以不管前面怎么分配,到最后一步时总能找到一个符合要求的结果,所以索性不算了。如果要处理更一般的情况,则还需要一些改动。


给pip换国内的pypi镜像

2014-02-27 by 煎挠橙

今天发现pypi.python.org挂了,用pip搜索安装都不正常,索性换个国内的镜像试试。

镜像地址

根据这个网站的说法,目前国内的镜像主要有

推荐电信/联通用户用前者,教育网用户用后者。

用法

单次下载可以指定镜像

$pip install -i http://pypi.hustunique.com/simple <package name>

全局设定

~/.pip/pip.conf配置文件中加上

[global]
index-url = http://pypi.hustunique.com/simple

地址最后的simple一定不能忘了。

改过之后速度有所提升,重要的是稳定多了。

read more

一个用python写的命令行发微博工具开发记录

2014-01-27 by 煎挠橙

源起

一个常见的情景:工作中有感而发,想要吐槽两句,下意识的打开浏览器,进入新浪微博页面,然后就没有知觉了。十几分钟后清醒过来,发现自己光顾着看微博,不仅之前想说的东西已经忘了,自己的工作也耽搁了,不禁懊恼万分,决心好好工作;若干分钟后上面的场景再次上演。

因为最近在终端里呆的时间比较多,正好又在学习python,就希望试着用python写一个命令行里的发微博工具,只能写,不能看,工作吐槽两不误。

准备工作

前期主要花时间看文档,找参考。

首先,新浪开放平台的官方文档是必要的参考资料。

另外,新浪推荐了来自廖雪峰python SDK,直接安装就能使用,不过因为追求独立性和扩展性,代码有些庞杂,反正我没看懂。

在别处找到了一个更轻量级的脚本,正如该项目所说的,使用了requests而不是urllib做相关的http操作,极大的简化了代码,提高了可读性。我主要以此为参考 ...

read more

ubuntu下python虚拟环境的配置问题

2014-01-15 by 煎挠橙

昨天重做了一下系统,今天配置的时候有些新收获,写在这里。

Enthought Canopy

注:下面有更新,Enthought Canopy现在并非首选。

自己用python首要是做计算的,昨天试着自己配置一下几个常用的包,弄了半天卡在依赖关系上了,没办法又用回Enthought的Canopy。

Canopy是一个集成环境,把IDE和扩展包管理器等东西绑在了一起,做计算用的包也都有,装完所有东西都齐活了可以直接使用,非常舒服,后续的升级维护也很方便。

但有一个问题是Canopy会推荐你将其设置为系统默认的python环境,好处是你在终端里面也可以用Canopy下面的包,这样就免得你再次配置,非常方便。问题是,如果后面用python做其他方面的开发,直接就乱套了。

Canopy的虚拟环境

Canopy采用了虚拟环境,所以并不影响系统本身的python环境,思路大致是把机器的环境复制一份出来放在其他路径下,运行维护就跟机器的环境再不相关了。

如果你也安装了Canopy,可以看一下,一般机器的python环境都在

/usr/lib/python2.7

而Canopy默认情况下则把环境装在了

/home/Enthought/Canopy

两者没有什么关联。

而之前提到的将Canopy设置为系统默认python环境,则是Canopy在~/.bashrc文件最后加了一句

VIRTUAL_ENV_DISABLE_PROMPT ...
read more