yutasの競技プログラミング勉強帖

競技プログラミングの問題についての解説記事を主に書いています。

D - Loong and Takahashi / AtCoder Beginner Contest 335(Sponsored by Mynavi)

問題

 n 行横  n 列のマスからなるグリッドがあり、上から  i 行目、左から  j 列目のマスをマス  (i, \, j) と表す。 また、  n 45 以下の奇数である。

このグリッドに、以下の条件を満たすように高橋君と  1 から  {n}^{2} - 1 までの番号がついた  {n}^{2} - 1 個のパーツからなる1匹の龍を配置する。

  • 高橋君はグリッドの中央、すなわちマス  \displaystyle \left(\frac{n+1}{2}, \, \frac{n+1}{2} \right) に配置しなければならない。
  • それ以外のマスには龍のパーツをちょうど1つ配置しなければならない。
  •  2 \leq x \leq {n}^{2} - 1 を満たすすべての整数  x について、龍のパーツ  x はパーツ  x - 1 があるマスと辺で隣接するマスに配置しなければならない。
    •  (i, \, j) (k, \, l) が辺で隣接するとは、  | i - k | + | j - l | = 1 であることを意味する。

このとき、条件を満たす配置方法を1つ出力せよ。

入力

1行に、整数  n が与えられる。

条件

  • 実行時間制限: 2s
  • メモリ制限: 1024MB
  •  3 \leq n \leq 45
  •  n は奇数である。

出力

 n 行に渡り、  {X}_{i, \, j} をマス  (i, \, j) に高橋君を配置するとき T、パーツ  x を配置するときに  x としたら、  i 行目に  {X}_{i, \, 1}, \, \cdots , \, {X}_{i, \, n} を空白区切りで出力すること。

解法

グリッドの中央のマスに高橋君が配置されるので、最終的には文字 T は以下の図のような位置に配置されます。

残りの空いているマスについては、以下のように龍のパーツを配置することで、条件を満たしながら龍を配置することができると言えます。

図のような渦巻き状に龍を配置するには、「下 → 右 → 上 → 左 → 下 → …」という順番で、「次のマスにパーツを配置していない限り、その方向に進み続ける」ということを繰り返して、中央のマスに辿り着くまで行うことによって達成できます。

具体的には、マス  (1, \, 1) にパーツ  1 を配置し、

  • グリッドの境界か、既にパーツが配置されているマスに到達する直前まで下方向(  (+1, \, 0) の方向)に動き、パーツを配置し続ける
  • グリッドの境界か、既にパーツが配置されているマスに到達する直前まで右方向(  (0, \, +1) の方向)に動き、パーツを配置し続ける
  • グリッドの境界か、既にパーツが配置されているマスに到達する直前まで上方向(  (-1, \, 0) の方向)に動き、パーツを配置し続ける
  • グリッドの境界か、既にパーツが配置されているマスに到達する直前まで左方向(  (0, \, -1) の方向)に動き、パーツを配置し続ける
  • グリッドの境界か、既にパーツが配置されているマスに到達する直前まで下方向(  (+1, \, 0) の方向)に動き、パーツを配置し続ける
  •  \cdots \cdots

という手順で、最終的に  {n}^{2} - 1 のパーツを配置するまでこれを繰り返せば問題を解くことができます。

ソースコード

main() 関数の中に、答えを出力する部分を直接実装しました。

int n, ans[50][50]; // ans はどのパーツが配置されたかを記録する
bool visited[50][50]; // visited はマス (i, j) にパーツを配置したかどうかを表す
const int di[4] = {1, 0, -1, 0}, dj[4] = {0, 1, 0, -1}; // di, dj はそれぞれ下、右、上、左方向への i, j の値の変分を表す

int main() {
  cin >> n; // n の値の入力

  visited[(n + 1) / 2][(n + 1) / 2] = true; // グリッドの中央は既に訪問済として記録する

  int cnt = 1, i = 1, j = 1, k = 0; // cnt は次に置くパーツの番号、 i, j は現在のマスの座標、 k はどの方向を現在表しているのかを表す

  // パーツ n ^ 2 - 1 が配置されるまで、パーツの配置を繰り返す
  while (cnt <= n * n - 1) {
    visited[i][j] = true; // マス (i, j) を訪問済にする
    ans[i][j] = cnt; // マス (i, j) にパーツ cnt を配置する
    cnt++; // cnt の値を 1 増やす

    // 次のマスが (i + di[k], j + dj[k]) であることを利用して、マスの移動を行う
    if (1 <= i + di[k] && i + di[k] <= n && 1 <= j + dj[k] && j + dj[k] <= n &&
        !visited[i + di[k]][j + dj[k]]) {
      // 次のマスが未訪問でグリッド内に収まっていれば、そのまま次のマスに移動する
      i += di[k];
      j += dj[k];
    } else {
      // 次のマスが上記の条件を満たしていない場合は、方向を変える
      k = (k + 1) % 4; // k の値を 1 増やすことにより、次の方向に切り替えられる
      i += di[k];
      j += dj[k];
    }
  }

  // 答えの出力
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j++) {
      if (ans[i][j] == 0) {
        // 中央のマスのみは ans[i][j] = 0 となっているため、 T を出力する
        cout << "T";
      } else {
        // そうでないマスは ans[i][j] をそのまま出力する
        cout << ans[i][j];
      }

      if (j == n) {
        cout << endl;
      } else {
        cout << " ";
      }
    }
  }

  return 0;
}