排列组合和回溯算法-面试题

排列组合

排列组合通常用于在字符串或序列的排列和组合中,其特点是固定的解法和统一的代码风格。通常有两种方法:第一种是类似动态规划的分期摊还的方式,即保存中间结果,依次附上新元素,产生新的中间结果;第二种是递归法,通常是在递归函数里,使用for循环,遍历所有排列或组合的可能,然后在for循环语句内调用递归函数。

回溯

回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来

,换一条路再试。用回溯算法解决问题的一般步骤为:

1、定义一个解空间,它包含问题的解。

2、利用适于搜索的方法组织解空间。

3、利用深度优先法搜索解空间。

4、利用限界函数避免移动到不可能产生解的子空间。

问题的解空间通常是在搜索问题的解的过程中动态产生的,这是回溯算法的一个重要特性。

方案一

对于长度为n的数组,我们需要依次确认每个位置上的元素。初始问题:从a[0]开始依次确认n个元素。如果我们确认a[0]之后,问题就变成从a[1]开始从剩余元素中选择一个元素确定a[1]。每次选择都有多种可能性,我们依次尝试(尝试后还原),并递归解决选择之后的产生的子问题,并定义出口。

    public List<List<Integer>> permute(int[] nums) {
    	ArrayList<Integer> numsArray=new ArrayList<Integer>();
    	for(int i:nums){
    		numsArray.add(i);
		}
    	Collections.sort(numsArray);
    	List<List<Integer>> res=new ArrayList<List<Integer>>();
    	solve(res,numsArray,0);
		return res;
    }
    private void solve(List<List<Integer>> res,ArrayList<Integer> nums,int index){
    	if(index>=nums.size()){
    		List<Integer> permutation=new ArrayList<Integer>(nums);
    		res.add(permutation);
    	}
    	for(int i=index;i<=nums.size()-1;i++){
    		Collections.swap(nums, i, index);
    		solve(res,nums,index+1);
    		Collections.swap(nums, i, index);
    	}
    }

方案二

用一个标记数组标记元素是否已经使用过,每次从剩余元素中尝试添加新元素到临时解中,当没有新的元素可添加时,临时解就为最终的一个解。此方案比方法一要更加直观。

    public List<List<Integer>> permute(int[] nums) {
    	ArrayList<Integer> numsArray=new ArrayList<Integer>();
    	for(int i:nums){
    		numsArray.add(i);
		}
    	boolean[] used=new boolean[numsArray.size()];
    	Collections.sort(numsArray);
    	List<List<Integer>> res=new ArrayList<List<Integer>>();
    	ArrayList<Integer> subSet=new ArrayList<Integer>();
    	solve(res,numsArray,subSet,used);
		return res;
    }
    private void solve(List<List<Integer>> res,ArrayList<Integer> nums,ArrayList<Integer> subSet,boolean[] used){
    	if(subSet.size()==nums.size()){
    		ArrayList<Integer> clone=new ArrayList<Integer>(subSet);
    		res.add(clone);
    		return;
    	}
    	for(int i=0;i<nums.size();i++){
    		if(used[i])continue;//不能重复使用
    		subSet.add(nums.get(i));//加入新元素,并递归调用下一个元素
    		used[i]=true;
    		solve(res,nums,subSet,used);
    		subSet.remove(subSet.size()-1);//还原
    		used[i]=false;
    	}
    }

分析

