1. 程式人生 > >回溯法(演算法分析與設計)

回溯法(演算法分析與設計)

0.回溯法的演算法框架

A.簡介

回溯法,又稱試探法。一般需要遍歷解空間,時間複雜度概況:子集樹Ω(2^n),排序樹Ω(n!),暴力法

B.回溯法解題三步驟

1)定義問題的解空間

如0-1揹包問題,當n=3時,解空間是(0,0,0)、(0,0,1)、(0,1,0)、(0,1,1)、(1,0,0)、(1,0,1)、(1,1,0)、(1,1,1)。1代表選擇該物品,0代表不選擇該物品

2)確定易搜尋的解空間結構

3)以深度優先方式搜尋解空間,並在搜尋過程中用剪枝函式避免無效搜尋

C.子集樹、排列樹及其他

1)子集樹

a.概念

當所給問題是從n個元素的集合S中找出S滿足的某種性質的子集時,相應的解空間樹稱為子集樹。例如,0-1揹包問題,要求在n個物品的集合S中,選出幾個物品,使物品在揹包容積C的限制下,總價值最大(即集合S的滿足條件<容積C下價值最大>的某個子集)。

另:子集樹是從集合S中選出符合限定條件的子集,故每個集合元素只需判斷是否(0,1)入選,因此解空間應是一顆滿二叉樹

b.回溯法搜尋子集樹的一般演算法
void backtrack(int t)//t是當前層數 
{
	if(t>n)//需要判斷每一個元素是否加入子集,所以必須達到葉節點,才可以輸出
	{
		output(x);
	}
	else
	{
		for(int i=0;i<=1;i++)//子集樹是從集合S中,選出符合限定條件的子集,故每個元素判斷是(1)否(0)選入即可(二叉樹),因此i定義域為{0,1} 
		{
			x[t]=i;//x[]表示是否加入點集,1表示是,0表示否
			if(constraint(t)&&bound(t))//constraint(t)和bound(t)分別是約束條件和限定函式 
			{
				backtrack(t+1);
			}
		}
	}
} 

2)排列樹

a.概念

當問題是確定n個元素滿足某種性質的排列時,相應的解空間稱為排列樹。排列樹與子集樹最大的區別在於,排列樹的解包括整個集合S的元素,而子集樹的解則只包括符合條件的集合S的子集。

b.回溯法搜素排列樹的一般演算法


void backtrack(int t)//t是當前層數 
{
	if(t>n)//n是限定最大層數 
	{
		output(x);
	}
	else
	{
		for(int i=t;i<=n;i++)//排列樹的節點所含的孩子個數是遞減的,第0層節點含num-0個孩子,第1層節點含num-1個孩子,第二層節點含num-2個孩子···第num層節點為葉節點,不含孩子。即第x層的節點含num-x個孩子,因此第t層的i,它的起點為t層數,終點為num,第t層(根節點為空節點,除外),有num-t+1個親兄弟,需要輪num-t+1回 
		{
			swap(x[t],x[i]);//與第i個兄弟交換位置,排列樹一條路徑上是沒有重複節點的,是集合S全員元素的一個排列,故與兄弟交換位置後就是一個新的排列
			if(constraint(t)&&bound(t))//constraint(t)和bound(t)分別是約束條件和限定函式 
			{
				backtrack(t+1);
			}
			swap(x[i],x[t]);
		}
	}
} 

3)解空間非子集樹,非排列樹

遞迴:

void backtrack(int t)
{
	if(t>n)
	{
		output(x);
	}
	else
	{
		for(int i=f(n,t);i<=g(n,t);i++)
		{
			x[t]=h(i);
			if(constraint(t)&&bound(t))
			{
				backtrack(t+1);
			}	
		}
	}
}
迭代:
void backtrack(int t)
{
	int t=1;
	while(t>0)
	{
		if(f(n,t)<=g(n,t))
		{
			for(int i=f(n,t);i<=g(n,t);i++)//f和g是當前擴充套件節點的起止點 
			{
		 		x[t]=h(i);
				if(constraint(t)&&bound(t))
				{
					if(solution(t))//原先的遞迴出口 
					{
						output(x);
					}
					else
					{
						t++;//未解決問題,但在限定條件之內,走下一層 
					}
				}
			}
		}
		else
		{
			t--;//回到上一層 
		}
	}
} 

