题目如下:
题目描述
现有 2n×2n(n≤10)2n×2n(n≤10) 名作弊者站成一个正方形方阵等候 kkksc03 的发落。kkksc03 决定赦免一些作弊者。他将正方形矩阵均分为 4 个更小的正方形矩阵,每个更小的矩阵的边长是原矩阵的一半。其中左上角那一个矩阵的所有作弊者都将得到赦免,剩下 3 个小矩阵中,每一个矩阵继续分为 4 个更小的矩阵,然后通过同样的方式赦免作弊者……直到矩阵无法再分下去为止。所有没有被赦免的作弊者都将被处以棕名处罚。
给出 nn,请输出每名作弊者的命运,其中 0 代表被赦免,1 代表不被赦免。
输入格式
一个整数 nn。
输出格式
2n×2n2n×2n 的 01 矩阵,代表每个人是否被赦免。数字之间有一个空格。
输入输出样例
输入 #1
3
输出 #1
0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 1 0 1 0 0 0 0 1 1 1 1 0 0 0 1 0 0 0 1 0 0 1 1 0 0 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1 1 1
题目分析&解答:
不难看出,这道题的0 1排布很有规律。
我们将首先定义一个数组,这个数组就是我们最后要打印的数组:
bool b[1025][1025] {};
//编程细节:对数组的部分元素初始化后,未被初始化的元素将会被默认设置为0,这里因为未对任何元素赋值,所以全部的元素都被初始化为0。
由于题目中给出n≤10,故数组元素最多只有1024个,为了能直观展现规律,我们从1开始计数,故为1025。
先暂时不谈中间的计算,由于题目要求我们对于未被赦免的罪犯输出1,这意味着我们要么先对数组的所有值初始化为1,要么在最后打印的时候对数组元素使用!运算符,这里我们选择的是后者。
然后开始分析,观察样例,不难看出0所形成的方块(以下简称0-0块)之间存在明显的关系,且题目所描述的赦免过程,可以用“母体块蔓延”的形式来表述,如图。
以n = 3时为例,在左上角形成了最初的0-0块,称之为母体块。母体块随后向右、下和右下蔓延,又形成了三个小的0-0块,称为二代母体块。然后每一个二代母体块又向其右、下和右下蔓延,形成三代母体块,然后结束蔓延。
不难看出,母体块蔓延存在一定的规律:
1.每一个母体的边长均为上一代母体的一半。
2.每一个母体块至少有一点与上一代母体块相邻。
由规律一可以推出:只有当前母体的边长可二分时,它才能蔓延。
所以,可以在用程序模拟“母体块蔓延”的过程,然后将被母体块蔓延的部分赋值为true即可。
由此,算法的理论方面已被我们完全分析出来,接下来就是实现部分。
对于母体块的坐标,我们考虑用结构来表示:
struct pos
{
int x;
int y;
};
对于蔓延的过程,我们用一个函数来实现:
void change(pos start,int n);
其中注释部分是调试语句,用以确认函数是否正确表达了蔓延过程。
由于此时不需要向调用函数传递信息,故将返回值声明为void。这里对于n进行简单的描述,因为程序需要知道应该在什么时候停止蔓延,所以传递向其传递整型 n 来表示蔓延的进度。
这个函数应该包含三个部分:
1.确定下一代母体的坐标
2.对下一代母体所覆盖的位置赋值
3.判断是否要进行蔓延,若是,则进行下一次蔓延
在蔓延过程完毕后,就是用for循环打印数组了。
最终代码如下:
#include <iostream>
#include <cmath>
using namespace std;
bool b[1025][1025] {};
struct pos
{
int x;
int y;
};
void change(pos start,int n);
int main()
{
int n;
cin >> n;
int s = pow(2,n);
pos start = {1,1};
for (int i = 1,a = s / 2;i <= a;++i){
for (int j = 1; j <= a;++j) {
b[i][j] = true;
//cout << "(" << j << "," << i << ") has been set true! ";
}
//cout << endl;
}
if (n > 1)
change(start,n);
int k = s - 1;
for (int i = 1;i <= s;++i)
{
for (int j = 1; j <= k;++j)
cout << !b[i][j] << " ";
cout << !b[i][s] << endl;
}
return 0;
}
void change(pos start,int n)
{
int w = n - 1;
int s = n - 2;
pos start1 {start.x + static_cast<int>(pow(2,w)),start.y};
pos start2 {start.x,start.y + static_cast<int>(pow(2,w))};
pos start3 {start1.x,start2.y};
pos end1 {start1.x + static_cast<int>(pow(2,s)) - 1,start1.y + static_cast<int>(pow(2,s)) - 1};
pos end2 {start2.x + static_cast<int>(pow(2,s)) - 1,start2.y + static_cast<int>(pow(2,s)) - 1};
pos end3 {start3.x + static_cast<int>(pow(2,s)) - 1,start3.y + static_cast<int>(pow(2,s)) - 1};
//cout << "right_up square of " << w << ": (" << start1.x << "," << start1.y << ") to (" << end1.x << "," << end1.y << ")" << endl;
//cout << "left_down square of " << w << ": (" << start2.x << "," << start2.y << ") to (" << end2.x << "," << end2.y << ")" << endl;
//cout << "middle square of " << w << ": (" << start3.x << "," << start3.y << ") to (" << end3.x << "," << end3.y << ")" << endl << endl;
for (int i = start1.y;i<=end1.y;++i) {
for (int j = start1.x; j <= end1.x; ++j) {
b[i][j] = true;
//cout << "(" << j << "," << i << ") has been set true! ";
}
//cout << endl;
}
for (int i = start2.y;i<=end2.y;++i) {
for (int j = start2.x; j <= end2.x; ++j){
b[i][j] = true;
//cout << "(" << j << "," << i << ") has been set true! ";
}
//cout << endl;
}
for (int i = start3.y;i<=end3.y;++i){
for (int j = start3.x;j<=end3.x;++j){
b[i][j] = true;
//cout << "(" << j << "," << i << ") has been set true! ";
}
//cout << endl;
}
if (s)
{
change(start1,w);
change(start2,w);
change(start3,w);
}
}
现在对上述程序中二个可能比较令人费解的点进行说明。
首先,下面这段代码乍一看感觉摸不着头脑,但仔细分析便能轻易明白他的作用。
int w = n - 1;
int s = n - 2;
pos start1 {start.x + static_cast<int>(pow(2,w)),start.y};
pos start2 {start.x,start.y + static_cast<int>(pow(2,w))};
pos start3 {start1.x,start2.y};
pos end1 {start1.x + static_cast<int>(pow(2,s)) - 1,start1.y + static_cast<int>(pow(2,s)) - 1};
pos end2 {start2.x + static_cast<int>(pow(2,s)) - 1,start2.y + static_cast<int>(pow(2,s)) - 1};
pos end3 {start3.x + static_cast<int>(pow(2,s)) - 1,start3.y + static_cast<int>(pow(2,s)) - 1};
这段可以不声明新的变量,这里这么做是为了让程序尽可能的清晰。start1,start2,start3分别代指这一次蔓延形成的右、下、右下母体块的左上角的坐标。之所以选择左上角是因为整体的蔓延趋势是自左上至右下的,选择左上作为起始坐标有助于让程序的逻辑清晰。而end1,end2,end3则分别代指这一次蔓延形成的右、下、右下母体块的左上角的坐标。只要知道母体块的起始坐标和当前蔓延的进程,就可以推算出蔓延出的母体块的边长与起始坐标,方法显而易见。
if (s)
{
change(start1,w);
change(start2,w);
change(start3,w);
}
对于编程基础牢固的人来说,这段是非常明了的。但是仍会有一些人感到迷惑:s如何作为判别式?对于这种情况,程序会进行隐式类型转换,将所有非零值转化为true,将 0 单独转化为false
结语:
对于这道题,似乎可以通过位运算来判定每一个位置的值是1还是0,但是考虑到时间可能会非常长,所以没有选择它,感兴趣的读者可以自行尝试~