相比上一个问题,元素可以重复,因此我们应该在算法中增加相应的判断来避免重复结果。例如元素[1,2,2,3,3,3],在选择添加第一个新元素时,只考虑第一个1,第一个2,第一个3。注:仔细理清楚避免重复的逻辑,后面会多次用到。

    public List<List<Integer>> permuteUnique(int[] nums) {
    	ArrayList<Integer> numsArray=new ArrayList<Integer>();
    	for(int i:nums){
    		numsArray.add(i);
		}
    	boolean[] used=new boolean[numsArray.size()];
    	Collections.sort(numsArray);
    	List<List<Integer>> res=new ArrayList<List<Integer>>();
    	ArrayList<Integer> subSet=new ArrayList<Integer>();
    	solve(res,numsArray,subSet,used);
		return res;
    }
    private void solve(List<List<Integer>> res,ArrayList<Integer> nums,ArrayList<Integer> subSet,boolean[] used){
    	if(subSet.size()==nums.size()){
    		ArrayList<Integer> clone=new ArrayList<Integer>(subSet);
    		res.add(clone);
    		return;
    	}
    	for(int i=0;i<nums.size();i++){
    	    //对于剩余可选元素,那些相同的元素,我们只考虑第一个
    	    //如果前一个元素与当前元素相同,且前一个元素已经使用了(之前的选择),说明当前元素是“剩余可选元素中所有与当前元素相同的元素”的第一个,则可以选择
    	    //如果前一个元素与当前元素相同,但是前一个元素没有使用,说明在此次选择过程中,已经有相同的元素尝试选择过然后还原了,因此将当前元素忽略
    	    if(used[i]||(i>0&&!used[i-1]&&nums.get(i).equals(nums.get(i-1)))) continue;
    		subSet.add(nums.get(i));//加入新元素,并递归调用下一个元素
    		used[i]=true;
    		solve(res,nums,subSet,used);
    		subSet.remove(subSet.size()-1);//还原
    		used[i]=false;
    	}
    }

分析

我们将排列看成一个n位整数,下一个排列组合就是当前整数增加并且保证增量最小。为了保证增量最小,因此我们需要保证变化的范围尽量限制在低位。因此我们采用如下策略:

1、从后往前寻找第一组相邻元素a[i]<a[i+1]。如果没找到,说明当前序列递减(最大整数),下一个排列只需将序列逆转成最小。如果找到,执行2.

2、为了让变化范围最小化,我们需要将a[i]后面“大于a[i]且最接近a[i]的元素”与a[i]交换,交换后的a[i]后面依然是递减序列,为了进一步减小增量,我们将a[i]后面的元素逆序,得到如下算法。

    public void nextPermutation(int[] nums) {
        int index=nums.length-1;
        //寻找第一对非递减序列
        while(index-1>=0&&nums[index-1]>=nums[index]) index--;
        if(index==0){
        	reverse(nums,0,nums.length-1);
        	return;
        }
        int smallerIndex=index-1,change=index;
        //寻找恰当交换元素
        while(change+1<nums.length&&nums[change+1]>nums[smallerIndex])change++;
        int t=nums[smallerIndex];nums[smallerIndex]=nums[change];nums[change]=t;
        reverse(nums,index,nums.length-1);
    }
    private void reverse(int[] nums,int begin,int end){
    	int t;
    	while(begin<end){
    		t=nums[begin];nums[begin]=nums[end];nums[end]=t;
    		begin++;end--;
    	}
    }

分析

方案一:我们可以采用暴力枚举,调用k-1次nextPermutation。我们只需要第k个排列,但是我们却计算了前k个,比较耗时。

方案二:n个不同元素的排列种数位n!,我们将[1,2,3,4,5]变换成[2,1,3,4,5]需要多少次变换呢?答案是4!次。

证明:[1,2,3,4,5]变换成[1,5,4,3,2]需要4!-1次,[1,5,4,3,2]变换成[2,1,3,4,5]需要一次,共4!次。

同理:[2,1,3,4,5]变换成[2,3,1,4,5]需要经过3!次变换。

综上所述:将a[i]与“其后大于a[i]且最接近a[i]的元素”进行交换,即代表(n-i)!,1<=i<=n(在这里下标从1开始)次变换。

