///////////////////////
// 8Puzzle.cs
// Copyright (c) 2025
// by Ysama
///////////////////////
using System;
namespace Eight_Puzzle //解説:namespaceの最初の文字に数字は許されないので。
{
public class App
{
static void Main()
{
EightPuzzle ep = new EightPuzzle();
Console.WriteLine(">>> 開始時の盤面 <<<");
ep.ShowStartBoard();
Console.Write(Environment.NewLine);
ep.Start();
Console.ReadLine();
}
}
//3 x 3 盤面クラス
public class NBoard
{ //メンバーフィールド
const int Num = 9; //ピース数
private int [] Pieces; //8パズルの9ピース配列
NBoard next = null; //子NBoardの盤面
NBoard back = null; //親NBoardの盤面
//メンバープロパティ
public NBoard Next //nextのプロパティ
{
get {return next;}
set {next = value;}
}
public NBoard Back //backのプロパティ
{
get {return back;}
set {back = value;}
}
public int[] ptPieces //Piecesのプロパティ
{
get {return Pieces;}
}
//メンバーメソッド
public NBoard() //コンストラクター
{
Pieces = new int [Num];
for(int i = 0; i < Num; i++)
Pieces[i] = i;
}
public NBoard(NBoard bd) //コピーコンストラクター
{
if(bd == null)
return; //何もしない
else
{
Pieces = new int [Num];
for(int i = 0; i < Num; i++)
Pieces[i] = bd.Pieces[i];
next = bd.Next;
back = bd.Back;
}
}
//0~Num - 1のピースの値を返す
public int Get(int pos)
{
if(pos < 0 || pos > Num - 1)
return -1;
return Pieces[pos];
}
//0-Num - 1のピースの値を代入
public bool Set(int pos, int val)
{
if(pos < 0 || pos > Num - 1)
return false;
Pieces[pos] = val;
return true;
}
//二つのNBoardが同一かチェックし、同一であれば真、異なれば偽を返す
public bool IsSame(NBoard bd)
{
if(bd == null)
return false; //nullであればそもそも比較不可
for(int i = 0; i < Num; i++)
if(Pieces[i] != bd.Pieces[i])
return false;
return true;
}
//pos1とpos2のピースの値を交換
public bool Swap(int pos1, int pos2)
{
if(pos1 < 0 || pos1 > Num - 1)
return false;
if(pos2 < 0 || pos2 > Num - 1)
return false;
int temp = Pieces[pos1];
Pieces[pos1] = Pieces[pos2];
Pieces[pos2] = temp;
return true;
}
//配列要素を検索し、あればその位置を返す(無ければ-1)
public int IndexOf(int val)
{
return Array.IndexOf(Pieces, val);
}
//配列要素が完成形か否か
public bool IsDone()
{
for(int i = 0; i < Num; i++)
if(Pieces[i] != i)
return false;
return true;
}
//盤面(Pieces配列)を表示する
public void Show()
{
for(int i = 0; i < Num; i++)
{
Console.Write(" " + Pieces[i].ToString());
if(i % 3 == 2)
Console.Write(Environment.NewLine);
}
}
}
//8Puzzleクラス
public class EightPuzzle
{ //メンバー定数
const int Num = 9; //配列要素数
public EightPuzzle() //コンストラクター
{
stBoard = new NBoard(); //開始盤面
SwapArr(); //初期値をランダムにばらす
Que = new NBoard[3] {null, stBoard, null}; //空白ピース周囲のNBoardのキュー(2→1→0)
}
public void ShuffleArr() //シャッフル方式
{ //要素数Numの配列を0 - Num - 1で初期化
for(int i = 0; i < Num; i++)
stBoard.Set(i, i); //乱数の初期化
Random rand = new Random((int)DateTime.Now.Ticks & 0x0000FFFF);
for(int i = Num - 1; i > 1; i--)
{
int n = rand.Next(i + 1);
stBoard.Swap(n, i);
}
if(stBoard.IsDone()) //結果が完成形であればやり直し
ShuffleArr();
}
public void SwapArr(bool ease = true) //スワップ方式
{
//{2, 3, 5, 7, 1, 6, 4, 8, 0}; //解ける初期設定(テスト用)
stBoard.Set(0, 2);
stBoard.Set(1, 3);
stBoard.Set(2, 5);
stBoard.Set(3, 7);
stBoard.Set(4, 1);
stBoard.Set(5, 6);
stBoard.Set(6, 4);
stBoard.Set(7, 8);
stBoard.Set(8, 0);
return; /* 解説:開発段階なので↑のテストデータを使用しました。↓も正しく動きます。
//要素数Numの配列を0 - Num - 1で初期化
for(int i = 0; i < Num; i++)
stBoard.Set(i, i);
int pos = Num - 1; //空白の位置(8)
//乱数の初期化
Random rand = new Random((int)DateTime.Now.Ticks & 0x0000FFFF);
//スワップ回数の設定
int maxswap;
if(ease)
maxswap = rand.Next(10, 20); //最長手数19、最短は10
else
maxswap = rand.Next(15, 31); //8Puzzleの最長手数は31、最短はその半分
for(int i = 0; i < maxswap; i++)
{
int dir = rand.Next(4); //↑(0)→(1)↓(2)←(3)
switch(dir)
{
case 0: //上
if(pos > 2) //3 x 3盤の下2行にある場合
{
stBoard.Swap(pos, pos - 3);
pos -= 3;
}
else
i--; //解説:条件に合致しなかった場合、やり直す為、カウンターを戻す。以下同じ。
break;
case 1: //右
if(pos % 3 < 2) //3 x 3盤の左2列にある場合
{
stBoard.Swap(pos, pos + 1);
pos += 1;
}
else
i--;
break;
case 2: //下
if(pos < 6) //3 x 3盤の上2行にある場合
{
stBoard.Swap(pos, pos + 3);
pos += 3;
}
else
i--;
break;
case 3: //左
if(pos % 3 > 0) //3 x 3盤の右2列にある場合
{
stBoard.Swap(pos, pos - 1);
pos -= 1;
}
else
i--;
break;
}
}
if(stBoard.IsDone()) //結果が完成形であればやり直し
SwapArr();
*/
}
//開始時の盤面を表示する
public void ShowStartBoard()
{
stBoard.Show();
}
//親がnullになるまで、親を遡る
public void TraceBack(NBoard bd)
{
int Counter = 0;
NBoard n;
for(n = bd; n != null; n = n.Back)
{
n.Show();
Console.Write(Environment.NewLine);
Counter++;
}
Console.WriteLine("TraceBack終了(盤面交換数:" + Counter.ToString() + "回、登録盤面数:" + NumOfBoards.ToString() + "回)");
}
CやC++はローカル変数をスタックに確保するので、「通用範囲はその変数が作成されたプログラムブロック内」が原則ですが、C#の場合は個別のメソッド内で作成される変数でもヒープ領域に確保されるのでGC迄は存続する訳ですが、その契機は「= null;」、「(他からの)参照を失う」のみならず、「プログラムブロック(プログラム制御)からの離脱」もあるのでしょうか?仮にそうだとすると、あるメソッド内で生成(= new (コンストラクター))されたローカル変数が上位プログラムブロック(例えばクラス変数)の参照を受ければ、プログラムブロックを抜けても存続する、という仕様になっているのですよね?
cpp
コピーする編集する
void Func() {int x = 123; // スタックに置かれる} // Func のスコープを抜けた時点で x の領域は破棄
✅ 2. C# の参照型ローカル変数:スコープ ≠ ライフタイム
C# では、class 型のオブジェクト(参照型)は、どこで new されようと ヒープに確保され、GC によりライフタイムが決定されます。そのため、ローカル変数のスコープが切れても、参照が残っていれば存続します。
例:
csharp
コピーする編集する
class MyClass { public int Value; }MyClass obj; // フィールド(クラス変数)void Foo() {MyClass local = new MyClass(); // ヒープにインスタンス作成obj = local; // クラス変数が参照を保持} // local のスコープはここで終了。でも obj が参照してるのでまだ生きている
この場合、local というローカル変数は破棄されます(スタック上から消えます)が、local が new したヒープ上のインスタンスは、obj が参照しているため GCの対象にはなりません。
私は元々1985年頃からC言語(8bitのBDS-C)を使うようになり、その後米国駐在時代にC++(Borland Turbo C++ for DOS)を知るようになりました。何れの言語もアセンブラーに近く、ハードウェアの動きを知らないと正しくプログラミングできないような低級言語でしたので、「プログラミングミス == コンピューターの暴走」というのが当時の常識でした。
ps. 上を書いたのは結構前なので、C#のクラスと構造体の違いを補足すると「(名前の付いた)変数が参照(変数のデータはアドレス)するのは、クラスのインスタンスではヒープ領域のメモリー、構造体はスタックに積まれたメモリーだそうです。(従ってCやC++と同じく、プログラムブロックを抜けると消滅する揮発性があります。)
int i;
for(i = 0; i < 9; i++) /* Looking for where "0" is */
if(b->cell[i] == 0) break;
if(i > 2) exchange(b, i, i - 3); /* Above */
if(i % 3 > 0) exchange(b, i, i - 1); /* Left */
if(i % 3 < 2) exchange(b, i, i + 1); /* Right */
if(i < 6) exchange(b, i, i + 3); /* Below */
}