算法笔记5-----回溯法

回溯的概念:

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法(深度优先),按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

回溯的算法框架:

1、问题的解为子集树

采取三段式:(3个if)
例如:0-1背包问题

//到达叶节点(退出条件)
if(i>n){

}
//进入左子树
if(cw+wi<=c){

}
//进入右子树
if(bound(i+1)>best){

}
//减枝函数
public void bound()
{
}

2、问题的解为排列树

采取两段式:(if+for)
例如:旅行售货员问题

//到达叶节点
if(i>n){

}
else{
//通过for循环遍历排列树
	for(j=t; j<=n; j++){
	if(...)
	{
	        Swap(x[i],x[j]);
	}
	
	}
}


经典问题:

一、装载问题

1.问题:

一批集装箱共n个要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为Wi且W1+W2+…+Wn<=c1+c2;试确定一个合理的装载方案使这n个集装箱装上这两艘轮船。

2.算法设计:

容易去证明:如果一个装载问题有解,则采用下面的策略可以得到最优装载方案:

(1)首先将第一艘轮船尽可能装满;

(2)然后将剩余的集装箱装在第二艘轮船上。

那么在这个过程中,我们需要找到尽可能把第一个轮船装满的解。如果用回溯法解决问题,我们可以首先分析问题得解空间结构,应该是一个子集树。然后我们可以把问题套入相应的模版进行解决。
上界函数:在以Z为根的子树中任意叶节点所相应的载重量均不超过CW+R,因此当BESTW>=CW+R时,可减去右子树。
在这里插入图片描述

3.时间复杂性

遍历完全二叉树所需的时间为(2^n),记录最优解需要(n)
所以算法复杂度为(n2^n)

二、批处理作业调度

1、问题描述

每一个作业Ji都有两项任务分别在2台机器上完成。每个作业必须先有机器1处理,然后再由机器2处理。作业Ji需要机器j的处理时间为tji。对于一个确定的作业调度,设Fji是作业i在机器j上完成处理时间。则所有作业在机器2上完成处理时间和f=F2i,称为该作业调度的完成时间和

2、简单描述

对于给定的n个作业,指定最佳作业调度方案,使其完成时间和达到最小。

区别于流水线调度问题:批处理作业调度旨在求出使其完成时间和达到最小的最佳调度序列;

流水线调度问题旨在求出使其最后一个作业的完成时间最小的最佳调度序列;
在这里插入图片描述
在这里插入图片描述

3、算法分析

按照回溯法搜索排列树的算法框架,设开始时x=[1,2, … , n]是所给的n个作业,则相应的排列树由x[1:n]的所有排列(所有的调度序列)构成。
二维数组M是输入作业的处理时间,bestf记录当前最小完成时间和,bestx记录相应的当前最佳作业调度。
  在递归函数Backtrack中,
    当i>n时,算法搜索至叶子结点,得到一个新的作业调度方案。此时算法适时更新当前最优值和相应的当前最佳调度。

当i<n时,当前扩展结点位于排列树的第(i-1)层,此时算法选择下一个要安排的作业,以深度优先方式递归的对相应的子树进行搜索,对不满足上界约束的结点,则剪去相应的子树。

4、前期准备

1、区分作业i和当前第i个正在执行的作业
   给x赋初值,即其中一种排列,如x=[1,3,2];M[x[j]][i]代表当前作业调度x排列中的第j个作业在第i台机器上的处理时间;如M[x[2]][1]就意味着作业3在机器1上的处理时间。
2、bestf的初值
   此问题是得到最佳作业调度方案以便使其完成时间和达到最小,所以当前最优值bestf应该初始化赋值为较大的一个值。
3、f1、f2的定义与计算
   假定当前作业调度排列为:x=[1,2,3];f1[i]即第i个作业在机器1上的处理时间,f2[j]即第j个作业在机器2上的处理时间;则:
      f1[1]=M[1][1] , f2[1]=f1[1]+M[1][2]
      f1[2]=f1[1]+M[2][1] , f2[2]=MAX(f2[1],f1[2])+M[2][2] //f2[2]不光要等作业2自己在机器1上的处理时间,还要等作业1在机器2上的处理时间,选其大者。
      f1[3]=f1[2]+M[3][1] , f2[3]=MAX(f2[2],f1[3])+M[3][2]
  1只有当前值有用,可以覆盖赋值,所以定义为int型变量即可,减少空间消耗;f2需要记录每个作业的处理时间,所以定义为int *型,以便计算得完成时间和。