因此第k个排列,即为初始排列变换k-1次。得到如下算法

    public String getPermutation(int n, int k) {
        int[] arr=new int[n+1];
        for(int i=1;i<=n;i++){
        	arr[i]=i;
        }
        k=(k-1)%factorial(n);
        int index=1;
        while(k>0){
        	if(k>=factorial(n-index)){
        		int change=index+1;
        		while(arr[change]<arr[index])change++;
        		int t=arr[index];arr[index]=arr[change];arr[change]=t;
        		k-=factorial(n-index);
        	}else{
        		index++;
        	}
        }
        StringBuilder res=new StringBuilder();
        for(int i=1;i<=n;i++){
        	res.append(arr[i]);
        }
        return res.toString();
    }
    private int factorial(int n){
    	int res=1;
    	for(int i=1;i<=n;i++){
    		res*=i;
    	}
    	return res;
    }

分析

我们采用分期摊还的方法,从数字字符串的第一个字符开始扫描,记录之前数字产生的所有组合,然后将当前数字映射的字符附加到之前产生的所有组合中,产生新的结果集。

   public List<String> letterCombinations(String digits) {
        String[] strMap={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
        ArrayList<String> res=new ArrayList<String>();
        if(digits.equals("")||digits==null) return res;
        res.add("");
        for(int i=0;i<digits.length();i++){
        	String map=strMap[digits.charAt(i)-'0'];
        	if(map.equals("")){
        		continue;
        	}
        	ArrayList<String> t=new ArrayList<String>();
        	for(int j=0;j<map.length();j++){
        		for(String str:res){
        			t.add(str+map.charAt(j));
        		}
        	}
        	res=t;
        }
        return res;
    }

分析

类似全排列,采用分期摊还方法,不过需要注意过滤重复组合。

    public List<List<Integer>> combine(int n, int k) {
    	List<List<Integer>> res=new ArrayList<List<Integer>>();
    	int[] nums=new int[n];
    	ArrayList<Integer> r=new ArrayList<Integer>();
    	boolean[] used=new boolean[n];
    	for(int i=0;i<n;i++)
    		nums[i]=i+1;
    	solve(res,r,nums,k,used);
    	return res;
    }
    public void solve(List<List<Integer>> res,ArrayList<Integer> r,int[] nums,int k,boolean[] used){
    	if(r.size()==k){
    		ArrayList<Integer> clone=new ArrayList<Integer>(r);
    		res.add(clone);
    		return ;
    	}
    	int index=r.size();
    	for(int i=index;i<nums.length;i++){
    		if(used[i]||(r.size()!=0&&nums[i]<r.get(r.size()-1)))continue;
    		r.add(nums[i]);
    		used[i]=true;
    		solve(res,r,nums,k,used);
    		r.remove(r.size()-1);
    		used[i]=false;

    	}
    }

分析

先将候选数组排序,我们将问题理解为从a[0]开始选择整数,使得和为target。如果我们选择了a[0],那么问题变换成从a[0]开始选择整数,使得和为target-a[0](因为可以重复选择)。如果没有选择a[0],那么问题变换为从a[1]开始选择整数,使得和为target。因此得到如下递归解法:

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
    	Arrays.sort(candidates);
    	List<List<Integer>> res=new ArrayList<List<Integer>>();
    	ArrayList<Integer> r=new ArrayList<Integer>();
    	solve(res,r,candidates,target,0);
    	return res;
    }
    public void solve(List<List<Integer>> res,ArrayList<Integer> r,int[] candidates,int target,int index){
    	if(target==0){
    		ArrayList<Integer> clone=new ArrayList<Integer>(r);
    		res.add(clone);
    		return;
    	}
    	if(index>=candidates.length||target<candidates[index])return;
    	//选择当前元素
    	r.add(candidates[index]);
    	solve(res, r, candidates, target-candidates[index], index);
    	r.remove(r.size()-1);
    	//不选择当前元素
    	solve(res, r, candidates, target, index+1);

    }

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    	Arrays.sort(candidates);
    	List<List<Integer>> res=new ArrayList<List<Integer>>();
    	ArrayList<Integer> r=new ArrayList<Integer>();
    	solve(res,r,candidates,target,0);
    	return res;
    }
    public void solve(List<List<Integer>> res,ArrayList<Integer> r,int[] candidates,int target,int index){
    	if(target==0){
    		ArrayList<Integer> clone=new ArrayList<Integer>(r);
    		res.add(clone);
    		return;
    	}
    	if(index>=candidates.length||target<candidates[index])return;
    	//选择当前元素
    	r.add(candidates[index]);
    	solve(res, r, candidates, target-candidates[index], index+1);
    	r.remove(r.size()-1);
    	//不选择当前元素
    	int nextIndex=index+1;
    	while(nextIndex<candidates.length&&candidates[nextIndex]==candidates[index])nextIndex++;
    	solve(res, r, candidates, target, nextIndex);
    }

    public List<List<Integer>> combinationSum3(int k, int n) {
    	List<List<Integer>> res=new ArrayList<List<Integer>>();
    	ArrayList<Integer> sub=new ArrayList<Integer>();
        solve(res,sub,k,n,1);
        return res;
    }
    private void solve(List<List<Integer>> res,ArrayList<Integer> sub,int k,int n,int start){
    	if(sub.size()==k&&n==0){
    		ArrayList<Integer> clone=new ArrayList<Integer>(sub);
    		res.add(clone);
    		return;
    	}
    	if(n<0||start==10||(sub.size()==k&&n!=0)){
    		return;
    	}
    	//选择当前元素
    	sub.add(start);
    	solve(res,sub,k,n-start,start+1);
    	sub.remove(sub.size()-1);
    	//不选择当前元素
    	solve(res,sub,k,n,start+1);
    }

