全排列、組合、組合總和、子集

聲明:把遞歸參數不變的,儘量統一設置爲成員變量。

一、全排列

全排列問題求解體系基本上分爲兩大類,一是基於選擇,二是基於交換。

1.1 全排列1:無重複

1.1.1 基於選擇的經典解法

優點是好記,遞歸時每次循環都從 0 開始。

class Solution {
	private int[] nums;
	private List<List<Integer>> result;
	
    public List<List<Integer>> permute(int[] nums) {
    	this.nums = nums;
        result = new ArrayList<>();
        boolean[] used = new boolean[nums.length];
        backtrack(new ArrayList<Integer>(), used);
        return result;
    }
    
    private void backtrack(List<Integer> temp, boolean[] used){
    	if(temp.size() == nums.length){
    		result.add(new ArrayList<>(temp));
    		return;
    	}
    	for(int i = 0; i < nums.length; ++i){
    		if(used[i]) continue;
    		used[i] = true;
    		temp.add(nums[i]);
    		backtrack(temp, used);
    		temp.remove(nums[i]);
    		used[i] = false;
    	}
    }
}

1.1.2 基於交換的經典解法

class Solution {
	private List<List<Integer>> result;
	
    public List<List<Integer>> permute(int[] nums) {
        result = new ArrayList<>();
        List<Integer> temp = new ArrayList<>();
        for(int e : nums) temp.add(e);
        backtrack(temp, 0);
        return result;
    }
    
    private void backtrack(List<Integer> temp, int start){
    	if(start == temp.size()) result.add(new ArrayList<>(temp));
    	for(int i = start; i < temp.size(); ++i){
    		Collections.swap(temp, i, start);
    		backtrack(temp, start+1);
    		Collections.swap(temp, i, start);
    	}
    }
}

1.1.3 另一種基於交換的解法

class Solution {
	private List<List<Integer>> result;
	
    public List<List<Integer>> permute(int[] nums) {
        result = new ArrayList<>();
        List<Integer> temp = new ArrayList<>();
        for(int e : nums) temp.add(e);
        backtrack(temp, 0);
        return result;
    }
    
    private void backtrack(List<Integer> temp, int start){
    	result.add(new ArrayList<>(temp));
    	for(int i = start; i < temp.size(); ++i) {
    		for(int j = i+1; j < temp.size(); ++j) {
    			Collections.swap(temp, i, j);
    			backtrack(temp, i+1);
            	Collections.swap(temp, i, j);
    		}
    	}
    }
}

1.2 全排列2:有重複

1.2.1 基於選擇的解法

這種解法,現在又不好記了,因爲去重的判斷比較複雜。

class Solution {
	private int[] nums;
	private List<List<Integer>> result;

	public List<List<Integer>> permuteUnique(int[] nums) {
		this.nums = nums;
		result = new ArrayList<>();
		boolean[] used = new boolean[nums.length];
		Arrays.sort(nums);
		backtrack(new ArrayList<>(), used);
		return result;
	}

	private void backtrack(List<Integer> temp, boolean[] used){
		if(temp.size() == nums.length){
			result.add(new ArrayList<>(temp));
			return;
		}
		for(int i = 0; i < nums.length; ++i){
			if(used[i]) continue;
			if(i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;
			used[i] = true;
			temp.add(nums[i]);
			backtrack(temp, used);
			used[i] = false;
			temp.remove(temp.size()-1);
		}
	}
}

1.2.2 基於交換的解法(不能通過排序去重,因爲交換會打亂有序性)

有重複全排列的推薦解法!!!

class Solution {
	private List<List<Integer>> result;
	
    public List<List<Integer>> permuteUnique(int[] nums) {
    	result = new ArrayList<>();
    	List<Integer> temp = new ArrayList<>();
    	for(int num : nums) temp.add(num);
    	backTrack(temp, 0);
        return result;
    }
    
    public void backTrack(List<Integer> temp, int start) {
    	if(start == temp.size()) result.add(new ArrayList<>(temp));
    	Set<Integer> set = new HashSet<>(); // 數據較少的情況下,for 循環判重更好
    	for(int i = start; i < temp.size(); ++i){
    		if(set.contains(temp.get(i))) continue;
    		Collections.swap(temp, i, start);
    		backTrack(temp, start+1);
    		Collections.swap(temp, i, start);
    		set.add(temp.get(i));
    	}
    }
}

枚舉當前層級的候選集時,第一個 2 已經和 1 交換,如果還讓第二個 2 和 1 交換,那麼下步交換就會導致重複。
重複情況
用數學語言嚴謹地描述一下:
約定:[start, …] 表示 start 及其以後的所有元素。

枚舉當前層級的候選集時,分別用 [start, …] 和 start 交換,如果 [start, …] 中有兩個相同的元素分別和 start 交換,會得到兩個候選集,這兩個候選集的 [0, start] 部分是完全相等的,[start+1, …] 部分除了順序外也是相等的。這兩個候選集在後續層級的遞歸中,會分別得到 [start+1, …] 的全排列,因爲 start 只會變大,[0, start] 部分不會改變。所以這兩個候選集就會得到兩個相同的結果子集,即重複。

1.2.3 基於交換的另一種解法:

class Solution {
	private List<List<Integer>> result;
	
