洛谷普及P2239 [NOIP 2014 普及组] 螺旋矩阵 和 B3751 [信息与未来 2019] 粉刷矩形
题目:
[NOIP 2014 普及组] 螺旋矩阵
[信息与未来 2019] 粉刷矩形
题号:
P2239
B3751
难度:普及一
两道题有一点类似,所以放到一个题解内。(都可以采用边界强行限制解题)
题目分析
初始化特定大小(n*m)的网格,然后粉刷k步颜色,覆盖式粉刷,
每次粉刷指定初始位置和粉刷方向,知道遇到边界。
引入思路
先观察这道较为简单的粉刷矩形
逐步移动访问,以及制定边界阻挡。
关于该题,其实要做的步骤很简单,编写一个函数,然后遍历k次的访问参数调用给函数。
函数的功能,在指定的x,y位置,向指定的方向粉刷,遇到边界便终止本次。
1,关于边界,我们将数组扩大一圈,在这一圈铺上指定的阻挡值。
for(int i=0;i<=m;i++)
{ a[0][i]='1';a[n+1][i]='1';}
for(int i=0;i<=n;i++)
{a[i][0]='1';a[i][m+1]='1';}
2,内层按照题中指定要求,初始化为 ‘ . ’
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)a[i][j]='.';
3,遍历调用绘图函数
for(int i=1;i<=k;i++) {
scanf("%d %d %c %c",&y,&x,&color,&direction);
getchar();
paint(a,y,x,color,direction);
}
3,核心步骤,编写绘画函数
我们先将传入的初始位置绘画指定颜色,然后沿着指定方向访问,继续绘画赋值,直到遇到边界为止。
void paint(char a[52][52],int x,int y,char color,char direction) {switch(direction) {//右 上 左 下
case 'R': {a[x][y]=color;
while(a[x][++y]!='1')a[x][y]=color;}
break;
case 'U':
{a[x][y]=color;
while(a[--x][y]!='1')a[x][y]=color;
}
break;
case 'L':
{a[x][y]=color;
while(a[x][--y]!='1')a[x][y]=color;
}
break;
case 'D':
{a[x][y]=color;
while(a[++x][y]!='1')a[x][y]=color;
}
break;}
}
到这里,粉刷矩形这道题便结束了。
核心思想:扩增一圈设定特定值的边界,逐步访问赋值。遍历调用。
到这里,我们接着进阶,看下一道,螺旋矩阵
首先,这道题在很多平台都出现过,属于比较经典的一道题,
例如 ACGO 平台上,作为一道入门题来使用,而洛谷上当做普及题。
同一类型的题造成难度差异的原因就在于是否限定了时间。
ACGO平台上的该题较为简单是因为它的时间范围放的很广,做题时只需要考虑数值正确就行。
来看代码
#include<stdio.h>int x=0;//初始位置,即将从左侧进入 一 行
int y=1;//初始位置,处在 一 行 (第二行)
int ctr = 1;//控制移动方向,初始化为1 向右
int num = 0;//控制螺旋数组的赋值。
int main()
{
int n;scanf("%d",&n);int a[n+2][n+2];for(int i=0;i<=n+1;i++){
a[i][0] = 1;
a[0][i] = 1;
a[n+1][i] = 1;
a[i][n+1] = 1;}//赋值边界
for(int q=1;q<=n;q++)
for(int p=1;p<=n;p++)
a[q][p]=0;//赋值内界
while(1)
{if(ctr==1){if(a[y][x+1]==0)
{x++;num++;a[y][x]=num;}else{ctr=2;}}if(ctr==2){if(a[y+1][x]==0){y++;num++;a[y][x]=num;}else{ctr=3;}}if(ctr==3){if(a[y][x-1]==0){x--;num++;a[y][x]=num;}else{ctr=4;}}if(ctr==4){if(a[y-1][x]==0){y--;num++;a[y][x]=num;}else{ctr=1;}}
if(num==n*n)
break;}//whilefor(int py=1;py<=n;py++){
for(int px=1;px<=n;px++)
{printf("%d ",a[py][px]);
}if(py!=n)printf("\n");} return 0;
}//main
该代码看着很长,但是逻辑也很简单,通过初始位置开始计算,然后初始向右开始移动访问赋值,直到遇到提前设定好的边界,然后转换指定方向( → ↓ ← ↑ )遇到边界或者已经赋值过的位置都会改变方向,如此便形成了一个螺旋的效果。
但是该带码稍微更改一下在洛谷平台该题是通过不了的,因为时间限制
洛谷平台上的 普及 螺旋矩阵
如果再用该代码,去强行通过,那么就会看到时间超时的警告
由于该算法逻辑比较简单,确实比较浪费时间,而且也没办法在该基础上优化,所以不得不更换算法。
我们尝试用数学规律来解析螺旋矩阵
首先是螺旋矩阵的构造规律
螺旋矩阵是按照顺时针螺旋顺序填充数字的,我们可以将一个 n x n
的螺旋矩阵看作是由多层嵌套的 “框” 组成。最外层是一个框,去掉最外层后,
里面又会形成一个新的 (n - 2) x (n - 2)
的螺旋矩阵,以此类推。
1. 第一行(i == 1
)
当元素位于第一行时,数字是从左到右依次递增填充的。例如,在一个 4 阶螺旋矩阵中,第一行的元素依次是 1, 2, 3, 4
。所以,对于第一行的元素,
其值就等于它所在的列号 j
,即 if(i == 1) return j;
。
2. 最后一列(j == n
)
在最后一列,数字是从上到下依次递增的。在第一行最后一列的元素是 n
,之后每往下一行,元素值就增加 1。所以,对于最后一列的元素,
其值为 n + i - 1
,即 if(j == n) return n + i - 1;
。
3. 最后一行(i == n
)
在最后一行,数字是从右到左依次递增的。在最后一列最后一行的元素是 2 * n - 1
,之后每往左一列,元素值就增加 1。所以,对于最后一行的元素,
其值为 3 * n - j - 1
,即 if(i == n) return 3 * n - j - 1;
。
4. 第一列(j == 1
)
在第一列,数字是从下到上依次递增的。在最后一行第一列的元素是 3 * n - 2
,之后每往上一行,元素值就增加 1。所以,对于第一列的元素,
其值为 4 * n - i - 2
,即 if(j == 1) return 4 * n - i - 2;
。
然后采用递归
当元素不在矩阵的边界上时,我们可以把矩阵缩小为去掉最外层后的子矩阵。去掉最外层后,新的矩阵阶数变为 n - 2
,同时要查找的元素在新矩阵中的行号变为 i - 1
,列号变为 j - 1
。
最外层元素的数量可以通过计算得到。最外层的四条边,每条边有 n
个元素,但四个角的元素会被重复计算一次,所以最外层元素的总数为 4 * n - 4
,即 4 * (n - 1)
。
因此,对于不在边界上的元素,其值等于去掉最外层后的子矩阵中对应位置元素的值加上最外层元素的数量,即 return a(n - 2, i - 1, j - 1) + 4 * (n - 1);
。
举个例子,孩子们
以一个 4 阶螺旋矩阵为例:
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7
假设我们要查找第二行第二列的元素(值为 13)。
- 初始时,
n = 4
,i = 2
,j = 2
,元素不在边界上,进入递归调用。 - 去掉最外层后,新的矩阵阶数
n' = 2
,在新矩阵中,i' = 1
,j' = 1
。 - 最外层元素的数量为
4 * (4 - 1) = 12
。 - 对于新矩阵中第一行第一列的元素,根据边界情况,其值为 1。
- 所以,原矩阵中第二行第二列的元素值为
1 + 12 = 13
。
通过这种递归的方式,我们可以根据元素在矩阵中的位置计算其在螺旋矩阵中的值
用数学算法优化过后,我们就得到了一个特别简便的代码
#include <stdio.h>
int x, y, z;
int a(int n, int i, int j) {if (i == 1)return j;if (j == n)return n + i - 1;if (i == n)return 3 * n - j - 1;if (j == 1)return 4 * n - i - 2;return a(n - 2, i - 1, j - 1) + 4 * (n - 1);
}
int main() {scanf("%d %d %d", &x, &y, &z);printf("%d", a(x, y, z));return 0;
}
这下就舒服多了。每每感叹数学对于编程算法的重要性。
当我问AI数学与编程的关系时,它只回了简短一句。