分析

对于合法的括号表达式,从左边往右边看时,每时每刻左括号的个数大于等于右括号的个数。

我们可以将问题看成是对于长为2 n的字符串,从第一个位置开始我们选择‘(‘或者‘)‘,同时要保证左括号个数永远大于右边括号的个数,并且最终左括号和右括号的个数都等于n。当我们做完所有的选择就得到了合法的括号表达式。

因此得到如下递归解法:

    public List<String> generateParenthesis(int n) {
        List<String> res=new ArrayList<String>();
        if(n==0){
        	res.add("");
        	return res;
        }
        StringBuilder r=new StringBuilder();
        solve(n,0,0,res,r);
        return res;
    }
    private void solve(int n,int left,int right,List<String> res,StringBuilder r){
    	if(r.length()==2*n){
    	    System.out.println(r.toString());
    		res.add(r.toString());
    		return;
    	}
    	if(left<right)return;
    	if(left<n){
            //添加左边括号
            r.append("(");
            solve(n,left+1,right,res,r);
            r.setLength(r.length() - 1);
    	}
    	//添加右边括号
    	r.append(")");
    	solve(n,left,right+1,res,r);
    	r.setLength(r.length() - 1);
    }

分析

采用分期摊还的方法。

    public List<List<Integer>> subsets(int[] nums) {
    	List<List<Integer>> res=new ArrayList<List<Integer>>();
    	res.add(new ArrayList<Integer>());
    	for(int i=0;i<nums.length;i++){
    		List<List<Integer>> t=new ArrayList<List<Integer>>();
    		for(List<Integer> r:res){
    			t.add(r);
    			ArrayList<Integer> clone=new ArrayList<Integer>(r);
    			clone.add(nums[i]);
    			t.add(clone);
    		}
    		res=t;
    	}
    	return res;
    }

分析

因为元素可以重复,并且需要过滤重复的集合,我们需要对生成子集的过程进行进一步的控制。我们将问题理解为,从a[0]开始求所有的子集,该问题可以分为两个问题1、选择a[0],从a[1]开始求所有子集并对每个子集加上a[0];2从a[1]开始求所有的子集。因此得到如下递归解法。

