目录
3.随机产生不同规模的图,用4色着色,分析算法效率与图规模的关系。
实验目的
1. 掌握回溯法算法设计思想。
2. 掌握地图填色问题的回溯法解法。
一、背景知识
为地图或其他由不同区域组成的图形着色时,相邻国家/地区不能使用相同的颜色。我们可能还想使用尽可能少的不同颜色进行填涂。一些简单的“地图”(例如棋盘)仅需要两种颜色(黑白),但是大多数复杂的地图需要更多颜色。
每张地图包含四个相互连接的国家时,它们至少需要四种颜色。1852年,植物学专业的学生弗朗西斯·古思里(Francis Guthrie)于1852年首次提出“四色问题”。他观察到四种颜色似乎足以满足他尝试的任何地图填色问题,但他无法找到适用于所有地图的证明。这个问题被称为四色问题。长期以来,数学家无法证明四种颜色就够了,或者无法找到需要四种以上颜色的地图。直到1976年德国数学家沃尔夫冈·哈肯(Wolfgang Haken,生于1928年)和肯尼斯·阿佩尔(Kenneth Appel,1932年-2013年)使用计算机证明了四色定理,他们将无数种可能的地图缩减为1936种特殊情况,每种情况都由一台计算机进行了总计超过1000个小时的检查。他们因此工作获得了美国数学学会富尔克森奖。在1990年,哈肯成为伊利诺伊大学高级研究中心的成员,他现在是该大学的名誉教授。
四色定理是第一个使用计算机证明的著名数学定理,此后变得越来越普遍,争议也越来越小。更快的计算机和更高效的算法意味着今天您可以在几个小时内在笔记本电脑上证明四种颜色定理。
二、实验内容
1.简单地图四色涂色
1. 用4色填色下面的这个小规模地图,测试算法的正确性。将其转换为平面图,每个地区表示为一个顶点,相邻地区用边连接,对这个图的顶点着色,并且相邻的两个顶必须使用不同的颜色。
图1 测试算法正确性的小规模地图
对改图各区域进行编号 ,然后保存在一个文档(参考第2部分的文件格式)用于后面程序的输入。
2.450点5色/15色/25色
分别读入附件中的3个图文件le450_5a.col, le450_15b.col, le450_25a.col,其中,le450_5a.col中的图用5色着色,le450_15b.col中的图用15色着色,le450_25a.col中的图用25色着色,测试3个图文件对应的算法运行时间,并将着色结果输出到文本中(按照顶点序号输出每个顶点的颜色,每个颜色占1行,颜色用1, 2, 3, …表示,着色结果文件名为相应的图文件名加上后缀out,即分别为le450_5a.col.out, le450_15b.col.out, le450_25a.col.out)。
文件读取:
例如文件le450_5a.col:先是c开头的一系列注释,然后一句p开头的点数、边数信息,接着是e开头的一条条边信息。须用fstream读入文件并提取相关点、边信息用于构建(注意获取信息时的格式,stoi(string str)可以将str转为int型 )。我自己是从p开头的那句开始获取,然后可用颜色数是人为输入。
3.随机产生不同规模的图,用4色着色,分析算法效率与图规模的关系。
三、算法思想
1、结构选择:邻接矩阵和一些列数组。
string infilename;//要涂色的文件名
string outfilename;//要存放涂色结果的文件名
class Map{
public:
int n,m;//点数,边数
int cn;//颜色种类数
int **v;//点相连情况,0未连,1相连,v[i][0]存放出度
int **cv;//点的剩余可选颜色和cv[i][0]剩余颜色的可涂情况cv[i][j],cv[i][j]==1可涂,cv[i][j]j<1不可涂
int *cl;//颜色填涂情况,-1表示还未被填涂
bool flag;//判断是否求得可行解,1为是,0为否
int *x;//降序后序号对原序号的映射,降度排序用
int *X;//降序后原序号对现序号的映射
int *xx;//记录降度排序后的度数
……
}
2、具体算法
2.1 未改进的回溯法:
void MLdef(int s){//第s个点
int i,j;
bool f;
if(s>n){//先判断是否已经有可行解
flag=true;
return;
}
//对每种颜色都进行尝试
for(i=1;i<=cn;i++){
cl[s]=i;//模拟颜色填涂
f=true;//先设置为可行
for(j=1;j<s;j++)
if(v[j][s]==1&&cl[j]==cl[s]){
f=false;//前面已经有与之相连而且颜色相同的点,不可行
break;
}
if(f){//如果该颜色可行,则继续填涂下一个点
MLdef(s+1);
cl[s]=-1;//返回来,说明后面涂色失败,取消当前涂色;
//可以不用考虑这个cl[s]=-1
}else cl[s]=-1;//可以不用
}
}
2.2 置换剪枝
对于一种涂色结果,如果对各种颜色进行轮换可以得到另一种涂色结果,m种颜色可以得到m!种解。所以,当求出第一种解时可以不再继续求解(事实上对该实验足够了,只需有解就行)。
所以,可以在for循环里添加一个条件判断 !flag,这样就可以只保证求出一种解后不继续求解(其实继续求解会覆盖前面正确的解的涂色情况,是要避免的)。
2.3 向前探查剪枝策略:
在搜索过程中,当某点选择了一个颜色后,在相邻节点的可选颜色中将该颜色删去。如果在删除前(搜索探查一遍)发现删除后有相邻点没有颜色可以填涂,就放弃该节点的这个颜色。
这里用二维数组 int **cv;//点的剩余可选颜色cv[i][0]和剩余颜色的可涂情况cv[i][j],cv[i][j]==1可涂,cv[i][j]j<1不可涂 cv[i][0]存放第i个点剩余的可涂颜色数目。每次涂色j时,将相邻点i的该色可涂情况--,即cv[i][j]--。 并判断是否需要减少可涂颜色数目:如果相邻点i的cv[i][j]原本是1,然后--变为0,则需要减少cv[i][0],即cv[i][0]--。 (cv[i][j]之所以用1表示可涂,<1表示不可涂,而不是单单用0表示不可涂,是因为,可能有多个点导致某个点i的某种颜色j执行减少操作,如果只用0表示不可涂,那么恢复时将难以判断是否要恢复成1的可涂情况)
如果回溯回来了,则需取消当前区域s的当前涂色,cl[s]=-1;并对前面的--操作进行恢复,即++。
一共增加了三部分:
1)搜索:当前点涂色后,检查相邻点是否只剩下这种颜色可涂(即当前点的该涂色会导致相邻点接下来出现无色可涂的情况)。
2)减少:减少相邻点改色的可涂情况cv[i][j]--及是否cv[i][0]--。
3)恢复:cv[i][j]++及是否需要cv[i][0]++。
只使用这种方法改进算法时效率提高不是很大。
2.4 最少剩余量选择(MRV)
优先选择剩余未填色节点中可填颜色最少的节点(通过比较cv[i][0]),可以增大该路径是可行解的几率。因为剩余颜色更少的点其确定性更大(分支少),更容易找到可行解。此时下一个要涂色的点就不一定是当前点序号加1了,需要增加一个“确定下一个点”的函数Fi():
//--------------------------------//找当前可涂元素最少的点
int Fi(){
int i,j=-1;//-1表示没有点未涂色了
for(i=1;i<=n;i++)
if(cl[i]==-1){//先找第一个还未涂色的点
j=i;
break;
}
for(i++;i<=n;i++)
if(cl[i]==-1&&cv[i][0]<cv[j][0])
//选未涂色且颜色更少的点,
//如果在开始涂色前完成了降度排序,就相当于寻找可涂颜色最少且度较大的点
j=i;
return j;
}
返回-1时将不用再递归了,将flag置真,return。
该方法对效率提高比较明显(在前面的置换剪枝、向前探测剪枝情况下加上该MVR,该实验的三个图均几十秒可得可行解)。
2.5 度最大选择(DH)
优先选择度最大的节点进行填色,因为度数越大的节点受到其他节点的约束就越大,先填涂其他节点容易导致度数大的节点无可填颜色,延长运行时间。 这里建议先MRV再DH(但在该程序中是在开始涂色前先DH排好序,在涂色时直接按下标递增的顺序找可涂颜色最少的点,效果一样)。因为单独使用DH无法保证下一个涂色的点与前面已经涂色的点区域块是相连的,相当于在另一个区域重新开始涂色(前面的方法的下一个点都是与已涂色区域块相连的),几个区域块间在涂色时更容易发送冲突,导致运行时间增长,甚至求不出解。
降度排序用到数组x[](存放降度排序后的点),xx[],X[],降度可以用选择排序(代码简单)
int *x;//降序后序号对原序号的映射,降度排序用
int *X;//降序后原序号对现序号的映射
int *xx;//记录降度排序后的度数,降度排序用
for(i=1;i<n;i++){//降度,选择排序法
t=i;
for(j=i+1;j<=n;j++)
if(xx[j]>xx[t])
t=j;
if(t!=i){
q=x[i];
x[i]=x[t];
x[t]=q;
q=xx[i];
xx[i]=xx[t];
xx[t]=q;
}
}
for(i=1;i<=n;i++){//获取降序后原序号对现序号的映射
for(j=1;j<=n;j++)
if(x[j]==i){
X[i]=j;
break;
}
}
然后根据x[[和X[]重构v[][](要用到一个临时的二维数组y[][]来拷贝v[][])。
3.简单随机地图的生成
这里用人为输入来确定生成的地图的点、边数。
cout<<"请输入要生成的地图的点数、边数和存放信息的文件名称:"<<endl;
cin>>n>>m>>name;
Birn(n,m,name);//地图生成函数
由于要分析效率,所以要确保生成的地图四色涂色有可行解。所以不能出现5个点彼此间两两直接相连的情况。
Birn(,,)执行地图的生成。这里每次生成一个边信息就进行一次判断(judge(n)函数)。
//一些生成地图要用到的全局变量
int **r;//存放邻接矩阵
int *w;//记录度数大于等于4的点
int rt=0;//记录度数大于等于4的点的个数
srand((unsigned)time(NULL));
for(i=1;i<=m;){//随机生成边
a=rand()%n+1;//1~n
b=rand()%n+1;//1~n
if(a==b||r[a][b]==1)//新边是否有效或重复
continue;
judge函数先计算度数(v[i][0],出度)大于等于4的点的个数rt是否大于等于5(有可能出现5个点彼此间两两直接相连的情况)并记录这些点的序号(保存在w[]数组中)。
bool judge(int n){
int i,j;
w=new int[n];
rt=0;
for(i=1;i<=n;i++)
if(r[i][0]>=4){//记录度数大于等于4的点
w[rt++]=i;
}
if(rt>=5)
if(jd())
return true;
return false;
}
如果rt大于等于5,就调用jd()函数进行判断是否出现5个点彼此间两两直接相连的情况(5层for循环)。
如果发现新生成的边导致出现5个点彼此间两两直接相连的情况,则放弃该边,继续下一条边的生成。
四、结论、注意/建议(请注意看此部分)
3.1 结论/效率(我自己电脑上)
(以下默认使用置换剪枝)
只使用向前探测剪枝20分钟内未求解出来。
使用MRV和向前探测剪枝可短时间内求出可行解。
MRV、DH、向前探测剪枝三种一起用效率最高,三个图都是1秒内可以求解。
以le450_5a.col,MRV、DH且向前探测剪枝的回溯法(main函数先注释掉随机地图生成调用的部分)为例,运行结果如下:
随机地图生成:
3.2 注意/建议
3.2.1 关于le450_15b
其中le450_15b这个图从大多数点开始求解是不只15种颜色才能有可行解的(可以输入可选颜色为50种,看最后涂色结果输出时最大的颜色序号。比如从降度排序后的第1个点开始求解,需要16种颜色才求出可行解)。这里建议可以以降度后的第4个点为起始点进行涂色。
3.2.2 涂色结果的存放
使用降度排序求得可行解时,注意在存储涂色结果时要按X[]的顺序存放而不是降度排序后的x[]顺序存放(实验用的check程序是按降度排序前的点顺序检测涂色结果是否正确的)。所以打印函数要两个,区分一下。
void Print1(){
ofstream outfile(outfilename,std::ofstream::trunc);//以trunc的方式打开可以清空文件内容
for(int i=1;i<=n;i++){
outfile<<(to_string(cl[i])+"\n");
}
outfile.close();
}
void Print2(){
ofstream outfile(outfilename,std::ofstream::trunc);//以trunc的方式打开可以清空文件内容
for(int i=1;i<=n;i++){
outfile<<(to_string(cl[X[i]])+"\n");
}
outfile.close();
}
3.2.3 拷贝构造
由于实验要分析算法效率,建议多编一个拷贝构造函数。
借鉴时记得修改润色一下。