微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

广度优先搜索花费过多时间取决于否决权插入位置

如何解决广度优先搜索花费过多时间取决于否决权插入位置

正在解决的问题是LeetCode #752 "Open the Lock"。总结:

您有一个带四个一位数轮子的组合锁,从 0000 开始。您想确定将锁定更改为显示 abcd 所需的最小移动次数。在每个动作中,您可以向前或向后旋转一个轮子,例如从 0109 的第一个轮子。

有“死角”组合。如果您的动作序列到达死胡同,锁就会卡住。所以你必须在不遇到死胡同的情况下到达目标组合。

例如:

 deadends = ["0201","0101","0102","1212","2002"],target = "0202"

一系列有效的移动将是“0000”->“1000”->“1100”->“1200”->“1201”->“1202”->“0202”。 请注意,像 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列将是无效的, 因为当显示器变成死角“0102”后,锁的轮子卡住了。 (复制自问题描述)

我设计了一个使用队列和两个集合的广度优先搜索。一组包含死胡同,一组包含已经考虑过的组合。

如果我在从队列中弹出后将一个组合插入到“已考虑的”集合中,我的解决方案需要太多时间来执行。如果我在组合生成后立即插入,那么我的解决方案执行得更快。

为什么?我认为它们实际上是相同的!对于“接近”0000 的组合,“慢”解决方案仍然设法收敛。对于距离较远的组合,在时限内不收敛。

class Solution {
public:
    int openLock(vector<string>& deadends,string target) {
        // "minimum total number of turns" suggests BFS (queue)
        
        // time complexity is O(C^N) where C is constant (number of choices for each digit,e.g. ten,or 24 if a...z) and N is the length of the combo
        // other terms are N^2 and + N_deadends
        // string operations are N^2: for each of the N digits,we need to construct a string of length N (itself an O(N) operation) -> O(N*N*2) (twice for +1 and -1)
        int nMoves = 0;
        
        std::queue<std::string> toConsider;
        toConsider.push("0000");
        std::set<std::string> considered;
        
        std::set<std::string> de;
        for (auto d : deadends) de.insert(d); // converting the deadends vector to a set improves speed significantly

        while (!toConsider.empty()) {
            int const nStatesInLevel = toConsider.size();
            
            for (int i = 1; i <= nStatesInLevel; ++i) {
                std::string state = toConsider.front();
                toConsider.pop();
                // considered.insert(state); // IF WE PUT THIS HERE INSTEAD OF BELOW,THE SOLUTION IS TOO SLOW!
                if (de.find(state) != de.end()) continue; // veto if dead-end
                if (state == target) return nMoves;
                
                for (int i : {0,1,2,3}) { // one out of four wheels to turn
                    int const oldWheelVal = state.at(i) - '0';
                    for (int c : {1,-1}) { // increase or decrease wheel value
                        int newWheelVal = oldWheelVal + c;
                        if (newWheelVal == -1) newWheelVal = 9;
                        if (newWheelVal == 10) newWheelVal = 0;
                        std::string newWheel = state;
                        newWheel.at(i) = newWheelVal + '0';
          
                        if (considered.find(newWheel) == considered.end()) {
                            toConsider.push(newWheel);
                            considered.insert(newWheel); // we need to put this here instead of after it is popped in order to avoid "time limit exceeded".
                            // I think both places should effectively be the same!
                        }
                    }
                }
            }
            
            nMoves++;
            
        }
        
        return -1;
    }
};

解决方法

慢版本将允许将重复的状态添加到队列中。只有在将它们从其中拉出时才会检测到它们是重复的,但是“伤害”已经完成。由于有许多不同的路径可以到达相同的状态,这将很快使队列变得不必要地大,并填充大量重复项。这不仅需要内存成本,而且需要时间成本,因为额外的内存分配需要时间。

首先从队列中取出初始状态 0000,并标记为已访问,然后将添加以下内容:

1000,9000,0100,0900,0010,0090,0001,0009

然后在下一次迭代中,会拉取1000,并将以下内容添加到队列中。 0000 将被找到,但不会添加到队列中(标记为 ----):

2000,----,1100,1900,1010,1090,1001,1009

然后 9000 将从队列中拉出,并添加以下内容:

----,8000,9100,9900,9010,9090,9001,9009

然后 0100 将从队列中拉出,并附加在后面。同样,0000 将不会被添加,但是,其他状态再次被发现没有,但尚未从队列中拉出,因此它们不会被检测为重复(用星号标记):

1100*,9100*,0200,0110,0190,0101,0109

...等等。我们走得越远,添加的重复项就越多。

更快的版本永远不会将重复的状态推送到队列中。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。