本文介绍回溯法的全排列问题及其两种衍生题。解法只需要在原来的基础上做些修改即可。
1.需要一个一维数组来保存结果集 。需要考虑以哪种属性来构造数组,即考虑数组中要存放什么类型的数据,反过来放行不行?具体看衍生题第2题。
2.需要进行两次比较,一次是 一次全排列后进行一次比较,另一次是在递归的过程中进行比较,进而排除那些极端情况,比如过程中产生的部分结果要比最优的总结果还要糟糕,进而排除,即剪枝。
3.*分析问题有时要考虑极端情况
for i = t to n do, t表示到下一层时不用从1开始,而是直接从t开始,t就相当于节点的出度。那么什么时候用t,什么时候用1呢,即什么时候是 for i = 1 to n do ? 当节点的出度为n时,即从i= 1开始算起。也就是说此时不再是n个数的不重复全排列了,也就是说前后可以重复。比如说这题可以 1 -> 2 -> 1.比如说n位数有2^n种肯能。也就是说进入下一层节点的时候要从头对所有可能对情况进行遍历。
后面几题均会以此题为模版出题
衍生问题1:
工作分配问题
设有n件工作要分配给n个人去完成,将工作i分配给第j个人所需费用为。试设计一个算法,为每个人分配1件不同的工作,并使总费用达到最小。
//在全排列对基础上筛选出最优解
void Backtrack(int i){
if(i > n){//一次全排列完进行比较
if(cc < minc)//cc存放当前总代价
return;
}
else {
for( int j = i; j <= n; j++){//从i开始对节点的出度进行遍历
if(cc < minc){//剪枝
swap(work[i], work[j]);//work[i]存放排列结果的下标
cc += c[i][work[i]];
Backtrack(i+1);//进入下一层节点
cc -= c[i][work[i]];//矫正cc的值,返回到上一层的状态
swap(work[i], work[j]);//矫正顺序,返回到上一层的状态
}
}
}
}
衍生问题2:
最佳调度问题
设有n个任务由k个可并行工作的机器来完成,完成任务i需要时间为。试设计一个算法找出完成这n个任务的最佳调度,使完成全部任务的时间最早。(要求给出调度方案)
该算法可抽象为子集树回溯算法,针对特定的任务数和机器数定义解空间,对于n个任务和k个机器,
解编码:(X1,X2,。。。,Xn),Xi表示给任务i分配的机器编号;
解空间:{(X1,X2,。。。,Xn)| Xi属于S,i=1到n},S={1,2,。。。,k}
这个问题可以想象成有k个槽,n个高矮不一的箱子,怎么样把n个箱子放到k个槽当中,从而使高度最高的那个槽的高度最短?方法有很多中,极端的做法是把n个箱子都放到一个槽中,从而使时间运行时间最大,也可以分摊开来,从而使并行的运行时间最少。
1.构造一维数组:time_mac[]表示第i个machine要运行的时间,x[task]表示当前的调度策略。best_x[i]表示最优的调度策略。
2.if(time_mac[i]<min_t) //为什么要跟min_t进行比较,因为过程中产生的非完全数据可能比最优的完全数据还要差,而如果继续进入下一层,只会时结果增加,因此要进行排除。考虑极端情况,将所有任务都分配到第一台机器中。
#define NUM_TASK 10
#define NUM_MAC 3
#include <iostream>
#include <climits>
using namespace std;
void output(int x[]);
void BackTrack(int task);
int getTime(int time_mac[]);
void output_assign(int best_x[]);
//所有数组下标从1开始
int x[NUM_TASK + 1];//x[task]表示给任务task分配机器x[task]
int best_x[NUM_TASK+1];//存储最优分配方案
int min_t=INT_MAX;//执行任务所需的最小时间
int t[NUM_TASK + 1] = { 0,1,7,4,0,9,4,8,8,2,4 };//每个任务所需时间
//int t[NUM_TASK + 1] = { 0,1,7,4 };//每个任务所需时间
int time_mac[NUM_MAC + 1] = {0};//每个机器运行结束的时间
void BackTrack(int task) {
if (task > NUM_TASK) {
int cur_time = getTime(time_mac);//当前已分配任务的完成时间
/*输出所有分配情况,以及对应的时间*/
//output(x);
//cout << "time=" << cur_time <<endl;
if (cur_time < min_t) { //剪枝
min_t = cur_time;
for (int i = 1; i <=NUM_TASK; i++) {
best_x[i] = x[i];
}
}
}
else{
for (int i = 1; i <= NUM_MAC; i++) {
x[task] = i;
time_mac[i] += t[task];//在第i台机器上添加任务task
if(time_mac[i]<min_t) //为什么要跟min_t进行比较,因为过程中产生的非完全数据可能比最优的完全数据还要差,而如果继续进入下一层,只会时结果增加,因此要进行排除。
BackTrack(task+1);/
time_mac[i] -= t[task];
}
}
}
void output(int x[]) {
for (int i = 1; i <= NUM_TASK; i++) {
cout << x[i]<< " ";
}
}
//返回time_mac[]数组当中的最大值
int getTime(int time_mac[]) {
int max_time=time_mac[1];
for (int i = 2; i <= NUM_MAC; i++) {
if (time_mac[i] > max_time) {
max_time = time_mac[i];
}
}
return max_time;
}
void output_assign(int best_x[]) {
for (int i = 1; i <= NUM_TASK; i++) {
cout << "任务" << i << "分配给机器" << best_x[i] << endl;
}
}
int main() {
BackTrack(1);
cout << "各个任务执行时间依次为:" << endl;
for (int i = 1; i <= NUM_TASK; i++) {
cout << t[i] << " ";
}
cout << endl;
cout << "所需要的最小时间为:"<<min_t << endl;
//output(best_x);
output_assign(best_x);
return 0;
}
目前关于递归的题目能采用套模版,然后在此基础上进行修改来解题。而要理解递归过程,必须画出递归调用树。