注:由于需要消除重复子集,因此我们在选择当前元素a[i]时,如果a[i-1]等于a[i]且我们没有选择,我们就必定不能选择a[i]。

    public List<List<Integer>> subsetsWithDup(int[] nums) {
    	Arrays.sort(nums);
    	boolean[] used=new boolean[nums.length];
    	List<List<Integer>> res=new ArrayList<List<Integer>>();
    	ArrayList<Integer> sub=new ArrayList<Integer>();
    	solve(res,0,sub,nums,used);
     	return res;
    }
    private void solve(List<List<Integer>> res,int start,ArrayList<Integer> sub,int[] nums,boolean[] used){
    	if(start==nums.length){
    		ArrayList<Integer> clone=new ArrayList<Integer>(sub);
    		res.add(clone);
    		return;
    	}
    	//选择当前元素
    	if(start>0&&nums[start]==nums[start-1]&&!used[start-1]){
    		//do nothing
    	}else{
    		used[start]=true;
    		sub.add(nums[start]);
    		solve(res,start+1,sub,nums,used);
    		sub.remove(sub.size()-1);
    		used[start]=false;
    	}
    	//不选择当前元素
    	solve(res,start+1,sub,nums,used);
    }

分析

我们从每一个位置开始回溯,并标记元素是否已经使用过。

    public boolean exist(char[][] board, String word) {
    	boolean used[][]=new boolean[board.length][board[0].length];
    	for(int i=0;i<board.length;i++){
    		for(int j=0;j<board[0].length;j++){
    			if(solve(board,word,i,j,used,0))
    			    return true;
    		}
    	}
        return false;
    }
    public boolean solve(char[][] board,String word,int row,int col,boolean[][] used,int start){
    	if(start==word.length()){
    		return true;
    	}
    	if(row<0||col<0||row==board.length||col==board[0].length){
    		return false;
    	}
    	if(board[row][col]!=word.charAt(start)||used[row][col]){
    		return false;
    	}
    	if(used[row][col]){
    		return false;
    	}
    	used[row][col]=true;
		boolean mark=false;
		mark=mark||solve(board,word,row+1,col,used,start+1);
		mark=mark||solve(board,word,row-1,col,used,start+1);
		mark=mark||solve(board,word,row,col+1,used,start+1);
		mark=mark||solve(board,word,row,col-1,used,start+1);
		used[row][col]=false;
		return mark;
    }

思考:如果我们有很多的单词需要查找时,如何避免重复的搜索过程呢?我们可以先建立trie树(单词查找树),然后对trie中的不同路径进行搜索。见leetcode 212 Word
Search II

分析

对于每一个字符,我们首先找到以该字符为首的回文字符串,然后我们依次其中的回文传,并递归求解选择后的问题。

    public List<List<String>> partition(String s) {
    	List<List<String>> res=new ArrayList<List<String>>();
    	List<String> sub=new ArrayList<String>();
    	solve(res,sub,s,0);
    	return res;
    }
    private void solve(List<List<String>> res,List<String> sub,String s,int start){
    	if(start==s.length()){
    		List<String> clone=new ArrayList<String>(sub);
    		res.add(clone);
    		return;
    	}
    	List<Integer> ends=new ArrayList<Integer>();
    	for(int i=start;i<s.length();i++){
    		if(isPalindrome(s,start,i)){
    			ends.add(i);
    		}
    	}
    	for(int end:ends){
    		sub.add(s.substring(start, end+1));
    		solve(res,sub,s,end+1);
    		sub.remove(sub.size()-1);
    	}
    }
    private boolean isPalindrome(String s,int start,int end){
    	while(start<=end){
    		if(s.charAt(start)!=s.charAt(end)){
    			return false;
    		}
    		start++;end--;
    	}
    	return true;
    }

分析

