我们小学的时候就学过,长方形的面积等于长乘以宽。
但是生活了几十年,你有没有想过:为什么长方形的面积等于长乘以宽?
或者为什么我们的祖先将定义为长方形的面积为长度乘以宽度?
(在继续之前,请忘记矩形面积等于长乘以宽这个“简单”的知识)。
想象你是一个非常好奇的农民。有一天,你无所事事,捧着一杯茶,盯着家里的一块土地,就像下面这个形状:
我们突然想知道这片土地有多大。于是我用尺子量了一下:
土地的大小是长100,宽50。
但是我们该死的好奇心并不满足于这个结果:它想知道这么大的矩形围成的面积(阴影区域)有多大——它想用一个数字来表达这个面积。
在找出答案之前,必须给它起一个名字。
你可以叫它任何东西,最好直观一点,比如面积,面积——你不妨叫它面积。
另外,我们想要求解一般矩形的面积(不是“这个”长为100、宽为50的矩形),所以我们决定使用一些符号来表示矩形的长和宽。您也可以使用 l(或任何其他符号)表示长度,使用 w 表示宽度。
l和w的含义是任意长度和宽度的矩形。
现在问题变成:长为l、宽为w的矩形的面积是多少?
注意我们如何符号化问题。
答案是:我不知道!
到目前为止,我们脑子里的数学知识只是数字、加减乘——面对这个陌生的“领域”,我们束手无策。
人类有一个天生的“优势”:善于欺骗自己和他人。
我们不知道矩形的面积是多少,对吧?你可以假装知道!
就像我们用符号表示矩形的长和宽一样,我们也可以给该面积一个符号。
你给它什么符号并不重要。没关系)。
现在我们好像又多了一点知识:我们知道长l宽w的长方形的面积是S。
但事实上我们什么都不知道——事实并非如此,我们从经验中感觉这个S与l和w有关(不清楚是什么关系)。
为了表达这个“重大”发现(S、l、w之间存在一定关系),我们给出如下缩写:S(l,w)。
(您可以使用任何其他缩写,例如 S[l,w]、l,w -> S 等。我们使用 S(l,w) 的原因纯粹是个人喜好。)
S(l,w) 和 S 是实际上表示同一事物的两个表达式。它们都表示矩形的面积,但前者还有更进一步的含义:矩形的面积与长l和宽w之间存在一定的关系。 。
接下来我们自然会想:他们之间是什么关系?
注意我们如何将解决问题转化为解决关系。
我们低着头想了半天也想不通。
就在我们快要放弃的时候,我们用眼角的余光瞥了一眼这三分之一英亩的土地,心想:既然有关系,那我试试改变一下长度和宽度,看看会发生什么到该地区。 ?
变量变量,既然是变量,那就让它变吧——只有在变化中才能找到关系。
首先,我们将长度l加倍:
我们发现长度增加一倍后,面积也明显增加了一倍,所以我们得到了以下方程:
S(2l,w) = 2S(l,w)
相信你第一次感受到象征的力量!
很有趣!
接下来尝试保持长度相同并将宽度加倍:
面积也扩大了一倍:
S(l,2w) = 2S(l,w)
我们认为还有希望!
于是我们拿起树枝,把它们画在地上——我们发现,无论长度和宽度增加(或减少)多少倍,面积都增加(或减少)相同的倍数。所以我们写了下面的公式:
S(#l,w) = #S(l,w)
S(l,#w) = #S(l,w)
#代表任意数字。上式的意思是:我们可以将矩形的长度分割成两个任意数字(#,l)的乘积,然后从括号中取出其中一个数字。宽度也是如此。
下一步是利用人类推理能力。
这也是考验人智商的时候。
S(l,w) 可以写成 S(l*1,w)。根据上面的公式,我们可以将l从括号中取出,得到:
S(l,w) = S(l*1,w) = l*S(1,w)
我们对 w 做同样的事情并得到:
S(l,w) = S(l*1,w) = l*S(1,w) = l*S(1,w*1) = l*w*S(1,1)
也就是说:S(l,w) = l*w*S(1,1)。
至此我们已经得出结论:长为l、宽为w的矩形的面积是l*w乘以长1、宽为1的矩形的面积。
我们得到的不是绝对数,而是长(正方形)长方形的面积的倍数,长1宽1。
因为这个长1宽1的矩形的面积总是固定的,所以我们不需要关心它的面积的绝对值。我们可以人为地定义为1,那么长为l、宽为w的矩形的面积为l*w。
这个想法非常重要:现实世界中我们理所当然的绝对值(比如这里的面积)实际上是相对值。
例如,3米有多长?这取决于1米有多长。
1分钟有多长?这取决于1秒有多长。
这些用作测量参考,我们称之为单位。
最低级别的单位是人为定义的,在人类历史的不同时期可能有不同的定义。例如,中国古代的1斤和现代的1斤在重量上并不相等,但并不妨碍两个时代人们的日常使用。
在普通人眼里,数学是人类最严谨的学科,仿佛是神灵赐予人类的超然礼物。
数学是严谨的,但数学的基础不是先验的,而是源自人类生活的经验。
数学虽然充满了大量的公理、证明等,但数学的最低层次是一些“简单”的公设(公理,例如平面几何中的五大公设)。小学老师告诉我们:所谓的公理是不需要证明的(不证自明的)。这个所谓的“不言而喻”只不过是根据经验。
不仅数学中最基本的公理是基于经验的,数学中的很多验证过程也是基于经验的——虽然听起来很不可思议。
我们回顾一下矩形面积的推导过程:
一开始我们只是出于好奇,想知道如何用数字来表示这样一个矩形区域(定义问题)。
我们发现不知道如何解决,只好给它起了一个代号(符号化)。
此外,我们发现矩形有两个属性:长度和宽度(参数化)。
经验告诉我们,长方形的面积和长方形的长、宽之间存在一定的关系(关系)。
因此,我们尝试根据经验找出它们之间存在哪些关系(寻找模式)。
经过详尽的研究,我们得到了几个有趣的方程。
经过进一步的思考和观察,我们发现这些方程可以推广为一般方程(即其中的具体值可以是可变的)。 (关系泛化)
面对广义方程,我们利用已有的知识(比如w = w * 1,即一个数乘以1等于它自己,这是由乘法的定义得出的结论),最终推导出我们想要什么(根据现有知识进行推理)
在整个过程中,经验、观察、实验、象征扮演着非常重要的角色。
这涉及到我们如何思考和解决问题。
当我们面对一个不熟悉(而且看似困难)的问题时,我们的本能反应是感到困惑。
然后他开始挠头。
我们要做的就是在挠头之前先冷静下来,看看远方,做几次深呼吸,把问题多思考几遍。
在了解问题是什么之前,不要开始解决问题。
我们以字符串编辑距离为例。
问题:给定两个字符串str1和str2,求str1变成str2所需的最少操作次数。
当您第一次遇到这个问题时有何感受?反正我很迷茫,就像狗咬刺猬一样,无处下牙。
然后我就觉得:我好像不太明白这个问题?
所以先分析问题本身。
“已经经过了多少次操作”是什么意思?
如果你想知道进行了多少次操作,你至少要知道有哪些操作,对吧?
我们记得老农夫想到的是三分之一英亩——而我们现在面临的是两个抽象(符号)str1和str2。
所以接下来需要将抽象问题具体化。
我们不妨写两个特定的字符串:
str1 =“abcd”
str2 =“acdb”
现在就变成了一个具体的问题:如何用最少的操作次数将字符串“abcd”变成“acdb”?
这个问题其实分为两部分:
一个字符串想要变成另一个字符串,必须一个字符一个字符地处理(不看一个字符就无法做出正确的决定)。
所以“操作”的概念就变成了“一个角色如何变成另一个角色”的问题。
我们看第一个字符:str1中的a如何变成str2中的a?
不用想,它们是平等的,不需要做任何操作——不做任何操作本身就是一种操作。
那么我们看第二个字符:str1中的b如何变成str2中的c?
它们是不同的,因此必须采取一些行动。
一种解决方案是将 b 替换为 为 c。
第二种解决办法是删除b,让其他字符(可能是插入的新字符)处理c字符。
第三个选项是在 a 和 b 之间插入 c。
没有其他计划。
至此我们已经弄清楚所谓的“操作”是什么了:什么都不做、替换、删除、插入,一共四种。
然后我们加上可怕的修饰语“最少”——你会发现你的头又变大了。
如果没有“最低”限制,其实很容易做到。最无脑的做法就是先把str1中的所有字符删除,然后再将str2中的每个字符一个个插入。步骤数为 n + m(两个字符串的长度之和)。
这个最无脑的解法虽然没什么用,但它揭示了这个问题的最大运算步骤数是n+m,所以如果你想出的解法比这个大,肯定有问题。
我们回到上面的具体字符串:如何通过至少操作将字符串“abcd”变成“acdb”?
我们一一尝试。
一种解决办法是:保持第一个a不变,删除第二个b,最后在d后面插入b。步数为2。
然后我们通过人肉尝试了其他解,发现所有解中最小的是2。
然而,问题似乎没有任何进展——我们仍然不知道如何找到的最小步数(人肉除外)。
如果我们的思维停留在具体问题上,我们就不太可能找到问题的解决方案。
人类区别于其他动物的地方就在于,人类可以把具体的事物(现象)提升为抽象的符号,进而形成思维模型(这也是数学的秘密,现在你应该能明白为什么数学家那么喜欢玩了)带符号)。
具体化问题有助于我们深入理解问题本身。现在是时候将我们的思维从具体的层面回归到抽象的层面了。
让我们首先对字符串本身进行抽象表示。
字符串是一个字符序列,其本质是一个集合(还有一个附加限制:与顺序相关,即里面的字符有特定的顺序)。
那么问题就变成了:如何通过最小数量的操作序列 \(\{x^\prime,y^\prime,z^\prime\cdots\}_m\)?
答案是:我不知道!
当农民找不到该区域时,他做了一个惊人的举动:他给了它一个符号!
我们也这样做。
我们假设 \(\{x,y,z\cdots\}_n\) -> \(\{x^\prime,y^\prime,z^\prime \cdots\}_m\)的最小编辑距离为N(无论如何计算)。
就像农民用S(l,w)代替S来反映面积和边长的关系一样,我们也将参数放入:
N(str1, str2)
这个组合符号的意思是:可以计算出将字符串str1转换为str2的最小操作步数(最小编辑距离)。
至于如何计算,我们暂时不关心。
这个东西看起来像编程中的接口吗?
作为一个程序员,你可能会觉得这个N太丑了,所以我们把符号(名字)改成一个“人性化”的点:shortestEditDist(str1, str2)。
然后——我们盯着这个奇怪的符号想了很久,却什么也做不了!
符号本身无法告诉我们更多信息,我们必须启动推理马达来处理符号。
对于集合类问题,常用的思维模式是:从子集的解推导出父集的解。
具体来说,对于“字符串”类型的问题,如果我们已经知道某个子字符串的解,看看是否可以通过这个子字符串推导出某个父字符串的解。
此问题解决假设的基础是: 子字符串和父字符串具有相同的模式。
所以我们稍微调整了一下“无知又牛逼”的组合符号(也就是功能):
最短编辑距离(str1, str2, i, j)
还有两个参数i和j。意思是:我知道子串 str1[0,i] -> str2[0,j](其中0<=i 如图:
我们想进一步推动这个问题。 所以我们问自己:如何得到 str1[0,i+1] -> str2[0,j+1] 的解? 根据我们之前对字符转换的分析,有四种操作: 选项一:当d等于\(d^\prime\)时,无需任何操作。 如图: 公式:shortestEditDist(str1, str2, i+1, j+1) = ShortestEditDist(str1, str2, i, j) 否则: 方案2:先求str1[0,i] -> str2[0,j+1]的解,然后删除d。 如图:
公式:最短编辑距离(str1, str2, i+1, j+1) = 最短编辑距离(str1, str2, i, j+1) + 1 +1代表删除d的操作。 方案三:先求str1[0,i+1] -> str2[0,j]的解,然后在结尾。 如图:
公式:最短编辑距离(str1, str2, i+1, j+1) = 最短编辑距离(str1, str2, i+1, j) + 1 选项4:先求str1[0,i] -> str2[0,j]的解,然后将d替换为\(d^\prime\)。 如图:
公式:最短编辑距离(str1, str2, i+1, j+1) = 最短编辑距离(str1, str2, i, j) + 1 四个选项,我该用谁?我不知道,只能全部找出来,选结果最小的一个。 看完上面的推导,你是否感到愧疚? 通过推销一堆你“一无所知”的公式来自娱自乐? 不要不服气。之前的面积公式不就是在“一无所知”的情况下自行推断出来的吗? 既然我们假设我们已经知道了shortestEditDist(str1, str2, i, j),而这里i和j的范围是0<=i 注:严格来说,i+1和j+1会越界,但我们暂时不考虑越界的情况。 所以我们可以将shortestEditDist(str1, str2, i+1, j)放在等式右边作为“已知值”来求shortestEditDist(str1, str2, i+1, j+1)。 有的同学可能会说:那我们也可以假设shortestEditDist(str1, str2, i+1, j+1)是已知的? 是的,你可以做出任何假设,但如果所有问题都“假设已知”,那么你真的是在欺骗自己和别人,对解决问题不会有任何好处。 这种看似文字游戏的逻辑把戏,本质上是在寻找关系。 是的,我们最看重的是关系,而不是某个公式的绝对解。因此,在推理时,我们可以将一些事物设定为已知,并进一步将其他事物符号化(想想高中学过的所谓“复合函数”,以及大学学过的高级推导——大多你都记不住)。 我们来写代码: 我们在之前的推导基础上实现了shortestEditDist(四种方案): 就是这样。 研究过最小编辑距离的同学可能知道,最小编辑距离可以通过动态规划来求解,性能更高。 本文之所以使用暴力递归,一方面是因为它与动态规划本质上是相同的思想,都是递归(或归纳)问题解决。懂动态规划的同学可以很轻松地将暴力递归转化为动态规划,但不会写递归的同学比看动态规划直观、清晰得多。 第一次接触递归思维的学生都觉得不可思议。他们怎么可能在不知情的情况下算出结果呢? 其实就是我们高中时学过的强大又陌生的数学归纳法。它要求问题具有三个特征: 思路很简单:因为\(S_{n+1}\)的解可以通过\(S_n\)的解得到,而我们知道了\(S_0\),那么我们就必须知道\(S_1\)的解,然后才能知道\(S_2\)的解 … 虽然推导矩形面积和求解字符串最小编辑距离在逻辑上有很大差异,但思维模式却有很多相似之处:
//我可以计算出将子串str1[0,i]转换为str2[0,j]的最小编辑距离,其中0 <= i < n, 0 <= j < m
func ShortestEditDist(str1, str2 string, i, j int) int {
// 我还没想好如何实现它...
}
//求str1转str2的最小编辑距离
func EditDistance(str1, str2 string) int {
// 既然你做了这个声明,我就这样使用它
// 我不明白,只有你在问!
返回最短编辑距离(str1, str2, len(str1)-1, len(str2)-1);
}
//我可以计算出将子串str1[0,i]转换为str2[0,j]的最小编辑距离,其中0 <= i < n, 0 <= j < m
// 实现方法:用shortestEditDist(str1, str2, i-1, j-1)求解shortestEditDist(str1, str2, i, j)
// 注意:之前的推导都是从i,j -> i+1,j+1,本质是一样的
func ShortestEditDist(str1, str2 string, i, j int) int {
// 在考虑具体情况之前,首先考虑递归函数的终止条件
如果 i == -1 && j == -1 {
// 都是空字符串,不需要做任何处理
返回0
}
如果 j == -1 {
// str2的子串为空串,str1的子串[0,i]只能通过删除操作转为str2的子串。
返回 i + 1
}
如果我==-1{
// str1 的子字符串是空字符串。这时只能使用插入操作将其转换为str2的子串。
返回j+1
}
// 一般来说,根据我们之前的讨论,有四种情况
// 情况1:当前两个字符相同
// 选项一
如果 str1[i] == str2[j] {// 两个字符相同,先将str1[0,i-1]转换为str2[j-1],然后这一步什么都不做
返回最短编辑距离(str1,str2,i-1,j-1)
}
//情况2:两个字符不同,有三种情况
// 因为我们不知道这一步中哪个解的步数最少,所以只能全部尝试一下,然后取最小值。
// 方案2:先用str1[0,i-1]转换str2[0,j],然后删除str1[i]
c1 := 最短编辑距离(str1, str2, i-1, j) + 1
// 方案3:先用str1[0,i]转换str2[0,j-1],然后再插入str2[j]个字符
c2 := 最短编辑距离(str1, str2, i, j-1) + 1
// 方案4:先用str1[0,i-1]转换str2[0,j-1],然后用str2[j]替换str1[i]
c3 := 最短编辑距离(str1, str2, i - 1, j - 1) + 1
// 取三种情况中成本最低的
返回 minInt(c1, c2, c3)
}
总结