算法篇——回溯法总结和经典题目(皇后问题和01背包问题)

 

一、回溯法的思路:

(1)回溯法,简单来讲就是一个走不通过就退回的过程。是穷举法的一种表现形式,有着通用解题法的美称。

(2)回溯法的基本计算过程每次只构造一个部分解,立即对此部分解进行评估,若此部分解有可能成为所求解,则继续扩展,否则继续尝试其他部分解,直到穷尽所有可能。

(3)从根节点出发,深度优先搜索整个解空间树。

二、几个与回溯有关的概念:

(1)解空间:所有可能解的集合。

(2)剪枝函数:为了避免无效搜索,提高回溯法的搜索效率而存在的函数。通常可以用两种方式进行剪枝:

                           1)用约束函数在扩展结点处减去不满足约束条件的子树。

                           2)用限界函数减去得不到最优解的子树。

(3)扩展结点:一个正在产生儿子的结点称为扩展结点

         活结点:一个自身已生成但其儿子还没有全部生成的节点称做活结点

        死结点:一个所有儿子已经产生的结点称做死结点

三、解题步骤:

(1)针对所给问题,定义问题的解空间。

(2)确定易于搜索得解空间结构。

(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。

四、模板框架:

(1)递归回溯:

void backtrack(int t)          //t表示递归深度,即当前扩展节点在解空间树的深度
{ 
    if ( t > n ) output(x);    //n控制递归深度,如果算法已经搜索到叶节点,记录输出可行解X
    else
    {
        for(int i = f(n,t) ; i <= g(n,t) ; i++)  //在深度t,i从未搜索过得起始编号到终止编号
        {
            x[t] = h(i);       //查看i这个点的值是否满足题目的要求
            if( constraint(t) && bound(t)) 
                backtrack(t+1)
       //constraint(t)为true表示在当前扩展节点处x[1:t]的取值满足问题的约束条件;
       //bound(t)为true表示当前扩展节点取值没有使目标函数越界;
       //为true表示还需要进一步的搜索子树,否则减去子树。
         }
    }
}

 

如果不满足条件,就返回上一层,这个搜索算法按深度优先方式进行,调用一次backtrack(1)便可完成整个搜索过程。

如果看不明白,可以看一下皇后问题,这里就很好理解了。

(2)迭代回溯:(以后补上)

 

五、例题

 

1.皇后问题

1)题目描述:在n*n的棋盘上放置旗子,同一行,同一列,同一斜线只能有一个旗子,找出符合题意的解。

2)解题思路:虽然棋盘是n*n的大小,但是由于棋盘是一行一行搜索,x的坐标就是递归的深度,即传给递归函数的t,这里我们只需要定义一个一维数组表示y。这样既节省了内存空间,又又方便了计算。从第一行开始向后进行计算,如果不满足条件(行列斜线不能保证唯一一颗)剪枝(约束函数),向上一层回溯,然后查找下一个可能的节点。

在约束函数里,判断能不能满足条件,同一行同一列比较一下即可。在斜线上的比较可以利用斜率的性质进行判断。在同一斜线上的斜率为+-1.

具体代码如下:

Queen.java


package trackback;

public class Queen 
{
	int n;       //棋盘的大小 n*n
	int sum;     //计算保存解得个数
	String all;  //保存解得值
	int []x;     //保存坐标的值
	
	Queen(int n)
	{
		this.n = n;
	}
	
	void calculate()   //计算
	{
		sum = 0;
		all = "";
		x = new int[n];
		backtrack(0);             //从根节点递归 即可遍历整颗解空间树
		System.out.println(all);  //全部递归完成,输出保存到字符串的字符
	}
	
	void backtrack(int t) 
	{
		if(t >= n)   //如果递归遍历到了最后一行,就进入output函数,记录解得值,然后再向上递归,因为此时并没有遍历玩整个数,就继续检查记录其他没有便利到的可行解。
		{
			output();
			return ;
		}
		
		for(int i = 0 ; i < n ; i++)  //从第t行的第0个数开始逐个检查到第n个数,检查是否满足条件
		{
			x[t] = i;     //检查第t行的第i个数是不是可行解
			if(ok(t))
			{
				backtrack(t+1);   //如果可行的话,就向上一行检查。不可行就继续i++,检查下一个数。
			}
		}
	}
	
	boolean ok(int t)
	{
		for(int i = 0 ; i < t ;i++)
		{
			if(x[t] == x[i]) return false;
			if(x[t] - x[i] == t-i) return false;
			if(x[t] - x[i] == i-t) return false;
		}
		return true;
	}
	void output()
	{
		sum++;
		for(int i = 0 ; i < n ; i++)
		{
			all+=x[i]+"  ";
		}
		all += "\n";
	}
}

