パズルゲーム|アルゴリズムの魅力を感じる

3.パズルゲーム

グレード 10 開始時間 2020年9月7日月曜日09:00
ディスカウント 0.8 割引時間 2020年9月15日火曜日09:00
遅延提出が可能 番号 閉店時間 2020年10月10日土曜日23:00

説明

Xiao Zhangは脱出ゲームのファンです。脱出ゲームでは、一連のパズルを解いてパスワードを取得し、外に出ることができます。Xiao Zhangは手がかりを含むボックスを開く必要がありますが、下の図に示すように組み合わせロックがあります。

各ドットはボタンであり、各ボタンの内側に小さなライトがあります。上の図に示すように、赤はライトがオンであることを意味し、白はライトがオフであることを意味します。ボタンが押されるたびに、このボタンのランプならびに垂直および水平ランプ4つの方向ボタン(ライトがオフになっている場合にライトがオフしている場合)、状態変化。Xiao Zhangがボタンを押してライトをオフにすると、ボックスを開けることができます。

このコードロックの場合、最初に左上隅のボタンを押すと、コードロックの状態が次の図に変わります。

右下のボタンを押すと、コードロック状態が下図のように変わります。

最後に中央のボタンを押すと、ライトがすべて消えます。

Xiao Zhangはコードロックステータスを提供します。少なくとも数回ボタンを押してすべてのライトをオフにするように言ってください。

入力

1行目の2つの整数n、m(1 \ leq n、m \ leq 16)

次のん行では、各行にメートル01の長さの文字列があり、0はライトの初期状態がオフであることを意味し、1はライトの初期状態がオンであることを意味します。

出力

線上の整数は、ボタンを少なくとも数回押すと、すべてのライトをオフにできることを意味します。

ノート

最初の例では、タイトルの説明を参照してください。2番目の例では、左上ボタンと右下ボタンを押します。

テストケースは解決されることが保証されています

  テスト入力 期待される出力 制限時間 メモリ制限 追加プロセス
テストケース1 テキストとして表示
  1. 33↵
  2. 100↵
  3. 010↵
  4. 001↵
テキストとして表示
  1. 3↵
1秒 64M 0
テストケース2 テキストとして表示
  1. 23↵
  2. 111↵
  3. 111↵
テキストとして表示
  1. 2↵
1秒 64M 0


       この質問について、私は2つの方法を考えました:ガウスの消去法または深さ優先探索です。あなたが最初の連絡先である場合、理解するのは難しいはずですが、完全に理解していなくてもかまいません。数回読んでからノックアウトしてください...

        深さ優先検索(dfs)メソッドについて話しましょう。

        次のようなディープ検索を理解できます。すべてのケースを順番に列挙し、列挙が完了するまで結果を検索します。


アイデア

この質問で、キーストロークの最小数を見つけるには、最初に次のことを知っておく必要があります。

  • ボタンを最大で1回押します。2回以上あると、オフセットが発生し、歩数が確実に少なくなることはありません。
  • キーの順序は、最終的な結果とは関係ありません。同じキーが異なる順序で押されます。もちろん、結果は同じです。

激しく列挙する        場合:キーごとに2つのケース(押すか移動しないか)があるため2 ^ {16 \ ast 16}ほとんどの場合、列挙する必要  があります。これは本当にひどい数です...それでは、より優れたアルゴリズムが必要です。emmmの下で考える理由は言い表せないだけで、考えは言うまでもありません。この方法を読んだ後で、この方法を理解できれば十分です。コアアルゴリズムに直接移動ます

  1.  最初に、ライトの最初の行のキーストロークのみを列挙します。次に2 ^ {16}ほとんどの場合列挙する必要がありますが  、これはまだ許容されます...列挙カウントごとに最初の行が押された回数を記録します
  2. 各列挙の結果(ライトの最初の行が操作された)について、それらを行ごとに処理します(たとえば、最初の行の特定のライトがオンになっている場合、次の行のライトを押して、ボタンの2番目の行を押すことで最初の行のライトがすべてオフになるようにします。次に、2番目の行について説明し、3番目の行を使用します行のボタンは2番目の行をオフにします...など)。最後から2番目の行が最終的に固定されている場合、最後の行も完全に削除され、この列挙が実行可能であることを示し、プロセス全体でのキーストロークの数が記録されます。そうでない場合、この列挙は実行不可能であり、他のアイテムを検索する必要があります。プログラムを引用します。
  3. 特定の列挙では、行ごとの操作はすべて正常に終了し、count + cur回で操作が完了したことを意味します。このとき、回答の最小値更新する必要があります。