D.方法

先判斷問題是子集樹、排列樹、還是非子集樹非排列樹,然後在做

1.裝載問題

A.遞迴

根據節1給出的回溯模板,模仿所得,子集樹

在編寫時未考慮最優解問題(C1上裝載越多,解越優),故本程式無法剪枝。若要考慮最優解,當總載重量小於最優載重量時,可以把右子樹(0,不選擇)全部剪去

#include<iostream>
#include<vector>
#include<cstdlib>
using namespace std;
int c1,c2,wsum;
vector<int> w;
vector<int> s;//是否加入點集C1
int random(int start,int end)
{
	return start+rand()%(end-start);
} 
int s1=0;
void maxLoading(int c1w,int t)
{
	if(t>=w.size())
	{
		if((wsum-c1w)<=c2)
		{
			cout<<"choice "<<++s1<<":\n\tc1 :";
			for(int i=0;i<s.size();i++)
			{
				if(s[i]==1)
				{
					cout<<i<<"\t";
				}
			}
			cout<<endl;
		}
	}
	else
	{
		for(int i=0;i<=1;i++)
		{
			s[t]=i;
			if(i==1)
			{
				c1w+=w[t];
			}
			if(c1w<=c1)
			{
				maxLoading(c1w,t+1);
			}
		}
	}	
}
int main()
{
	int num;
	cin>>num>>c1>>c2;
	wsum=0;
	for(int i=0;i<num;i++)
	{
		int temp=random(1,100);
		w.push_back(temp);
		s.push_back(-1);
		wsum+=temp;
		cout<<temp<<"\t";
	}
	cout<<endl;
	maxLoading(0,0);
	return 0;
}

課本中和上述其實是一樣的,一個用for,i=0,i=1來訪問左右子樹,棄用for,直接上

void maxLoading(int c1w,int t)
{
	if(t>=w.size())
	{
		if((wsum-c1w)<=c2)
		{
			cout<<"choice "<<++s1<<":\n\tc1 :";
			for(int i=0;i<s.size();i++)
			{
				if(s[i]==1)
				{
					cout<<i<<"\t";
				}
			}
			cout<<endl;
		}	
	} 
	else
	{
		if(c1w+w[t]<=c1)//左子樹
		{
			c1w+=w[t];
			s[t]=1;
			maxLoading(c1w,t+1);
			c1w-=w[t];
		}
		s[t]=0;
		maxLoading(c1w,t+1);//右子樹
	}
}
B.迭代

其實就是二叉樹遍歷的非遞迴版本(直接爬的程式碼)

template <class Type>
Type MaxLoading(Type w[],Type c,int n,int bestx[])
{
    //迭代回溯法,返回最優裝載量及其相應解,初始化根節點
    int i =1;
    int *x = new int[n+1];
    Type bestw = 0,
        cw = 0,
        r = 0;
    for(int j=1;j<=n;j++)
        r+=w[j];
    while(true)
    {
        while(i<=n && cw+w[i]<=c)
        {
            r -= w[i];
            cw +=w[i];
            x[i] =1;
            i++;
        }
        if(i>n)
        {
            for(int j=1;j<=n;j++)
                bestx[j] = x[j];
            bestw = cw;
        }
        else
        {
            r -= w[i];
            x[i] = 0;
            i++;
        }
        while(cw+w[i] <= bestw)
        {
            i--;
            while(i>0 && !x[i])
            {
                r+=w[i];
                i--;
            }
            if(i == 0)
            {
                delete[] x;
                return bestw;
            }
            x[i] =0;
            cw -= w[i];
            i++;
        }
    }
}

2.批處理作業排程

排列樹

#include<iostream>
#include<vector>
#include<cstdlib>
#include<climits>
#include<algorithm>
using namespace std;
struct Node
{
	int tm1;
	int tm2;
};
vector<int> x;//當前路徑 
vector<int> bestx;//最優路徑 
vector<int> f2;//機器2完成處理時間 
vector<Node> v;//每個作業需要的處理時間 

int bestT;
int f;//完成時間和 
int f1;//機器1完成處理時間 
int num;//作業數 

int random(int start,int end)
{
	return start+rand()%(end-start);
}

