本文已参与「新人创作礼」活动,一起开启掘金创作之路。
【C语言】回溯法的设计与实现
一、目的
掌握回溯法的设计思想、具体实现和时间复杂度分析。
二、实验内容
先用伪代码或流程图描述利用回溯法解决的算法解决方案,再用程序实现,计算时间复杂度,记录最终测试数据和测试结果 2.1实验内容: 2.1.1 素数环问题 2.1.2 图着色问题 2.1.3 哈密顿回路问题
三、设计和编码
3.1算法设计
3.1.1素数环问题
输入:要填写到环中的整数个数n
输出:素数环的元素a[n]
1.将存储素数环的数组a[]中所以元素初始化为0
2.将a[0]初始化为1, k初始化为1
3. while(k>= 1)
3.1 a[k] = a[k]+1
3.2 while(a[k] <= n)
3.2.1 如果a[k]满足约束条件,结束循环
3.2.2如果a[k]不满足约束条件,a[k] = a[k]+1
3.3 如果a[k]<=n, 且k==n-1,输出素数环,结束程序
3.4如果a[k]<=n,且k<n-1,k= k+1
3.5 否则,a[k]=0, k=k-1,即回溯
3.1.3图着色问题
- color[1]=1; //顶点1着颜色1
- for (i=2; i<=n; i++) //其他所有顶点置未着色状态
color[i]=0, - k=0;
4.循环直到所有顶点均着色
4.1 k++; //取下一个颜色
4.2 for(i=2;i<=n; i++) //用颜色k为尽 量多的顶点着色
4.2.1若顶点i记着色, 则转步骤4.2,考虑下一个顶点;
4.2.2若顶点i着颜色k与图中与顶点i邻接的顶点不冲突,
则color[j]=k;
5.输出k;
3.1.3哈密顿回路问题
dfs(当前位置,走过的点数,过的边权和){
if(已走过n个点 and 当前的点与起点之间有边 and 当前边权为已找到的方案中最小的){
记录这个方案的的路径;
}
遍历与当前节点相连的边{
if(指向的节点没被访问过){
标记为访问过;
在当前路径中加入这个节点;
dfs(指向的节点,点数+1,边权和+这条边边权);
标记为未访问;
在当前路径中去除这个节点;//回溯
}
}
}
main(){
建图;
dfs(任意点,1,0);
输出路径;
}
3.2程序代码
3.2.1.素数环问题
#include<iostream>
using namespace std;
int n;
int a[20];
bool vist[20];
bool isPrime(int x){
if(x < 2) return false;
for(int i = 2; i*i <= x; i++){
if(x%i == 0) return false;
}
return true;
}
void print(){
for(int i = 0; i <= n-1; i++){
cout << a[i] << " ";
}
cout << endl;
}
void dfs(int cur){
int i;
if(cur == n && isPrime(a[n-1]+a[0])){
print();
return;
}
for(i = 2; i <= n; i++){
if(!vist[i]&&isPrime(i+a[cur-1])){
a[cur] = i;
vist[i] = 1;
dfs(cur+1);
vist[i] = 0;
}
}
}
int main(){
cin >> n;
if(n%2 == 0){
a[0] = 1;
vist[1] = 1;
dfs(1);
}
return 0;
}
3.2.2.图着色问题
#include <iostream>
using namespace std;
#define n 5
static int arc[100][100];
static int color[100];
int Ok(int i)
{
for(int j=0; j<n; j++)
if(arc[i][j]==1 && color[i]==color[j])
return 0;
return 1;
}
void ColorGraph()
{
int k=0;
int flag=1; //表示图中还有尚未着色的顶点
while(flag==1)
{
k++;
flag=0;
for(int i=0; i<n; i++)
{
if(color[i]==0)
{
color[i]=k;
if(!Ok(i))
{
color[i]=0;
flag=1;
}
}
int main()
{
cout<<"输入颜色的个数:";
int m;
cin>>m;
cout<<"输入无向图点和边的关系:"<<endl;
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
{
int a;
cin>>a;
arc[i][j]=a;
}
ColorGraph();
cout<<"从1到5序号填色分别为:"<<endl;
for(int i=0; i<n; i++)
{
cout<<color[i]<<" ";
}
cout<<endl;
return 0;
}
3.2.3.哈密顿回路问题
#include<iostream>
#include<vector>
using namespace std;
const int INF=0x7fffffff;
int n,m,minn=INF,min_path[50001],flag[50001],map[1000][1000],fflag=0;
vector <int> now_path;
void dfs(int x,int s,int tot){
if(s==n){
if(map[x][1]!=INF&&tot+map[x][1]<minn){
fflag=1;
minn=tot+map[x][1];
for(int i=0;i<n;i++)
min_path[i]=now_path[i];
}
return;
}
for(int i=1;i<=n;i++){
if(map[x][i]!=INF&&!flag[i]){
flag[i]=1;
now_path.push_back(i);
dfs(i,s+1,tot+map[x][i]);
flag[i]=0;
now_path.pop_back();
}
}
}
int main(){
cin>>n>>m;//n为节点个数,m为边个数
int x,y,z;
for(int i=0;i<=n;i++){
for(int j=0;j<=n;j++){
map[i][j]=INF;
}
}
for(int i=0;i<m;i++){
cin>>x>>y>>z;
map[x][y]=map[y][x]=z;
}
flag[1]=1;
dfs(1,1,0);
if(fflag){
cout<<1<<"-";
for(int i=0;i<n-1;i++){
cout<<min_path[i]<<"-";
}
cout<<1;
}
else
cout<<"No Solution!";
return 0;
}
四、运行结果及分析
4.1运行结果 4.1.1素数环问题
时间复杂度:O(n^n)
4.1.2图着色问题
时间复杂度:O(n^2)
4.1.3哈密顿回路问题
时间复杂度:O(n^2)
4.2分析
确定了问题的解空间结构后,回溯法将从开始结点(根结点)出发,以深度优先的方式搜索整个解空间。开始结点成为活结点,同时也成为扩展结点。在当前的扩展结点处,向纵深方向搜索并移至一个新结点,这个新结点就成为一个新的活结点,并成为当前的扩展结点。如果在当前的扩展结点处不能再向纵深方向移动,则当前的扩展结点就成为死结点。此时应往回移动(回溯)至最近的一个活结点处,并使其成为当前的扩展结点。回溯法以上述工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已无活结点时为止。
运用回溯法解题的关键要素有以下三点:
(1) 针对给定的问题,定义问题的解空间;
(2) 确定易于搜索的解空间结构;
(3) 以深度优先方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索
五、实验小结
回溯法中,每次扩大当前部分解时,都面临一个可选的状态集合,新的部分解就通过在该集合中选择构造而成。这样的状态集合,其结构是一棵多叉树,每个树结点代表一个可能的部分解,它的儿子是在它的基础上生成的其他部分解。树根为初始状态,这样的状态集合称为状态空间树。
回溯法对任一解的生成,一般都采用逐步扩大解的方式。每前进一步,都试图在当前部分解的基础上扩大该部分解。它在问题的状态空间树中,从开始结点(根结点)出发,以深度优先搜索整个状态空间。这个开始结点成为活结点,同时也成为当前的扩展结点。在当前扩展结点处,搜索向纵深方向移至一个新结点。这个新结点成为新的活结点,并成为当前扩展结点。如果在当前扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。此时,应往回移动(回溯)至最近的活结点处,并使这个活结点成为当前扩展结点。回溯法以这种工作方式递归地在状态空间中搜索,直到找到所要求的解或解空间中已无活结点时为止。
回溯法与穷举法有某些联系,它们都是基于试探的。穷举法要将一个解的各个部分全部生成后,才检查是否满足条件,若不满足,则直接放弃该完整解,然后再尝试另一个可能的完整解,它并没有沿着一个可能的完整解的各个部分逐步回退生成解的过程。而对于回溯法,一个解的各个部分是逐步生成的,当发现当前生成的某部分不满足约束条件时,就放弃该步所做的工作,退到上一步进行新的尝试,而不是放弃整个解重来。