【问题标题】:Solving Lights out for AI Course解决人工智能课程的熄灯问题
【发布时间】:2015-10-12 18:50:07
【问题描述】:

所以我被分配了以下任务:假设 5x5 版本的游戏中的所有灯都已打开,请使用 UCS / A* / BFS / Greedy best first search 编写一个找到解决方案的算法。

我首先意识到 UCS 是不必要的,因为从一个状态移动到另一个状态的成本是 1(按下一个可以翻转自身和相邻状态的按钮)。所以我所做的就是写 BFS。事实证明,它的工作时间太长并填满了一个队列,即使我在完成父节点时注意删除父节点,以免内存溢出。它会工作大约 5-6 分钟,然后因为内存而崩溃。 接下来,我所做的是编写 DFS(尽管没有提到它是一种可能性),它确实在 123 秒内找到了解决方案,深度为 15(我使用了深度优先限制,因为我知道深度有解决方案15).

我现在想知道的是我错过了什么吗?是否有一些好的启发式方法可以尝试使用 A* 搜索来解决这个问题?当涉及到启发式算法时,我一无所获,因为在这个问题中找到一个似乎并不容易。

非常感谢。期待大家的帮助

这是我的源代码(我认为它很容易理解):

struct state
{
    bool board[25];
    bool clicked[25];
    int cost;
    int h;
    struct state* from;
};

int visited[1<<25];
int dx[5] = {0, 5, -5};
int MAX_DEPTH = 1<<30;
bool found=false;

struct state* MakeStartState()
{
    struct state* noviCvor = new struct state();

    for(int i = 0; i < 25; i++) noviCvor->board[i] = false, noviCvor->clicked[i] = false;
    noviCvor->cost = 0;
    //h=...
    noviCvor->from = NULL;

    return noviCvor;
};

struct state* MakeNextState(struct state* temp, int press_pos)
{
    struct state* noviCvor = new struct state();

    for(int i = 0; i < 25; i++) noviCvor->board[i] = temp->board[i], noviCvor->clicked[i] = temp->clicked[i];
    noviCvor->clicked[press_pos] = true;

    noviCvor->cost = temp->cost + 1;
    //h=...
    noviCvor->from = temp;

    int temp_pos;

    for(int k = 0; k < 3; k++)
    {
        temp_pos = press_pos + dx[k];

        if(temp_pos >= 0 && temp_pos < 25)
        {
            noviCvor->board[temp_pos] = !noviCvor->board[temp_pos];
        }
    }

    if( ((press_pos+1) % 5 != 0) && (press_pos+1) < 25 )
        noviCvor->board[press_pos+1] = !noviCvor->board[press_pos+1];

    if( (press_pos % 5 != 0) && (press_pos-1) >= 0 )
        noviCvor->board[press_pos-1] = !noviCvor->board[press_pos-1];

    return noviCvor;
};

bool CheckFinalState(struct state* temp)
{
    for(int i = 0; i < 25; i++)
    {
        if(!temp->board[i]) return false;
    }

    return true;
}

int bijection_mapping(struct state* temp)
{
    int temp_pow = 1;
    int mapping = 0;

    for(int i = 0; i < 25; i++)
    {
        if(temp->board[i])
            mapping+=temp_pow;

        temp_pow*=2;
    }

    return mapping;
}

void BFS()
{
    queue<struct state*> Q;

    struct state* start = MakeStartState();
    Q.push(start);
    struct state* temp;
    visited[ bijection_mapping(start) ] = 1;

    while(!Q.empty())
    {
        temp = Q.front();
        Q.pop();
        visited[ bijection_mapping(temp) ] = 2;

        for(int i = 0; i < 25; i++)
        {
            if(!temp->clicked[i])
            {
                struct state* next = MakeNextState(temp, i);
                int mapa = bijection_mapping(next);

                if(visited[ mapa ] == 0)
                {
                    if(CheckFinalState(next))
                    {
                        printf("NADJENO RESENJE\n");
                        exit(0);
                    }
                    visited[ mapa ] = 1;
                    Q.push(next);
                }
            }
        }

        delete temp;
    }
}

附言。由于我不再使用地图(切换到数组)来访问状态,我的 DFS 解决方案从 123 秒提高到 54 秒,但 BFS 仍然崩溃。

