// 15パズル ( 反復深化法 下限値枝刈り法 )
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<stack>
#include<algorithm>
#include<iostream>
#include<cmath>

#include<vector>
#include<map>
#include<queue>

using namespace std;

const int PANEL_NUM = 16;
// 15パズルの最大手数
const int MAX_STEP = 80;

// 並び替えの目標の状態
int ary_Target[PANEL_NUM] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0};

// パネルの移動量を保持する構造体
typedef struct
{
    int x;
    int y;
}st_move;

// パネルの移動座標を保持する配列
st_move move_p[] = { {-1, 0},   // 上
                     {0, -1},   // 左
                     {0, 1},    // 右
                     {1, 0}     // 下
                    };

// パネルの移動状態を表現する列挙型
enum E_Move
{
    UP = 0,
    LEFT,
    RIGHT,
    DOWN,
    MOVE_LAST
};

// 各パネルを正解の位置まで動かすのに必要な最短距離
int moveDistance[PANEL_NUM][PANEL_NUM] =
{
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},   // ダミー
    {0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6},   // 1
    {1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4, 4, 3, 4, 5},   // 2
    {2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3, 5, 4, 3, 4},   // 3
    {3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2, 6, 5, 4, 3},   // 4
    {1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5},   // 5
    {2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4},   // 6
    {3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3},   // 7
    {4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2},   // 8
    {2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4},   // 9
    {3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3},   // 10
    {4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2},   // 11
    {5, 4, 3, 2, 4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1},   // 12
    {3, 4, 5, 6, 2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3},   // 13
    {4, 3, 4, 5, 3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2},   // 14
    {5, 4, 3, 4, 4, 3, 2, 3, 3, 3, 1, 2, 2, 1, 0, 1},   // 15
};


// パネルの状態を保持する配列
int ary_panel[PANEL_NUM];

// 動かしたパネルを記憶しておく配列
int ary_movedPanel[MAX_STEP];

// 検索中かを保持するフラグ
// true : 検索中, false : 検索終了
bool searchState;

void panelMoveOperation(int stepLimit, int moveCount, int zeroPos, int lowerCount);
int calcArrayNum(int x, int y);
int searchZeroPos(int panel[]);
bool reachGoal(int panel[]);
int calcMoveDistance(int panel[]);

// メイン関数
int main(void)
{
    int lowerMoveCount = 0;

    searchState = true;

    // 初期状態を設定する
    for(int i = 0; i < PANEL_NUM; i++)
    {
        cin >> ary_panel[i];
    }

    // 初期設定
    ary_movedPanel[0] = 0;

    // 正解に到達するまでの最短手数を求めておく
    lowerMoveCount = calcMoveDistance(&ary_panel[0]);

    for(int stepLimit = lowerMoveCount; stepLimit <= MAX_STEP; stepLimit++)
    {
        int zeroPos = searchZeroPos(&ary_panel[0]);

        // 検索可能な手数を順番に増やして検索する
        // 最短手数は分かっているので, stepLimit を最短手数から開始する
        panelMoveOperation(stepLimit, 0, zeroPos, lowerMoveCount);

        if( searchState == false )
        {
            // 検索終了
            printf("%d\n", stepLimit);
            break;
        }
    }

    return 0;
}

// パネルの移動操作を実施する関数
// stepLimit  : 検索可能な手数の上限値
// moveCount  : パネルを移動させた回数
// zeroPos    : 配列中のパネルのゼロの位置
// lowerCount : 正解までに必要な最短の移動距離
void panelMoveOperation(int stepLimit, int moveCount, int zeroPos, int lowerCount)
{
    int newLowerCount = 0;

    if( stepLimit == moveCount )
    {

        // 並び替えた状態がゴールに到達しているかを確認する
        if( reachGoal(&ary_panel[0]) == true )
        {
            // 検索終了
            searchState = false;
        }

        return;
    }

    // 上下左右にパネルを移動させてみる
    for(int i = 0; i < MOVE_LAST; i++)
    {
        int x, y;

        //zeroPos = searchZeroPos();

        // ゼロの位置から x, y 座標に変換する
        x = zeroPos / 4;
        y = zeroPos % 4;

        int tx, ty;

        // 移動後の座標を計算する
        tx = x + move_p[i].x;
        ty = y + move_p[i].y;

        if( ( 0 <= tx && tx <= 3 ) && ( 0 <= ty && ty <= 3 ) )
        {
            int moveNum, movePos;

            // 座標から配列の要素No.へ変換する
            movePos = calcArrayNum(tx, ty);
            moveNum = ary_panel[movePos];

            if( moveNum == ary_movedPanel[moveCount] )
            {
                // 前回動かしたパネルなら, 元に戻るだけなので何もしない
                continue;
            }

            // 動かしたパネルを記憶しておく
            ary_movedPanel[moveCount + 1] = moveNum;

            // 数値を入れ替える
            swap(ary_panel[zeroPos], ary_panel[movePos]);

            // 正解までに必要な移動の最短距離を再計算する
            // [現在の距離の下限値] - [移動前のパネル位置での正解までの距離] + [移動後のパネル位置での正解までの距離]
            // パネルを移動させたので, 移動の下限値から, 移動前のパネル距離は引いておいて, 新たに移動後のパネル距離を足しておく
            newLowerCount = lowerCount - moveDistance[moveNum][movePos] + moveDistance[moveNum][zeroPos];

            // 下限値枝刈り法の実施
            // [今の移動手数 + 下限値] と [上限値] を比較する
            if( ( moveCount + newLowerCount ) <= stepLimit )
            {
                // 再帰呼び出しを実施
                panelMoveOperation(stepLimit, moveCount + 1, movePos, newLowerCount);
            }

            // 再帰呼び出しの前の状態に戻す
            ary_panel[zeroPos] = 0;
            ary_panel[movePos] = moveNum;
        }
    }

    return;
}

// x, y 座標から配列の要素数へ変換する関数
int calcArrayNum(int x, int y)
{
    return 4 * x + y;
}

// 配列中のゼロの位置を探索する関数
int searchZeroPos(int panel[])
{
    for(int i = 0; i < PANEL_NUM; i++)
    {
        if( panel[i] == 0 )
        {
            return i;
        }
    }

    return -1;
}

// ゴールに到達したかを判定する関数
bool reachGoal(int panel[])
{
    for(int i = 0; i < PANEL_NUM; i++)
    {
        if( panel[i] != ary_Target[i] )
        {
            return false;
        }
    }

    return true;
}

// パネルの並びから, 正解までに必要な移動距離を求める
int calcMoveDistance(int panel[])
{
    int num = 0;
    int distance = 0;

    for(int i = 0; i < PANEL_NUM; i++)
    {
        num = panel[i];

        // 最短の移動距離の和を求める
        distance += moveDistance[num][i];
    }

    return distance;
}



Measured memory_bytes: 3220