对于每个待填的空位我们可以尝试1-9所有可能性,如果解决了问题就直接结束。如果所有尝试都没能解决问题,就需要调整之前已经确认的空位,回溯到调整上一个空位的值。因此,此处调用子问题时必须返回是否能够解决问题的转态,以便回溯。

    public void solveSudoku(char[][] board) {
        ArrayList<ArrayList<Integer>> emptyLocations=
        		new ArrayList<ArrayList<Integer>>();
        for(int row=0;row<9;row++){
        	for(int col=0;col<9;col++){
        		if(board[row][col]=='.'){
        			ArrayList<Integer> location=new ArrayList<Integer>();
        			location.add(row);location.add(col);
        			emptyLocations.add(location);
        		}
        	}
        }
        solve(board,0,emptyLocations);
    }
    private boolean solve(char[][] board,int index,ArrayList<ArrayList<Integer>> emptyLocations){
    	if(index==emptyLocations.size()){
    		return true;
    	}
    	ArrayList<Integer> location=emptyLocations.get(index);
    	int row=location.get(0),col=location.get(1);
    	for(char c='1';c<='9';c++){
    		if(isValid(board,row,col,c)){
    			board[row][col]=c;
    			if(solve(board,index+1,emptyLocations)){
    				return true;
    			}else{
    				board[row][col]='.';
    			}
    		}
    	}
    	return false;
    }
    public boolean isValid(char[][] board,int row,int col,char c){
    	//验证行
    	for(int i=0;i<9;i++){
    		if(board[row][i]==c)
    			return false;
    	}
    	//验证列
    	for(int i=0;i<9;i++){
    		if(board[i][col]==c)
    			return false;
    	}
    	//验证3*3格子
    	for(int i=(row/3)*3;i<(row/3)*3+3;i++){
    		for(int j=(col/3)*3;j<(col/3)*3+3;j++){
    			if(board[i][j]==c)
    				return false;
    		}
    	}
    	return true;
    }

分析

我们知道N皇后问题通常采用回溯法求解。

方案一

对于每行每个位置进行尝试,并判断是否合法。

    public List<List<String>> solveNQueens(int n) {
    	ArrayList<Integer> locations=new ArrayList<Integer>();
    	List<List<String>> res=new ArrayList<List<String>>();
    	solve(res,locations,n);
    	return res;
    }
    private void solve(List<List<String>> res,ArrayList<Integer> locations,int n){
    	if(n==locations.size()){
    		addRes(res,locations);
    		return;
    	}
    	for(int i=0;i<n;i++){
    		if(isValid(locations,i)){
    			locations.add(i);
    			solve(res,locations,n);
    			locations.remove(locations.size()-1);
    		}
    	}
    }
    private boolean isValid(ArrayList<Integer> locations,int location){
    	for(int i=0;i<locations.size();i++){
    		if(location-locations.get(i)==locations.size()-i||
    				location-locations.get(i)==i-locations.size())
    			return false;
    		if(location==locations.get(i))
    			return false;
    	}
		return true;
    }
    private void addRes(List<List<String>> res,ArrayList<Integer> locations){
    	List<String> r=new ArrayList<String>();
    	for(int i=0;i<locations.size();i++){
    		StringBuilder builder=new StringBuilder();
    		for(int j=0;j<locations.size();j++){
    			if(locations.get(i)==j){
    				builder.append("Q");
    			}else{
    				builder.append(".");
    			}
    		}
    		r.add(builder.toString());
    	}
    	res.add(r);
    }

方案二

