当前位置:编程学堂 > 程序员的数学思维:如何求出长方形的面积

程序员的数学思维:如何求出长方形的面积

  • 发布:2023-10-05 13:44

长方形的面积

我们小学的时候就学过,长方形的面积等于长乘以宽。

但是生活了几十年,你有没有想过:为什么长方形的面积等于长乘以宽?

或者为什么我们的祖先将定义为长方形的面积为长度乘以宽度?

在继续之前,请忘记矩形面积等于长乘以宽这个“简单”的知识)。

想象你是一个非常好奇的农民。有一天,你无所事事,捧着一杯茶,盯着家里的一块土地,就像下面这个形状:

我们突然想知道这片土地有多大。于是我用尺子量了一下:

土地的大小是长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”?

这个问题其实分为两部分:

  1. 什么样的操作
  2. 最少的操作?

一个字符串想要变成另一个字符串,必须一个字符一个字符地处理(不看一个字符就无法做出正确的决定)。

所以“操作”的概念就变成了“一个角色如何变成另一个角色”的问题。

我们看第一个字符: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)是已知的?

是的,你可以做出任何假设,但如果所有问题都“假设已知”,那么你真的是在欺骗自己和别人,对解决问题不会有任何好处。

这种看似文字游戏的逻辑把戏,本质上是在寻找关系。

是的,我们最看重的是关系,而不是某个公式的绝对解。因此,在推理时,我们可以将一些事物设定为已知,并进一步将其他事物符号化(想想高中学过的所谓“复合函数”,以及大学学过的高级推导——大多你都记不住)。

我们来写代码:

//我可以计算出将子串str1[0,i]转换为str2[0,j]的最小编辑距离,其中0 <= i < n, 0 <= j < m
func Sh​​ortestEditDist(str1, str2 string, i, j int) int {
// 我还没想好如何实现它...
}
//求str1转str2的最小编辑距离
func EditDistance(str1, str2 string) int {
// 既然你做了这个声明,我就这样使用它
// 我不明白,只有你在问!
返回最短编辑距离(str1, str2, len(str1)-1, len(str2)-1);
}

我们在之前的推导基础上实现了shortestEditDist(四种方案):

//我可以计算出将子串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 Sh​​ortestEditDist(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)
}

就是这样。

研究过最小编辑距离的同学可能知道,最小编辑距离可以通过动态规划来求解,性能更高。

本文之所以使用暴力递归,一方面是因为它与动态规划本质上是相同的思想,都是递归(或归纳)问题解决。懂动态规划的同学可以很轻松地将暴力递归转化为动态规划,但不会写递归的同学比看动态规划直观、清晰得多。

第一次接触递归思维的学生都觉得不可思议。他们怎么可能在不知情的情况下算出结果呢?

其实就是我们高中时学过的强大又陌生的数学归纳法。它要求问题具有三个特征:

  1. 集合与其任何子集具有相同的模式;
  2. 可以通过子集\(S_n\) 的解来求解
  3. 能够知道初始值的解(如n=0);

思路很简单:因为\(S_{n+1}\)的解可以通过\(S_n\)的解得到,而我们知道了\(S_0\),那么我们就必须知道\(S_1\)的解,然后才能知道\(S_2\)的解 …


总结

虽然推导矩形面积和求解字符串最小编辑距离在逻辑上有很大差异,但思维模式却有很多相似之处:

  1. 首先明确问题(什么是面积;什么是编辑距离);
  2. 用具体的例子来深入理解和探究问题(50*100的矩形;“abcd”和“acdb”两个字符串);
  3. 参数化解对象(矩形的长l、宽w;最小编辑距离中的i、j);
  4. 符号化未知解(区域S;最小编辑距离N);
  5. 将符号和参数组合成新的复合符号(函数。例如面积中的S(l,w);编辑距离中的N(str1,str2,i,j)),并建立问题与解决方案的初步关系;
  6. 通过观察和实验,探究不同参数对解的影响(矩形边长加倍,面积加倍;可以用str1[i] -> str2[j]的解求出str1[i +1] -> str2[j +1];
  7. 推广第6步,得到最终解;






相关文章

最新资讯