4、f2[0]的初值
   f2[i]的计算都是基于上一个作业f2[i-1]进行的,所以要记得给f2[0]赋值为0。
在这里插入图片描述

#include<iostream>
using namespace std;
int x[100];    //当前作业调度————其中一种排列顺序
int bestx[100]; //当前最优作业调度
int m[100][100];////各作业所需的处理时间
                //M[j][i]代表第j个作业在第i台机器上的处理时间
int f1=0;//机器1完成处理时间
int f2=0;//机器2完成处理时间
int cf=0;//完成时间和
int bestf=10000;//当前最优值,即最优的处理时间和
int n;//作业数
 
void swap(int &a,int &b)
{
    int temp=a;
    a=b;
    b=temp;
}
 
void Backtrack(int t)
{    //t用来指示到达的层数(第几步,从0开始),同时也指示当前执行完第几个任务/作业
    int tempf,j;
    if(t>n) //到达叶子结点,搜索到最底部
    {
        if(cf<bestf)
        {
            for(int i=1; i<=n; i++)
                bestx[i]=x[i];//更新最优调度序列
            bestf=cf;//更新最优目标值
        }
    }
    else    //非叶子结点
    {
        for(j=t; j<=n; j++) //j用来指示选择了哪个任务/作业(也就是执行顺序)
        {
            f1+=m[x[j]][1];//选择第x[j]个任务在机器1上执行,作为当前的任务
            tempf=f2;//保存上一个作业在机器2的完成时间
            f2=(f1>f2?f1:f2)+m[x[j]][2];//保存当前作业在机器2的完成时间
            cf+=f2;               //在机器2上的完成时间和
            //如果该作业处理完之后,总时间已经超过最优时间,就直接回溯。
            //剪枝函数
            if(cf<bestf) //总时间小于最优时间
            {
                swap(x[t],x[j]);  //交换两个作业的位置,把选择出的原来在x[j]位置上的任务调到当前执行的位置x[t]
                Backtrack(t+1);   //深度搜索解空间树,进入下一层
                swap(x[t],x[j]); //进行回溯,还原,执行该层的下一个任务 //如果是叶子节点返回上一层
            }
            //回溯需要还原各个值
            f1-=m[x[j]][1];
            cf-=f2;
            f2=tempf;
        }
    }
}

3、时间复杂性

遍历排列树所需的时间为(n!)
所以算法复杂度为(n!)

三、符号三角形问题

1.问题描述

问题描述:

由14个“+”号和14个“-”号组成的符号三角形。

2个同号下面是“+”号,2个异号下面是“-”号。

如图:

+   +   _   +   _   +   +

+  _   _   _   _   +

_   +  +  +  _

_   +   +  _

_   +  _

_  _

+

在一般情况下,符号三角形第一行有N个符号,该问题要求对于给定n计算有多少种不同的符号三角形。使其所含的 + 和 - 个数相同。

2.算法分析

1 x[i] =1 时,符号三角形的第一行的第i个符号为+

2 x[i] =0时,表示符号三角形的第一行的第i个符号位-

共有i(i+1)/2个符号组成的符号三角形。

3 确定x[i+1]的值后,只要在前面确定的符号三角形的右边加一条边就扩展为x[1:i+1]所相应的符号三角形。

4 最后三角形中包含的“+”“-”的个数都为i(i+1)/4,因此搜索时,个数不能超过…若超直接可以剪去分枝。

5 当给定的n(n+1)/2为奇数时,也不符合三角形要求。

3.时间复杂性

遍历完全二叉树所需的时间为(2^n),记录最优解需要(n)
所以算法复杂度为(n2^n)

四、N后问题

1.问题描述:

八皇后问题,是一个古老而著名的问题.该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法?

2.算法设计

在这里插入图片描述
由题易知:每一行必有放置一个皇后。
开一个二维数组来保存皇后,则皇后的坐标必为[1][x].[2][x].[3][x].[4][x]…
横坐标是固定的,只需要确定皇后的列即可。
经过分析,N后问题为一个排列问题。
只需判断对角线是否冲突即可。

