六边形网格算法

时间:2017-09-03 09:36:51

标签: c++ algorithm

六角形网格由具有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;
}

4 个答案:

答案 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)