Skip to content

四火的唠叨

一个纯正程序员的啰嗦

Menu
  • 所有文章
  • About Me
  • 关于四火
  • 旅行映像
  • 独立游戏
  • 资源链接
Menu

换个角度思考问题

Posted on 10/03/201306/23/2019 by 四火

think最近在看一本书,叫做 《思考的乐趣》,第 26 节 “我最爱的证明”,里面介绍了这样一则有趣的问题(文章链接在此):

设想一个平面上布满间距为 1 的横纵直线,形成由一个个 1×1 正方形组成的网格。任意给一个面积小于 1 个单位的图形,证明这个图形总能放在网格中而不包含任何一个格点。

example

初看这个论断,觉得似乎是正确的,但又不知从何下手。文中指出证明的思路很巧妙,让人感到数学的美妙。我的感触是,文中的证明大大地换了个角度,很有峰回路转的感觉。正在读这篇文章的你,不妨先思考一下,别急着往下看答案。

如果陷入了拼命去构造各种各样的图形类别,去思考不同类别图形的情况下,怎么去摆放这样的图形,使得图形不覆盖到格点,就如同陷入了泥沼,很难绕清楚路线了。思考问题的情况往往类似,如果一条路走不通,又觉得似乎并不复杂,不愿意轻易放弃,就很容易陷进去难以自拔。这样的问题思考情形并不特指常规意义上的算法题,而包含了各种各样的实际问题。

再来看看文中的解答:

我们可以这样考虑这个问题:把图形随意放在网格中,如何移动网格使每个格点都在图形外面。

现在我们把给定的图形随意放在网格中。然后沿着网格线把包含有图形的网格切成 1×1 的小格子,从网格中拿出来。把它们重叠起来(不旋转),再想像这些格子是透明的,而图形是不透明的。从上往下看这一叠格子,你看到的会是这个图形的各部分重叠地放在一个格子中,仿佛一个沾有污渍的方块。很显然这些污渍不会布满整个方块(图形面积小于一个格子的面积),方块上总有一块干净的地方。现在我们用一颗针从一个干净的地方刺下去,把这些重起来的格子刺穿。把这些格子放回原来的网格中,你看到的会是每一个有图形的方格内都有一个针眼,这些针眼都不在图形内。现在可以把原来的网格擦掉了,这几个针眼可以看作是新网格的格点。按针眼的位置重画新的网格,那么这个图形内决不会有新网格的格点,此时,结论也就证到了。

answer

怎么样,是不是很巧妙?由相对固定网格,去摆放图形;改为相对固定图形,去摆放网格。问题居然一下子就清晰起来。我们都知道要换个角度去认识和思考问题,但是真正遇到问题的时候,又有多少人能够做到这一点呢?

再来看一道我很喜欢问的面试题:

假如说有这样一个 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 来获取用户的积分和积分排名信息时要做的查询,全部都是常数阶的。这两个数据结构本身都很简单,但是在遇到实际问题的时候,将实际问题是用最合适的数学和工程模型来解决的能力,这本身应当是属于比传统意义上纯粹的算法问题更有考察价值的部分。

我还在 《再谈大楼扔鸡蛋的问题》里面介绍了一个使用等差数列求和公式来解题的证明,其中的思路也是 “换个角度思考问题”,把 “给定大楼层总数情况下,思考最少要扔多少次鸡蛋来确定鸡蛋恰好破碎的临界层”,变成 “给定扔鸡蛋的最多次数的情况下,思考最多可以在多少层高的大楼内确定鸡蛋破碎的临界层”。如果您感兴趣请移步。

也许我们都做过很多需要换个角度思考的题目,例如在物理中我们有时需要改变参考系,在数学中有时我们需要改变坐标系,但是要真正理解它却并不容易。前两天在解决一个并不复杂的几何问题的时候,一眼就看出问题可以通过解析几何的办法来解答,但是仅仅利用初等的三角形的性质,却觉得无从下手,最后费了好大劲才搞定。为什么一定要用那么强大的工具来解决简单的问题呢?“换个角度” 的实质在于需要改变思考问题的切入点和方向,而当我们掌握了通用的解题思路以后,掌握了更强大的解决问题的技巧以后,为什么原本或开阔或自然的思路反而被压制了呢?

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

