回溯算法、动态规划的分析框架

回溯算法 (backtracking)和动态规划(dynamic programming)的思路可以解决很多类型的算法题。特地整理一下两种思路的框架、应用前提和例题分析。

这篇文章的目标不是为某些算法题寻找时间、空间最优的解决方案,而是提供一种分析思路的框架。有了成型的框架之后,我们可以针对具体问题进行优化。

问题建模

拿过一道算法题,第一步是对它进行建模:

class ProblemExample(object):
    def __init__(self, *args):
        # initialize the parameters in problem
        ...
    def init_state(self):
        # where to start
        return ...
    def is_end(self, state) -> bool:
        # check if this state corresponds to a end state
        ...
        return ...
    def is_valid(self, action) -> bool:
        # check if this action could satisfy the constraints of pbm
        ...
        return ...
    def succ_cost(self, state):
        # return (action, new_state, cost), based on the current state
        # sometimes, no need to return all this triplets.
        ...
        return ...

以 Leetcode 第46题为例:
给定一个不含重复数字的数组nums,返回其所有可能的全排列。可以按任意顺序返回答案。

我对这个问题的建模是, state 代表当前的列表;action 代表下一步要加入该列表中的数字;该问题不考虑 cost。程序如下:

class Permutations(object):
    # state: the current list of numbers
    # action: the next number to be added into list
    # cost: no cost
    def __init__(self, nums):
        self.nums = nums
    def init_state(self):
    	# empty list as init state
        return []
    def is_end(self, state):
        if len(state) == len(self.nums):
            return True
        return False
    def is_valid(self, action, state):
    	# if "action" is already in "state", we can not add it.
        if action in state:
            return False
        return True
    def succ_cost(self, state):
        nexts = []
        for i in self.nums:
            if self.is_valid(i, state):
                nexts.append(state+[i])
        return nexts

回溯算法

回溯算法的思路说起来很简单:利用函数的递归调用(自己调用自己),遍历所有的可能性。如果所在的分支已经不满足限制条件,就回溯到母节点,继续寻找。

所以,回溯算法有两个要点:终止条件、选择下一步的action的限制条件

当前状态如果满足终止条件,取决于具体的问题,要么在solutions里添加一个解;要么比较当前的解与之前最好的解的 cost,如有必要,更新最好的解。如果状态不满足终止条件,那么在当前状态下继续寻找所有可能的 action,在 new_state 的基础上递归调用

下面是一个回溯算法的整体框架:

def backtracking_search(problem):
    best = {
        'cost': float('+inf'),
        'history': None
    }
    def recurse(state, history, total_cost):
        # At state, having undergone history, accumulated totalCost
        # Explore the rest of the subtree under state
        if problem.is_end(state):
            # update
            if total_cost < best['cost']:
                best['cost'] = total_cost
                best['histoty'] = history
            return
        # recurse on children
        for action, new_state, cost in problem.succ_cost(state):
           	recurse(new_state, history+[(action, new_state, cost)], total_cost+cost)

    recurse(problem.init_state(), history=[], total_cost=0)
    return (best['cost'], best['history'])

Leetcode N·46

上面的框架看起来有些抽象。实际上,在解决不同问题时,需要对它进行一些调整。比如,要解决 Leetcode 第46题,可以给出如下框架:

def backtracking_search(problem):
    solutions = []
    def recurse(state):
        # At state, having undergone history, accumulated totalCost
        # Explore the rest of the subtree under state
        if problem.is_end(state):
            # update
            solutions.append(state)
            return
        # recurse on children
        for new_state in problem.succ_cost(state):
            recurse(new_state)

    recurse(problem.init_state())
    return solutions

好,现在我们只需要实例化一个 Permutations 问题,把该实例传入回溯算法即可:

fc = FullCombination(nums=[1,2,3])
backtracking_search(fc)
>>> [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

当然,如果思路非常清晰,我们完全可以抛开框架,直接解决 Permutations 问题:

def FullCombination_backtracking(nums):
    solutions = []
    def recurse(state):
        if len(state) == len(nums):
            solutions.append(state)
            return
        for i in nums:
            if i not in state:
                recurse(state+[i])
    recurse(state=[])
    return solutions

Leetcode N·22

数字 n 代表生成括号的对数,请你设计一个函数,生成所有可能并且有效的括号组合。

该问题没有 costaction space 等于 {'(', ')'}state 是三元组:当前所剩的左括号个数、当前所剩右括号个数,以及当前字符串。当然,这个state可以简化,由当前字符串长度以及剩余左括号个数,可以推出所剩右括号个数。
is_end 函数返回 True 当且仅当当前字符串长度等于 2n

is_valid 函数考虑当前应该选择左括号还是右括号,还是两个都可以选择。 注意,为了满足限制(生成的括号组合是有效的),当剩余左括号数量小于或等于右括号数量时,必须选择左括号;否则两个都可以选择。

程序:

def generate_parentheses(n):
    solutions = []
    def backTracking(l_remain, r_remain, str_current):
        if len(str_current) == 2*n:
            solutions.append(str_current)
            return 
        if l_remain > 0:
            backTracking(l_remain-1, r_remain, str_current + "(")
        if r_remain > l_remain:
            backTracking(l_remain, r_remain-1, str_current + ")")
    backTracking(n, n, "")
    return solutions

Leetcode N·77

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

该问题没有 cost
action 可以是 [1, n] 范围内的整数。
state :选择起始点 start(当前可以从这个数字开始选择 action,它是为了避免重复组合,如 (1,2), (2,1)、当前的列表(它包含已经选择的数字)。

is_end 函数返回 True 当且仅当当前列表长度等于 k

is_valid 考虑的是限制条件。题干里没有显式限制,但包含了一个隐形限制:组合不能重复。start 这个状态分量就是用于辅助避免重复的,可选的 action 只能是大于 start 的数。

def KCombination(n, k):
    combinations = []
    def recurse(start, history):
        if len(history) == k:
            combinations.append(history)
        for i in range(start+1, n+1):
            if i not in history:
                recurse(i, history+[i])
    recurse(start=0, history=[])
    return combinations

动态规划

动态规划的基本思路是,递归地把问题拆分成许多子问题,构建母问题与子问题之间的依赖关系,子问题解决了,母问题也就解决了。

动态规划有一个最基本的前提假设:当前状态能够反映过往所有 action 的信息;因此,知道当前状态就可以优化地选择未来的 action

具体来说:
F u t u r e C o s t ( s t a t e ) = m i n a c t i o n { c o s t ( s t a t e , a c t i o n ) + F u t u r e C o s t ( n e w _ s t a t e ) } FutureCost(state) = min_{action} \{ cost(state, action) + FutureCost(new\_state) \} FutureCost(state)=minaction{cost(state,action)+FutureCost(new_state)}

动态规划的框架:

# Framework 
def dynamic_programming(problem):
    cache = {} # store the result (future cost) of states already seen
    def future_cost(state):
        if problem.isEnd(state):
            return 0
        if state in cache:
            return cache[state]
        result = min(cost + future_cost(newState) 
                    for action, newState, cost in problem.succ_cost(state))
        cache[state] = result
        return result
    return future_cost(problem.initState())

你可能感兴趣的:(算法,动态规划,leetcode)