全排列、组合、组合总和、子集

声明:把递归参数不变的,尽量统一设置为成员变量。

一、全排列

全排列问题求解体系基本上分为两大类,一是基于选择,二是基于交换。

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));
			}
		}
	}
}

二、组合

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
无重复元素,基于选择:

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);
    	}
    }
}

除了全排列有基于交换的方法,其余的基本都使用基于选择的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值