181 回转游戏(IDA*--技巧使得代码编写简单一点)

1. 问题描述:

如下图所示,有一个 # 形的棋盘,上面有 1,2,3 三种数字各 8 个。给定 8 种操作,分别为图中的 A∼H。这些操作会按照图中字母和箭头所指明的方向,把一条长为 7 的序列循环移动 1 个单位。例如下图最左边的 # 形棋盘执行操作 A 后,会变为下图中间的 # 形棋盘,再执行操作 C 后会变成下图最右边的 # 形棋盘。给定一个初始状态,请使用最少的操作次数,使 # 形棋盘最中间的 8 个格子里的数字相同。

输入格式

输入包含多组测试用例。每个测试用例占一行,包含 24 个数字,表示将初始棋盘中的每一个位置的数字,按整体从上到下,同行从左到右的顺序依次列出。输入样例中的第一个测试用例,对应上图最左边棋盘的初始状态。当输入只包含一个 0 的行时,表示输入终止。

输出格式

每个测试用例输出占两行。第一行包含所有移动步骤,每步移动用大写字母 A∼H 中的一个表示,字母之间没有空格,如果不需要移动则输出 No moves needed。第二行包含一个整数,表示移动完成后,中间 8 个格子里的数字。如果有多种方案,则输出字典序最小的解决方案。

输入样例:

1 1 1 1 3 2 3 2 3 1 3 2 2 3 1 2 2 2 3 1 2 1 3 3
1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3
0

输出样例:

AC
2
DDHH
2

来源:https://www.acwing.com/problem/content/description/183/

2. 思路分析:

这道题目属于IDA*算法的经典题目,因为可以由两个相反的方向进行移动,所以操作的步数可能非常多,但是感觉最优解的深度可能没有那么深,对于这种搜索树中某些分支可能特别深,但是答案深度不是特别深的时候的题目特别适用于迭代加深来解决,而且迭代加深加上启发函数剪枝那么就可以升级为IDA*算法,所以可以使用IDA*来解决,因为题目中要输出最小字典序,我们只需要在dfs搜索的时候按照最小字典序来搜索最终得到的一定最小字典序的操作序列,IDA*算法需要一个启发函数,启发函数预估一下当前状态至少需要多少步才能够到达目标状态,每一次移动操作我们都会移出去一个格子,并且移进来一个格子,所以每一次最多可以修正一个数字,我们可以统计一下当前状态中间八个格子的出现次数,8减去出现的最大次数就是当前状态到达目标状态最少的移动次数,当前状态递归下去发现无论如何递归的深度都大于当前最大深度,那么可以直接剪掉这个分支,因为这道题目的操作比较复杂,所以我们可以使用一个技巧使得代码编写变得简单一点,可以为棋盘中的每一个数字一个编号,将棋盘中每一个由7个横条为一组的编号,每一个相反操作的编号,中心八个位置的编号存储下来,这样在编写代码的时候可以直接找到这个编号代码会简单很多。

3. 代码如下:

c++:

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 24;

int op[8][7] = {
    {0, 2, 6, 11, 15, 20, 22},
    {1, 3, 8, 12, 17, 21, 23},
    {10, 9, 8, 7, 6, 5, 4},
    {19, 18, 17, 16, 15, 14, 13},
    {23, 21, 17, 12, 8, 3, 1},
    {22, 20, 15, 11, 6, 2, 0},
    {13, 14, 15, 16, 17, 18, 19},
    {4, 5, 6, 7, 8, 9, 10}
};

int oppsite[8] = {5, 4, 7, 6, 1, 0, 3, 2};
int center[8] = {6, 7, 8, 11, 12, 15, 16, 17};

int q[N];
int path[100];

int f()
{
    static int sum[4];
    memset(sum, 0, sizeof sum);
    for (int i = 0; i < 8; i ++ ) sum[q[center[i]]] ++ ;

    int maxv = 0;
    for (int i = 1; i <= 3; i ++ ) maxv = max(maxv, sum[i]);

    return 8 - maxv;
}

void operate(int x)
{
    int t = q[op[x][0]];
    for (int i = 0; i < 6; i ++ ) q[op[x][i]] = q[op[x][i + 1]];
    q[op[x][6]] = t;
}

bool dfs(int depth, int max_depth, int last)
{
    if (depth + f() > max_depth) return false;
    if (f() == 0) return true;

    for (int i = 0; i < 8; i ++ )
        if (last != oppsite[i])
        {
            operate(i);
            path[depth] = i;
            if (dfs(depth + 1, max_depth, i)) return true;
            operate(oppsite[i]);
        }

    return false;
}

int main()
{
    while (cin >> q[0], q[0])
    {
        for (int i = 1; i < 24; i ++ ) cin >> q[i];

        int depth = 0;
        while (!dfs(0, depth, -1)) depth ++ ;

        if (!depth) printf("No moves needed");
        else
        {
            for (int i = 0; i < depth; i ++ ) printf("%c", 'A' + path[i]);
        }
        printf("\n%d\n", q[6]);
    }

    return 0;
}

python(超时):python使用dfs确实很慢:

import collections
from typing import List


class Solution:
    # 将所有的位置信息定义为全局变量
    op = [[0, 2, 6, 11, 15, 20, 22], [1, 3, 8, 12, 17, 21, 23], [10, 9, 8, 7, 6, 5, 4],
          [19, 18, 17, 16, 15, 14, 13], [23, 21, 17, 12, 8, 3, 1], [22, 20, 15, 11, 6, 2, 0],
          [13, 14, 15, 16, 17, 18, 19], [4, 5, 6, 7, 8, 9, 10]]
    # 相反方向的坐标
    opposite = [5, 4, 7, 6, 1, 0, 3, 2]
    # 中心的八个方向的下标
    center = [6, 7, 8, 11, 12, 15, 16, 17]
    # 记录答案
    path = None

    def f(self, q):
        s = collections.defaultdict(int)
        for i in range(8):
            # 对中间的八个位置出现的次数进行计数
            s[q[self.center[i]]] += 1
        res = 0
        # 求解最大操作次数
        for i in range(1, 4):
            res = max(res, s[i])
        return 8 - res

    # 进行一次操作, k表示在哪一条上进行操作
    def operate(self, q: List[int], k: int):
        c = q[self.op[k][0]]
        for i in range(1, 7):
            q[self.op[k][i - 1]] = q[self.op[k][i]]
        q[self.op[k][6]] = c
    
    # 迭代加深
    def dfs(self, depth: int, last: int, maxdepth: int, q: List[int]):
        # 启发函数剪枝
        if depth + self.f(q) > maxdepth: return False
        if self.f(q) == 0: return True
        for i in range(8):
            # 不能是当前操作的逆操作, 要不然做的是无用功
            if last == self.opposite[i]: continue
            self.operate(q, i)
            self.path[depth] = i
            if self.dfs(depth + 1, i, maxdepth, q): return True
            self.operate(q, self.opposite[i])
            # 不用回溯, 因为当前depth都会被后面递归的值覆盖掉
        return False

    def process(self):
        while True:
            # 输入的数据
            q = list(map(int, input().split()))
            if len(q) == 1: break
            depth = 0
            self.path = [0] * 100
            while not self.dfs(0, -1, depth, q): depth += 1
            if depth == 0:
                print("No moves needed")
            else:
                for i in range(depth):
                    print(chr(self.path[i] + 65), end="")
                print()
            print(q[6])


if __name__ == '__main__':
    Solution().process()

おすすめ

転載: blog.csdn.net/qq_39445165/article/details/121869955