×Scan to share with WeChat

你可能也喜欢看:

  1. 那些糟糕的面试和那些屎问题
  2. Algorithm In Interview
  3. Issue record: “No thread for socket” about Memcached
  4. 不适合 Hadoop 解决的问题
  5. 再谈榔头和钉子

12 thoughts on “换个角度思考问题”

  1. Anonymous says:
    04/19/2014 at 11:18 PM

    SNS 那个方案貌似有点小问题,就是如果一个积分上有很多用户的话,没法用简单的数组表达,而是该数组的每个元素都是一个 userInfo 的链表,这样也没法直接通过数组的下标就知道该用户的全局排名。

    Reply
  2. manxisuo says:
    12/12/2013 at 10:14 AM

    擦,这个证明太牛逼了!!

    Reply
  3. 魏广强 says:
    11/28/2013 at 5:23 PM

    SNS 网站用户排名那个楼主的思想其实就是对 UID 和“ 积分” 两个字段同时做索引。UID 因为不变所以 UID 索引永远不更新,而改积分时使用原来的积分索引对积分索引进行更新。其实在极端情况下它的复杂度并不低:所有人的积分相差不大,一个人积分一修改,他的排名就会改变很多。这时数组中需要移动位置的用户就会很多。用数组存储时不知道是不是能用位运算来处理这个。
    思路有了,实现起来却并不容易。

    Reply
    1. 徐慧斌 says:
      04/25/2016 at 5:23 PM

      可以做成两层数组结构,第一层数组 A 下标是积分,而不是积分排名,第二层 B 则是该积分下的所有 uid。 当一个人 u 积分发生变更时(比如从 a 变为 b)则只需要把 A[a] 下 数组移除 u, 在 A[b] 下添加 u 即可。并积分天然就是排名。

      Reply
      1. Anonymous says:
        04/27/2016 at 4:20 AM

        积分天然不是排名,因为同一积分可以有多个人

        Reply
  4. cowrylee says:
    10/04/2013 at 2:47 PM

    横向挪动网格 1 单位,纵向挪动网格 1 单位做得到的结果与原来相同。
    所以在 1*1 单位里挪动网格就可以得到所有的结果
    假设现有的图片占用网格 m*n
    则挪动后最多占用网格 (m+1)*(n+1)
    将这 (m+1)*(n+1) 张图片按正常顺序叠在一起,
    由于图片面积<1,一定可以找到同样位置的一个点贯穿所有网格而在图片之外
    将网格恢复原样,可生成新网格。

    Reply
  5. cowrylee says:
    10/04/2013 at 2:33 PM

    不对   是相同位置的点,不必在格线上

    Reply
  6. cowrylee says:
    10/04/2013 at 2:07 PM

     
    请问怎么按照新的针眼重画新的网格
    这个图形不一定只横跨四张正方形,如果有好几十张,如何重画新网格

    Reply
    1. cowrylee says:
      10/04/2013 at 2:13 PM

      我明白了   新找的点不是随意的,而是摞起来后格线上的并且位置相同的点
      这个确实可以
      但是这个点真的一定存在吗

      Reply
      1. cowrylee says:
        10/04/2013 at 2:22 PM

        如果不能证明这个点存在
        那这个证明是没有意义的

        Reply
        1. 四火 says:
          10/04/2013 at 6:22 PM

          你还没有绕过弯来。在这里把图案固定,而网格的格点不是固定的。只需要保证网格格点之间的位置关系就可以了。

          Reply
      2. funnuy says:
        10/30/2013 at 1:28 AM

        这个点肯定是存在的。一个网格的大小是 1,图行的面积少于 1,所以图形被原来网格划分成 N(N>=1)份之后,每分的面积少于 1,重叠起来面积更少于 1,也就是说重叠之后肯定有空白,从空白里选一个点就可以了。
        这样选出来的点在原来网格上的位置是相同的。N = 1 的时候不用重新画网格,N > 1  时把原来网格的点平移到空白的点就行了。

        Reply

Leave a Reply to 四火 Cancel reply

Your email address will not be published. Required fields are marked *

订阅·联系

四火,啰嗦的程序员一枚,现居西雅图

