python产生一个总和为n的随机数列_让“全家福”更加随机 —— 自动生成照片布局之Python实现...

86e78f13633c3606139811cafe1e5360.png

高考已经结束,先预祝各位考生成绩如意!

虽然在学校里工作了那么久,却没有留意过从哪一年开始高考由七月改到了六月。只是每到南风吹起、凤凰花开的时节,便会发现朋友圈里挂出了好多毕业照,于是就会想起1996年的7月9日,和随之而来的20年校园生活 …… 

580c5abd35094e2cee2dc81e81c6a376.png

说到毕业照,我们上期公众号里解析的“Excel自动全家福”受到了很多朋友的关注。

94cc66636f94b1aedbdaaca237984064.png

在上次的文章里,杨老师分析了上财陈同学这个VBA程序的实现技巧,也就是怎样把图片随机排列到事先画好的合并单元格中,并且自动裁剪对齐。

4eefd8bedbbfbcf6dd0725cc2aa19ce2.png

文中的一句话引起了很多同学的兴趣,那就是“如果愿意投入更多的精力,我们可以把合并单元格的布局也交给程序去自动生成,而不是事先由人工划分好”。所以今天我们就接着分析一下这个有趣的小问题:给定一个矩形区域,怎样将其随机分割成若干个小矩形

比如下面这个Excel表格中,如果我们要把32张照片随机放置到B2:N8区域,也就是7行13列的矩形中,该怎样编写程序呢?考虑到这几周公众号讲的都是VBA,今天我们改用Python试一试。

cea15f33a3f8d2180d1c7325ab674451.png

ukd

b49547679b781718157f883682c740d6.gif

一、 整体思路

相信很多同学看到这个问题时都会觉得无从下手,毕竟涉及图形、又要求像拼瓷砖一样对齐工整,恐怕手工设计也要费一些波折才行。不过实际上,这个问题并没有我们想的那么复杂,因为咱们小学时就已经做过类似的甚至更复杂的题目:

2f55197721adf93e7a6f140d4a7ec991.png

这是一道几乎所有人都见过的题目,标准答案是“3刀”,也就是前两刀切出十字形,然后在蛋糕中层横切,变成8块。杨老师当年回答了“4刀”,结果没有得分,对此本人至今仍不服气:大多数蛋糕都是上面奶油下面松饼,如果横切成8块,岂不是4块全奶油、4块全松饼,怎么分?你分?你细分!

408b352cb2f079ffde7a8058610146a0.png

不好意思跑题了。回到“划分矩形”这个问题上,道理其实是一样的:我们可以先把整个“全家福”区域看作一个大的矩形(蛋糕),然后在它的范围里随机生成一条横线或竖线,就可以把它分成两个小矩形。然后在这两个小矩形中随机挑出一个,再在它的范围里随机生成一条横线或竖线,就可以把这个小矩形划分为两个小小矩形”。这样经过两次操作,我们就把原区域随机划分成三个矩形。

a5b1c1a0588bd057456694f8948840c2.png

以此类推,如果我们想划分为4个随机矩形,只要执行一步:从上面这三个矩形中随机选取一个、然后在它的范围里随机生成一个横线或竖线即可。

b8df5edeafa1e0d468639de034c4c0b0.png

因此我们就得到了一个很好理解的算法:每次在区域内随机挑一个矩形(开始时一共只有一个矩形,就是这个区域本身),然后在它内部随机生成一个横线或竖线,如此反复。这样执行N次操作,就可以得到N+1个随机矩形。

ukd

b49547679b781718157f883682c740d6.gif

二、 怎样表示和分割“矩形”

思路有了,但是要想变成程序代码,却还有一个坎要迈:怎样把“矩形”、“线段”这些几何形状写到代码里处理?

其实也很简单:一个矩形不过就是4个端点”的组合。比如上面划分为4个矩形的表格中,左上角第一个矩形C2:F4可以表示为 (2, 3, 4, 6) ,也就是左上角为第2行第3列(即C2)、右下角为第4行第6列(即F4)。同理,最右侧的矩形可以表示为(2, 8, 5, 9),也就是左上角H2、右下角I5

(2, 3, 4, 6) 对应到Python代码,显然就是我们在《全民一起玩Python 基础篇》中学过的概念:元组。更进一步,如果用1个元组表示1个矩形,那么4个矩形就要用4个元组。所以我们可以创建一个列表,把这些元组都装进去,于是这个列表就代表了当前已经划分出来的“所有矩形”。

f90b99b354bfef26894dcd423dde494c.png

当然,程序刚开始运行时,整个区域还从来没有划分过,所以只有一个矩形,因此列表中也只有一个元组。而接下来的任务,就是在这个矩形区域中随机找两个点(也就是一条线段),把它划成两个区域。怎样随机生成这两个点呢?

假设这个矩形(也就是一个元组)是用 r 来代表,比如 r = (2,3,4,6),也就是下面的图形。那么我们可以知道:它的宽度范围是从第3列到第6列,也就是 (r[1] , r[3] ) ,而它的高度范围则是从第2行到第4行,也就是 ( r[0] , r[2] ) 。

a981c7739ce5ae0e75fba4ce752483ac.png

那么如果我们想在矩形 中生成一个随机竖线,从而将其划分为左右两个矩形,只需要调用Pythonrandom模块,生成一个 3到5之间的随机数即可,也就是 random.randint( r[1] , r[3]-1 ) 。假如这一次生成的随机数是 5 ,那么就相当于把第5列(E列)作为分割线(严格的说,是把E列作为第一个矩形的边界),也就是分成下面两个矩形:

