历届试题 打印十字图
问题描述
小明为某机构设计了一个十字型的徽标(并非红十字会啊),如下所示:
对方同时也需要在电脑 dos 窗口中以字符的形式输出该标志,并能任意控制层数。输入格式
一个正整数 n ( n ≤ 30 ) n (n \le 30) n(n≤30) 表示要求打印图形的层数。
输出格式
对应包围层数的该标志。
样例输入1
1
样例输出1
样例输入2
3
样例输出2
提示
请仔细观察样例,尤其要注意句点的数量和输出位置。
——分割线之初入江湖——
分析:
这是一道典型的文字图形类题,其要求是能根据输入,打印出对应的图形
读完这道题,让我想起了当初学 C++,在学到 for 循环时的一个题目,如下:
要求编程实现对于任何的输入
n
n
n,程序都能根据上述的规则输出对应的图案。
这道题现在看来很简单,但是在当时还是很懵的。我们来说说这道题的解题思路,显然这里存在一个规律:对于每一行(假设为第
i
i
i 行(
i
i
i 从 1 开始),当前输入的值为
n
n
n),前面的空格数
都满足:空格数 =
n
−
i
n - i
n−i;中间的 *
个数满足: *
个数=
2
×
i
−
1
2\times i - 1
2×i−1。于是可以很快的写出这道题的代码为:
void print(int n)
{
for(int i=1;i<=n;i++){
for(int j=1;j<=n-i;j++)
cout<<” ”;
for(int j=1;j<=2*i-1;j++)
cout<<”*”;
cout<<endl;
}
}
这道题的特点是图形与其对应的序号有着简单的对应关系,这个对应关系简单到能够直接根据当前行数得出当前行所需要打印的图像,也就是说对该图像的每一行都存在着一个通用的公式。因此我们可以直接通过最多两层的循环将整个图像打印出来,并且不需要任何的提前处理就能直接输出。
——分割线之艰难磨砺——
反观本题,发现这里的图像复杂很多。为了能更直观地看清本题中的图像特征,下面我先将本题中的部分图像绘出(其中红色背景替代”$”,无背景替代”.”)。首先是 n = 1 n=1 n=1 的图像:
然后是 n = 2 n=2 n=2 的图像:
可以很明显地发现,本题中的图像不具备像上面那道题一样简单的对应关系(存在对每行都通用的公式),也不具备直接输出的条件。且对于每个图像而言,当
n
n
n 增加 1 时,其就会在最外层再多增加一层“十字架”。而这样的动态变化模式也导致了对于整个图像中的每一行而言,其并不存在一个通用的公式。这样的下场是,我们必须用一个二维数组来将整个图像的特征保存(当然,这里面可能有多道步骤),最终再通过这个点阵数组将整个图像打印出(比如说在数组中用 1 表示某个点为 $
,用 0 表示某个点为 .
)。
① 首先我们来观察每个图中最中间的那个 “十字架” 的位置,如下图所示(蓝色部分):
可以发现,无论 n n n 取值多少,最中间的那个 “十字架” 都在图层的最中央,且长度为 5,于是可以得到最中间的这个 “十字架” 的位置为: ( N 2 + 1 , N 2 + 1 ) (\frac{N}{2+1},\frac{N}{2+1}) (2+1N,2+1N)。
注:假设当前图像的规格为 N × N N \times N N×N,且存储在二维数组中的横纵坐标均从1开始,下同。
进而可以通过一重循环将整个最中间的 “十字架” 位置标记出:
for(int i=N/2-1;i<=N/2+3;i++)
map[N/2+1][i]=map[i][N/2+1]=1; //一横一竖
② 接下来观察外围 “十字架” 在其四个边角部位的位置,如下图所示(蓝色部分):
由于外围的 “十字架” 有多层,故我在这里规定 “十字架” 的层数从最外层到最里层,层数分别为 1 , 2 , … … , n 1,2,……,n 1,2,……,n。若假设当前 “十字架” 为第 i i i 层,则可以得到第 i i i 层 “十字架” 的四个边角坐标与 i i i 和 N N N 存在以下关系:
- 左上部分的边角(格点 1 )坐标为 ( x , y ) (x,y) (x,y),其中 x = 2 × i + 1 , y = 2 × i + 1 x=2\times i+1,y=2 \times i+1 x=2×i+1,y=2×i+1 。则格点 1 上方格子的坐标为: ( x − 1 , y ) (x-1,y) (x−1,y)、格点 1 左边格子的坐标为: ( x , y − 1 ) (x,y-1) (x,y−1)。
- 右上部分的边角(格点 2 )坐标为 ( x , y ) (x,y) (x,y),其中 x = 2 × i + 1 , y = N − 2 × i x=2\times i+1,y=N-2 \times i x=2×i+1,y=N−2×i。则格点 2 上方格子的坐标为: ( x − 1 , y ) (x-1,y) (x−1,y)、格点2右边格子的坐标为: ( x , y + 1 ) (x,y+1) (x,y+1)。
- 右下部分的边角(格点3)坐标为 ( x , y ) (x,y) (x,y),其中 x = N − 2 × i , y = N − 2 × i x=N-2\times i,y=N-2 \times i x=N−2×i,y=N−2×i。则格点 3 下方格子的坐标为: ( x + 1 , y ) (x+1,y) (x+1,y)、格点3右边格子的坐标为: ( x , y + 1 ) (x,y+1) (x,y+1)。
- 左下部分的边角(格点4)坐标为 ( x , y ) (x,y) (x,y),其中 x = N − 2 × i , y = 2 × i + 1 x=N-2\times i,y=2 \times i+1 x=N−2×i,y=2×i+1。则格点 4 下方格子的坐标为: ( x + 1 , y ) (x+1,y) (x+1,y)、格点4左边格子的坐标为: ( x , y − 1 ) (x,y-1) (x,y−1)。
于是可以通过一层循环就实现这四个边角位置的标记,如下:
for(int i=1;i<=n;i++)
{
int x=2*i+1,y=2*i+1;
//左上边角
map[x][y]=map[x][y-1]=map[x-1][y]=1;
//右上边角
y=N-2*i;
map[x][y]=map[x][y+1]=map[x-1][y]=1;
//右下边角
x=N-2*i;
map[x][y]=map[x][y+1]=map[x+1][y]=1;
//左下边角
y=2*i+1;
map[x][y]=map[x][y-1]=map[x+1][y]=1;
}
③ 接下来观察外围 “十字架” 在其 “四个墙壁” 的位置,如下图所示(蓝色部分):
这里同样规定 “十字架” 的层数从最外层到最里层,层数分别为
1
,
2
,
…
…
,
n
1,2,……,n
1,2,……,n;图像规格为
N
╳
N
N ╳ N
N╳N 。可以发现,其实这 “四个墙壁” 和第 ② 部分中的四个边角部分具有几乎等同的规则。首先可以看出的是,这四条边的长度都是一致的;其次这些边的长度都等于整个图像的总长(
N
N
N)减去前后各两个格子的长度(当然,随着 “十字架” 由最外层逐渐往内层移动时,其减去的前后的格子也在逐渐增加)。为了能把中间的这些格子的位置标记出,我们就需要对这些位置的起始坐标进行统计(循环赋值需要起始两个边界值)。
-
首先来看最外层 “十字架” 的最上方 “墙壁”(图中蓝色部分)的起始坐标:
行数:第一层“十字架”的行数显然为 1,而第二层行数变为了 3,第三层则变成了 5,于是可以得到 “十字架” 上方 “墙壁” 的行数随 “十字架” 层数的增加,其变化规律为: 2 × i − 1 2 \times i - 1 2×i−1( i i i 为当前 “十字架” 的层数)。当然,对于 “十字架” 上方 “墙壁” 而言,其行数是固定的,列数是变化的;
列数:通过上图的规律,可以直接给出列数的起点坐标为: 2 × i + 1 2 \times i+1 2×i+1;终点坐标为 N − 2 × i N - 2 \times i N−2×i。则可以给出对 “十字架” 上方 “墙壁” 的位置进行标记的代码为:
for(int i=1;i<=n;i++) //“十字架”层数的增加 for(int j=2*i+1;j<=N-2*i;j++) //上方“墙壁”列数的增加 map[2*i-1][j]=1;
-
现在来看 “十字架” 的右侧 “墙壁” 的起始坐标(显然此时行数是变化的,而列数是固定的):
行数:可以直接给出行数的起点坐标也为: 2 × i + 1 2 \times i +1 2×i+1;终点坐标也为 N − 2 × i N-2 \times i N−2×i;
列数:第一层 “十字架” 的列数显然为 N N N,而第二层行数变为了 N − 2 N - 2 N−2,第三层则变成了 N − 4 N - 4 N−4,于是可以得到 “十字架” 右侧 “墙壁” 的列数随 “十字架” 层数的增加,其变化规律为: N − 2 ╳ ( i − 1 ) N-2 ╳ ( i - 1) N−2╳(i−1)。则可以给出对“十字架”右侧“墙壁”的位置进行标记的代码为:
for(int i=1;i<=n;i++) //“十字架”层数的增加 for(int j=2*i+1;j<=N-2*i;j++) //右侧“墙壁”行数的增加 map[j][N-2*(i-1)]=1;
-
再看 “十字架” 的下方 “墙壁” 的起始坐标(此时行数是固定的,而列数是变化的):
行数:第一层 “十字架” 的列数显然为 N N N,而第二层行数变为了 N − 2 N-2 N−2,第三层则变成了 N − 4 N-4 N−4,于是可以得到 “十字架” 右侧 “墙壁” 的列数随 “十字架” 层数的增加,其变化规律为: N − 2 × ( i − 1 ) N-2 \times ( i - 1) N−2×(i−1);
列数:可以直接给出列数的起点坐标为: 2 × i + 1 2 \times i+1 2×i+1;终点坐标为 N − 2 × i N-2 \times i N−2×i。则可以给出对“十字架”下方“墙壁”的位置进行标记的代码为:
for(int i=1;i<=n;i++) //“十字架”层数的增加 for(int j=2*i+1;j<=N-2*i;j++) //下方“墙壁”列数的增加 map[N-2*(i-1)][j]=1;
-
最后看 “十字架” 的左侧 “墙壁” 的起始坐标(此时行数是变化的,而列数是固定的):
行数:可以直接给出行数的起点坐标也为: 2 × i + 1 2 \times i+1 2×i+1;终点坐标也为 N − 2 × i N - 2 \times i N−2×i;
列数:第一层“十字架”的列数显然为 1,而第二层行数变为了 3,第三层则变成了 5,于是可以得到“十字架”左侧“墙壁”的列数随“十字架”层数的增加,其变化规律为: 2 × i − 1 2 \times i - 1 2×i−1。则可以给出对“十字架”左侧“墙壁”的位置进行标记的代码为:
for(int i=1;i<=n;i++) //“十字架”层数的增加 for(int j=2*i+1;j<=N-2*i;j++) //左侧“墙壁”行数的增加 map[j][2*i-1]=1;
其实不难发现在上述 4 点中,行和列的变化用代码表示时,其外层条件均一致。于是我们可以把这所有的行列变化都放在一个循环体之下,如下:
for(int i=1;i<=n;i++)
for(int j=2*i+1;j<=N-2*i;j++)
map[2*i-1][j]=map[j][N-2*(i-1)]=map[N-2*(i-1)][j]=map[j][2*i-1]=true;
至此,整个图像的点阵特征均被放进了一个二维数组中。我们可以直接通过事先设定好的约定(1替代 $
,0替代 .
)来遍历整个二维数组,从而将整个图像打印出,代码如下:
for(int i=1;i<=N;i++)
{
for(int j=1;j<=N;j++)
if(map[i][j]) cout<<"$";
else cout<<".";
cout<<endl;
}
——分割线之炉火纯青——
下面直接给出本题的完整代码:
#include<iostream>
using namespace std;
const int MAX=130; //需设置为大于5+4*30的数
bool map[MAX][MAX];
void print(int n)
{
//通过n计算出打印十字图的规格(N*N)
int N=5+4*n;
//首先标记最内层的十字架
for(int i=N/2-1;i<=N/2+3;i++)
map[N/2+1][i]=map[i][N/2+1]=true; //一横一竖
//然后从最外层到里层逐个标记十字架
for(int i=1;i<=n;i++)
{
//接下来标记四个边角部分
int x=2*i+1,y=2*i+1;
//左上边角
map[x][y]=map[x][y-1]=map[x-1][y]=true;
//右上边角
y=N-2*i;
map[x][y]=map[x][y+1]=map[x-1][y]=true;
//右下边角
x=N-2*i;
map[x][y]=map[x][y+1]=map[x+1][y]=true;
//左下边角
y=2*i+1;
map[x][y]=map[x][y-1]=map[x+1][y]=true;
//接下来标记十字架的非边角部位(即“四个墙壁”)
for(int j=2*i+1;j<=N-2*i;j++)
map[2*i-1][j]=map[j][N-2*(i-1)]=map[N-2*(i-1)][j]=map[j][2*i-1]=true;
}
//打印
for(int i=1;i<=N;i++){
for(int j=1;j<=N;j++)
if(map[i][j]) cout<<"$";
else cout<<".";
cout<<endl;
}
}
int main()
{
int n;
cin>>n;
print(n);
return 0;
}