签到

06月20日
尚未签到

共有回帖数 0

    孤单的狼

    等级:
    《C语言循环的小艺术》很久以前我就复制下来收藏了,不过一直没有研究。Uval的AOAPC I: Beginning Algorithm Contests (Rujia Liu)做到Triangle Wave(http://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=94&page=show_problem&problem=429)的时候又看到了这种对称图形,虽然用那种“分段写”的方法也顺利AC,但是既然知道也许有更好的方法自然要去探索一下,于是有了这篇解读。
    不过这篇解读最只适用于看了很久原文也不知其解的人,如果你有把握自己看懂,那最好还是自己先搞清楚,这样印象更深刻,再回来看本文进行对照最好。
    先看一下《C语言循环的小艺术》中的原文(注意百度处理空格不当,所以图形不太规整,不过大家应该都知道怎么回事):
    菱形打印

    很多人,打印菱形在控制台的思路是,把菱形上下拆分,分两段很接近的代码来打印,
    其实这样代码很不好看,并且不好阅读
    我们知道,要打印的图案是这种:
    *
    ***
    *****
    ***
    *

    满足上下对称,左右对称,那么,你能不能也弄一个二重循环,同样是对称的?
    很简单,首先我们要抛开习惯性思维,for循环不一定要在0开始或者0结束
    我们可以让循环从 -c 到 c ,这样不就轻松产生一个对称的吗?(只要取个绝对值)
    我们把菱形的中心看成是坐标0,0,那么,会输出星号的坐标,是 |x| + |y| = c 的点

    由此可得
    #include stdio.h
    #define IABS(x) ( (x) = 0 ? (x) : -(x) ) //定义一个计算绝对值的宏
    void print(int size) // size是这个菱形的半径,直径会是size * 2 + 1
    {
    int x, y;
    for (y = -size; y = size; y++)
    {
    for (x = -size; x = size; x++)
    {
    if ( IABS(x) + IABS(y) = size ) //x和y各自的绝对值的和,即 |x| + |y| = size
    putchar('*');
    else
    putchar(' ');
    }
    putchar('n');
    }
    }

    int main()
    {
    print(5); //输出一个半径为5的菱形
    getchar();
    return 0;
    }

    如果我需要得到空心菱形呢?非常非常简单,因为菱形边界上的点,满足的是|x| + |y| == c
    所以,我们只要把那个if里的小于等于号,改成双等于号 == 就可以了

    再类似地,如果我不要*号,我要最外层是字母A,然后里一层是B这样呢?即:
    A
    ABA
    ABCBA
    ABA
    A

    那么,我们只要在putchar那里做一个字符计算:
    void print(int size) // size是这个菱形的半径,直径会是size * 2 + 1
    {
    int x, y;
    for (y = -size; y = size; y++)
    {
    for (x = -size; x = size; x++)
    {
    if ( IABS(x) + IABS(y) = size ) //x和y各自的绝对值的和,即 |x| + |y| = size
    putchar( 'A' + (size - IABS(x) - IABS(y)) ); //留意这里的计算方法
    else
    putchar(' ');
    }
    putchar('n');
    }
    }

    类似地,如果我们要打印的是X形:
    * *
    * *
    *
    * *
    * *
    同样可以利用这个思路完成,这题就作为思考题吧
    ——————————————————————————————
    其实include stdlib.h后可以直接用abs()- -,而且严格来说int main(void)是最正确的,当然这些不是重点。拿到一个算法程序,最简单的分析方式就是先跟踪看一下运行过程,由于size理论上可以取任意值,我们只要弄懂一种情况就等于弄懂了所有情况,所以为了简化分析先size=3,看一下第一次大的循环:y=-3x=-3 ' 'x=-2 ' 'x=-1 ' 'x=0 '*'x=1 ' 'x=2 ' 'x=3 ' '到这里我们会发现,开始|x|+|y|size,所以前面输出了三个' ',然而由于|x|渐渐减小,之后输出了一个'*',由于|x|又渐渐增大,又输出了三个' ',这样就巧妙地完成了左右的对称。又由于下一次循环|y|比这次循环小1,所以就“更容易”输出'*',由于|y|也有对称的特性,所以完成了上下的对称。至此,我们大概就对为什么可以上下左右对称有了一个感性的认识,不过恐怕更多的是对这些代码的惊叹:为什么这样写就能巧妙地做到这些?IABS(x) + IABS(y) = size这个表达式也太神奇了!御坂美琴みさか是怎么想到的?!其实我们上来看程序的运行过程,只是为了从运行过程中试着找到入口去探索这个算法的本质是什么,这是一个从现象到本质的过程,而不能被现象吓到了。实际上御坂美琴みさか在文中已经写到了:我们把菱形的中心看成是坐标0,0,那么,会输出星号的坐标,是 |x| + |y| = c 的点那么程序的原理实际上就是通过枚举{(x,y), |x| = size, |y| = size, x, y ∈Z}中的点(即下图中红色部分,包括边界),再通过一个式子来确认合法点,如果合法,就输出'*',反之输出' ':


    那么我们又有疑问了,为什么是|x| + |y| = c?
    我们来证明一下上面右上角的红色三角区域(包括边界)的所有点(x,y)(x,y∈N),x+y=size(自己先试试看):
    通过观察,我们可以发现y的取值范围随着x在改变,至于“怎么改变”则与size有关,那么我们可以想一下,如果我们把其中的y用size和x表示,这个不等式不就容易得证了吗?
    那 么我们可以得知y∈[0, size-x](x≠size),取两边分别讨论,当y=0时,x+y=x=size,当y=size-x时,x+y=x+size- x=size,当y∈(0, size-x)(x≠size)时,x+ysize。综上所述,x+y=size,证毕。
    其实即使你不会证明,也可以当作公式默认下来,这都无关紧要。
    于是我们就能自己写出|x| + |y| = c这个式子了,也就知道了这个算法的本质,现在一切都掌握在我们手中,甚至我们可以对原程序做一点改动:
    由于是枚举(x,y)∈{(x,y), |x| = size, |y| = size, x, y ∈Z},而且有绝对值的对称做保障,那么无论x、y谁在外面只要全枚举到就行了,所以也可以把x放在外层循环:
    for (x = -size; x = size; x++)
    {
    for (y = -size; y = size; y++)
    {
    if (IABS(x) + IABS(y) = size) //x和y各自的绝对值的和,即 |x| + |y| = size
    putchar('*');
    else
    putchar(' ');
    }
    putchar('n');
    }
    观察原程序的输出,我们可以发现由于对称的缘故,程序输出的右侧多输出了我们看不见的空格,我们也可以不输出它:
    if (IABS(x) + IABS(y) = size ) //x和y各自的绝对值的和,即 |x| + |y| = size
    putchar('*');
    else if (x  0)
    putchar(' ');
    else
    break;
    虽然上述改动没有太大意义,但是说明了如果我们掌握了算法的本质,我们就掌握了全局,那么就可以进行任何改动的道理。

    楼主 2015-11-19 14:19 回复

共有回帖数 0
  • 回 帖
  • 表情 图片 视频
  • 发表

登录直线网账号

Copyright © 2010~2015 直线网 版权所有,All Rights Reserved.沪ICP备10039589号 意见反馈 | 关于直线 | 版权声明 | 会员须知