	public List<List<Integer>> permuteUnique(int[] nums) {
		result = new ArrayList<>();
		List<Integer> temp = new ArrayList<>();
		for(int e : nums) temp.add(e);
		backtrack(temp, 0);
		return result;
	}
	
	private void backtrack(List<Integer> temp, int li) {
		result.add(new ArrayList<>(temp));
		for(int i = li; i < temp.size(); ++i) {
            Set<Integer> set = new HashSet<>();
            set.add(temp.get(i));
			for(int j = i+1; j < temp.size(); ++j) {
				if(set.contains(temp.get(j))) continue;
				Collections.swap(temp, i, j);
				backtrack(temp, i+1);
				Collections.swap(temp, i, j);
				set.add(temp.get(j));
			}
		}
	}
}

二、組合

無重複元素,基於選擇:

class Solution {
    private List<List<Integer>> result;

    public List<List<Integer>> combine(int n, int k) {
        result = new ArrayList<>();
        backtrack(n, k, 1, new ArrayList<>(k));
        return result;
    }

    private void backtrack(int n, int k, int start, List<Integer> temp){
        if(k == 0){
            result.add(new ArrayList<>(temp));
            return;
        }
        for(int i = start; i <= n-k+1; ++i){
            temp.add(i);
            backtrack(n, k-1, i+1, temp);
            temp.remove(temp.size()-1);
        }
    }
}

三、組合總和

和爲給定值的所有組合。

3.1 組合總和1:無重複元素,元素可以多次被選中

class Solution {
    private List<List<Integer>> result;
    private int[] candidates;

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        result = new ArrayList<>();
        this.candidates = candidates;
        backtrack(new ArrayList<Integer>(), 0, target);
        return result;            
    }

    private void backtrack(List<Integer> temp, int start, int target){
        if(target == 0){
            result.add(new ArrayList<>(temp));
            return;
        }
        for(int i = start; i < candidates.length; ++i){
            if(candidates[i] <= target){
                temp.add(candidates[i]);
                backtrack(temp, i, target-candidates[i]);
                temp.remove(temp.size()-1);
            }
        }
    }
}

3.2 組合總和2:有重複元素,每個元素只能使用一次

class Solution {
    private List<List<Integer>> result;
    private int[] candidates;

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        result = new ArrayList<>();
        this.candidates = candidates;
        Arrays.sort(candidates);
        backtrack(target, new ArrayList<>(), 0);
        return result;
    }

    private void backtrack(int target, List<Integer> temp, int start){
        if(target == 0){
            result.add(new ArrayList<>(temp));
            return;
        }
        for(int i = start; i < candidates.length; ++i){
            if(i > start && candidates[i] == candidates[i-1]) continue;
            if(candidates[i] <= target){
                temp.add(candidates[i]);
                backtrack(target-candidates[i], temp, i+1);
                temp.remove(temp.size()-1);
            }else return;
        }
    }
}

四、子集

4.1 子集1:無重複元素

二進制位掩碼

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
		List<List<Integer>> result = new ArrayList<>();
		int n = 1 << nums.length;
		for(int i = 0; i < n; ++i) {
			List<Integer> subSet = new ArrayList<>();
			for(int j = 0; j < nums.length; ++j) {
				if((i & (1 << j)) != 0) {
					subSet.add(nums[j]);
				}
			}
			result.add(subSet);
		}
		return result;
    }
}

4.2 子集2:有重複元素

class Solution {
    private List<List<Integer>> result;
    private int[] nums;
    
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        result = new ArrayList<>();
        this.nums = nums;
        Arrays.sort(nums);
        backtrack(0, new ArrayList<>());
        return result;
    }
    
    private void backtrack(int k, List<Integer> temp){
    	result.add(new ArrayList<>(temp));
    	for(int i = k; i < nums.length; ++i){
    		if(i > k && nums[i] == nums[i-1]) continue;
            temp.add(nums[i]);
            backtrack(i+1, temp); 
            temp.remove(temp.size()-1);
    	}
    }
}

除了全排列有基於交換的方法,其餘的都使用基於選擇的方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章