상세한 되돌아 알고리즘 [전원 버튼 46 : 전체 어레이]

사실, 의사 결정 트리 탐색을 문제를 해결하기 위해 역 추적. 당신은 세 가지 질문에 대해 생각해야합니다 :

1, 경로 :이며, 선택을했다.

목록을 선택하십시오 :, 당신은 현재의 선택을 할 수 있습니다.

3, 마지막 조건 :이며, 조건을 선택하지, 의사 결정 트리의 하단에 도달.

당신이이 세 단어의 설명을 이해하지 못한다면 우리가 당신은이 단어가 무슨 뜻인지 이제 첫번째 킵 이해 "전체 배열"과 "도움말 알고리즘을 역 추적의 N 여왕 '이 고전적인 문제를 사용, 그것은 중요하지 않아 인상.

코드 측, 알고리즘 프레임 워크를 역 추적 :

결과 = [] 
: DEF BackTrack의 (경로 선택리스트) 
    경우는 종료 조건이 만족된다 
        result.add (경로) 
        
    
    을위한 선택 에서 선택 목록 : 
        선택할 
        BackTrack의 (경로 선택에서) 
        선택 취소

그것의 핵심은 특히 간단한 "메이크업의 선택", 재귀 호출 후 "선택 해제"로 재귀 호출하기 전에, 재귀 루프 내부입니다.

선택과 선택 해제,이 프레임 워크의 기본 원칙이 무엇이다는 무엇입니까? 여기에서 우리는이 문제를 비밀에 대한 자세한 문의를 통해 해결 될 전에 "전체 배열이"의아해있다!

첫째, 전체 배열의 문제를
우리가 고등학교 순열과 조합의 계산을 수행 한, 우리는 또한 알고 비 반복의 n 개의! n 번째의 총의 전체 배열.

추신 : 단순하고 명확하게하기 위해, 모든 문제는 우리는이 논의가 중복되는 번호가 포함되어 있지 않습니다 배열.

우리는 어떻게 철저한으로 가득 배열을했다? 3하자 말의 수 [1,2,3], 당신이 확실히 불규칙하지 철저한 혼란이 같은 보통 일 것입니다 :

먼저 고정 제 비트, 다음 제 3 개만 인 후 제 2 일 수 있고, 1 일 후 3 번째 위치에 설정할 수 3 위은 2 것까지, 다음 만 변화 할 수 ...... 처음 두 후에는 완전한 2가되고,

사실,이 직접 나무 뒤로 나무 아래에 그리는 알고리즘, 교사없이 우리의 고등학교, 사용할 수 있습니다 또는 일부 학생들을 역 추적한다 :

 

 

 

언제 까지나 사실에 트리 탐색, 루트에서 경로에 디지털 기록, 전체 배열의 모든있다. 우리는 호출 트리 역 추적 알고리즘을 얻고 자하는 "의사 결정 트리를."

실제로 각 노드에서 의사 결정을하기 때문에 왜이 의사 결정 트리라고 말할 않습니다. 예를 들어, 아래 그림에서 빨간 노드에 서있다 :

 

 

 

당신은 결정을 내릴 지금, 당신은 나뭇 가지의 조각을 선택할 수 있습니다, 가지도 3 조각을 선택할 수 있습니다. 이유는 단지 할 일 1 ~ 3을 선택할 수 있습니다? 때문에 뒤에이 두 가지, 전체 배열이 다시 사용 번호에 허용되지 않는 동안, 전에했던 선택.

이제 당신은 처음 몇 용어에 응답 할 수 있습니다 : [2]을 "경로", 당신이 수행 한 기록을 선택하고, [1,3]은 "선택 목록은"당신이 할 수있는 현재의 선택은 대표된다 "최종 조건은" 목록이 비어있을 때 나무의 아래로 통과, 여기에 선택입니다.

당신이이 용어를 이해한다면, 당신은 여러 노드의 속성 다음 차트 목록과 같은 트리의 각 노드의 속성과 같은 목록 "을 선택" "경로"를 넣을 수 있습니다 :

 

 

 

우리는 역 추적 기능은 각 노드의 정확한 특성을 유지하면서, 나무마다의 아래로 내려 가서, 단지 포인터,이 나무 거리처럼 정의의 "경로"전체 순서입니다.

또한, 어떻게 트리를 탐색하는? 이것은 어렵지 않을 것이다. 리콜은 전에 "생각의 프레임의 학습 데이터 구조,"다중 분기 나무의 프레임 워크를 통과하는 것은 다음과 같이있는 동안 다양한 검색 문제, 사실은 트리 탐색 문제입니다 쓴 :

무효 트래버스합니다 (TreeNode를 루트) {
     에 대한 합니다 (TreeNode를 아이 : root.childern)
         // 예약 주문 탐색 원하는 운영 
        트래버스 (어린이);
         // 작업이 필요 postorder 
}

而所谓的前序遍历和后序遍历,他们只是两个很有用的时间点,我给你画张图你就明白了:

 

 

 

前序遍历的代码在进入某一个节点之前的那个时间点执行,后序遍历代码在离开某个节点之后的那个时间点执行。

回想我们刚才说的,「路径」和「选择」是每个节点的属性,函数在树上游走要正确维护节点的属性,那么就要在这两个特殊时间点搞点动作:

 

 

 

现在,你是否理解了回溯算法的这段核心框架?

for 选择 in 选择列表:
    # 做选择
    将该选择从选择列表移除
    路径.add(选择)
    backtrack(路径, 选择列表)
    # 撤销选择
    路径.remove(选择)
    将该选择再加入选择列表

我们只要在递归之前做出选择,在递归之后撤销刚才的选择,就能正确得到每个节点的选择列表和路径。

下面,直接看全排列代码:

List<List<Integer>> res = new LinkedList<>();

/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
    // 记录「路径」
    LinkedList<Integer> track = new LinkedList<>();
    backtrack(nums, track);
    return res;
}

// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
    // 触发结束条件
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的选择
        if (track.contains(nums[i]))
            continue;
        // 做选择
        track.add(nums[i]);
        // 进入下一层决策树
        backtrack(nums, track);
        // 取消选择
        track.removeLast();
    }
}

 

我们这里稍微做了些变通,没有显式记录「选择列表」,而是通过 nums 和 track 推导出当前的选择列表:

 

 

 

至此,我们就通过全排列问题详解了回溯算法的底层原理。当然,这个算法解决全排列不是很高效,应为对链表使用 contains 方法需要 O(N) 的时间复杂度。有更好的方法通过交换元素达到目的,但是难理解一些,这里就不写了,有兴趣可以自行搜索一下。

但是必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。


链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-xiang-jie-by-labuladong-2/
来源:力扣(LeetCode)

추천

출처www.cnblogs.com/lixuejian/p/12119281.html