、実際には、決定木のトラバーサルを問題を解決するためにバックトラック。あなただけの3つの質問を考える必要があります:
1、パス:で、選択を行っています。
リストを選択2.:で、あなたは現在の選択を行うことができます。
3、終了条件:決定木の底に到達するためには、条件を選択しないでください。
あなたはこれらの三つの言葉の説明を理解していない場合、我々は「完全な配列」と「あなたはこれらの言葉の意味を理解するためのアルゴリズムをバックトラックのNクイーンは、2つの古典的な問題、今最初のキープを使用するよう、それは、問題ではありません印象。
コード側、アルゴリズムのフレームワークをバックトラック:
=結果[] DEFバックトラック(経路選択リスト): IF 終了条件が満たされる: result.add(パス) のリターン のための選択で選択リスト: 選択する バックトラック(パス選択リスト)を 選択解除
その中核は、「メークの選択」、再帰呼び出しの後、「選択解除」、特に簡単に再帰呼び出しの前に、再帰的なループの内側のためです。
このフレームワークの基本原理は、それ何です、それの選択と選択解除は何ですか?ここでは、この問題は秘密に関する詳細なお問合せを通じて解決される前に、「完全な配列は」困惑持っています!
まず、完全な配列の問題
私たちは高校で順列と組み合わせの数学を行っている、我々はまた、非繰り返しの数n、n個!のThの合計の全体配置することを知っています。
PS:簡単明瞭にするために、我々はこの議論を配置し、すべての問題は、重複する番号が含まれていません。
我々はそれを網羅する方法のように完全な配列でしたか?3へレッツ・発言[1,2,3]の数、あなたは確かに不規則に網羅混沌、このような通常のものではないでしょう。
次いで、3二位になっても、第三の場所はわずか2アップであろう;それだけで変更することができ、第1の固定最初のビットは、第2の第三はわずか3であり、次に、2であることができる、1 ......最初の二つの後、それは、網羅次いで2なり、及び
実際には、これはアルゴリズムをバックトラックされ、教師なしの私たちの高校が利用できるようになります、またはいくつかの学生が直接、ツリーの下位ツリーの下に描画します:
長いツリートラバーサル、ルートから、実際には、すべての全体配置のパス上のデジタル録音と同じくらい。私たちは、と呼ばれる木のバックトラッキングアルゴリズム持つことを望む「決定木を。」
なぜあなたは、実際には各ノード上で意思決定をしているので、これは、決定木であることを言います。たとえば、下図の赤ノードの上に立っています。
あなたが意思決定に今ある、あなたは小枝の部分を選択することができ、枝も3曲を選択することができます。なぜ唯一の1と3 DOの間で選ぶことができますか?あなたの後ろにこの二つの分岐するので、全体の配置を再利用番号に許可されていない一方で、選択はあなたが前にしました。
今、あなたは、最初のいくつかの用語を答えることができます[2]は、「パス」で、あなたがやったレコードを選択し、[1,3]「リストを選ぶ」され、あなたが作ることができ、現在の選択を表し、「終了条件」でありますリストが空の場合、ツリーの下に横断し、ここでの選択があります。
あなたはこれらの用語を理解していれば、あなたは「パス」を入れて、以下のグラフのリストのようなツリー上の各ノードの属性、複数のノードのプロパティとしてリストを「選択」することができます:
私たちは、その「道」はフルオーダーで、各ノードの正しい属性を維持しながら、バックトラック機能だけでポインタのようなものです。このツリーウォークを定義し、ツリーの下に毎回行きます。
さらに、どのようにツリーをトラバースするには?これは難しいことではありません。前の「思考のフレームの学習データ構造、」多分岐木の枠組みを横断しながら、さまざまな検索の問題は、実際にはツリートラバーサルの問題で書いたことを思い出しては、このようなものです:
ボイドトラバース(ツリーノードルート){ ため(ツリーノードの子供:root.childern) // 予約限定トラバーサル所望の動作 トラバース(子); // 操作を必要と後順 }
而所谓的前序遍历和后序遍历,他们只是两个很有用的时间点,我给你画张图你就明白了:
前序遍历的代码在进入某一个节点之前的那个时间点执行,后序遍历代码在离开某个节点之后的那个时间点执行。
回想我们刚才说的,「路径」和「选择」是每个节点的属性,函数在树上游走要正确维护节点的属性,那么就要在这两个特殊时间点搞点动作:
现在,你是否理解了回溯算法的这段核心框架?
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)