算法分析:大中小三个水桶分水问题

摘要:
可以看出,状态树遍历的关键是找到三个桶之间的所有合法注水动作,并使用这些注水动作来“生成”它们相应的新状态。除了通过“剪枝”提高算法的效率外,对于深度优先状态搜索,还需要防止由于状态的循环生成而导致深度优先搜索无法终止的问题。

题目要求:有一个容积为8升的水桶里装满了水,另外还有一个容积为3升的空桶和一个容积为5升的空桶,如何利用这两个空桶等分8升水?附加条件是三个水桶都没有体积刻度,也不能使用其它辅助容器。

一直以为只有一种方法2333,想了一个早上,再结合网上大神的解释(https://blog.csdn.net/orbit/article/details/6596521)(http://www.it610.com/article/5031244.htm),才发现方法很多。。。。终于弄明白了一点。

涉及python算法参考(python七大查找算法):https://www.cnblogs.com/lsqin/p/9342929.html

算法分析:大中小三个水桶分水问题第1张

                             解决问题的思路

        如果用人的思维方式,那么解决这个问题的关键是怎么通过倒水凑出确定的1升水或能容纳1升水的空间,考察三只水桶的容积分别是3、5和8,用这三个数做加减运算,可以得到很多组答案,例如:

3 – (5 - 3) = 1

这个策略对应了上面提到的第一种解决方法,而另一组运算:

(3 + 3)- 5 = 1

则对应了上面提到的第二种解决方法。

        但是计算机并不能理解这个“1”的重要性,很难按照人类的思维方式按部就班地推导答案,因此用计算机解决这个问题,通常会选择使用“穷举法”。为什么使用穷举法?因为这不是一个典型意义上的求解最优解的问题,虽然可以暗含一个求解倒水次数最少的方法的要求,但就本质而言,常用的求解最优解问题的高效的方法都不适用于此问题。如果能够穷举解空间的全部合法解,然后通过比较找到最优解也是一种求解最优解的方法。不过就本题题意而言,并不关心什么方法最快,能求出全部等分水的方法可能更符合题意。

        如果我们把某一时刻三个水桶中存水的容积称为一个状态,则问题的初始状态是8升的水桶装满水,求解的解出状态(最终状态)是8升水桶中4升水,5升水桶中4升水。穷举法的实质就是把从初始状态开始,根据某种状态变化的规则搜索全部可能的状态,每当找到一个从初始状态到最终状态的变化路径,就可以理解为找到了一种答案。这样的状态变化搜索的结果通常是得到一棵状态搜索树,根节点是初始状态,叶子节点可能是最终状态,也可能是某个无法转换到最终状态的中间状态,状态树有多少个最终状态的叶子节点,就有多少种答案。根据以上分析结果,解决本问题的算法关键有三点:首先,建立算法的状态模型;其次,确定状态树的搜索算法(暗含状态转换的规则);最后,需要一些提高算法效率的手段,比如应用“剪枝”条件避免重复的状态搜索,还要避免状态的循环生成导致搜索算法在若干个状态之间无限循环。

状态和动作的数学模型

        建立状态模型是整个算法的关键,这个状态模型不仅要能够描述静止状态,还要能够描述并记录状态转换动作,尤其是对状态转换的描述,因为这会影响到状态树搜索算法的设计。所谓的静止状态,就是某一时刻三个水桶中存水的容积,我们采用长度为3的一维向量描述这个状态。这组向量的三个值分别是容积为8升的桶中的水量、容积为5升的桶中的水量和容积为3升的桶中的水量。因此算法的初始状态就可以描述为[8 ,0, 0],则终止状态为[4, 4, 0]。

        对状态转换的描述就是在两个状态之间建立关联,在本算法中这个关联就是一个合法的倒水动作。某一时刻三个水桶中的存水状态,经过某个倒水动作后演变到一个新的存水状态,这是对状态转换的文字描述,对算法来讲,倒水状态描述就是“静止状态”+“倒水动作”。我们用一个三元组来描述倒水动作:{from, to, water},from是指从哪个桶中倒水,to是指将水倒向哪个桶,water是此次倒水动作所倒的水量。本模型的特例就是第一个状态如何得到,也就是[8, 0, 0]这个状态对应的倒水动作如何描述?我们用-1表示未知的水桶编号(上帝水桶),因此第一个状态对应的倒水动作就是{-1, 1, 8}。应用本模型对前面提到的第一种解决方法进行状态转换描述,整个过程如图(1)所示:

算法分析:大中小三个水桶分水问题第2张

图1 一个解决方法的状态转换图

 

状态树搜索算法

 

        确定了状态模型后,就需要解决算法面临的第二个问题:状态树的搜索算法。一个静止状态结合不同的倒水动作会迁移到不同的状态,所有状态转换所展示的就是一棵以状态[8, 0, 0]为根的状态搜索树,图(2)画出了这个状态搜索树的一部分,其中一个用不同颜色标识出来的状态转换过程(状态树的一个分支)就是本问题的一个解:

算法分析:大中小三个水桶分水问题第3张

 

图2状态树一部分的展示

 

      状态树的搜索就是对整个状态树进行遍历,这中间其实暗含了状态的生成,因为状态树一开始并不完整,只有一个初始状态的根节点,当搜索(也就是遍历)操作完成时,状态树才完整。树的遍历可以采用广度优先遍历算法,也可以采用深度优先遍历算法,就本题而言,要求解所有可能的等分水的方法,暗含了要记录从初始状态到最终状态,所以更适合使用深度优先遍历算法。状态树的遍历暗含了一个状态生成的过程,就是促使状态树上的一个状态向下一个状态转换的驱动过程,这是一个很重要的部分,如果不能正确地驱动状态变化,就不能实现状态树的遍历(搜索)。

 

        建立状态模型一节中提到的动作模型,就是驱动状态变化的关键因子。对一个状态来说,它能转换到哪些新状态,取决于它能应用哪些倒水动作,一个倒水动作能够在原状态的基础上“生成”一个新状态,不同的倒水动作可以“生成”不同的新状态。由此可知,状态树遍历的关键是找到三个水桶之间所有合法的倒水动作,用这些倒水动作分别“生成”各自相应的新状态。遍历三个水桶的所有可能动作,就是对三个水桶任取两个进行全排列(常用的排列组合算法可以参考《排列组合算法》一文),共有6种水桶的排列组合,也就是说有6种可能的倒水动作。将这6种倒水动作依次应用到当前状态,就可以“生成”6种新状态,从而驱动状态发生变化(有些排列并不能组合出合法的倒水动作,关于这一点后面“算法优化”部分会介绍)。

 

算法优化和避免状态循环(看不懂的地方)

 

        从图(2)可以看出来,对于三个水桶这样小规模的题目,其整个状态树的规模也是相当大的,更何况是复杂一点的情况,因此类似本文这样对搜索整个状态树求解问题的算法都不得不面对一个算法效率的问题,必须要考虑如何进行优化,减少一些明显不必要的搜索,加快求解的过程。

 

        前文讲过,状态搜索的核心是对三个水桶进行两两排列组合得到6种倒水动作,但是并不是每种倒水动作都是合法的,比如,需要倒出水的桶中没有水的情况和需要倒进水的桶中已经满的情况下,都组合不出合法的倒水动作。除此之外,因为水桶是没有刻度的,因此倒水动作也是受限制的,也就是说合法的倒水动作只能有两种结果:需要倒出水的桶被倒空和需要倒进水的桶被倒满。加上这些限制之后,每次组合其实只有少数倒水动作是合法的,可以驱动当前的状态到下一个状态。利用这一点,就可以对状态树进行“剪枝”,避免对无效(非法)的状态分支进行搜索。

 

        除了通过“剪枝”提高算法效率,对于深度优先的状态搜索还需要防止因状态的循环生成造成深度优先搜索无法终止的问题。状态的循环生成有两种表现形式:一种是在两个桶之间互相倒水;另一种就是图(2)中展示的一个例子,[3, 5, 0] -> [3, 2, 3] -> [6, 2, 0] -> [3, 5, 0]形成一个状态环。要避免出现状态环,就需要记录一次深度遍历过程中所有已经搜索过的状态,形成一个当前搜索已经处理过的状态表,每当生成一个新状态,就先检查是否是状态表中已经存在的状态,如果是则放弃这个状态,回溯到上一步继续搜索。如果新状态是状态表中没有的状态,则将新状态加入到状态表,然后从新状态开始继续深度优先遍历。在这个过程中因重复出现被放弃的状态,可以理解为另一种形式的“剪枝”,可以使一次深度优先遍历很快收敛到初始状态。

--------------------- 本文来自 吹泡泡的小猫 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/orbit/article/details/6596521?utm_source=copy

免责声明:文章转载自《算法分析:大中小三个水桶分水问题》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇计算机启动过程使用sql创建表并添加注释下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

随便看看

解决cookie跨域访问

如果用户在登录(记录用户上下文信息)后在浏览器客户端上写入cookie,则只能访问与创建cookie的页面相同目录或创建cookie页面的子目录中的网页。这是因为cookie可以由其路径下的页面访问。然后,如果您想在域a中生成一个可以被域a和域B访问的cookie,则需要将cookie的域设置为.test.com;...

解决安卓手机连接wifi总弹出“已登录到Wlan网络”

一些Android机型在连接到wifi时会不断提示,“如果你不小心打开了这个提示,你会跳转到一些广告网站。这是因为谷歌服务器会在本地Android系统通电后发送连接请求。”。一些手机制造商的工程师将此连接服务器设置到一些广告网站并登录网络“我们可以管理设备或手机模拟器的状态,还可以执行许多手机操作,如安装软件、升级系统、运行shell命令等。adb是连接安卓...

GPU与CPU

GPU和CPU CPU,也称为中央处理单元,主要由控制器、运算单元、寄存器、高速缓冲区和数据/控制/状态总线组成。GPU GPU称为GraphicsProcessingUnit,即图形处理器。GPU最初是为终端游戏设计的。由于对游戏中的大量数据重复相同的操作,GPU面临着类型高度统一、相互依赖的大规模数据。GPU的内核远多于CPU。它向多个内核发送相同的指令...

怎么使用vscode合并分支

1.切换分支到本地开发2.代码完成后提交代码到本地仓库3.切换分支到需要merge的test分支先pull一下,之后再合并分支—我开发的是这个feature,就合并这个分支4.当合并分支后,需要重新提交到远程:点击一下,直接提交...

antd中,popover 不同情境下设置不同背景图,无法设置className的情况

于是就想通过设置不同的status值来添加不同的className,以设置.ant-popover-inner的样式来设置背景图,当然,这样做有一个不完美的就是不能一步到位的全部改变,需要手动更改.ant-popover-placement-bottom˃.ant-popover-content˃.ant-popover-arrow来替换那个角角的值。问题就...

Ubuntu安装时怎样分区

应该首先放置启动分区。并将引导设置为主分区。如果是双系统或多系统安装,通常可以选择逻辑分区。首先,Grub可以在1024柱面后面引导Linux内核;第二,即使有多个Linux安装,/boot也可以完全不共享。此外,非独立/引导分区仅占用根文件夹下约20MB的空间。所以决定是否启动。...