具体可参考本人博客GitHub
HEAD算法论文中得到了K=47的结果,但是运算量太大,本人并没有得到。
算法主要参考了 hujinglovekmg的《禁忌搜索算法解决图着色问题》和华科吕志鹏老师的教学ppt《Chapter 4 Hybrid Evolutionary Algorithm: A Case Study on Graph Coloring》。
核心代码部分
01 判断条件优化
在ppt中,关于禁忌算法的描述如下图:
禁忌搜索算法
步骤3.3,会计算出delt相同的moves,而相同的moves中,tabu & non-tabu moves可能同时存在。而在步骤3.4中,在上述情况存在时,若满足aspiration,则会优先选择tabu moves。而我们希望的,应该是在此种情况时,tabu & non-tabu moves一起随机选择。因此,可以把判断条件写为
if (tmp_delt <= delt && ( iter> h_tabu[j]|| (tmp_delt + f)<best_f)) { // 1&&(2||3)
if (tmp_delt < delt) {//当前解小于本次迭代最优解,则重新开始保存解
equ_count = 0;
delt = tmp_delt;
}
equ_delt[equ_count][0] = i;
equ_delt[equ_count][1] = j;
equ_count++;
}
在计算完所有的critical one-moves后,再从equ_delt保存的moves中,随机选一个。
int tmp = rand() % equ_count;//有多个解时,随机选择
sel_vertex = equ_delt[tmp][0];
sel_color = equ_delt[tmp][1];
其中,变量delt表示本次迭代的最好解,变量tmp_delt表示当次计算出的delt,变量best_f表示历史最好解。i和 j 分别表示顶点和颜色。
若满足条件(1&&2),则是non-tabu move;若满足条件(1&&3),则是tabu move。
02 随机选解的另一种方法
另有一种并不需要每次都保存下相同delt的move的方法。
if (tmp_delt <= delt && ( iter> h_tabu[j]|| (tmp_delt + f)<best_f)) { // 1&&(2||3){
if (tmp_delt < delt) {
equ_count = 1;
delt = tmp_delt;
sel_vertex = i;
sel_color = j;
}
if (rand() % equ_count == 0) {
sel_vertex = i;
sel_color = j;
}
equ_count++;
}
- equ_count==1时,第一个move有1/1的概率取得(此时(rand() % equ_count == 0)的概率为1);
- equ_count==2时,第一个相等的move有1/1*(1-1/2)=1/2的概率取得,第二个相等的move有1/2的概率取得;
- equ_count==3时,第一个相等move有1/1*1/2*(1-1/3)=1/3的概率取得,第二个相等的move有1/2*(1-1/3)=1/3的概率取得,第三个相等的move有1/3的概率取得;
……
依次类推.
但是rand()函数和取余的运算,耗时较大。
if (rand() % equ_count == 0) {
sel_vertex = i;
sel_color = j;
}// 耗时约为3/10000毫秒
equ_delt[equ_count][0] = i;
equ_delt[equ_count][1] = j;
//耗时约为7/10000000毫秒
所以在equ_count已知范围,并且范围不大(避免数组出现问题)的情况下,用前节的方法会更好。
在本例中,equ_count的最大值为N*(K-1),N为总顶点数,K为总颜色数。即在一次迭代中,可以得到相同delt的moves,最多有N*(K-1)个。
运行结果
- i5-7400 CPU,8.00GB
- vs 2017
- 采用O2(代码速度)最快编译。vs->属性->c/c++->优化->O2
- release版本
- 算例为DSJC500.5.col,颜色数为49
迭代次数 | 时间(ms) | 迭代频率(次/s) | |
---|---|---|---|
1 | 6637998 | 20931 | 317137 |
2 | 23955577 | 75391 | 317751 |
3 | 38922336 | 123361 | 315516 |
4 | 96602832 | 302124 | 319746 |
5 | 12942501 | 40556 | 319127 |
6 | 2570572 | 8239 | 312000 |
7 | 139941246 | 439614 | 318328 |
8 | 218616457 | 690711 | 316509 |
9 | 52939800 | 168495 | 314192 |
10 | 7037286 | 21899 | 321352 |
11 | 9168899 | 28408 | 322758 |
12 | 13672252 | 41232 | 331593 |
平均值 | 51917313 | 163413.4 | 318834 |
非核心部分
01 初始化读取文件
算例DSJC500.5.col的格式为
- “edge”后的数字分别代表顶点总数和边的总数;
- “e”后为两相邻顶点,其中,第一个数字都是大于第二个数字的,不会有重复。
因此,需要以空格和换行符为界,把每个词分开。
代码如下:
#include<string>
#include<fstream>
using namespace std;
void readData(string fileName)
{
ifstream ifs;
ifs.open(fileName);
string str;
ifs>>str;
while(!ifs.eof())
{
if(str=="edge")
{
ifs>>N; //int N,顶点数目
//其他代码
}
if(str=="e")
{
int m,n;
ifs>>m>>n;
//其他代码
}
ifs>>str;
}
ifs.close();
}
ifstream ifs;
即可实现把文件内容读入不同类型的变量。
02 随机生成初始解
#include<stdlib.h>
sol = new int[N];//每个顶点的颜色
srand(time(NULL));
for (int i = 0; i < N; i++)
{
sol[i] = rand() % K; //给每个点随机着色
}
注意,rand()
生成的随机数,实际上是伪随机数,srand()
则决定了选择的随机数序列是哪一个。srand(time(NULL))
则是按照系统实际,选择随机数序列,若不加上这一句,则每次都会得到相同的初始解。
小结
本文中的禁忌搜索算法的迭代次数与初始解的关系很大,怎样才能迅速收敛到最优解,还需思考。
算例DSJC500.5.col的历史最优解为48,用纯禁忌算法已经很难算出来。
越简单的数据类型,效率越高。最开始我用了vector,后来用了链表,再后来全改成了数组,速度有了很大的提升。
网友评论