コード

        この質問のコード実装はより複雑で、プログラミング能力を発揮します。あなたがそれを補うことができないなら、あまり落胆しないでください、あなたはゆっくりとそれに慣れることができるだけです。また、問題の解決策をプログラミングする機能を提供することも困難です。この機能は、あなたからの少しのコードに基づいていますね。しかし、参照用に読みやすいコードを提供することが必要です。プログラミングの学習非常に重要な部分は学習です。他人のコード

        たぶん、あなたはまだ貧弱なメイン関数ですべてのコードを書いています(そうでなければ、私はそれを言っていませんでした)、プログラムの可読性を改善するための最初のステップ:関数を関数ごとに書く。

        まず、使用するいくつかの汎用モジュールコメントで理解できる場合は、c / c ++構文を最初に学習する必要があります)と、後で呼び出される関数を記述します。グローバル変数にはこれらがあります

#define MAX_LEN 17

int n, m, a[MAX_LEN][MAX_LEN];  //题中的输入
int cur[MAX_LEN][MAX_LEN];  //存储处理完第一排后的状态
int ans = 256;   //存储答案:最小步骤。初始为最大值256步

1.整数のXOR演算を実装する関数

/* 将p位置上的整数做一个反(异或)操作:
 * 1变成0, 0变成1 */
void change(int *p) {
    if (*p == 1)
        *p = 0;
    else
        *p = 1;
}

2.ライトプレスの変更を実現する機能

/* 设定将a[i][j]处按下所产生的反应
 * 注意:a数组根据传递的地址而定 */
void push(int a[][MAX_LEN], int i, int j) {
    change(&a[i][j]);
    if (i - 1 >= 0)
        change(&a[i - 1][j]);
    if (j - 1 >= 0)
        change(&a[i][j - 1]);
    if (i + 1 < n)
        change(&a[i + 1][j]);
    if (j + 1 < m)
        change(&a[i][j + 1]);
}

3.深さ優先検索(ライトの最初の行のボタンステータスを列挙)

         ここで8行目で呼び出されたcalc関数に関係なく、calc関数が特定の列挙の後続のステップ数を計算できるようになるまで待つ必要があります。レイヤーごとに理解するには、最初に理解するのが最も難しいdfsの操作を理解します。余談ですが、初めてdfsプログラムを作成したときはとてもめまいでした... dfsは本質的に再帰的です。dfsプログラムを見る秘訣は、マクロ的に見れば、再帰的に自分を呼び出すときに、ここで再帰的な呼び出しをマクロで理解できることです。その結果、再帰関数に深く入り込む必要はありません。うーん、この文を理解していなければ、遅かれ早かれ理解できるでしょう...多くの複雑な再帰的な打撃の後、あなたはこの文を非常に認識します

     たとえば、次のdfを理解してもらいましょう。

     最初の行のステップ1ボタンの選択を列挙します。最初に押してから、次のボタンの列挙を検討し、復元(つまり、押さない)してから、次のボタンの列挙を検討します。このアイデアをマクロレベルで理解でき、このコードの再帰的な実装を注意深く検討すれば、はるかに早く理解できます。あなたが兄であるなら、私がそれを言わなかったようにそれを扱ってください、あなたが理解していないなら、あなたはそれを数回読むでしょう)。

/* 深度优先搜索,枚举第一排按键的所有按法
 * step: 当前讨论第1排第step + 1个按键
 * count: step之前按键一共被操作了的次数和 */
void dfs(int step, int count) {
    // 深度优先搜索的终点:讨论完第一排最后一个按键了
    // 已经按照一种方式将第一排操作完成
    if (step == m) {
        int t = calc();  //计算该基础上熄灭所有的灯需要的步数
        if (t == -1)  //无解
            return;
        if (t + count < ans)  //有解,更新最小值
            ans = t + count;
        return;
    }

    push(a, 0, step);  //按下第一排第step-1个按键
    dfs(step + 1, count + 1);

    push(a, 0, step); //再次按下(相当于还原)第一排第step-1个按键
    dfs(step + 1, count);
}

4. Calc関数

        getCur()関数は、主に列挙結果の状態をcur配列にコピーするためにcalcで呼び出されます。それ以外の場合、配列の直接操作を復元することは困難です。これは、後で説明に影響します。calc関数のアイデアは、アルゴリズムで要約されている2番目のステップで、1行ずつ実行されます。

/* 当第一排的灯被弄完后,把灯的状态复制到cur数组中
 * 便于后面的计数与操作,不会影响到原数组a */
