最近在刷算法题时遇到了一个有趣的矩阵旋转问题,题目要求实现对一个二维矩阵中指定子矩阵的顺时针或逆时针90度旋转。这类问题在实际开发中其实有不少应用场景,比如图像处理中的局部旋转、游戏开发中的地图变换等。
题目描述可以这样理解:给定一个n×n的方阵,需要进行m次操作。每次操作会指定一个中心点(x,y)和半径r,形成一个(2r+1)×(2r+1)的子矩阵,然后根据参数z的值(0或1)决定是顺时针还是逆时针旋转这个子矩阵90度。
注意:这里的旋转半径r决定了子矩阵的大小。比如r=1时,子矩阵是3×3的;r=2时,子矩阵是5×5的,以此类推。
在C++中,我们通常用二维数组来表示矩阵。题目中使用了两个全局数组arr和brr,大小都是501×501,这足够应付大多数算法题中的矩阵大小要求。
初始化部分很简单,就是按行优先顺序依次填充数字1到n²。这种初始化方式在调试时很有帮助,因为可以清楚地看到每个元素的位置变化。
cpp复制for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
arr[i][j] = k;
k++;
}
}
旋转操作是本题的核心难点。我们需要理解如何将一个子矩阵旋转90度。这里的关键在于找到旋转前后坐标的对应关系。
对于顺时针旋转90度,坐标变换公式为:
code复制新x = 中心x - (原y - 中心y)
新y = 中心y + (原x - 中心x)
而对于逆时针旋转90度,坐标变换公式为:
code复制新x = 中心x + (原y - 中心y)
新y = 中心y - (原x - 中心x)
在代码中,这个变换是这样实现的:
cpp复制// 顺时针旋转
brr[x - (y - j)][y + (i - x)] = arr[i][j];
// 逆时针旋转
brr[x + (y - j)][y - (i - x)] = arr[i][j];
实现旋转时需要注意几个关键点:
代码中的双重循环限定了子矩阵的范围:
cpp复制for (int i = x - r; i <= x + r; i++)
for (int j = y - r; j <= y + r; j++)
理解旋转操作的关键是将绝对坐标转换为相对于中心点的相对坐标。设中心点为(a,b),任意一点(x,y)的相对坐标为:
code复制dx = x - a
dy = y - b
旋转90度的变换在数学上可以表示为矩阵乘法。对于顺时针旋转:
code复制[ 0 1 ]
[-1 0 ]
所以新的相对坐标为:
code复制new_dx = dy
new_dy = -dx
然后转换回绝对坐标:
code复制new_x = a + new_dx = a + dy = a + (y - b)
new_y = b + new_dy = b - dx = b - (x - a)
这与代码中的实现完全一致。
同理,逆时针旋转的变换矩阵是:
code复制[ 0 -1 ]
[ 1 0 ]
所以新的相对坐标为:
code复制new_dx = -dy
new_dy = dx
转换回绝对坐标:
code复制new_x = a + new_dx = a - dy = a - (y - b)
new_y = b + new_dy = b + dx = b + (x - a)
cpp复制int arr[501][501]; // 原始矩阵
int brr[501][501]; // 临时矩阵
int n, m; // 矩阵大小和操作次数
cin >> n >> m;
这里定义了足够大的矩阵空间,可以处理最大500×500的矩阵(因为数组下标从1开始使用)。
cpp复制int k = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
arr[i][j] = k++;
}
}
这种初始化方式使得每个元素的值等于其线性索引,方便调试时观察旋转效果。
cpp复制for (int i = 1; i <= m; i++) {
cin >> x >> y >> r >> z;
if (z == 0) { // 顺时针旋转
for (int i = x - r; i <= x + r; i++)
for (int j = y - r; j <= y + r; j++)
brr[x - (y - j)][y + (i - x)] = arr[i][j];
} else { // 逆时针旋转
for (int i = x - r; i <= x + r; i++)
for (int j = y - r; j <= y + r; j++)
brr[x + (y - j)][y - (i - x)] = arr[i][j];
}
// 将旋转结果复制回原矩阵
for (int i = x - r; i <= x + r; i++)
for (int j = y - r; j <= y + r; j++)
arr[i][j] = brr[i][j];
}
cpp复制for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++)
cout << arr[i][j] << " ";
cout << endl;
}
在实际编码中,最容易出错的就是边界条件的处理。需要注意:
虽然这个解法的时间复杂度是O(m×(2r+1)²),对于算法题来说通常足够,但在实际应用中可能需要考虑:
这个问题的解法可以扩展到其他角度的旋转,比如180度或270度。180度旋转相当于连续两次90度旋转,270度旋转相当于三次90度旋转。
当前的实现使用了额外的临时矩阵,实际上可以实现原地旋转,只需要更复杂的坐标变换和交换顺序。这对于内存敏感的应用场景很有价值。
类似的思路可以扩展到三维空间的旋转,虽然坐标变换会更复杂,但基本原理是相通的。这在计算机图形学中有广泛应用。
理解矩阵旋转在实际中有很多应用:
在实现这个算法时,我最初犯了一个常见错误:直接在原矩阵上进行旋转操作,导致部分数据被覆盖。后来意识到需要使用临时矩阵来存储中间结果。这个经验告诉我,在处理这类原地修改的问题时,一定要仔细考虑数据依赖关系。
另一个收获是理解了旋转操作的数学本质。通过将问题分解为相对坐标变换和绝对坐标恢复两个步骤,大大简化了思考过程。这种"分解-变换-恢复"的思路在很多几何变换问题中都适用。
最后,我发现用小的测试案例手动验证算法特别有效。比如用一个3×3的矩阵,手动计算旋转后的结果,再与程序输出对比,可以快速发现逻辑错误。