我们将每行中皇后的位置用1-N表示,共N行。这1-N的任意排列,即可满足任意两个皇后不在同行或同列,我们只需要对产生的全排列进行验证即可。

    public List<List<String>> solveNQueens(int n) {
    	int[] locations=new int[n+1];
    	for(int i=1;i<=n;i++){
    		locations[i]=i;
    	}
    	List<List<String>> res=new ArrayList<List<String>>();
    	solve(res,locations,1);
    	return res;
    }
    private void solve(List<List<String>> res,int[] locations,int index){
    	if(index==locations.length){
    		addRes(res,locations);
    		return;
    	}
    	for(int i=index;i<=locations.length-1;i++){
    		if(isValid(locations,index,i)){
    			int t=locations[index];locations[index]=locations[i];locations[i]=t;
    			solve(res,locations,index+1);
    			t=locations[index];locations[index]=locations[i];locations[i]=t;
    		}
    	}
    }
    private boolean isValid(int[] locations,int index,int change){
		for(int i=1;i<index;i++){
			if(locations[i]-locations[change]==index-i||
					locations[i]-locations[change]==i-index)
				return false;
		}
		return true;
    }
    private void addRes(List<List<String>> res,int[] locations){
    	List<String> r=new ArrayList<String>();
    	for(int i=1;i<=locations.length-1;i++){
    		StringBuilder builder=new StringBuilder();
    		for(int j=1;j<=locations.length-1;j++){
    			if(locations[i]==j){
    				builder.append("Q");
    			}else{
    				builder.append(".");
    			}
    		}
    		r.add(builder.toString());
    	}
    	res.add(r);
    }

注:在统计结果数量时,由于Java本身都是按值传递参数的(对于对象传递的是地址值),因此我们不用用int类型统计结果数量,同时由于Integer是不可变的,因此也不能使用Integer。这里我采用Integer容器来统计数量,此外还可以利用AtomicInteger原子整型或自定义引用类型来进行统计,也可以在方法调用中返回结果数量。如有更好的方法忘指教,谢谢!!

    public int totalNQueens(int n) {
    	ArrayList<Integer> locations=new ArrayList<Integer>();
    	Stack<Integer> count=new Stack<Integer>();
    	count.add(new Integer(0));
    	solve(locations,n,count);
    	return count.get(0);
    }
    private void solve(ArrayList<Integer> locations,int n,Stack<Integer> count){
    	if(n==locations.size()){
    		count.push(new Integer(count.pop()+1));
    		return;
    	}
    	for(int i=0;i<n;i++){
    		if(isValid(locations,i)){
    			locations.add(i);
    			solve(locations,n,count);
    			locations.remove(locations.size()-1);
    		}
    	}
    }
    private boolean isValid(ArrayList<Integer> locations,int location){
    	for(int i=0;i<locations.size();i++){
    		if(location-locations.get(i)==locations.size()-i||
    				location-locations.get(i)==i-locations.size())
    			return false;
    		if(location==locations.get(i))
    			return false;
    	}
		return true;
    }

时间: 2024-10-11 13:31:26

排列组合和回溯算法-面试题的相关文章

LeetCode46 回溯算法求全排列,这次是真全排列

本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是LeetCode的26篇文章,我们来实战一下全排列问题. 在之前的文章当中,我们讲过八皇后.回溯法,也提到了全排列,但是毕竟没有真正写过.今天的LeetCode46题正是让我们生成给定元素的全排列. 题意很简单,只有一句话,给定一个没有重复元素的序列,让我们返回这个序列所有的全排列,并且我们不需要考虑这些排列的顺序. 回溯法 我们在之前的文章当中分析过,全排列问题,可以看成是搜索问题,从而近似成八皇后问题.在八皇后问题当中,我们枚

