实验目的
(1)掌握回溯法算法设计思想。
(2)掌握地图填色问题的回溯法解法。
实验内容与结果
四色问题:“任何一张地图只用四种颜色就能使具有共同边界的国家着上不同的颜色。”
也就是说在不引起混淆的情况下一张地图只需四种颜色来标记就行。
问题描述:
我们可以将地图转换为平面图,每个地区变成一个节点,相邻地区用边连接,我们要为这个图形的顶点着色,并且两个顶点通过边连接时必须具有不同的颜色。
附件是给出的地图数据,请针对三个地图数据尝试分别使用5个(le450_5a),15个(le450_15b),25个(le450_25a)颜色为地图着色。
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
回溯法的基本思想是:
(1)针对具体问题,定义问题的解空间;
(2)确定易于搜索的解空间结构(数据结构的选择)。
(3)一般以DFS的方式搜索解空间。
(4)在搜索过程中,可以使用剪枝函数等来优化算法。
(剪枝函数:用约束函数和限界函数剪去得不到最优解的子树,统称为剪枝函数。)
实验要求:
1、对下面这个小规模数据,利用四色填色测试算法的正确性;
、
算法思想:用领接矩阵记录图块相邻信息,使用dfs逐个图块搜寻可行解;即给图块逐一上色,上色后遍历当前图块的所有相邻图块,判断是否满足要求,若不满足要求,则返回到上一个图块;若满足要求,则继续往下尝试;当图块全部上色后,解的个数+1。
算法实现:利用color数组记录每个节点的颜色,在dfs尝试中chck函数通过领接矩阵判断是否满足要求,若满足则继续递归。
伪代码如下
编号如下
编码存入1.txt文件
求出有480个解
算法复杂度分析:
每个节点可涂的颜色为4种,因此回溯法中dfs的时间复杂度为 O(4n);check函数遍历领接矩阵判断相邻节点是否同色,复杂度为O(n2); 因此总的算法复杂度为O(n2×4n )。
2、对附件中给定的地图数据填涂;
当对大数据量(450点5色)处理时,发现运行时间非常久,效率非常低。从算法复杂度的分析来看,当节点个数n较大时,复杂度也非常大,所以需要对算法进行改进。
优化策略
1. 贪心剪枝思想。优先搜索未着色的可选颜色最少的顶点,如果可选颜色数最少的点不止一个,则在这些顶点中优先搜索相邻的点数最多的顶点。这样子可以使得先处理限制条件最多的节点,免得前面已经搜索过大量的点后因该点导致失败耗费大量时间。
2.设立color二维数组,color[k][0]表示第k点有几种颜色可用,color[k][1]表示第k点的第一种颜色是否可用,可用时为1,不可用时为0;这样可以使得不需要遍历邻接矩阵或者邻接表检验涂色是否合法,而是通过color数组就可以判断。
3.用领接表替代领接矩阵,在输入时建立领接表而不用领接矩阵,领接表的时间复杂度为O(n+e),领接矩阵的时间复杂度为O(n2),用领接表替换领接矩阵后在涂色后修改相邻节点可以节省不少时间。
4.链表记录修改值。当一个节点涂色成功后,需要修改相邻节点的color数组,此时还需要记录被修改的节点和颜色,以便在回溯时恢复。这里采用,方便快速记录和回收。
5.向前两步探测剪枝。
在向前一步探测的基础上再向前探测一步,可以极大的排除不满足要求的颜色,避免上色后递归多次进行大量尝试后最终失败导致耗费大量时间。
6.采用颜色轮寻(置换剪枝)加速得到结果。
当固定第一个点的颜色后,求出该点的所有解后,再乘以可用颜色即可得到所有解的个数。
举例说明如下图所示。
主要部分伪代码如下
数据结构部分采用的是结构体,方便对同一节点的id,颜色处理,以及链表的建立。
涂色成功后,调用paichu函数修改相邻节点的color数组避免重复涂相同的色,与此同时还要用jilu链表记录被修改的部分以便回溯时进行还原。
当修改完当前节点的相邻节点的color数组后,调用search函数查找下一个进行尝试的节点。
当回溯时,调用恢复函数遍历记录链表恢复相邻节点被修改的color数组,避免影响后面上色。
向前探测剪枝,此处为向前两步探测剪枝,可以更快的排除不合法的颜色,大大加快算法运行效率。
dfs函数主体部分,当k为-1时,说明已经全部涂色成功,解的个数ans+1。
运行代码
小地图部分
450点5色
450点15色
450点25色
3、随机生成地图
产生不同规模的图,分析算法效率与图规模的关系(四色)。
边数设定为2n,附加输出dfs深度确保检验结果准确性。
固定可选颜色数为4,分别取n=200,500,1000,1500,2000,2500,3000,3100,3200,3300十组数据,以确保结果的可信度。
200点4色时
500点4色
1000点4色
1500点4色
2000点4色
2500点4色
3000点4色
3100点4色
3200点4色
3300点4色
制表如下
折线图如下
可以看出,图的大小会对搜索规模造成极大影响,随着节点个数的增加,所耗费的时间也越多。并且随着节点个数的增加所耗费时间会急剧增加。
之所以该图像没有呈现指数级迅速增加,是因为算法采用了多种策略进行剪枝优化,减少了运行时间。
代码最主要的时间复杂度在回溯部分,设节点个数为n,可用颜色为m,则时间复杂度为O(mn),在该算法中采用了color数组记录可用颜色以及向前探测剪枝等策略,所以实际运行时间会减小。
实验总结
此次实验较为困难,策略理解起来倒是较为简单,主要难在代码的实现(改了无数遍)。有很多需要注意的细节,而且出现bug时很难查找。因为小地图可能很容易就过了,但是小地图由于数据规模小的限制,过了并不意味着算法就是正确的,小地图过了但大地图搜索失败是很常见的。但是大地图又很难debug,只能根据代码设置输出并进行猜想和尝试。这次实验遇到了非常非常多的问题,耗费了大量的时间精力,好在最后总算是成功完成了。
当图的数据量较大时,运行时间极长,需要优化算法来减少运行时间,常用各种剪枝策略进行优化。当可用颜色相同时,图越大,所耗费时间越多。
回溯时注意要将当前节点的颜色恢复为未涂色状态,因为dfs到某个节点时会给该节点上色,然后递归搜寻下一个节点,若搜寻不到则会回溯到上一个节点,此时如果不将该节点恢复为未涂色状态,则会影响其他节点的判断。
总的来说,此次实验就是想方设法的降低算法复杂度。从一般算法到多种策略剪枝,从领接矩阵到邻接表,从向前探测一步到向前探测两步,再到color二维数组记录颜色可用情况,链表记录更改情况,颜色轮寻更快得到总的解个数等等,无一不是为了优化算法,使得其在大数据量的情况下仍然能有较高的运算效率。通过这次实验,我掌握了各种优化算法的方法以及在大数据量情况下的有效调试方法以及回溯法的了解与掌握实现。
实验伪代码
check(int node,int c) //判断当给node节点涂上c颜色时是否合法 for i=0 to n-1 //遍历相邻节点,若颜色相同则不合法 if(juzhen[node][i]&&c==color[i]) return 0 return 1 dfs(int node) if(node==n) //所有节点均上色,解的个数++ ans++ return for i=1 to 4 //尝试给节点上色 if check(node,i) color[node]=i dfs(node) color[node]=0 //数据结构采用结构体 struct node { int id=0,c = 0; //c记录当前点颜色,c=0表示未涂色 node* next=NULL; } //当前节点涂色后,修改相邻节点color数组并记录修改部分 paichu(int k,int c,node *jilu) p=adj[k].next //获取邻接表相邻节点 id=p->id //获取相邻节点id color[id][c] = 0 //修改color数组 color[id][0]-- jilu(p) //链表记录 //查找下一个递归节点的id search() for i=1 to n if (dians[i].c==0&&color[i][0] > 0) if (color[i][0] <= min||(color[i][0] == min && adj[i].id >= adj[minid].id)) minid = i, min = color[i][0]; return minid; //恢复被修改的相邻节点的color数组 huifu(node *jilu) p=jilu->next id=p->id c=p->c color[id][c] = 1 color[id][0]++ //向前探测剪枝 front(int k,int c) node* p = adj[k].next while (p) int id = p->id; if (dians[id].c != 0) p = p->next continue if (color[id][0] == 0) return -1 //如果k的相邻节点只剩下颜色c一种可以选的时候,说明k不能选c色 if (color[id][0] == 1 && color[id][c] == 1) return 0 p=p->next return 1 //dfs递归 dfs(int k) //k为当前点的id if k==-1 if(jishu!=n) return ans++ //记录解的个数 return for j=1 to colornum if color[k][j]==1 if(front(k,j)==0) //向前探测剪枝 continue dians[k].c = j node *jilu = new node paichu(k, j,jilu) //排除相邻节点的j颜色 dfs(search()) // 搜索下一个节点 huifu(jilu) // 恢复 dians[k].c = 0 if(jishu==0)//利用颜色轮寻加速得到解的个数 cout<<"颜色轮寻得到解的个数是" << ans * colornum << endl;
实验代码
origin.cpp
#include#include #include using namespace std; const int MAXN = 500; // 最大节点数 int n; // 节点数 int color[MAXN]; // 记录每个节点的颜色 int cnt = 0; // 解的个数 int juzhen[MAXN][MAXN]; // 图的邻接矩阵 bool check(int node, int c) { for (int i = 0; i < n; i++) { if (juzhen[node][i] && c == color[i]) { return false; } } return true; } void dfs(int node) { if (node == n + 1) { // 找到一组可行解 cnt++; return; } for (int i = 1; i <= 4; i++) { if (check(node, i)) { color[node] = i; // 给当前节点涂色 dfs(node + 1); // 搜索下一个节点 color[node] = 0; // 恢复 } } } int main() { ifstream ifs("D:\\1.txt"); ifs >> n; memset(juzhen, 0, sizeof(juzhen)); memset(color, 0, sizeof(color)); int u, v; char ch; while (ifs >> ch >> u >> v) { // 输入相邻节点 juzhen[u][v] = juzhen[v][u] = 1; } int qi, zhi; qi = clock(); dfs(1); zhi = clock(); cout << "解的个数为: " << cnt << endl; cout << "运行时间为: " << zhi - qi << endl; return 0; }
medium.cpp
#include#include #include #include using namespace std; # define lujin "D:\\1.txt" # define colornum 4 int n; // 节点数 const int MAXN = 10000; // 最大节点数 int qi, zhi; int color[MAXN + 1][colornum + 1];//color[k][0]表示有几种颜色可用,color[k][1]表示第k点的第一种颜色是否可用(1或0) int ans = 0; // 解的个数 //int juzhen[MAXN][MAXN]; // 图的邻接矩阵 struct node { int id=0,c = 0; node* next=NULL; }dians[MAXN], * adj; void paichu(int k,int c,node *jilu) { node* p = adj[k].next; node* s = jilu; while (p) { int id = p->id; if (color[id][c] == 1) { color[id][c] = 0; color[id][0]--; node* t = new node; t->id = id, t->c = c; s->next = t; s = t; } p = p->next; } } int search() { int i; int min = 999; int minid=1; for (i = 1; i <= n; i++) if (dians[i].c==0&&color[i][0] > 0) if (color[i][0] < min||(color[i][0] == min && adj[i].id > adj[minid].id)) minid = i, min = color[i][0]; if (min == 999) return -1; return minid; } void huifu(node *jilu) { node* p = jilu->next; while (p) { int id = p->id,c=p->c; color[id][c] = 1; color[id][0]++; p = p->next; } } void dfs(int k) { //cout << "k=" << k << endl; if (k == -1) { // 找到一组可行解 ans++; if (ans == 1) { int first = clock(); cout <<"450点"<< colornum << "色 找到第一个解运行时间为: " << first - qi << "ms" << endl; } //cout << ans << endl; return; } for (int j = 1; j <= colornum; j++) { if (color[k][j] == 1) { dians[k].c = j; node *jilu = new node; paichu(k, j,jilu);//排除相邻节点的j颜色 dfs(search()); // 搜索下一个节点 huifu(jilu); // 恢复 dians[k].c = 0; while (jilu->next) { node* s = jilu->next->next; delete jilu->next; jilu->next = s; } delete jilu; } } } int main() { /*随机生成地图,需要手动修改n for (int i = 1; i <= n; i++) { color[i][0] = colornum; for (int j = 1; j <= colornum; j++) color[i][j] = 1; } adj = new node[MAXN]; int u, v; srand(time(0)); node* p; for (int i = 1; i <= 100; i++) { u = (rand() % n) + 1; v = (rand() % n) + 1; if (u == v) continue; dians[u].id = u; dians[v].id = v; p = &adj[u]; while (p->next != NULL) p = p->next; node* s = new node; s->id = v; p->next = s; adj[u].id++;//记录相邻点个数 p = &adj[v]; while (p->next != NULL) p = p->next; node* t = new node; t->id = u; p->next = t; adj[v].id++; } */ // /* //从文件中读入 ifstream ifs(lujin); ifs >> n; for (int i = 1; i <= n; i++) { color[i][0] = colornum; for (int j = 1; j <= colornum; j++) color[i][j] = 1; } adj = new node[MAXN]; int u, v; char ch; node* p; while (ifs >> ch >> u >> v) { // 输入相邻节点 // juzhen[u][v] = juzhen[v][u] = 1; dians[u].id = u; dians[v].id = v; p = &adj[u]; while (p->next!=NULL) p = p->next; node* s = new node; s->id = v; p->next = s; adj[u].id++;//记录相邻点个数 p = &adj[v]; while (p->next != NULL) p = p->next; node* t = new node; t->id = u; p->next = t; adj[v].id++; } // */ //for (int i = 1; i <= n; i++) // cout << adj[i]. id << " "; qi = clock(); dfs(search()); zhi = clock(); cout << "解的个数为: " << ans << endl; cout << "运行时间为: " << zhi - qi<<"ms" << endl; }
final.cpp
#include#include #include #include #include #include using namespace std; # define lujin "D:\\1.txt" # define colornum 4 int n; // 节点数 const int MAXN = 500; // 最大节点数 int flag=0; //记录第几次jishu为0 int qi, zhi; int color[MAXN + 1][colornum + 1];//color[k][0]表示有几种颜色可用,color[k][1]表示第k点的第一种颜色是否可用(1或0) int tried[MAXN+1][colornum + 1]; //记录当前节点的该颜色是否已经尝试过 int kexuan[colornum + 1]; //记录相邻节点每个颜色可选的次数 int ans = 0; // 解的个数 //int juzhen[MAXN][MAXN]; // 图的邻接矩阵 int jishu = 0; //数据结构 struct node { int id = 0, c = 0; node* next=NULL; }dians[MAXN+1], * adj; //查找下一个点,优先考虑最大度,其次考虑颜色最少剩余量 int search1() { int i; int max = 0; int maxid = 1; for (i = 1; i <= n; i++) if (dians[i].c == 0 && color[i][0] > 0) if (adj[i].id >= max || (adj[i].id == max && color[i][0] <= color[maxid][0])) maxid = i, max = adj[i].id; if (max == 0) return -1; return maxid; } //查找下一个点,优先考虑颜色最少剩余量,其次考虑最大度 //亲测此搜索方法更好用 int search() { int i; int min = 999; int minid=1; for (i = 1; i <= n; i++) if (dians[i].c==0 && color[i][0] > 0) if (color[i][0] <= min || (color[i][0] == min && adj[i].id >= adj[minid].id) ) minid = i, min = color[i][0]; if (min == 999) return -1; return minid; } //k节点涂c色后,将k节点的相邻节点的该颜色置为0 void paichu(int k, int c, node* jilu) { node* p = adj[k].next; node* s = jilu; while (p) { int id = p->id; if (dians[id].c != 0) { p = p->next; continue; } if (color[id][c] == 1) { color[id][c] = 0; color[id][0]--; node* t = new node; t->id = id, t->c = c; s->next = t; s = t; } p = p->next; } } //回溯后恢复被更改的颜色值 void huifu(node *jilu) { node* p = jilu->next; while (p) { int id = p->id,c=p->c; color[id][c] = 1; color[id][0]++; p = p->next; } } //向前探测 int front(int k, int c) { node* p = adj[k].next; while (p) { int id = p->id; if (dians[id].c != 0) { p = p->next; continue; } if (color[id][0] == 0) return -1; if (color[id][0] == 1 && color[id][c] == 1) //如果k的相邻节点只剩下颜色c一种可以选的时候,说明k不能选c色 return 0; /* if (color[id][0] == 2 && color[id][c] == 1)//向前两步探测 //似乎增加了运行时间?可能是front持续递归 { int cc; for (int i = 1; i <= colornum; i++) if (color[id][i] == 1 && i != c) cc = i; dians[k].c = 1; //避免当前点的影响,注意后面要还原 int zhi = front(id, cc); dians[k].c = 0; if (zhi != 1) return zhi; } */ if (color[id][0] == 2 && color[id][c] == 1) //向前两步探测——非front递归法 { int cc; for (int i = 1; i <= colornum; i++) //找到剩下一种颜色 if (color[id][i] == 1 && i != c) cc = i; dians[k].c = 1; //避免当前点的影响,注意后面要还原 node* s = adj[id].next; int flagg = 0; while (s) { int idd = s->id; if (dians[idd].c != 0) { s = s->next; continue; } if (color[idd][0] == 0 || (color[idd][0] == 1 && color[idd][cc] == 1)) { flagg = 1; break; } s = s->next; } dians[k].c = 0; if (flagg == 1) return 0; } p = p->next; } return 1; } //优先选择相邻节点可涂颜色最少的颜色,以便更快地得到第一个解 int nextcolor(int k) { for (int j = 1; j <= colornum; j++) { kexuan[j] = 0; } node* p = adj[k].next; while (p) { int id = p->id; for (int i = 1; i <= colornum; i++) if (color[id][i] == 1&&dians[id].c==0) kexuan[i]++; p = p->next; } int minc = 1, min = 99; for (int i = 1; i <= colornum; i++) { if (kexuan[i] < min && color[k][i] == 1&& tried[k][i]==1) // 该颜色可选且没有尝试过 { min = kexuan[i]; minc = i; } } if (min == 99) return 0; tried[k][minc] = 0; return minc; } //递归搜索函数 void dfs(int k) { // cout << "k=" << k << endl; // cout << "dfs深度" << jishu << endl; if (k == -1) { // 找到一组可行解 if (jishu != n) return; ans++; if (ans == 1) { int first = clock(); cout < next) { node* s = jilu->next->next; delete jilu->next; jilu->next = s; } delete jilu; if (jishu == 0) { cout << "颜色轮寻得到解的个数是" << ans * colornum << endl; zhi = clock(); cout << "运行时间为: " << zhi - qi << "ms" << endl; exit(0); } } } /* for (int i = 1; i <= colornum; i++) tried[k][i] = 1; int j = nextcolor(k); while (j) { if (front(k, j) == 0) //向前一步检验 { j = nextcolor(k); continue; } // /* if (jishu == 0 && flag==1) { cout << "也许解的个数是" << ans * colornum << endl; zhi = clock(); cout << "运行时间为: " << zhi - qi << "ms" << endl; exit(0); } // jishu++; dians[k].c = j; node* jilu = new node; paichu(k, j, jilu);//排除相邻节点的j颜色 dfs(search()); // 搜索下一个节点 huifu(jilu); // 恢复 dians[k].c = 0; jishu--; while (jilu->next) { node* s = jilu->next->next; delete jilu->next; jilu->next = s; } delete jilu; j = nextcolor(k); if (jishu == 0) flag++; }*/ } int main() { /* //随机生成地图,需要手动修改n for (int i = 1; i <= n; i++) { color[i][0] = colornum; for (int j = 1; j <= colornum; j++) color[i][j] = 1; } adj = new node[MAXN+1]; int u, v; srand(time(0)); node* p; for (int i = 1; i <=n*2; i++) { u = (rand() % n) + 1; v = (rand() % n) + 1; while (u == v) //排除两点相同的情况 v = (rand() % n) + 1; dians[u].id = u; dians[v].id = v; p = &adj[u]; while (p->next != NULL) p = p->next; node* s = new node; s->id = v; p->next = s; adj[u].id++;//记录相邻点个数 p = &adj[v]; while (p->next != NULL) p = p->next; node* t = new node; t->id = u; p->next = t; adj[v].id++; } */ // /* //从文件中读入 ifstream ifs(lujin); ifs >> n; for (int i = 1; i <= n; i++) //初始化数组 { color[i][0] = colornum; for (int j = 1; j <= colornum; j++) { color[i][j] = 1; } } adj = new node[MAXN+1]; int u, v; char ch; node* p; while (ifs >> ch >> u >> v) { // 输入相邻节点 // juzhen[u][v] = juzhen[v][u] = 1; dians[u].id = u; dians[v].id = v; p = &adj[u]; while (p->next!=NULL) p = p->next; node* s = new node; s->id = v; p->next = s; adj[u].id++;//记录相邻点个数 p = &adj[v]; while (p->next != NULL) p = p->next; node* t = new node; t->id = u; p->next = t; adj[v].id++; } // */ //for (int i = 1; i <= n; i++) // cout << adj[i]. id << " "; qi = clock(); dfs( search() ); zhi = clock(); cout << "解的个数为: " << ans << endl; cout << "运行时间为: " << zhi - qi<<"ms" << endl; }
(by 归忆)