【问题讨论】:

    标签: artificial-intelligence dijkstra depth-first-search a-star


    【解决方案1】:

    首先,您可能已经认识到,在 Lights Out 中,您永远不必多次拨动同一个开关,而且拨动开关的顺序并不重要。因此,您可以用两种不同的方式描述当前状态:根据哪些灯亮着,或者根据哪些开关已被翻转。后者,加上灯光的起始图案,为您提供前者。

    要使用图搜索算法来解决问题,您需要一个邻接的概念。这从第二个特征更容易得出:如果两个状态恰好有一个开关不同,则两个状态是相邻的。该特征还直接编码到每个节点的路径长度(= 已翻转的开关数量),并且它减少了需要为所考虑的每个状态考虑的后续移动的数量,因为到每个节点的所有可能路径以开关模式编码。

    您可以相对容易地在广度优先搜索中使用它(这可能是您实际上尝试过的)。在这种情况下,BFS 等效于 Dijkstra 算法,即使不使用显式优先级队列,因为您将新节点排入队列以按优先级(路径长度)顺序进行探索。

    您还可以通过添加合适的启发式将其转换为 A* 搜索。例如,由于每次移动最多关闭五个灯,因此可以将每次移动后仍然亮着的灯的数量除以 5 作为启发式。虽然这有点粗略,但我倾向于认为它会是一些帮助。但是,您确实需要一个真正的优先级队列。

    就实现而言,请认识到您可以将当前打开的灯的模式和已按下的开关模式表示为位向量。每个模式都适合 32 位整数,访问状态列表需要 225 位,这完全在现代计算系统的能力范围内。即使你使用了那么多 bytes,你也应该能够处理它。此外,您可以使用按位算术运算符执行所有需要的操作,尤其是 XOR。因此,这个问题(在其给定的大小)应该可以相对快速地计算。

    更新:

    正如我在 cmets 中提到的,我决定自己解决这个问题,并且——在我看来——非常成功。我使用了多种技术来实现良好的性能并最​​大限度地减少内存使用,在这种情况下,这些技术大多是互补的。以下是我的一些技巧:

    • 我用一个 uint64_t 表示每个整个系统的状态。顶部 32 位包含一个位掩码,其中开关已被翻转,底部 32 位包含一个位掩码,其结果是灯亮。我将它们包装在struct 中,并带有一个将它们作为队列元素链接在一起的指针。一个给定的状态可以通过一个按位与运算和一个整数比较来测试。

    • 我创建了一个由 25 个 uint64_t 位掩码组成的预初始化数组,表示每次移动的效果。每个顶部 32 中设置的一位代表被翻转的开关,底部 32 中设置的 3 到 5 位之间代表作为结果切换的灯。翻转一个开关的效果可以简单地计算为new_state = old_state ^ move[i]

    • 我实现了 plain 广度优先搜索而不是 A*,部分原因是我试图快速将某些内容组合在一起,特别是因为这样我可以使用常规队列代替优先级队列。

    • 我以一种自然地避免两次访问同一状态的方式构建了我的 BFS,而不必实际跟踪哪些状态曾经被排队。这是基于对如何在不重复的情况下有效地生成不同的位模式的一些见解,具有较少位集的那些先于具有更多位集的那些生成。无论如何,BFS 所需的基于队列的方法很自然地满足了后一个标准。

    • 在动态分配的队列节点从主队列中移除后,我使用了第二个(普通)队列来回收它们,以尽量减少对 malloc() 的调用次数。

    总体代码略少于 200 行,包括空白行和注释行、数据类型声明、I/O、队列实现(纯 C,无 STL)——所有内容。

    顺便提一下,标准 Dijkstra 和 A* 中使用的优先级队列主要是为了找到正确的答案(最短路径),其次是为了有效地这样做。从标准队列入队和出队都可以是O(1),而优先级队列上的这些操作在队列中的元素数量上是o(log m)。 A* 和 BFS 在状态总数中的最坏情况队列大小上限均为 O(n)。因此,在问题规模上,BFS 将比 A* 更好地扩展;唯一的问题是前者是否可靠地给你正确的答案,在这种情况下,它确实。

    【讨论】:

    • FWIW,我决定自己解决这个问题。我按照上述思路使用了一个简单的广度优先搜索,用 C 语言编写。它在大约 0.5 秒内找到了一个 15 步的解决方案(在相当大的机器上)。
    • 你所描述的正是我所做的,我的意思是我写完这篇文章后想到的想法,以及 A* 的启发式方法。问题是,我想我使用了太多空间来存储我的结构,因此我的程序运行缓慢。我存储了电路板状态和按下状态,并且没有通过按下之前按下的相同按钮让状态转换到下一个状态。问题是,我将它们存储为大小为 25 的布尔数组,所以在我的情况下,它是字节。我猜我的时间是跟踪访问过的状态,我使用了地图。我稍后会尝试优化。谢谢!
    • 所以我在编辑中发布了我的源代码。各位大侠能看一下吗?提前致谢。附言。即使我不再使用地图,它仍然会在工作一段时间后崩溃
    • @LukaBulatovic,我不准备对您的代码进行详细分析,但我更新了我的答案,描述了我在解决方案中采用的一些技术以使其高效。尽管您可能已经围绕等效算法构建了自己的算法,但看起来细节要麻烦得多。
    • @LukaBulatovic,顺便说一下,我添加了一些工具来检查内存使用情况,发现我的队列的高水位线大约是 520 万个元素(超过 3200 万个可能的状态) )。对于我的 16 字节元素,大约 80 MB。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-03-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多