这是主函数的调用。

package trackback;

public class test 
{

	public static void main(String[] args) 
	{
		// TODO Auto-generated method stub
		Queen queen = new Queen(4);
		queen.calculate();
	}

}

最后的运行结果:

1  3  0  2  
2  0  3  1

完善上面的算法:

我们可以发现,我们定义的sum这个值,并没有用到。试着想一想,当n过大的时候,这个算法的 计算量就会过大,及结果也会很多,如果想只输出其中的部分解,那么就可以在ok这个函数的开头加入: if(sum >= 10 ) return false; 这条语句,这样当backtrack在进行i++的时候,检测到sum>=10了之后,就会不在向下递归了。

紧接着问题又来了,当sum已经>=10了之后,在for循环里,i++还会每次都进入ok函数,会浪费时间空间,为了避免这个现象,在calculate()的函数前,加入: for(int i = 0 ; i < n ; i++) x[i] = i;会减少检查的次数。

 

这个算法就用到了递归的那个模板。递归的方法求回溯比迭代要更多一些,也更好一点。

2.0-1背包问题

(1)问题描述:

给定n件物体和一个容量为C的背包,物品i的重量为wi,价值为vi。试求如何选择装入背包的物品,使得物品的总价值最大。

(2)解题思路:

01背包是子集选取问题,通常是NP难的(NP为何物见下一篇博客)。所以先构造01背包的解空间树。在搜索的时候,左节点是可行节点的时候,搜索就进入左子树。当右子树可能包含最优解的时候,就进入右子树搜索。否则减去右子树。

举例:

n = 4 ,c = 7. value ={9,10,7,4}  weight  = {3,5,2,1}

第一步,按单位价值排序:

序号价值重量单位价值
3933
41052
2723.5
1414

 

第二步,从单位价值最大的开始,依次装入背包。直至放不下,然后开始用限界函数推

算,如果继续下去的最大价值()。和上几次作对照,大于继续求解,回溯到上一个不为0的地方,将其置于0,界限函数继续判断价值,向后继续,回溯到x[1]的地方。到尽头停止。

在解空间树的当前扩展节点处是才需要计算上界,以判断右子树是否可以减去。进入左子树不需要计算上界,因为其上界与父节点相同。

package OneZero;

public class Knapsack
{
	static double c;
	static int n;
	static double []weight;
	static double []value;
	static double nowWeight;
	static double nowValue;
	static double bestValue;
	
	public static  class Element implements Comparable
	{
		int id;
		double d;
		
		private Element(int id , double d)
		{
			this.id = id;
			this.d = d;
		}
		
		@Override
		public int compareTo(Object x)   //比较d的大小
		{
			// TODO Auto-generated method stub
			double xd = ((Element)x).d;
			if( d < xd) return -1;
			if( d == xd) return 0;
			return 1;
		}
		
		public boolean equals(Object x)
		{
			return d ==((Element)x).d;
		}
	}
	
	public double knapsack(double []pp , double []ww , double cc)
	{
		c = cc;
		n = pp.length - 1;
		nowWeight = 0;
		nowValue = 0;
		bestValue = 0;
		
		Element []q = new Element[n];
		
		for(int i = 1; i <= n ; i++)
		{
			q[i - 1] = new Element(i , pp[i]/ww[i]);
		}
		MergeSort.mergeSort(q);   //排序函数,需要自己定义排序
		
		value = new double[n+1];
		weight = new double[n+1];
		
		for(int i = 1; i <= n ; i++)
		{
			value[i] = pp[q[n-i].id];
			weight[i] = ww[q[n-i].id];
		}
		backtrack(1);
		
		return bestValue;
	}
	
	private static void backtrack(int i)
	{
		if(i > n)
		{
			bestValue = nowValue;
			return ;
		}
		
		if(nowWeight + weight[i] <= c)
		{
			nowWeight += weight[i];
			nowValue += value[i];
			backtrack(i+1);
			nowWeight -= weight[i];
			nowValue -=value[i];
		}
		
		if(bound(i+1) > bestValue)
		{
			backtrack(i+1);
		}
	}
	
	private static double bound(int i)
	{
		double cleft = c - nowValue;
		double bound = nowWeight;
		
		while( i <= n && weight[i] <= cleft)
		{
			cleft -= weight[i];
			bound += value[i];
			i++;
		}
		
		if(i <= n)
		{
			bound += value[i] * cleft /weight[i];
		}
		
		return bound;
	}
}

 

 

 

 

 

 

        

    原文作者:回溯法
    原文地址: https://blog.csdn.net/lw_beauty/article/details/84573668
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