void flowShop(int t)//t>=1
{
	if(t>num)
	{
		for(int i=1;i<=num;i++)
		{
			bestx[i]=x[i];
		}
		bestT=f;
	}
	else
	{
		for(int i=t;i<=num;i++)
		{
			f1+=v[x[i]].tm1;
			f2[t]=((f2[t-1]>f1)?f2[t-1]:f1)+v[x[i]].tm2; 
			f+=f2[t];
			swap(x[t],x[i]); 
			if(f<bestT)//剪枝剪去f>=bestT的分支 
			{
				flowShop(t+1);
			}
			swap(x[i],x[t]);
			f1-=v[x[i]].tm1;
			f-=f2[t];
		}
	}	
} 
int main()
{
	cin>>num;
	for(int i=0;i<=num;i++)
	{
		Node t;
		t.tm1=random(1,10);
		t.tm2=random(1,10);
		v.push_back(t); 
		x.push_back(i);//初始化路徑為作業輸入順序 
		f2.push_back(0);
		bestx.push_back(-1);
	}
	f=0;
	f1=0;
	bestT=INT_MAX;
	
	flowShop(1);
	for(int i=1;i<=num;i++)
	{
		cout<<bestx[i]<<"\t";
	}
	cout<<"\n"<<bestT<<endl;
}

3.符號三角形問題

行一的每個元素都具有兩種選擇(0,+,1,-),行n的符號由行n-1來決定。

子集樹,未剪枝

#include<iostream>
#include<vector>
using namespace std;
vector<int> v;
int num;
int countSame=0;
bool isEqual()
{
	int count0=0;
	int k=num;
	for(int j=num;j>0;j--)//確定符號
	{
		for(int i=k;i<k+j-1;i++)
		{
			v[i]=(v[i-j]==v[i-j+1])?0:1;
		}
		k+=j-1;
	}
	for(int i=0;i<(num+1)*num/2;i++)//統計減號個數
	{
		if(v[i])
		{
			count0++;
		}
	}
	if(count0*2==num*(num+1)/2)//是否相等
	{
		return true;
	}
	return false;
}
void Triangels(int t)
{
	if(t>=num)
	{
		if(isEqual())//相等加1
		{
			countSame++;
		}
	}
	else
	{
		for(int i=0;i<=1;i++)//行一num個元素,均有兩種選擇
		{
			v[t]=i;
			Triangels(t+1);
		}
	}
}
int main()
{
	cin>>num;
	for(int i=0;i<(num+1)*num/2;i++)
	{
		v.push_back(-1);
	}
	Triangels(0);
	cout<<countSame<<endl;
} 

4.n後問題

n後問題,解空間非子集樹,非排列樹(一直往這方向上靠,做了上面3個題,有些先入為主了)

每行皇后只有一位,用v[ i ] = j來表示皇后在( i , j ),確實沒有想到(一直考慮用二維陣列,或者一維陣列模擬二維陣列)

用斜率來判斷是否在一條斜線上,這個也沒想到

A.遞迴

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
vector<int> v;//v[i]=j,表示第i行,第j列是皇后 
int num;//棋盤行長,列長,也是皇后個數 
int sum;//可行方案個數 
bool isOk(int t)
{
	for(int j=1;j<t;j++)//待選皇后v[t],是否與t行之前的皇后有衝突(每行一個皇后) 
	{
		if(v[t]==v[j]||abs(t-j)==abs(v[t]-v[j]))//用斜率來判斷是否在一條斜線上!!! 
		{
			return false;
		}
	}
	return true;
}
void nQueen(int t)
{
	if(t>num)
	{
		sum++;
	}
	else
	{
		for(int i=1;i<=num;i++)
		{
			v[t]=i;//第t行的皇后放在第i列 
			if(isOk(t))
			{
				nQueen(t+1);
			}
		}
	}	
}
int main()
{
	sum=0;
	cin>>num;
	for(int i=0;i<=num;i++)
	{
		v.push_back(0);
	}
	nQueen(1);
	cout<<sum<<endl;
} 