待写

3.时间复杂性

遍历排列树所需的时间为(n!)
计算可行性约束需要(n)
所以算法复杂度为(n*n!)

五、旅行售货员问题

1.问题描述

某售货员要到若干城市去推销商品,已知各城市之间的路程,他要选定一条从驻地出发,经过每个城市一遍,最后回到住地的路线,使总的路程最短。

2.算法分析

旅行售货员问题的解空间是一颗排序树,对于排序树的回溯法搜索与生成1,2,3,4,…,n的所有排列的递归算法Perm类似,开始时,x = [1,2,…,n],则相应的排序树由x[1:n]的所有排序构成。以下解释排序算法Perm:

(1)假设Perm(1)的含义是对x = [1,2,…,n]进行排序,则Perm(i)的含义是对[i,i+1,i+2,…,n]进行排序。为了方便描述,规定一个操作P(j),表示在数组x中,第j个与第一个交换,于是得到递归关系式: Perm(i) = P(i)Perm(i+1) + P(i+1)Perm(i+1) + …+P(n)Perm(i+1)

(2)旅行售货员的回溯法Backtrack在Perm的基础上使用了剪枝函数,将无效的全排列和有效的全排列非最优的次序统统都舍去。
在这里插入图片描述

void Traveling::Backtrack(int i) //对数组x中第i起到结尾进行全排列的试探,数组x下标为0的元素保留不用
{
	if(i == n) //找到符合条件的全排列
	{
		if (a[x[i-1]][x[i]] != NoEdge 
		&& a[x[i]][x[1]] != NoEdge 
		&& (bestc > cc + a[x[i-1]][x[i]] +a[x[i]][x[1]] || bestc == NoEdge)) //判断是否有回路、发现最优值
		{
			bestc = cc + a[x[i-1]][x[i]] +a[x[i]][x[1]]; //保存最优值
			for (int i = 1; i <= n; i++)
			{
				bestx[i] = x[i]; //保存最优解
			}			
		}
	}
	else
	{
		for (int j =i; j <= n; j++)
		{
			if(a[x[i-1]][x[j]] != NoEdge && (cc + a[x[i-1]][x[j]] < bestc || bestc == NoEdge))
			{
				Swap(x[i],x[j]);
				cc += a[x[i-1]][x[i]];
				Backtrack(i+1);
				cc -= a[x[i-1]][x[i]];
				Swap(x[i],x[j]);
			}
		}
	}
}
 

3.时间复杂性

遍历排列树所需的时间为((n-1)!),更新BESTX(n)
所以算法复杂度为(n!)

六、0-1背包问题在这里插入图片描述

public void backtrack(int i)
	{   
		
		if (i>n-1) 
		{
			if(cp>bestp)
			{
			bestp = cp;
				for(int x=0;x<n;x++)
				{
					fin[x]=put[x];
				}
			}
			return;
			
		}
		if (cw + goods[i].weight <= c)
		{
			cw += goods[i].weight;
			cp += goods[i].value;
			put[i] = 1;
			backtrack(i + 1);
			cw -= goods[i].weight;
			cp -= goods[i].value;
			put[i] = 0;
			
		}
		if (bound(i + 1) > bestp)
		{
			backtrack(i + 1);
		}
	}

	//计算上界函数,功能为剪枝
	public double bound(int i)
	{   //判断当前背包的总价值cp+剩余容量可容纳的最大价值<=当前最优价值
		double leftw = c - cw;
		
		double b = cp;//最大价值
					  
		while (i <= n-1 && goods[i].weight <= leftw)
		{
			leftw -= goods[i].weight;
			b += goods[i].value;
			i++;
		}
		//装满背包
		if (i <= n-1)
			b += goods[i].value / goods[i].weight * leftw;
		return b;//返回计算出的上界

	}

3.时间复杂性

遍历完全二叉树所需的时间为(2^n),记录最优解需要(n)
所以算法复杂度为(n2^n)

发布了23 篇原创文章 · 获赞 2 · 访问量 487

猜你喜欢

转载自blog.csdn.net/weixin_42385782/article/details/103348997