最近在看一本书,叫做 《思考的乐趣》,第 26 节 “我最爱的证明”,里面介绍了这样一则有趣的问题(文章链接在此):
设想一个平面上布满间距为 1 的横纵直线,形成由一个个 1×1 正方形组成的网格。任意给一个面积小于 1 个单位的图形,证明这个图形总能放在网格中而不包含任何一个格点。
初看这个论断,觉得似乎是正确的,但又不知从何下手。文中指出证明的思路很巧妙,让人感到数学的美妙。我的感触是,文中的证明大大地换了个角度,很有峰回路转的感觉。正在读这篇文章的你,不妨先思考一下,别急着往下看答案。
如果陷入了拼命去构造各种各样的图形类别,去思考不同类别图形的情况下,怎么去摆放这样的图形,使得图形不覆盖到格点,就如同陷入了泥沼,很难绕清楚路线了。思考问题的情况往往类似,如果一条路走不通,又觉得似乎并不复杂,不愿意轻易放弃,就很容易陷进去难以自拔。这样的问题思考情形并不特指常规意义上的算法题,而包含了各种各样的实际问题。
再来看看文中的解答:
我们可以这样考虑这个问题:把图形随意放在网格中,如何移动网格使每个格点都在图形外面。
现在我们把给定的图形随意放在网格中。然后沿着网格线把包含有图形的网格切成 1×1 的小格子,从网格中拿出来。把它们重叠起来(不旋转),再想像这些格子是透明的,而图形是不透明的。从上往下看这一叠格子,你看到的会是这个图形的各部分重叠地放在一个格子中,仿佛一个沾有污渍的方块。很显然这些污渍不会布满整个方块(图形面积小于一个格子的面积),方块上总有一块干净的地方。现在我们用一颗针从一个干净的地方刺下去,把这些重起来的格子刺穿。把这些格子放回原来的网格中,你看到的会是每一个有图形的方格内都有一个针眼,这些针眼都不在图形内。现在可以把原来的网格擦掉了,这几个针眼可以看作是新网格的格点。按针眼的位置重画新的网格,那么这个图形内决不会有新网格的格点,此时,结论也就证到了。
怎么样,是不是很巧妙?由相对固定网格,去摆放图形;改为相对固定图形,去摆放网格。问题居然一下子就清晰起来。我们都知道要换个角度去认识和思考问题,但是真正遇到问题的时候,又有多少人能够做到这一点呢?
再来看一道我很喜欢问的面试题:
假如说有这样一个 SNS 网站,每一个注册用户都有自己的主页。而网站中有这样一套积分系统,对于用户的各种各样的行为,都会引起用户积分的微小变化,例如,用户登陆一次+1 分,用户加一个好友+2 分,用户发表一篇评论+1 分,用户评论包含不良内容-3 分等等。现在,注册用户的个人主页上需要展示用户的个人信息(例如用户姓名),以及用户的积分,同时,还要展示用户的积分在网站所有用户(大约两千万用户)中精确的排名。现在为了简化问题,我们假设用户信息和积分信息全部存放在内存中。如果你要设计这样一个网站,你会怎样设计内存中存储这些信息的数据结构,以便在访问用户主页的时候迅速展示用户的积分和积分排名信息,同时,在用户积分发生上述变更的时候能够使排名得到快速更新?
我很喜欢问这样的问题是因为这样的问题在可以考察数据结构等等基础知识的同时,非常明确地考察了解决实际问题的能力。这样的问题远远比分析一个字符串、处理一个表达式这样的传统意义上的纯算法题有实际意义得多。在实际应用中,我通常会把它拆成几问,以降低难度,而且可以增加区分度。这道问题没有所谓的标准答案,有很多人都给出了非常漂亮的解答。我在此举一例而已,您也不妨先思考一下再往下阅读。
几乎所有的人都会首先想到使用 HashMap 来存储用户信息,key 为用户 id(uid),value 为用户信息的对象,这样的好处是访问用户首页时,理论上获取用户信息的的时间复杂度是常数阶的,非常快。可是很多聪明的人也会忽然意识到,存储积分容易,但是排名信息的更新呢?如果排名信息作为 value 的一部分存储在这个 HashMap 里,当分数发生变更的时候,有那么多的用户,我需要遍历所有的 value,取出积分来重新排序,这样的时间复杂度是很难接受的。
如果只是把思路限制在 “根据用户 id 去取得排名”,就很难得到最优的解决方案,比较好的同学也只是想出使用树等等可以让时间复杂度为 log(n) 的设计。但是,如果我们换个角度思考问题,变成 “根据用户排名去取得用户信息”,问题说不定就豁然开朗了。“排名” 有一个天然的优势是一定是从 1 开始的连续正整数列表,它的长度就等于所有用户的数量。当我提示到这一句话的时候,有些本来没有头绪的同学也能够自己把问题解答下去了——这样的数据结构形式,非常适合使用数组来表达,根据下标去访问元素,是非常快的。于是设计一个数组 Array,下标表示用户排名,值表示用户信息对象(例如用户名、描述等等):
Array[rank] = userInfo
这样的设计还有什么好处?当用户积分发生变更的时候,请注意题中的说明,每次变更都是 “微小变化”,对于这样一个偌大的数组而言,每次积分变更引起的用户排名的调整往往非常小,例如用户积分为 29876 分,增加了 3 分,变成了 29879 分,那么引起变化的只有从 29876 到 29879 积分的用户排名,换言之,只需要在数组中向上游或者下游简单地寻溯遍历几个元素而已;而且,对于这样的调整来说,不需要锁住整个数组,而只需要锁住其中小小的一个区段而已。
别急,问题还没有解决,现在的问题变成了,给定用户 uid 的时候,我怎样能够快速取得用户的排名(rank)呢?因为有了排名我才能从刚才的数组里面去取得用户信息啊。回过头来看一看,原本的 HashMap 不是可以派上用场吗?设计这样的 HashMap,让 key 为 uid,value 为用户的排名信息(rank),每次在用户需要获取排名信息的时候,先根据 uid 在 HashMap 中取得 rank,再根据 rank 在 Array 中取得用户信息:
HashMap<uid, rank>
这样一来,rank 变成了 HashMap 和 Array 的桥梁,在每次分数发生微小变更时要做的调整,以及每次根据 uid 来获取用户的积分和积分排名信息时要做的查询,全部都是常数阶的。这两个数据结构本身都很简单,但是在遇到实际问题的时候,将实际问题是用最合适的数学和工程模型来解决的能力,这本身应当是属于比传统意义上纯粹的算法问题更有考察价值的部分。
我还在 《再谈大楼扔鸡蛋的问题》里面介绍了一个使用等差数列求和公式来解题的证明,其中的思路也是 “换个角度思考问题”,把 “给定大楼层总数情况下,思考最少要扔多少次鸡蛋来确定鸡蛋恰好破碎的临界层”,变成 “给定扔鸡蛋的最多次数的情况下,思考最多可以在多少层高的大楼内确定鸡蛋破碎的临界层”。如果您感兴趣请移步。
也许我们都做过很多需要换个角度思考的题目,例如在物理中我们有时需要改变参考系,在数学中有时我们需要改变坐标系,但是要真正理解它却并不容易。前两天在解决一个并不复杂的几何问题的时候,一眼就看出问题可以通过解析几何的办法来解答,但是仅仅利用初等的三角形的性质,却觉得无从下手,最后费了好大劲才搞定。为什么一定要用那么强大的工具来解决简单的问题呢?“换个角度” 的实质在于需要改变思考问题的切入点和方向,而当我们掌握了通用的解题思路以后,掌握了更强大的解决问题的技巧以后,为什么原本或开阔或自然的思路反而被压制了呢?
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》
SNS 那个方案貌似有点小问题,就是如果一个积分上有很多用户的话,没法用简单的数组表达,而是该数组的每个元素都是一个 userInfo 的链表,这样也没法直接通过数组的下标就知道该用户的全局排名。
擦,这个证明太牛逼了!!
SNS 网站用户排名那个楼主的思想其实就是对 UID 和“ 积分” 两个字段同时做索引。UID 因为不变所以 UID 索引永远不更新,而改积分时使用原来的积分索引对积分索引进行更新。其实在极端情况下它的复杂度并不低:所有人的积分相差不大,一个人积分一修改,他的排名就会改变很多。这时数组中需要移动位置的用户就会很多。用数组存储时不知道是不是能用位运算来处理这个。
思路有了,实现起来却并不容易。
可以做成两层数组结构,第一层数组 A 下标是积分,而不是积分排名,第二层 B 则是该积分下的所有 uid。 当一个人 u 积分发生变更时(比如从 a 变为 b)则只需要把 A[a] 下 数组移除 u, 在 A[b] 下添加 u 即可。并积分天然就是排名。
积分天然不是排名,因为同一积分可以有多个人
横向挪动网格 1 单位,纵向挪动网格 1 单位做得到的结果与原来相同。
所以在 1*1 单位里挪动网格就可以得到所有的结果
假设现有的图片占用网格 m*n
则挪动后最多占用网格 (m+1)*(n+1)
将这 (m+1)*(n+1) 张图片按正常顺序叠在一起,
由于图片面积<1,一定可以找到同样位置的一个点贯穿所有网格而在图片之外
将网格恢复原样,可生成新网格。
不对 是相同位置的点,不必在格线上
请问怎么按照新的针眼重画新的网格
这个图形不一定只横跨四张正方形,如果有好几十张,如何重画新网格
我明白了 新找的点不是随意的,而是摞起来后格线上的并且位置相同的点
这个确实可以
但是这个点真的一定存在吗
如果不能证明这个点存在
那这个证明是没有意义的
你还没有绕过弯来。在这里把图案固定,而网格的格点不是固定的。只需要保证网格格点之间的位置关系就可以了。
这个点肯定是存在的。一个网格的大小是 1,图行的面积少于 1,所以图形被原来网格划分成 N(N>=1)份之后,每分的面积少于 1,重叠起来面积更少于 1,也就是说重叠之后肯定有空白,从空白里选一个点就可以了。
这样选出来的点在原来网格上的位置是相同的。N = 1 的时候不用重新画网格,N > 1 时把原来网格的点平移到空白的点就行了。