B.迭代

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
vector<int> v;//v[i]=j,表示第i行,第j列是皇后 
int num;//棋盤行長,列長,也是皇后個數 
int sum;//可行方案個數 
bool isOk(int t)
{
	for(int j=1;j<t;j++)//待選皇后v[t],是否與t行之前的皇后有衝突(每行一個皇后) 
	{
		if(v[t]==v[j]||abs(t-j)==abs(v[t]-v[j]))//用斜率來判斷是否在一條斜線上!!! 
		{
			return false;
		}
	}
	return true;
}
void nQueen(int t)
{
	v[t]=0;
	while(t>0)
	{
		v[t]+=1;//第t行,第v[t]+1列 
		while(!isOk(t)&&v[t]<=num)//列範圍內,尋找符合條件的v[t]=j 
		{
			v[t]+=1;
		}
		if(v[t]<=num)//找到 
		{
			if(t==num)
			{
				sum++;//找到後,下個迴圈v[t]會大於num,有t--處理 
			}
			else
			{
				t++;
				v[t]=0;
			}
		}
		else//未找到 
		{
			t--;
		}
	}	
}
int main()
{
	sum=0;
	cin>>num;
	for(int i=0;i<=num;i++)
	{
		v.push_back(0);
	}
	nQueen(1);
	cout<<sum<<endl;
} 

5.0-1揹包問題

子集樹

#include<iostream>
#include<vector>
#include<cstdlib>
using namespace std;
struct Node
{
	int w;
	int p;
};
vector<Node> v;//物品 
vector<int> x;//當前方案 
vector<int> bestx;//儲存最佳方案 
int num;//物品數 
int c;//揹包容量 
int maxP;//最大價值
 
int random(int start,int end)
{
	return start+rand()%(end-start);
}
void storage(int cp)
{
	if(cp>maxP)
	{
		maxP=cp;
		for(int i=0;i<num;i++)
		{
			bestx[i]=x[i];
		}
	}
}
void knapsack(int cw,int t,int cp)
{
	if(t>=num)
	{
		return;
	}
	else
	{
		if(cw<=c)
		{
			for(int i=0;i<=1;i++)
			{
				x[t]=i;
				if(i==1)
				{
					cw+=v[t].w;
					cp+=v[t].p;
				}
				if(cw<=c)
				{
					storage(cp);
					knapsack(cw,t+1,cp);	
				}
			}
		}
	}
}
int main()
{
	maxP=-1;
	cin>>num>>c;
	for(int i=0;i<num;i++)
	{
		Node temp;
		temp.w=random(1,20);
		temp.p=random(1,100);
		v.push_back(temp);
		x.push_back(0);
		bestx.push_back(0);
	}	
	knapsack(0,0,0);
	cout<<maxP<<endl;
}

6.最大團問題

子集樹

#include<iostream>
#include<vector>
using namespace std;

vector< vector<int> > v;
vector<int> x;
vector<int> bestx;
int maxv;//最優解頂點個數 
int num;

void storage(int nowv)
{
	if(nowv>maxv)
	{
		maxv=nowv;
		for(int i=0;i<num;i++)
		{
			bestx[i]=x[i];
		}
	}
}
bool judge(int t)//是否都與t相連
{
	for(int i=0;i<t;i++)
	{
		if(x[i]==1&&v[t][i]==0)
		{ 
			return false;
		}
	}
	return true;
} 
void MaxClique(int t,int nowv)
{
	if(t>=num)
	{
		storage(nowv);
	}
	else
	{
		for(int i=0;i<=1;i++)
		{
			x[t]=i;
			if(i==0)
			{
				MaxClique(t+1,nowv);
			}
			else if(i==1&&judge(t))
			{
				MaxClique(t+1,nowv+1); 
			}
		}
	}	
}

int main()
{
	maxv=-1; 
	cin>>num;
	//初始化全無邊 
	for(int i=0;i<num;i++)
	{
		vector<int> temp;
		for(int j=0;j<num;j++)
		{
			temp.push_back(0);		
		}
		v.push_back(temp);
		x.push_back(-1);
		bestx.push_back(-1);
	}
	//輸入邊 
	while(true)
	{
		int x1,y1;
		if(cin>>x1>>y1)
		{
			v[x1][y1]=1;
			v[y1][x1]=1;
		}
		else
		{
			break;
		}
	}
	MaxClique(0,0);
	
	cout<<maxv<<endl;
	for(int i=0;i<num;i++)
	{
		if(bestx[i]==1)
		{
			cout<<i+1<<"\t";
		}
	}
	cout<<endl;
}

7.圖的m著色問題

子集樹,解空間子集樹,不是非要往0-1二叉樹上靠,也可以是m叉樹