Amazon Google Groovy Hadoop Haskell Java JavaScript LeetCode Oracle Spark 互联网 亚马逊 前端 华为 历史 同步 团队 图解笔记 基础设施 工作 工作流 工具 工程师 应用系统 异步 微博 思考 技术 数据库 曼联 测试 生活 眼界 程序员 管理 系统设计 缓存 编程范型 美股 英语 西雅图 设计 问题 面向对象 面试

分类

  • Algorithm and Data Structure (30)
  • Concurrency and Asynchronization (6)
  • System Architecture and Design (43)
  • Distributed System (18)
  • Tools Frameworks and Libs (13)
  • Storage and Data Access (8)
  • Front-end Development (33)
  • Programming Languages and Paradigms (55)
  • Testing and Quality Assurance (4)
  • Network and Communication (6)
  • Authentication and Authorization (6)
  • Automation and Operation Excellence (13)
  • Machine Learning and Artificial Intelligence (6)
  • Product Design (7)
  • Hiring and Interviews (14)
  • Project and Team Management (14)
  • Engineering Culture (17)
  • Critical Thinking (25)
  • Career Growth (57)
  • Life Experience and Thoughts (45)

推荐文章

  • 聊一聊分布式系统中的时间
  • 谈谈分布式锁
  • 常见分布式系统设计图解(汇总)
  • 系统设计中的快速估算技巧
  • 从链表存在环的问题说起
  • 技术面试中,什么样的问题才是好问题?
  • 从物理时钟到逻辑时钟
  • 近期面试观摩的一些思考
  • RSA 背后的算法
  • 谈谈 Ops(汇总 + 最终篇):工具和实践
  • 不要让业务牵着鼻子走
  • 倔强的程序员
  • 谈谈微信的信息流
  • 评审的艺术——谈谈现实中的代码评审
  • Blog 安全问题小记
  • 求第 K 个数的问题
  • 一些前端框架的比较(下)——Ember.js 和 React
  • 一些前端框架的比较(上)——GWT、AngularJS 和 Backbone.js
  • 工作流系统的设计
  • Spark 的性能调优
  • “残酷” 的事实
  • 七年工作,几个故事
  • 从 Java 和 JavaScript 来学习 Haskell 和 Groovy(汇总)
  • 一道随机数题目的求解
  • 层次
  • Dynamo 的实现技术和去中心化
  • 也谈谈全栈工程师
  • 多重继承的演变
  • 编程范型:工具的选择
  • GWT 初体验
  • java.util.concurrent 并发包诸类概览
  • 从 DCL 的对象安全发布谈起
  • 不同团队的困惑
  • 不适合 Hadoop 解决的问题
  • 留心那些潜在的系统设计问题
  • 再谈大楼扔鸡蛋的问题
  • 几种华丽无比的开发方式
  • 我眼中的工程师文化
  • 观点的碰撞
  • 谈谈盗版软件问题
  • 对几个软件开发传统观点的质疑和反驳
  • MVC 框架的映射和解耦
  • 编程的未来
  • DAO 的演进
  • 致那些自嘲码农的苦逼程序员
  • Java 多线程发展简史
  • 珍爱生命,远离微博
  • 网站性能优化的三重境界
  • OSCache 框架源码解析
  • “ 你不适合做程序员”
  • 画圆画方的故事

近期评论

  • Ticket: TRANSACTION 1.922915 BTC. Go to withdrawal >> https://yandex.com/poll/enter/BXidu5Ewa8hnAFoFznqSi9?hs=20bd550f65c6e03103876b28cabc4da6& on 倔强的程序员
  • panshenlian.com on 初涉 ML Workflow 系统:Kubeflow Pipelines、Flyte 和 Metaflow
  • panzhixiang on 关于近期求职的近况和思考
  • Anonymous on 闲聊投资:亲自体验和护城河
  • 四火 on 关于近期求职的近况和思考
  • YC on 关于近期求职的近况和思考
  • mafulong on 常见分布式基础设施系统设计图解(四):分布式工作流系统
  • 四火 on 常见分布式基础设施系统设计图解(八):分布式键值存储系统
  • Anonymous on 我裸辞了
  • https://umlcn.com on 资源链接
© 2025 四火的唠叨 | Powered by Minimalist Blog WordPress Theme