阿西莫夫机器人三大定律?
2016年,master横空出世,在网络平台上取得对中日韩顶尖棋手60连胜0负1平的逆天战绩,1平还是因为棋手掉线。网络有很多有趣的段子: 李世石赢AlphaGo,可能是人类对机器围棋的最后一场胜利。同时关于机器超越人类的担忧,像以前科幻电影各种机器人战胜人类的事好像不再是杞人忧天。不过阿西莫夫的《我,机器人》中提到机器人三大法则:
第一定律:机器人不伤害人类个体,或者目睹人类将遭受危险而袖手不管。
第二定律:机器人必须服从人给予它的命令,当该命令与第零定律或者第一定律冲突时例外。
第三定律:机器人在不违反第零,第一,第二定律的情况下要尽可能保护自己的生存。
看到这三大定律,悬着的心才稍微安定下来。
Karel机器人三大定律?
Karel是一个弱小,还有点缺陷的机器人,他还不足以对人类产生威胁,但是它也有自己的定律。那它的定律是什么?我们先看一个任务。Karel很忙,经常接受到各种任务。
Karel从当前街角开始,不断往前走,边走边放方块,直到将这一行都填满。如下图:
拿到这个任务很高兴,写了一个新的方法createBeeperLine(),总共就6次,如下面代码:
public void createBeeperLine(){
putBeeper();
move();
putBeeper();
move();
putBeeper();
move();
putBeeper();
move();
putBeeper();
move();
putBeeper();
}
Karel顺利完成任务,值得庆祝一下。但是这个方法好像有些问题,如果100行,难道我们要重复写move, putBeerper, 100次吗?好像很傻一样。有没有一种办法处理这种重复的事。
Karel的第二定律
Karel的第二定律就是:我们总是不断重复着昨天的故事。
怎么直接就第二定律了,那第一定律是什么?
第一定律,我们之前已经体验过了:我们很多时候总是按照顺序执行任务,比如大家都是先上小学,然后上中学,最后上大学,这样按顺序执行的。
那程序如何处理反复执行的事情了?程序员称这个为循环,有两种方式,一种是for 循环,for语句用于当你想按预定的次数重复执行一组命令的时候,while语句用于当你想在某些条件满足时候,重复执行一组命令的时候。
我们先看看for循环,等讲完第三条定律的时候,再看看while循环。
循环有四个要素:
1) 开始的时候,
2) 结束的时候,
3) 迭代因子(计数器递增或者递减),也就是从开始到结束,比如电梯从1楼上到10楼,每一次递进1楼,没有递进,永远停留在开始,形成死循环。
4) 循环要做的事,比如在我们刚才的任务里,反复做的事就是putBeeper(),move()。
我们看一看for循环的语法结构.
for (int i = 0; i <= count; i = i + 1; )
{
重复执行的语句。
}
int i;i 是一个变量,这个跟初中代数变量的意思相近,就是值可以变化,比如定义x 变量,x 可以等于不同的数。int 表示变量的类型是整形,也就是i 必须是一个整数。同样表示小数的变量,类型是double,float,必须定义一个小数型的变量格式为:double j = 5.20;
int i = 0:表示i是 一个整形变量,我们将0赋值给i。
我们看看for循环的结构i= 0;就是开始的时候。 i < count; count就是结束的时候, i= i+ 1 ;就是迭代因子,每次递进1步,然后{}体内就是循环的内容。
我们看看for循环的流程图
for (int i = 0(初始化部分); i <= count(条件判断); i = i + 1; (迭代因子))
{
重复执行的语句。(循环体)
}
我们再回到最初的任务,怎么将下面的代码改成循环
public void createBeeperLine(){
putBeeper();
move();
putBeeper();
move();
putBeeper();
move();
putBeeper();
move();
putBeeper();
move();
putBeeper();
}
总共有放了6个方块,那么我们改写代码如下:
public void createBeeperLine(){
for (int i = 0; i < 6; i++){
putBeeper();
move();
}
}
我们简单看一下for循环代码执行顺序
那么程序的执行顺序就是
这里我们对for循环基本很熟悉。
我们把刚才代码测试一下是否完成了预期的目标。
是不是我们再一次体会到了这个世界的恶意,Karel执行完上面的循环撞墙了。
当它走到角落里放完Beeper之后不能再往前走了。我们只能循环5次了,我们把判断条件改为 i < 5,试试。好像Karel没有撞墙,但是新的问题又出现了,最后一个位置是空的
解决这个问题很容易,等循环结束之后,我们在最后的位置执行putBeeper的命令,就完美解决问题。
代码如下:
public void createBeeperLine(){
for (int i = 0; i < 5; i++){
putBeeper();
move();
}
putBeeper();
}
结果如下:
现在我们已经非常熟悉了for循环。Karel现在又接收到了新任务,将整个屏幕都填满方块。
我们执行完放满一行的任务之后,Karel现在处在屏幕第1街第6道,也就是第1街的最右边,现在Karel如何执行填满第2行的任务:首先上到第2街,然后调整好方向,然后接着执行createBeeperLine()命令。
我们把相应的代码写在Run方法里出来:
turnLeft();
move();
turnLeft();
createBeeperLine();
好像完成了我们的任务,Karel虽然有点残疾,但是他也有普通人没有的功能,就是倒立行走,跟西毒欧阳锋一样,倒立行走。
我们完成了第二行的任务。我们看看第三行怎么做?
是不是调整好方向就跟其他行没什么区别,如何调整方向?
向右转,往上走一步,然后再向右转,是不是跟第一行一样了?
我们把相应的代码写出来:
turnRight();
move();
turnRight();
createBeeperLine();
是不是第三行也放好了。我们继续这样放好第四行,第五行…直到最后一行。是不是顺利完成任务了。
大家回过头看看,是不是觉得这个方法一样不科学,他们是不是在重复着做某些内容。
第一行,第三行,第五行….奇数行是不是方向相同?
第二行,第四行,第六行…..偶数行是不是方向相同?
但是奇数行与偶数行不一致,有没有办法用一个循环解决?
有的人马上想到以放两行为一个循环,这样是不是比较完美地解决问题。我们先试试,先完成放两行的命令:
public void createBeeperTwoLine(){
createBeeperLine();
turnLeft();
move();
turnLeft();
createBeeperLine();
}
放完两行之后,如果我们要继续再放两行是不是先要走上去,那么执行的命令的是先向右转,然后前进一步,然后再向传,是不是可以重复进行第二次放两行的命令:
我们现在可以向以前一样重复执行放两行的命令了,那么我们现在可以完整的写下代码:
public void run(){
for ( int k = 0 ; k < 3 ; k++){
createBeeperTwoLine();
turnRight();
move();
turnRight();
}
}
Karel的第三定律
这样是不是能够完成我们的任务,我们把代码检验一下,满怀期待,再一次感受到世界的恶意,是不是我们放完了,机器人并没有像我们预计的那样停下来,继续往上走,碰壁了。
我们如何解决这个问题,如果我们知道已经到了最后一行的时候,是不是可以告诉机器人不往前走了,我们现在来看Karel的第三大定律:人生总是充满着很多选择:比如向左走,还是向右走?对于我们的karel机器人当前遇到的问题是:是不是到了最后一行?是的话,不向上走了,如果没到,就继续向上走了?我们在程序有什么语法对应这种需要选择的情况。我们看看经典的if语句
if(条件检测){
只有当条件满足时才会执行的语句
}else{
只有当条件不满足时才会执行的语句
}
女生都是这种语句使用高手,谈过恋爱的男生都知道,女生一旦想要达成某项目标的时候,比如想让你给她买个包的时候,她都会用上这个语句:
if (你是不是不爱我了){
如果是,就分手。
}else{
如果不是,就买包。
}
基本这招屡试不爽。当然,有时候只有if,也就是意味着else的情况什么也不做。我们后面会经常见到。
比如我们怎么修改刚才的代码,可以避免机器人完成任务之后继续往上走撞墙,是不是如果是最后一行,就不往上走了,如果不是,那么机器人继续往向上走。对应的else的代码就是什么也不做了,完整代码如下:
public void run(){
for ( int k = 0 ; k < 3 ; k++){
createBeeperTwoLine();
if (k != 2){
turnRight();
move();
turnRight();
}
}
}
是不是完美地解决了这个撞墙的问题。
好,我们现在想想一次循环完成放两行的代码有什么缺陷?如果总行数是偶数的话,每次执行两行这样没有任何问题,但是如果总行数是奇数,比如7行,这个代码是不是不行。我们如何解决?是不是我们得先判断一下是不是奇数行,还是偶数行,又是一次判断。
我们有没有更好的解决方案,我们还是回到最初的地方去?
我们将任务分解成两件事:先铺满一行,如果是最后一行就结束任务,如果不是Karel就到上一行,然后接着铺满一行,如此反复。我们现在考虑如何完成这个任务了。我们可以看到在不同的行,Karel想往上走一步,行为是不一样:比如Karel完成铺满一行的时候,如果Karel现在面朝东方的,它要上去,是需要完成以下几个命令:
1) 向左转。
2) 往前走一步。
3) 向左转,调整方向。
如果Karel现在面朝西方,则与这个命令相反。
如果我们有一个命令能判断Karel现在是面朝东方,还是西方,就能完美解决这个任务。这个世界不仅仅让你感觉到恶意,但是更多的时候,是更多的善意,正如你所愿,系统真的提供了完整的命令:
facingEast() :Karel是不是面朝东方的;
facingWest():Karel是不是面朝西方的;
当然也有北方和南方:
facingNorth():Karel是不是面朝北方的;
facingSouth():Karel是不是面朝南方的;
我们判断是不是Karel现在面朝东方还是西方,便是很容易了。
if(Karel是不是面朝东方的){
是的话,做什么?
}
else{
不是的话,做什么?
}
具体的代码如下:
public void run( ){
for ( int k = 0 ; k <6 ; k++){
createBeeperLine();
moveToNextLine ();
}
}
public void moveToNextLine (){
if (facingEast()){
turnLeft ();
move();
turnLeft ();
}else{
turnRight();
move();
turnRight();
}
}
是不是很清楚了?是不是又感觉大功告成了。我们测试一下,是不是再次发现世界的恶意,大家发现问题在哪?是不是又没有停下来?我们如何去解决这个问题。留给大家自己去做一做。
我们在这里是不是经常遇到大小差一的错误(off by one)就是指某个变量的最大值和最小值可能会和正常值差1,或者循环多执行一次/少执行一次。一般在临界情况时发生。我们在边界条件要格外注意,在我们这里经常遇到多走一步撞墙,少做一次,方块放不全的问题。比如小学时候经典问题如果你要建造一个100米长的栅栏,其栅栏柱间隔为10米,那么你需要多少根栅栏柱呢?11根或9根都是正确答案,而10根却是不正确的答案。一只青蛙掉在井底.井深10米,青蛙白天爬三米,晚上向下掉2米,问青蛙几天爬上来?这样经典问题,我们常常在最后边界条件出了问题。
关于Off by one 有个既浪漫又悲伤的笑话:
有一个男孩他爱上了一个女孩,女孩知道后接见了他,
男孩见到女孩之后更爱她了,就对女孩说出了心里话。
女孩就说你在我的窗下等上100天我就会爱上你!
男孩就高兴的答应了,从那天开始,男孩每天都在窗下等着就着样一天又一天,
一个星期又一个星期的过去了,男孩不论刮风还是下雨都痴痴的等在窗下,
但是窗户从来都没打开过,但是女孩吗她当然会信守诺言的!
可是到了女孩数到99天男孩再一次看着窗户,
傍晚他回身了,微笑着走了没有回头,从此在没人看到过他。
为什么?
因为女孩是程序员,她从零开始计数的。
我们这里是不是循环多了一次,最后结束到了第6行时候,铺完最后一行的时候是不是应该停下来,我们如何去修改这个问题?大家想一想就有办法了。
我们基本上能完成铺满整个屏幕的问题,但是现在又有了新的问题,我们现在做的都是已经知道具体的行数或者列数的。我们现在要适用于所有的地图该如何做?当我们不知道地图大小的时候,机器人是否有办法顺利完成任务。这个任务留在下一次课再探讨。
今天我们总结一下机器人三大定律:
1) 顺序
2) 循环
3) 选择
在程序的世界,只有这三种情况,暂时没见到第四种情况。就像牛顿运动三大定律一样,所有宏观物体运动,大到星球,小到分子,电子运动,无比符合三大定律。
所有的代码要么是顺序,要么循环,要么选择。没有第四种选择。
欢迎收看我们的下一节课:机器人三大定律(下),我们继续使用这三种情况完成更复杂的任务,比如适应所有的地图。