void getCur() {
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            cur[i][j] = a[i][j];
}

/* 当第一排的灯被弄完后
 * 逐排操作,计算是否能够全部灭完
 * 如果不可以:返回-1;否则:返回操作次数 */
int calc() {
    getCur();
    int step = 0;
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < m; j++) {
            if (cur[i][j] == 1) {
                step++;
                push(cur, i + 1, j);
            }
        }

    for (int i = 0; i < m; i++)
        if (cur[n - 1][i] == 1)
            return -1;
    return step;
}


完全なコード

       よし、手羽先はベビーシッターにすぎて、関数を1つずつ分解しなければならない...以下の完全なコードを投稿してみましょう。大きな人が完全なコードを表示するように来るかもしれません。主に小学校時代の初めにdfsを書いたのですが、読者の方はご了承いただけないのではないかと思いますので、非常に長引きだと思われるかもしれません。この問題は深夜3時に解決されるかもしれません。本当に長引いています...

       以下は完全なコードで、主な機能は主に入力の処理です。特に注意してください:各行の改行文字を削除します!そうでなければ...手羽先がなぜ夜遅くまで問題になるのかわからない。なぜなら、毎回改行文字を吸わなかったので丸一日だったからだ...私はさまざまな再帰的かつ詳細な検索場所をチェックしてきた。 、その結果、バグが2時にデバッグされたときに私の涙が流されました...

#include <cstdio>

#define MAX_LEN 17

int n, m, a[MAX_LEN][MAX_LEN];
int cur[MAX_LEN][MAX_LEN];  //存储处理完第一排后的状态
int ans = 256;   //存储答案:最小步骤。初始为最大值256步

/* 将p位置上的整数做一个反(异或)操作:
 * 1变成0, 0变成1 */
void change(int *p) {
    if (*p == 1)
        *p = 0;
    else
        *p = 1;
}

/* 设定将a[i][j]处按下所产生的反应
 * 注意:a数组根据传递的地址而定 */
void push(int a[][MAX_LEN], int i, int j) {
    change(&a[i][j]);
    if (i - 1 >= 0)
        change(&a[i - 1][j]);
    if (j - 1 >= 0)
        change(&a[i][j - 1]);
    if (i + 1 < n)
        change(&a[i + 1][j]);
    if (j + 1 < m)
        change(&a[i][j + 1]);
}

/* 当第一排的灯被弄完后,把灯的状态复制到cur数组中
 * 便于后面的计数与操作,不会影响到原数组a */
void getCur() {
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            cur[i][j] = a[i][j];
}

/* 当第一排的灯被弄完后
 * 逐排操作,计算是否能够全部灭完
 * 如果不可以:返回-1;否则:返回操作次数 */
int calc() {
    getCur();
    int step = 0;
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < m; j++) {
            if (cur[i][j] == 1) {
                step++;
                push(cur, i + 1, j);
            }
        }

    for (int i = 0; i < m; i++)
        if (cur[n - 1][i] == 1)
            return -1;
    return step;
}

/* 深度优先搜索,枚举第一排按键的所有按法
 * step: 当前讨论第1排第step + 1个按键
 * count: step之前按键一共被操作了的次数和 */
void dfs(int step, int count) {
    // 深度优先搜索的终点:讨论完第一排最后一个按键了
    // 已经按照一种方式将第一排操作完成
    if (step == m) {
        int t = calc();  //计算该基础上熄灭所有的灯需要的步数
        if (t == -1)  //无解
            return;
        if (t + count < ans)  //有解,更新最小值
            ans = t + count;
        return;
    }

    push(a, 0, step);  //按下第一排第step-1个按键
    dfs(step + 1, count + 1);

    push(a, 0, step); //再次按下(相当于还原)第一排第step-1个按键
    dfs(step + 1, count);
}

int main() {
    scanf("%d%d\n", &n, &m);  //一定要加上\n,作用:吸去换行符

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            char c;
            c = getchar();
            a[i][j] = c - '0';  //将字符转化为整数考虑
        }
        getchar();  //吸去换行符
    }

    dfs(0, 0);
    printf("%d\n", ans);
}


個人のパブリックアカウント チキンウィングプログラミング」注意を払うことを歓迎しますここには、真面目で行儀のよいコードファーマーがいます。

----最も行儀の良いブロガーであり、最も堅実なプログラマーになる----

それぞれの記事を注意深く書くことを目指し、通常はメモをプッシュ更新にまとめます〜

 

おすすめ

転載: blog.csdn.net/weixin_43787043/article/details/108506008