回溯算法之素数环

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SeqListSort { /// <summary> /// <ather> /// lihonglin /// </ather> /// <content> /// 把从1到20这20个数摆成一个环,要求相邻的两个数的和是一个素数. ///分析:用回溯算法,考察所有

穷举递归和回溯算法终结篇

穷举递归和回溯算法 在一般的递归函数中,如二分查找.反转文件等,在每个决策点只需要调用一个递归(比如在二分查找,在每个节点我们只需要选择递归左子树或者右子树),在这样的递归调用中,递归调用形成了一个线性结构,而算法的性能取决于调用函数的栈深度.比如对于反转文件,调用栈的深度等于文件的大小:再比如二分查找,递归深度为O(nlogn),这两类递归调用都非常高效. 现在考虑子集问题或者全排列问题,在每一个决策点我们不在只是选择一个分支进行递归调用,而是要尝试所有的分支进行递归调用.在每一个决策点有多种

谈谈递归和回溯算法的运用

递归和回溯算法的运用 题目描述 有n个士兵站成一列,从第1个士兵前面向后望去,刚好能看到m个士兵,如果站在后面的士兵身高小于或者等于前面某个士兵的身高,那么后面的这个士兵就不能被看到,问这n个士兵有多少种排列方式,刚好在观测位能看到m个士兵? 第一行输入 n 个士兵和 m 个可以看到的士兵(n >= m),第二行输入 n 个士兵的身高,输出为排列方式的种数. 输入: 4 3 1 1 2 3 输出: 6 也就是说,输入数 n, m (n < m),然后输入 n 个正整数到一个数组 a 中,a 数

哈密尔顿回路(旅行售货员问题)的回溯算法

1. 回溯法的基本原理: 回溯算法也叫试探法,它是一种系统地搜索问题的解的方法.回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试.用回溯算法解决问题的一般步骤为: 1.定义一个解空间,它包含问题的解. 2.利用适于搜索的方法组织解空间. 3.利用深度优先法搜索解空间. 4.利用限界函数避免移动到不可能产生解的子空间. 问题的解空间通常是在搜索问题的解的过程中动态产生的,这是回溯算法的一个重要特性. 2.旅行售货员问题的回溯算法实现 算法具体实现主要代码如下: // T

从零开始学回溯算法

本文在写作过程中参考了大量资料,不能一一列举,还请见谅. 回溯算法的定义:回溯算法也叫试探法,它是一种系统地搜索问题的解的方法.回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试. 解题的一般步骤是: 1.定义一个解空间,它包含问题的解: 2.利用适于搜索的方法组织解空间: 3.利用深度优先法搜索解空间: 4.利用限界函数避免移动到不可能产生解的子空间. 问题的解空间通常是在搜索问题的解的过程中动态产生的,这是回溯算法的一个重要特性. 话不多说,我们来看几个具体的例子慢

回溯算法-C#语言解决八皇后问题的写法与优化

结合问题说方案,首先先说问题: 八皇后问题:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行.同一列或同一斜线上,问有多少种摆法. 嗯,这个问题已经被使用各种语言解答一万遍了,大多还是回溯法解决的. 关于回溯算法:个人理解为就是优化的穷举算法,穷举算法是指列出所有的可能情况,而回溯算法则是试探发现问题"剪枝"回退到上个节点,换一条路,能够大大提高求解效率. 具体到8皇后问题上来说,需要考虑以下几点: 1)将8个皇后定义为8行中的相对位置来标识,考虑增

回溯算法入门及经典案例剖析(初学者必备宝典)

前言 基于有需必写的原则,并且当前这个目录下的文章数量为0(都是因为我懒QAQ),作为开局第一篇文章,为初学者的入门文章,自然要把该说明的东西说明清楚,于是...我整理了如下这篇文章,作者水平有限,有不足之处还望大家多多指出~~~ 概念 首先,回溯是什么意思?很多初学者都会问这样的一个问题.我们可以举这样一个例子: 1 1 1 1 0 1 0 1 0 1 0 1 0 1 1 1 我们看到了如图所示的一个4*4的迷宫了,我们假设数字1标记的位置为道路,数字0标记的位置为一堵墙,一个人由起点(0.0

递归、回溯-算法框架

之前已经学习过回溯法的一些问题,从这篇文章开始,继续深入学习一下回溯法以及其他经典问题. 回溯法有通用的解题法之称.用它可以系统的搜索一个问题的所有解或任一解,回溯法是一个既带有系统性又带有跳跃性的搜索算法. 它的问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树.算法搜索至解空间树的任一结点时,先判断该结点是否包含问题的解.如果肯定不包含,则跳过对以该结点为根的子树的搜索,逐层向其祖先结点回溯.否则,进入该子树,继续按深度优先策略搜索.回溯法求问题的所有解时,要回溯到根,且根结点的所有