減一技術,與二分搜尋一樣,是一種通用演算法設計技術。它是分治法的一種特殊形式,通過建立問題執行個體P(n) 與問題執行個體P(n-1)的遞推求解關係式而實現;最經典的例子莫過於插入排序了。這裡,給出減一技術在產生排列組合方面的應用。
(一) 排列問題: 產生自然數 1,2,,,,,n 的所有排列。
演算法描述:
使用減一技術,建立自然數12...n的排列與12...n-1的遞推關係。假設 P(n-1) 是 自然數 12...n-1的所有排列 p1, p2,..., p(m)的集合,則P(n)通過如下方式得到: 對所有排列p1, p2, ... , p(m) , 將 n 插入到 這些排列的 n 個位置上,得到的所有排列。 例如 12的排列為 12, 21, 則123的排列通過如下方式得到:
1. 將3插入到12中,得到 312,132,123;2. 將3 插入到21中,得到 321, 231, 213. 通過小例子往往能夠為問題的求解指明道路。
“最小變化”要求: 有時,要求產生的所有排列中,相鄰排列只有兩個相鄰位置不同。比如1234和1324是滿足的,而1234和1432則不滿足。上述方法產生的排列 312, 132,123, 321, 231,213 , 123和321就不滿足這個要求。解決方案是,當對排列pi採用從左往右插入完成時,對相鄰排列pi+1採用從右往左插入。比如1中將3插入排列12是從左往右插入;則2中應該將3從右往左插入,得到213,231,321,這樣,與前面的312,132,123就滿足最小變化要求了。
詳細設計:
1. 輸入: 自然數 n
2. 輸出: 所有排列的集合,每個排列用一個鏈表來表示(考慮到插入操作); 所有排列用鏈表的可變列表(ArrayList)來表示。之所以採用這樣的方式,考慮到之後可能要取出排列進行求解,比如分配問題。代價是空間效率很低。
3. 資料結構: 使用隊列來儲存 n-1 的所有排列; 然後取出隊列中的每個元素, 將 n 插入到其中,得到 n 的一個排列。
Java 代碼實現:
package algorithm.permutation;<br />import java.util.ArrayList;<br />import java.util.Iterator;<br />import java.util.LinkedList;<br />/**<br /> * Permutation:<br /> * Generate all the permutations of a given number.<br /> *<br /> * 產生 n 個數 的全排列。<br /> *<br /> */<br />public class Permutation {</p><p> /**<br /> * 每一個排列使用一個 LinkedList<Integer> 來儲存,<br /> * 使用 LinkedList<Integer> 的 列表來儲存所有的排列<br /> *<br /> */</p><p>private ArrayList<LinkedList<Integer>> perms;</p><p>/**<br /> * 使用 flag 作為交替 從左往右 和 從右往左 掃描的 標誌<br /> * 這樣,可以實現排列“最小變化”的要求。<br /> * 即:每相鄰的兩個排列,只有兩個相鄰位置的不同。<br /> *<br /> */</p><p>private int flag = 1;</p><p>/** 構造器 */</p><p>public Permutation() {</p><p>if (perms == null)<br />perms = new ArrayList<LinkedList<Integer>>();</p><p>}</p><p>/** 產生 n 個數的全排列,並儲存在 perms 中 */</p><p>private void createPerm(int n) {</p><p>if (n <= 0)<br />throw new IllegalArgumentException();</p><p>if (n == 1) {<br />LinkedList<Integer> init = new LinkedList<Integer>();<br />init.add(1);<br />perms.add(init);<br />}<br />else {<br /> createPerm(n-1);</p><p> // 對每一個 n-1 的排列P(n-1), 將 n 插入到該排列 P(n-1) 的 n 個可能位置上,<br /> // 即可得到 n 個 相應的 n 元素排列 P(n)</p><p> int length = perms.size();<br /> for (int i=0; i < length; i++) {<br /> LinkedList<Integer> p = perms.get(i);<br /> if (flag == 0) {</p><p> // flag = 0: 從左向右掃描插入</p><p> for (int j=0; j <= p.size(); j++) {<br /> LinkedList<Integer> pcopy = copylist(p);<br /> pcopy.add(j, n);<br /> perms.add(pcopy);<br /> flag = 1;<br /> }</p><p> }<br /> else {</p><p> // flag = 1: 從右向左掃描插入</p><p> for (int j=p.size(); j >=0; j--) {<br /> LinkedList<Integer> pcopy = copylist(p);<br /> pcopy.add(j, n);<br /> perms.add(pcopy);<br /> flag = 0;<br /> }</p><p> }<br /> } </p><p> // 刪除所有的 P(n-1) 排列</p><p> for (int i=0; i < length; i++) {<br /> perms.remove(0);<br /> }</p><p>}</p><p>}</p><p>/** 擷取 n 個元素的全排列 */</p><p>public ArrayList<LinkedList<Integer>> getPerm(int n) {</p><p>createPerm(n);<br />return perms;<br />}</p><p>/** 複製 list 的元素到另一個列表, 並返回該列表 */</p><p>private LinkedList<Integer> copylist(LinkedList<Integer> list) {</p><p>LinkedList<Integer> copylist = new LinkedList<Integer>();<br />Iterator iter = list.iterator();<br />while (iter.hasNext()) {<br />Integer i = (Integer) iter.next();<br />copylist.add(i);<br />}<br />return copylist;<br />}<br />}<br />
(二) 產生給定集合 {1,2,...,n} 的冪集,即給定自然數3,要產生其冪集: { {0}, {1}, {2}, {1,2}, {3}, {1,3},{2,3},{1,2,3} }
演算法描述:
使用減一技術,建立問題執行個體P(n)與問題執行個體P(n-1)的遞推求解關係。若P(n-1)是問題執行個體n-1的冪集,則問題執行個體n的冪集通過如下方式得到: powerset(n) = powerset(n-1) + powerset(n-1)∪ {n} ,即,對於n-1的冪集的每一個集合與{n}求並集,然後將得到的集合與n-1的冪集求並集。例如 {1} 的冪集為 {{0}, {1}} ,則 {1,2} 的冪集為 { {2} {1,2}, {0},
{1} } ,其中 {0} 表示空集, {0} ∪ {n} = {n}
詳細設計:
1. 輸入: 自然數 n
2. 輸出: {1,2,..,n}的冪集,每一個子集使用一個LinkedList來表示,冪集使用LinkedList的可變列表ArrayList來表示和儲存。
Java代碼實現:
package algorithm.permutation;<br />import java.util.ArrayList;<br />import java.util.Iterator;<br />import java.util.LinkedList;<br />/**<br /> * PowerSet<br /> * Generate all the subsets of a given set.<br /> *<br /> * 產生給定集合的所有子集。<br /> *<br /> */<br />public class PowerSet {</p><p>/**<br /> * 每一個子集使用一個 LinkedList<Integer> 來儲存,<br /> * 使用 LinkedList<Integer> 的 列表來儲存所有的子集<br /> *<br /> */</p><p>private ArrayList<LinkedList<Integer>> powerset;</p><p> /** 構造器 */</p><p>public PowerSet() {</p><p>if (powerset == null)<br />powerset = new ArrayList<LinkedList<Integer>>();</p><p>}</p><p>/** 產生 {1,2,3,……, n} 的 冪集 */</p><p>private void createPowerset(int n) {</p><p>if (n < 0)<br />throw new IllegalArgumentException();</p><p>if (n == 0) {<br />LinkedList<Integer> empty = new LinkedList<Integer>();<br />powerset.add(empty);<br />}</p><p>if (n == 1) {<br />LinkedList<Integer> empty = new LinkedList<Integer>();<br />LinkedList<Integer> init = new LinkedList<Integer>();<br />init.add(1);<br />powerset.add(empty);<br />powerset.add(init);<br />}<br />else {<br />createPowerset(n-1);</p><p>// powerset(n) = powerset(n-1) + powerset(n-1)∪ {n}</p><p> int length = powerset.size();<br /> for (int i=0; i < length; i++) {<br /> LinkedList<Integer> p = powerset.get(i);<br /> LinkedList<Integer> pcopy = copylist(p);<br /> pcopy.add(p.size(), n);<br /> powerset.add(pcopy);</p><p> } </p><p> }</p><p> }<br />/** 擷取 n 個元素集合的冪集 */</p><p>public ArrayList<LinkedList<Integer>> getPowerset(int n) {</p><p>createPowerset(n);<br />return powerset;<br />}<br />/** 複製 list 的元素到另一個列表, 並返回該列表 */</p><p>private LinkedList<Integer> copylist(LinkedList<Integer> list) {</p><p>LinkedList<Integer> copylist = new LinkedList<Integer>();<br />Iterator iter = list.iterator();<br />while (iter.hasNext()) {<br />Integer i = (Integer) iter.next();<br />copylist.add(i);<br />}<br />return copylist;<br />}</p><p>}<br />
演算法分析:
減一技術的時間複雜度通常是: T(n) = T(n-1) + G(n) .
① 若 G(n) 為常數,則 T(n) = O(n) ; (連續子數組的最大和)
② 若 G(n) 為 O(logn), 則 T(n) = O(nlogn) ; (堆排序)
③ 若 G(n) = an+b(a!=0) ,則 T(n) = O(n^2) . (插入排序)
因此, 當 G(n) 為常數或對數時, 減一技術是比較高效的。