六边形网格上的算法

Algorithm on hexagonal grid

本文关键字:算法 网格 六边形      更新时间:2023-10-16

六边形网格由一个包含 R 行和 C 列的二维数组表示。在六边形网格结构中,第一行总是"之前"第二(见下图)。设 k 为圈数。每转一圈,网格的一个元素为 1,当且仅当该元素的相邻元素在前一圈为 1 时为奇数。编写C++代码,在 k 圈后输出网格。

局限性:

1 <= R <= 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

模拟:图像,黄色元素代表"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;
}

对于给定的R和 C,你有N=R*C单元格。

如果将这些单元格表示为 GF(2) 中的元素向量,即0s 和1s,其中算术执行 mod2(法是 XOR,乘法是AND),那么从一个匝到下一个匝的变换可以用 N*N矩阵M表示,因此:

转[i+1] = M*转[i]

您可以对矩阵进行指数运算,以确定单元格在k圈上的变换方式:

turn[i+k] = (M^k)*turn[i]

即使 k 非常大,如 2^63-1,您也可以通过平方使用幂快速计算M^k: https://en.wikipedia.org/wiki/Exponentiation_by_squaring 这只需要O(log(k))矩阵乘法。

然后,您可以将初始状态乘以矩阵以获得输出状态。

从你问题中给出的R,C,k和时间的限制来看,很明显这是你应该想出的解决方案。

有几种方法可以加快算法速度。

你做邻居计算,每个回合都会检查越界。做一些预处理,并在开始时计算每个像元的相邻项。(Aziuth已经提出了这个建议。

这样,您就不需要计算所有单元格的邻居。如果在上一轮中打开了奇数个相邻单元格,则每个单元格都处于打开状态,否则将关闭。

你可以换个角度思考:从一块干净的板开始。对于上一个移动的每个活动单元格,切换周围所有单元格的状态。当偶数个邻居导致切换时,单元处于打开状态,否则切换会相互抵消。查看示例的第一步。这就像玩《熄灯》一样,真的。

如果电路板只有几个活动单元,并且其最坏的情况是电池全部打开的电路板,则此方法比计算邻居更快,在这种情况下,它与相邻计数一样好,因为您必须触摸每个单元格的每个邻居。

下一个合乎逻辑的步骤是将电路板表示为一系列位,因为位已经具有自然的切换方式,即独占或或异或oerator^。如果将每个单元格的邻域列表保留为位掩码m,则可以通过b ^= m切换电路板b

这些是可以对算法进行的改进。最大的改进是注意到模式最终会重复。(切换与康威的生命游戏相似,其中也有重复的模式。此外,给定的最大可能迭代次数 2⁶³ 大得可疑。

游戏板很小。您问题中的示例将至少在 2¹⁶ 转后重复,因为 4×4 板最多可以有 2¹⁶ 布局。在实践中,转弯 127 到达原始移动后第一个移动的环形模式,并从那时开始以 126 的周期循环。

较大的电路板可能具有多达 2¹⁰⁰ 的布局,因此它们可能不会在 2¶³ 圈内重复。10×10 板的中间附近有一个活动单元,AR 周期为 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 板上有一个活动单元格,使用有序映射花费的时间明显超过一秒钟。

不确定你是如何做到的 - 你真的应该总是在这里发布代码 - 但让我们尝试在这里优化一些东西。

首先,它和二次网格之间并没有真正的区别。不同的邻居关系,但我的意思是,这只是一个小的翻译函数。如果你在那里有问题,我们应该单独处理,也许在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上,让人们把它拆开,直到它完美为止。

这最初是作为评论开始的,但我认为除了已经陈述的内容之外,作为答案可能会有所帮助。

您陈述了以下限制:

1 <= R <= 10, 1 <= C <= 10

鉴于这些限制,我将冒昧地表示常量空间中R行和C列的网格/矩阵M(即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)