#include<iostream>
#include<vector>
using namespace std;

vector< vector<int> > v;
vector<int> x;
int num;
int m;
int sum;

bool judge(int t,int color)
{
	for(int i=0;i<t;i++)
	{
		if(x[i]==color&&v[i][t]==1)
		{
			return false;
		}
	}	
	return true;
}
void coloring(int t)
{
	if(t>=num)
	{
		cout<<sum<<": ";
		for(int i=0;i<num;i++)
		{
			cout<<x[i]<<"\t";
		}
		cout<<endl;
		sum++;
	}	
	else
	{
		for(int i=0;i<m;i++)
		{
			x[t]=i;
			if(judge(t,i))
			{
				coloring(t+1);
			}
			x[t]=0;
		}
	}
}

int main()
{
	sum=0;
	cin>>num>>m;
	for(int i=0;i<num;i++)
	{
		vector<int> temp;
		for(int j=0;j<num;j++)
		{
			temp.push_back(0);
		}
		v.push_back(temp);
		x.push_back(-1);
	}
	while(true)
	{
		int x1,y1;
		if(cin>>x1>>y1)
		{
			v[x1][y1]=1;
			v[y1][x1]=1;
		}
		else
		{
			break;
		}
	}
	coloring(0);
	cout<<sum<<endl;
} 

8.旅行售貨員問題

排列樹

#include<iostream>
#include<vector>
#include<climits>
#include<algorithm>
#include<cstdlib>
using namespace std;

vector< vector<int> > v;
vector<int> x;
int num;
int costbest;

int random(int s,int e)
{
	return s+rand()%(e-s);
}
int countDis()
{
	int cost=0;
	for(int i=1;i<x.size();i++)
	{
		cost+=v[x[i-1]][x[i]];
	}
	return cost+v[x[0]][x[x.size()-1]];
}
void Bttsp(int firstcity,int t)
{
	if(t>=num)
	{
		int costx=countDis();
		if(costx<costbest)
		{
			costbest=costx;
		}
	}
	else
	{
		for(int i=t;i<num;i++)
		{
			if(x[0]==firstcity)
			{
				swap(x[t],x[i]);
				Bttsp(firstcity,t+1);
				swap(x[t],x[i]);
			}
		}
	}
}
int main()
{
	costbest=INT_MAX;
	cin>>num;
	for(int i=0;i<num;i++)
	{
		vector<int> temp;
		for(int j=0;j<num;j++)
		{
			temp.push_back(0);
		}
		v.push_back(temp);
		x.push_back(i);
	}
	for(int i=0;i<num;i++)
	{
		for(int j=i+1;j<num;j++)
		{
			int temp=random(1,50);
			v[i][j]=temp;
			v[j][i]=temp;
			cout<<"v["<<i<<"]["<<j<<"]="<<temp<<endl;
		}
	}
	Bttsp(0,0);
	cout<<costbest<<endl;
} 


9.圓排列問題

排列樹

#include<iostream>
#include<vector>
#include<cmath>
#include<climits>
#include<algorithm>
using namespace std;

vector<int> x;
vector<int> r;
int num;
double disbest;

double countDis(int a,int b)
{
	return sqrt(pow(a+b+0.0,2)-pow(a-b+0.0,2));
} 
void circle(int t,double discost)
{
	if(t>=num)
	{
		if(discost<disbest)
		{
			disbest=discost;
		}
	}
	else
	{
		for(int i=t;i<num;i++)
		{
			swap(x[t],x[i]);
			double temp;
			if(t+1==num)
			{
				temp=r[x[t]];
			}
			else
			{
				temp=countDis(r[x[t]],r[x[t+1]]);
			}
			if(discost+temp<disbest)
			{
				circle(t+1,discost+temp);
			}
			swap(x[i],x[t]);
		}
	}
}
void change()
{
	int temp=x[0];
	for(int i=1;i<num;i++)
	{
		x[i-1]=x[i];
	}
	x[num-1]=temp;
}
int main()
{
	disbest=INT_MAX;
	cin>>num;
	for(int i=0;i<num;i++)
	{
		int temp;
		cin>>temp;
		r.push_back(temp);
		x.push_back(i);
	}
	for(int i=0;i<num;i++)
	{
		circle(0,r[x[0]]+0.0);
		change();
	} 
	cout<<disbest<<endl;
}