六角形网格由具有R行和C列的二维数组表示。第一行总是在六边形网格结构中“前”第二行(见下图)。设k为转数。每次转弯时,网格的一个元素是1,当且仅当该元素的前一个转弯为1的邻居数是奇数时。编写在k转后输出网格的C ++代码。
限制:
1&lt; = R&lt; = 10,1 <= C <= 10,1 <= k <= 2 ^(63)-1
输入的示例(在第一行中是R,C和k,然后是起始网格):
4 4 3
0 0 0 0
0 0 0 0
0 0 1 0
0 0 0 0
模拟:image,黄色元素代表'1',空白代表'0'。
如果我每回合模拟并生成一个网格,这个问题很容易解决,但是如果k足够大则变得太慢了。什么是更快的解决方案?
编辑:代码(使用n和m代替R和C):
#include <cstdio>
#include <cstring>
using namespace std;
int old[11][11];
int _new[11][11];
int n, m;
long long int k;
int main() {
scanf ("%d %d %lld", &n, &m, &k);
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) scanf ("%d", &old[i][j]);
}
printf ("\n");
while (k) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
int count = 0;
if (i % 2 == 0) {
if (i) {
if (j) count += old[i-1][j-1];
count += old[i-1][j];
}
if (j) count += (old[i][j-1]);
if (j < m-1) count += (old[i][j+1]);
if (i < n-1) {
if (j) count += old[i+1][j-1];
count += old[i+1][j];
}
}
else {
if (i) {
if (j < m-1) count += old[i-1][j+1];
count += old[i-1][j];
}
if (j) count += old[i][j-1];
if (j < m-1) count += old[i][j+1];
if (i < n-1) {
if (j < m-1) count += old[i+1][j+1];
count += old[i+1][j];
}
}
if (count % 2) _new[i][j] = 1;
else _new[i][j] = 0;
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) old[i][j] = _new[i][j];
}
k--;
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
printf ("%d", old[i][j]);
}
printf ("\n");
}
return 0;
}
答案 0 :(得分:3)
对于给定的 R 和 C ,您有 N = R * C 单元格。
如果您将这些单元格表示为 GF(2)中元素的向量,即 0 s和 1 s,其中算术是执行mod 2(加法是 XOR ,乘法是 AND ),然后从一个回合到下一回合的转换可以用 N * N 矩阵 M ,以便:
转[i + 1] = M *转[i]
您可以对矩阵取幂,以确定单元格如何在 k 转弯时进行转换:
转[i + k] =(M ^ k)*转[i]
即使 k 非常大,如 2 ^ 63-1 ,您也可以通过求平方式快速计算 M ^ k : https://en.wikipedia.org/wiki/Exponentiation_by_squaring这只需 O(log(k))矩阵乘法。
然后,您可以将初始状态乘以矩阵以获得输出状态。
根据 R , C , k 的限制以及问题中给出的时间,很明显这是你应该提出的解决方案。
答案 1 :(得分:2)
有几种方法可以加快算法速度。
您可以在每个回合中使用越界检查进行邻居计算。做一些预处理并在开始时计算每个单元的邻居。 (Aziuth已经提出过。)
然后你不需要计算所有细胞的邻居。如果奇数个相邻单元在最后一个转弯处打开,则每个单元都打开,否则它将关闭。
您可以这样想:从干净的电路板开始。对于上一次移动的每个活动单元格,切换所有周围单元格的状态。当偶数个邻居导致切换时,单元格开启,否则切换相互抵消。看看你的例子的第一步。这就像玩Lights Out,真的。
如果电路板只有很少的活动单元,这种方法比计算邻居更快,最糟糕的情况是单元全部打开的电路板,在这种情况下,它与邻居计数一样好,因为你必须触摸每个每个细胞的邻居。
下一个逻辑步骤是将电路板表示为一个位序列,因为位已经有一种自然的切换方式,即独占或者xor oerator ^
。如果您将每个单元格的neigbours列表保留为位掩码m
,则可以通过b
切换板b ^= m
。
这些是可以对算法进行的改进。最大的改进是注意到模式最终会重复出现。 (切换的熊与Conway's Game of Life相似,其中也有重复模式。)此外,给定的最大可能迭代次数,2⁶3可疑量很大。
游戏板很小。您的问题中的示例将至少在2⁶转后重复,因为4×4板最多可以有2¹⁶布局。在实践中,转弯127到达原始后的第一个移动的环形图案,并从那时开始循环126周期。
较大的电路板可能具有高达2¹的布局,因此它们可能不会在2⁶³转弯内重复。 10×10板在中间附近有一个活动单元,其周期为2,162,622。正如Aziuth所暗示的那样,这可能确实是数学研究的主题,但我们将用亵渎的手段来解决它:保留所有先前状态的哈希图以及它们发生的转弯,然后检查模式是否在每个状态之前发生过转动。
我们现在有:
这是我的尝试:
#include <iostream>
#include <map>
/*
* Bit representation of a playing board, at most 10 x 10
*/
struct Grid {
unsigned char data[16];
Grid() : data() {
}
void add(size_t i, size_t j) {
size_t k = 10 * i + j;
data[k / 8] |= 1u << (k % 8);
}
void flip(const Grid &mask) {
size_t n = 13;
while (n--) data[n] ^= mask.data[n];
}
bool ison(size_t i, size_t j) const {
size_t k = 10 * i + j;
return ((data[k / 8] & (1u << (k % 8))) != 0);
}
bool operator<(const Grid &other) const {
size_t n = 13;
while (n--) {
if (data[n] > other.data[n]) return true;
if (data[n] < other.data[n]) return false;
}
return false;
}
void dump(size_t n, size_t m) const {
for (size_t i = 0; i < n; i++) {
for (size_t j = 0; j < m; j++) {
std::cout << (ison(i, j) ? 1 : 0);
}
std::cout << '\n';
}
std::cout << '\n';
}
};
int main()
{
size_t n, m, k;
std::cin >> n >> m >> k;
Grid grid;
Grid mask[10][10];
for (size_t i = 0; i < n; i++) {
for (size_t j = 0; j < m; j++) {
int x;
std::cin >> x;
if (x) grid.add(i, j);
}
}
for (size_t i = 0; i < n; i++) {
for (size_t j = 0; j < m; j++) {
Grid &mm = mask[i][j];
if (i % 2 == 0) {
if (i) {
if (j) mm.add(i - 1, j - 1);
mm.add(i - 1, j);
}
if (j) mm.add(i, j - 1);
if (j < m - 1) mm.add(i, j + 1);
if (i < n - 1) {
if (j) mm.add(i + 1, j - 1);
mm.add(i + 1, j);
}
} else {
if (i) {
if (j < m - 1) mm.add(i - 1, j + 1);
mm.add(i - 1, j);
}
if (j) mm.add(i, j - 1);
if (j < m - 1) mm.add(i, j + 1);
if (i < n - 1) {
if (j < m - 1) mm.add(i + 1, j + 1);
mm.add(i + 1, j);
}
}
}
}
std::map<Grid, size_t> prev;
std::map<size_t, Grid> pattern;
for (size_t turn = 0; turn < k; turn++) {
Grid next;
std::map<Grid, size_t>::const_iterator it = prev.find(grid);
if (1 && it != prev.end()) {
size_t start = it->second;
size_t period = turn - start;
size_t index = (k - turn) % period;
grid = pattern[start + index];
break;
}
prev[grid] = turn;
pattern[turn] = grid;
for (size_t i = 0; i < n; i++) {
for (size_t j = 0; j < m; j++) {
if (grid.ison(i, j)) next.flip(mask[i][j]);
}
}
grid = next;
}
for (size_t i = 0; i < n; i++) {
for (size_t j = 0; j < m; j++) {
std::cout << (grid.ison(i, j) ? 1 : 0);
}
std::cout << '\n';
}
return 0;
}
可能还有改进的余地。特别是,我不太确定如何为大板买单。 (上面的代码使用有序的映射。我们不需要订单,因此使用无序映射将产生更快的代码。上面的示例在10×10板上具有单个活动单元花费的时间明显长于有序的第二个地图。)
答案 2 :(得分:0)
不确定你是怎么做到的 - 你应该总是在这里发布代码 - 但是我们试着在这里优化一些东西。
首先,它与二次网格之间并没有真正的区别。不同的邻居关系,但我的意思是,这只是一个小的翻译功能。如果你有问题,我们应该单独处理,也许在CodeReview上。
现在,天真的解决方案是:
for all fields
count neighbors
if odd: add a marker to update to one, else to zero
for all fields
update all fields by marker of former step
这显然是在O(N)中。迭代两次是实际运行时间的两倍,但不应该那么糟糕。尽量不要在每次执行此操作时分配空间,而是重用现有结构。
我建议这个解决方案:
at the start:
create a std::vector or std::list "activated" of pointers to all fields that are activated
each iteration:
create a vector "new_activated"
for all items in activated
count neighbors, if odd add to new_activated
for all items in activated
set to inactive
replace activated by new_activated*
for all items in activated
set to active
*这可以通过将它们放入智能指针并使用移动语义
来有效地完成此代码仅适用于激活的字段。只要他们留在一些较小的区域内,这就更有效率了。但是,我不知道这种情况何时发生变化 - 如果整个地方都有激活的字段,这可能 有效。在这种情况下,天真的解决方案可能是最好的解决方案。
编辑:在您发布代码之后......您的代码非常具有程序性。这是C ++,使用类并使用事物的表示。可能你正确地搜索邻居,但你很容易在那里犯错,因此应该在函数或更好的方法中隔离该部分。原始数组很糟糕,像n或k这样的变量很糟糕。但在我开始撕开你的代码之前,我反复重复我的建议,将代码放在CodeReview上,让人们拆开它直到完美。
答案 3 :(得分:0)
这开头是一个评论,但我认为除了已经陈述的内容之外,它还可以作为答案。
您说明了以下限制:
1 <= R <= 10, 1 <= C <= 10
鉴于这些限制,我将自由地在恒定空间(即M
)中表示R
行和C
列的网格/矩阵O(1)
,并在O(1)
而不是O(R*C)
时间检查其元素,从而将此部分从我们的时间复杂度分析中删除。
也就是说,网格可以简单地声明为bool grid[10][10];
。
键输入是大量转弯k
,声明在以下范围内:
1 <= k <= 2^(63) - 1
问题是,AFAIK,你需要来执行k
次转弯。这使得算法位于O(k)
。因此,没有提出的解决方案可以比O(k)
[1]做得更好。
要以有意义的方式提高速度,必须以某种方式降低此上限[1],但看起来如果不改变问题约束就无法做到这一点。
因此,没有提出的解决方案可以比O(k)
[1]做得更好。
k
可能如此之大的事实是主要问题。大多数人可以做的是改进其余的实现,但这只会通过常量因子来改善;无论如何看待它,你都必须经过k
轮次。
因此,除非找到一些允许降低此限制的聪明事实和/或细节,否则别无选择。
[1]例如,与尝试确定某个数字n
是否为素数不同,您可以在range(2, n)
中检查所有数字是否为n
,将其设为O(n)
进程,或者注意到某些改进仅包括在检查n
不均匀后查看奇数数字(常数因子;仍为O(n)
),然后检查奇数到√n
,即range(3, √n, 2)
,这有意义地将上限降低到O(√n)
。