Skip to content

Project 1: Search

实现各种类型的搜索算法,让吃豆人可以找到目的地(一颗豆子)

Before

为了理解如何实现搜索代码,我们先关注下面的样例代码,其通过硬编码路线完成一个特定迷宫:

1
2
3
4
5
6
7
8
9
def tinyMazeSearch(problem):
    """
    Returns a sequence of moves that solves tinyMaze.  For any other maze, the
    sequence of moves will be incorrect, so only use this for tinyMaze.
    """
    from game import Directions
    s = Directions.SOUTH
    w = Directions.WEST
    return  [s, s, w, s, w, w, s, w]

函数返回一个完整的路径作为搜索后的结果,那么函数如何感知迷宫呢?我们关注这个虚拟类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class SearchProblem:
    """
    This class outlines the structure of a search problem, but doesn't implement
    any of the methods (in object-oriented terminology: an abstract class).

    You do not need to change anything in this class, ever.
    """

    def getStartState(self):
        """
        Returns the start state for the search problem.
        """
        util.raiseNotDefined()

    def isGoalState(self, state):
        """
          state: Search state

        Returns True if and only if the state is a valid goal state.
        """
        util.raiseNotDefined()

    def getSuccessors(self, state):
        """
          state: Search state

        For a given state, this should return a list of triples, (successor,
        action, stepCost), where 'successor' is a successor to the current
        state, 'action' is the action required to get there, and 'stepCost' is
        the incremental cost of expanding to that successor.
        """
        util.raiseNotDefined()

    def getCostOfActions(self, actions):
        """
         actions: A list of actions to take

        This method returns the total cost of a particular sequence of actions.
        The sequence must be composed of legal moves.
        """
        util.raiseNotDefined()

这是一个没有具体实现的虚拟类,搜索函数通过调用这些 "api" 来实时了解当前搜索问题的各种状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 返回起始坐标
def getStartState(self)

# 当且仅当 state 为目标位置,返回 true
def isGoalState(self, state)

# 获取当前位置的所有后继位置(可抵达的位置的相关信息)
# 返回三元组 (successor, action, stepCost) 的列表
# 分别表示后继坐标,移动方向,移动代价
def getSuccessors(self, state)

# 返回某个特定移动序列的总代价值
def getCostOfActions(self, actions)

另外在 util.py 中提供了栈、队列、单调队列的封装实现


我们先考虑这样的搜索问题:从某个位置出发,抵达左下角的最短路径。我们只需要考虑到左下角的最短路

DFS 的思路:将起始节点入栈,循环内始终取出栈顶元素直到栈空,对尚未访问过的后继节点(用 set() 维护)进行访问。注意栈需要同时存储当前坐标和当前完整路径

Solution Q1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def depthFirstSearch(problem: SearchProblem):
    """Search the deepest nodes in the search tree first."""
    "*** YOUR CODE HERE ***"
    st = util.Stack()
    vis = set()
    st.push((problem.getStartState(), []))

    while not st.isEmpty():
        state, path = st.pop()

        if problem.isGoalState(state):
            return path

        if state in vis:
            continue
        vis.add(state)

        for successor, action, _ in problem.getSuccessors(state):
            if(successor not in vis):
                st.push((successor, path + [action]))

    return []

DFS 的思路:将起始节点入队,循环内始终取出队首元素直到队空,对尚未访问过的后继节点(用 set() 维护)进行访问。注意队列需要同时存储当前坐标和当前完整路径

(和 DFS 代码除了边缘的数据结构表示以外完全相同)

Solution Q2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def breadthFirstSearch(problem: SearchProblem):
    """Search the shallowest nodes in the search tree first."""
    "*** YOUR CODE HERE ***"
    q = util.Queue()
    vis = set()
    q.push((problem.getStartState(), []))

    while not q.isEmpty():
        state, path = q.pop()

        if problem.isGoalState(state):
            return path

        if state in vis:
            continue
        vis.add(state)

        for successor, action, _ in problem.getSuccessors(state):
            if(successor not in vis):
                q.push((successor, path + [action]))

    return []

Q3 (3 pts): Varying the Cost Function

UCS 的思路:将起始节点入优先队列,优先队列按照总 cost 排序,循环内始终取出队首元素直到队空,对尚未访问过的后继节点(用 set() 维护)进行访问。注意优先队列需要同时存储当前坐标、当前完整路径、当前总 cost,同时还要存储 priority(虽然等于 cost 但是需要单独存储,pop() 时也不会弹出 priority)

Solution Q3
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def uniformCostSearch(problem: SearchProblem):
    """Search the node of least total cost first."""
    "*** YOUR CODE HERE ***"
    pq = util.PriorityQueue()
    vis = set()
    pq.push((problem.getStartState(), [], 0), 0)

    while not pq.isEmpty():
        state, path, cost = pq.pop()

        if problem.isGoalState(state):
            return path

        if state in vis:
            continue
        vis.add(state)

        for successor, action, stepCost in problem.getSuccessors(state):
            if(successor not in vis):
                pq.push((successor, path + [action], cost + stepCost), cost + stepCost)

    return []

上述实现在下面的两个用例下差异巨大:

1
2
python pacman.py -l mediumDottedMaze -p StayEastSearchAgent
python pacman.py -l mediumScaryMaze -p StayWestSearchAgent

因为其成本函数的设计不同,前者尽量向东走,后者尽量向西走。以 StayEastSearchAgent() 为例,这个成本函数对靠西的位置惩罚更大,因此 Agent 总会尝试选择靠东的路径:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class StayEastSearchAgent(SearchAgent):
    """
    An agent for position search with a cost function that penalizes being in
    positions on the West side of the board.

    The cost function for stepping into a position (x,y) is 1/2^x.
    """
    def __init__(self):
        self.searchFunction = search.uniformCostSearch
        costFn = lambda pos: .5 ** pos[0]
        self.searchType = lambda state: PositionSearchProblem(state, costFn, (1, 1), None, False)

class StayWestSearchAgent(SearchAgent):
    """
    An agent for position search with a cost function that penalizes being in
    positions on the East side of the board.

    The cost function for stepping into a position (x,y) is 2^x.
    """
    def __init__(self):
        self.searchFunction = search.uniformCostSearch
        costFn = lambda pos: 2 ** pos[0]
        self.searchType = lambda state: PositionSearchProblem(state, costFn)

在 UCS 的基础上,对 priority 加上 heuristic(successor)

Solution Q4
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def aStarSearch(problem: SearchProblem, heuristic=nullHeuristic):
    """Search the node that has the lowest combined cost and heuristic first."""
    "*** YOUR CODE HERE ***"
    pq = util.PriorityQueue()
    vis = set()
    pq.push((problem.getStartState(), [], 0), 0)

    while not pq.isEmpty():
        state, path, cost = pq.pop()

        if problem.isGoalState(state):
            return path

        if state in vis:
            continue
        vis.add(state)

        for successor, action, stepCost in problem.getSuccessors(state):
            if(successor not in vis):
                weigh = cost + stepCost
                pq.push((successor, path + [action], weigh), weigh + heuristic(successor, problem))

    return []

Q5 (3 pts): Finding All the Corners