92b62420de1dcc25063cc0a82c27f18e.png

用Python形式描述就是:

1. 取出原矩形 r = (2,3,4,6) 的宽度范围 (3,6) ,也就是 r[1] 和 r[3]

2. 生成该范围(左闭右开区间)内的随机数 randint( r[1], r[3]-1) ,得到一个随机数 x (假设为5)

3. 创建两个新的矩形元组:r1=(2,3,4,5) 和 r2=(2,6,4,6) ,也就是 (r[0],r[1],r[2], x ) 和 (r[0], x+1 , r[2], r[3] )

显然,这两个新的元组就是我们随机划分出的两个“小矩形”,把它们添加到列表中,我们的“全部矩形”就被更新为划分后的状态了。

不过还有一个步骤不要忘记:既然列表中原来的矩形元组 r 被分成了r1和 r2 两个新的矩形元组,那么原来的矩形 r 也就没有存在意义,应该从列表中删除掉。

所以把上面的过程写成代码,就是下面的样子:

0ceedf2b8558142c154f1bc9f90300eb.png

上面的代码实现了“按照宽度进行一次横向拆分”。同样的思路,我们也可以实现“按照高度纵向拆分”,也就是取行号 r[0] 和 r[2] 之间的随机数 x ,然后用 (r[0], r[1], x, r[3] ) 和 (x+1 , r[1], r[2], r[3] ) 得到两个新矩形。

ukd

b49547679b781718157f883682c740d6.gif

三、 最终方案

上面的代码实现了“拆分一次”,那么如果我们需要拆成20个矩形,只要把这段代码循环19次就够了。具体来说,还是从只包含一个矩形的列表开始,然后做一个19次的循环,每次循环都从列表中随机抽取出一个“倒霉的”矩形执行拆分即可(可以使用《全民一起玩Python 提高篇》介绍的random.sample函数)。

不过这里还有几个问题:

1. 按宽度拆分和按高度拆分是两个不同的代码,那么怎样在二者之间随机选择一种方式呢?这一点既可以用随机数方式解决(生成一个随机数,如果大于0.5就选宽度、否则选高度),也可以像下面杨老师的示例中那样:如果待拆分矩形的宽度大于高度,就按宽度横向拆分,否则按高度竖向拆分。

2. 假如要按照宽度进行横向拆分,但是待拆分矩形已经很窄,只有一列或两列宽,该怎么办呢?对于这种情况,我们可以在拆分前先进行一次判断:如果宽度 r[3]-r[1] 大于 2,我们才执行宽度拆分,否则重新循环重新抽取一个矩形。这个思路同样适用于对“扁矩形”按高度拆分的情况

3. 由于问题2的存在,有可能某次循环时没有执行任何拆分操作。这样会带来一个隐患:为了拆分出20个矩形,我们设计了一个19次的循环;但19次循环里只有17次执行了拆分,所以最终只得到18个矩形。为了避免这种情况,我们可以使用一个计数器模式的while循环,不过只有成功执行一次拆分时对循环变量(计数器)增加1。这样当发生问题2的情况时,由于循环变量(计数器)并没有增加,所以可以“多循环一次”。

搞清楚上面几个问题,最终算法就可以出炉了(篇幅所限、删除了注释):

d4b509ceb39c4a9d0e2b923c17839a7b.png

不过最后还有一个问题:上面算法的结果都是数字,可是我们怎样把它们画到Excel中呢?这种问题,当然还是找老朋友 xlwings !

在xlwings中,我们可以使用下面的格式执行单元格合并操作:

worksheet.range((左上行号,左上列号) (右下行号,右下列号) ).api.merge

所以在执行完前面的算法,得到最终矩形列表 rects 之后,只要循环读取其中每个矩形的坐标元组,然后调用 xlwings 把指定行列的单元格合并,就可以看到最终的效果:

5558b7c7b270aa0eb880fe0a484e6674.pngdd1ce53c2f499451228bbeb5b36a791c.png

这就是“随机拆分矩形”的一个基本思路。其中还有很多细节可以进一步完善,比如怎样避免过于狭长的小矩形、怎样在中心位置留出一个最大的矩形作为C位等等。此外虽然我们使用的是Python,但所有细节也都可以用VBA对等实现。所以这些问题,就留给有兴趣的朋友们自己慢慢打磨吧。

最后,很多关心我们新课制作进度的同学没有看到前几期的公告,所以这里简单重述一下:目前我们正在制作《全民一起玩SQL 基础篇》和《全民一起玩Python 实战篇》,同时我们的新版在线教学平台也在紧张开发中。我们计划将新课程与新平台同步上线,以便为大家提供全方位沉浸式的学习资源。按我们的计划,预期发布时间为9月初,详情请随时关注我们的公众号。再次感谢各位的关注和耐心!

ukd

8fac4d8abeab990540cedb486af47a73.gif

更多精彩阅读

ed4acbdfb8ba3898495ecc354b7d267a.gif

接下来,我们会怎么走 —— 关于新的课程和教学方式

不变的,是初心 —— 写在又一门课程收尾之际

为什么又是游戏?—— 摘一段二十多年前的青涩文章

82cabb003ffbb18b8b6c0f4041a7a067.png

扫码听课

杨老师课程全集

全民一起玩Python

全民一起VBA

欢迎加入、一起进步

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值