记忆化搜索

定义

  记忆化搜索其实很好理解。当我们用DFS,BFS暴力搜索的时候,有很多状态其实是重复计算了很多次的,这时候,我们就可以用空间换取时间,将这些状态全都装在数组里,当我们再次搜索到该状态的时候,便可以直接返回记录的值。

例题

e.g.1 检查是否有合法括号字符串路径

  这道题就是万恶之源,本来我也不怎么想去学记忆化搜索的,毕竟暴搜不用动脑子嘛。但是在这场周赛中某蒟蒻遗憾2题,很懊恼,便学了记忆化(暂时不想碰dp)。
  这题嘛,主要难点在怎么有效率地处理合法括号。一开始,我的想法是憨憨栈加DFS暴搜,过不了是必然的。于是,赛后就学到了一种巧妙的记录状态的方法,+1/-1。
  接下来就是愉快的代码实现环节。由于Python是真的香,我这次的代码是py版本的。

class Solution:
    def hasValidPath(self, grid: List[List[str]]) -> bool:
        row,col=len(grid),len(grid[0])
        if(row+col)%2==0 or grid[row-1][col-1]=='(' or grid[0][0]==')': return False
        @lru_cache(None)  #将每次计算结果记录在缓存里,相当于visit数组
        def dfs(x:int,y:int,c:int)->bool:
            if x==row-1 and y==col-1:return c==1
            if row-x+col-y-1<c:return False
            c+=1 if grid[x][y]=='(' else -1
            return c>=0 and (x<row-1 and dfs(x+1,y,c) or y<col-1 and dfs(x,y+1,c))
        return dfs(0,0,0)

e.g.2 网格中的最短路径

  这题也是一道极好的记忆化例题。前面是个DFS,这里来一道BFS(此题DFS会T,因为DFS的绕路太多,而BFS扩散法可以有效减少绕路的情况)。

class Solution:
    def shortestPath(self, grid: List[List[int]], k: int) -> int:
        row,col=len(grid),len(grid[0])
        if row==1 and col==1:return 0
        k=min(k,row+col-3)
        q=collections.deque([(0,0,k)])
        visit=set([(0,0,k)])
        step=0
        while q:
            step+=1
            cur_len=len(q)
            for _ in range(cur_len):
                x,y,c=q.popleft()
                for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
                    nx,ny=x+dx,y+dy
                    if 0<=nx<row and 0<=ny<col:
                        if grid[nx][ny]==0 and (nx,ny,c) not in visit:
                            if nx==row-1 and ny==col-1:
                                return step
                            visit.add((nx,ny,c))
                            q.append((nx,ny,c))
                        elif grid[nx][ny]==1 and c>0 and (nx,ny,c-1) not in visit:
                            visit.add((nx,ny,c-1))
                            q.append((nx,ny,c-1))
        return -1

e.g.3 切披萨的方案数

  这题巧妙融合了二维前缀和与记忆化搜索,是道好题。

  • 主要思路是DFS+记忆化记录还需要分几块(状态)
  • 拿到题第一个想法就是二维前缀和,但是后来发现用右下角前缀和的话在切的时候需要倒序遍历(写起来有点麻烦),所以就学了个左上角前缀和
class Solution:
    def ways(self, pizza: List[str], k: int) -> int:
        row,col=len(pizza),len(pizza[0])
        pre_apple=[[0 for _ in range(col)]for _ in range(row)]
        for i in range(row-1,-1,-1):
           for j in range(col-1,-1,-1):
                if i==row-1 and j==col-1:
                   pre_apple[i][j]=1 if pizza[i][j]=='A' else 0
                elif i==row-1:
                    pre_apple[i][j]=pre_apple[i][j+1]+1 if pizza[i][j]=='A' else pre_apple[i][j+1]
                elif j==col-1:
                    pre_apple[i][j]=pre_apple[i+1][j]+1 if pizza[i][j]=='A' else pre_apple[i+1][j]
                else:
                    pre_apple[i][j]=pre_apple[i+1][j]+pre_apple[i][j+1]-pre_apple[i+1][j+1]
                    if pizza[i][j]=='A':
                        pre_apple[i][j]+=1
        dp = [[[-1 for _ in range(k+1)] for _ in range(col) ]for _ in range(row)]
        def dfs(x,y,c:int)->int:
            apple=pre_apple[x][y]
            if dp[x][y][c]!=-1: #经历过这个状态,则直接返回相应值
                return dp[x][y][c]
            if pre_apple[x][y]<c:  #剪枝,如果接下来的苹果不够c人分就返回
                return 0
            if c==1:               #剩下一块不用切
                return 1
            dp[x][y][c]=0
            for i in range(x+1,row):
                if pre_apple[i][y]<apple:   #如果切下来的里面有苹果
                    dp[x][y][c]+=dfs(i,y,c-1)
            for i in range(y+1,col):
                if pre_apple[x][i]<apple:
                    dp[x][y][c]+=dfs(x,i,c-1)
            return dp[x][y][c]%(10**9+7)
        return dfs(0,0,k)

e.g.4 滑雪

  也是记忆化搜索比较经典的题目。记录每个搜到的点的可行步数状态。

static constexpr int dir[4][2] = { {-1,0},{0,-1},{1,0},{0,1} };
int main() {
    int row = read(),col = read();
    vector<vector<int>> grid(row, vector<int>(col));
    vector<vector<int>> dp(row, vector<int>(col, 0));
    for (int i = 0; i < row; ++i) {
        for (int j = 0; j < col; ++j) {
            grid[i][j] = read();
        }
    }
    function<int(int, int)> dfs = [&](int x, int y)->int {
        if (dp[x][y])return dp[x][y];
        dp[x][y] = 1;
        for (int i = 0; i < 4; ++i) {
            int nx = x + dir[i][0], ny = y + dir[i][1];
            if (nx >= 0 and nx < row and ny >= 0 and ny<col and grid[x][y]>grid[nx][ny]) {
                grid[x][y] = max(dp[x][y], 1 + dp[nx][ny]);
            }
        }
        return dp[x][y];
    };
    int ans = 0;
    for (int i = 0; i < row; ++i) {
        for (int j = 0; j < col; ++j) {
            ans = max(dfs(i, j), ans);
        }
    }
    printf("%d", ans);

    return 0;
}

e,g.5 火柴拼正方形

  由于这是一道medium,朴素暴搜\(O({4}^{n})\)还是可以过这道题的。当然,我们是要学习的,怎么能不用一下记忆化呢。
  记忆化搜索的复杂度为\(O(n*{2}^{n})\),每根棍子有\({2}^{n}\)个状态(其他棍子选或不选)。

class Solution:
    def makesquare(self, matchsticks: List[int]) -> bool:
        c=sum(matchsticks)
        if c%4!=0:return False
        h,n=c//4,len(matchsticks)
        final=(1<<n)-1
        @lru_cache(None)
        def dfs(state,cur_l):
            if cur_l==h:
                cur_l=0
                if state==final:return True
            for i in range(n):
                if not 1<<i & state and cur_l+matchsticks[i]<=h:
                    if dfs(1<<i|state,cur_l+matchsticks[i]):return True
            return False
        return dfs(0,0)
posted @ 2022-05-10 20:31  Wasser007  阅读(83)  评论(0编辑  收藏  举报