算法设计与分析 实验三 回溯法求解地图填色问题(下)

简介: 算法设计与分析 实验三 回溯法求解地图填色问题

(4)矩阵记录可行解避免多次搜索


①算法描述:

在进行回溯搜索过程中,每次对点进行颜色合法性检查时都要遍历当前点,每一次每一层的搜索都需要检查合法性,成千上万次操作将大大提高这部分的时间消耗。而每次拓展节点时,新节点与原节点都会有公共边,因而点的可用性不会差别特别大,因此,可以设计一个数组对每个点的可用性进行检测并存储,从而避免多次对可用性的检测。


②具体实现:

为了存储每个点的颜色可用性,设置了colorMatrix进行存储。其中,colorMatrix[i][1]表示第i点1颜色是否可用(可用为1,反之为0),colorMatrix[j][2]表示第j点2颜色是否可用(可用为1,反之为0)。特殊地,colorMatrix[i][0]表示第i点共有几种可用颜色

因此对于每个点回溯时,只需通过colorMatrix遍历点i的可用颜色,再删除邻接点colorMatrix可用颜色并继续搜索即可。完成搜索则恢复该可用颜色即可。


a.搜索函数:


//backtrack to search
void backtrack(Map &map, int index, int rest) {
    //judge if find the solution
    if (rest == 0) {
        //store the solution
        result.push_back(vector<int>(map.colorArray + 1, map.colorArray + 1 + map.vNum));
        for (int i = 0; i < map.vNum; i++) {
            ofs << result[result.size() - 1][i];
        }
        ofs << endl;
        return;
    }
    //dissatisfied solution: check each valid color
    for (int i = 0; i < map.colorType; i++) {
        if (map.colorMatrix[index][i + 1] == 0) {
            //if the color solution is invalid
            if (deleteColor(map, index, i) != -1) {
                recoverColor(map, index, i);
                continue;
            }
            //search point[index]
            map.colorArray[index] = i;
            //backtrack to search
            backtrack(map, next(map), rest - 1);
            //recover the validation of the color and return
            map.colorArray[index] = -1;
            recoverColor(map, index, i);
        }
    }
    return;
}

首先回溯函数中对是否为解进行判断,如果是解,则将结果存入结果容器中,并直接返回。

如果当前不为可行解,且当前点没有合法可用颜色,则进入下一个点的判断。如果有可用颜色,则对当前节点的可用颜色进行拓展,并使用colorMatrix对颜色情况进行记录,搜索前删除该可用颜色,搜索后恢复该可用颜色。


b.恢复颜色函数:

void recoverColor(Map &map, int index, int color) {
    MapNode candidate = map.vArray[index];
    MapNode *tmp = candidate.next;
    while (tmp) {
        int candidateIndex = tmp->value;
        //if the point is uncolored and its adjacent point is not colored with that color
        if (map.colorArray[candidateIndex] == -1 && map.colorMatrix[candidateIndex][color + 1] == 1 + index) {
            map.colorMatrix[candidateIndex][color + 1] = 0;
            if (map.colorMatrix[candidateIndex][0] == 0) {
                map.colorMatrix[candidateIndex][0]++;
                return;
            }
            map.colorMatrix[candidateIndex][0]++;
        }
        tmp = tmp->next;
    }
}

对于每个搜索的点,当他下一个邻接点不为空时,则进行搜索。首先判断当该点未涂色且该点邻接点颜色合法时,则可以认为该点的当前涂色合法,将colorMatrix对应值加一并返回即可。


c.删除颜色函数:


int deleteColor(Map &map, int index, int color) {
    MapNode candidate = map.vArray[index];
    MapNode *tmp = candidate.next;
    while (tmp) {
        int candidateIndex = tmp->value;
        //if the point is uncolored and its adjacent point is not colored with that color
        if (map.colorArray[candidateIndex] == -1 && map.colorMatrix[candidateIndex][color + 1] == 0) {
            map.colorMatrix[candidateIndex][color + 1] = 1 + index;
            map.colorMatrix[candidateIndex][0]--;
            if (map.colorMatrix[candidateIndex][0] == 0)
                return candidateIndex;
        }
        tmp = tmp->next;
    }
    return -1;
}


对于每个搜索点,如果该点的下一个邻接点的可涂色颜色为0,则返回当前点的序号,如果不存在下一个邻接点,则返回-1。对于邻接点可涂颜色不为0的情况,则更新邻接点可涂颜色直至搜索到末端点。


(5)数据结构的选择:

①算法描述:

对于储存图的数据结构,常见的有邻接表和邻接矩阵两种。因此如何选择合适的数据结构也将从一定程度上节省程序的运行时间。


对于邻接矩阵,寻找相邻区域时需要遍历所有区域;对于邻接表而言,可以直接获取相邻区域。由于本地搜索程序与深度优先搜索类似,而采用邻接表的深度优先搜索的时间复杂度为O ( n + e ) ( e 为 边 数 ) O(n+e)(e 为边数)O(n+e)(e为边数),采用邻接矩阵的深度优先搜索的时间复杂度为O ( n 2 )。因此在搜索时,可以采用邻接表进行搜索。但对于邻接表而言,对于一些数据的存储没有邻接矩阵方便,因此可以采用邻接矩阵和邻接表共用的方式对图结构进行存储。

②具体实现:


struct Map {
    int vNum;          //number of vertex
    MapNode *vArray;   //adjacency list
    int *colorArray;   //store type of color, -1 for uncolored
    int colorType;     //type of color
    int *oldToNew;     //change after sort by degree, origin id-> new id
    int *newToOld;     //change after sort by degree, new id-> origin id
    bool **matrix;     //adjacency matrix
    int **colorMatrix; //store the validation of each point and number of valid colors
};


使用colorMatrix二维矩阵存储各个点的颜色可用情况,使用vArry存储邻接表,使用colorArray存储点的涂色情况。

③运行并测试:

对给定的大数据(450点5色)进行填涂,并将可行解全部输出,将程序运行20次取时间平均值做表如下:


优化前 优化后
240.5s 169.4s


这证明,使用邻接表存储数据并进行优化是有效的优化方式。


3、时间与效率分析:


利用以上算法优化,完成不同规模下数据的试涂色记录并整理如下(搜索结果在‘result’文件夹中):


(1)三组数据的涂色:


①得到第一个可行解:



450点5色
450点15色 450点25色
时间消耗 860.1ms 386.4ms 210.6ms



可以看到随着可用颜色的增多,涂色消耗时间依次缩短。这是由于可用颜色更多,搜索过程中更容易搜索到可行解,无需进行多次搜索就可以找到可行解。

②得到全部可行解(15色和25色全部可行解个数超过10^9无法一定时间内得到完备搜索结果):



450点5色
时间消耗 61.199s
可行解个数 3840


可以看到采用贪心剪枝,置换剪枝,向前探查剪枝,矩阵记录并选择邻接表进行数据存储这5种优化后,实现了从最开始未优化时短时间内无法完成完备搜索变为在一分钟左右的时间即可完成全部搜索,算法的效率得到极大提升。


(2)自行生成地图涂色并分析:


通过随机数,自行生成了不同规格的地图,对于每个地图,均将程序运行10次并取平均值作为实际值。对于每次比较时,均需要保证其余变量的值不变,在有解的情况下,分析统计作表如下:


①图规模对运行时间的影响:


100点5色 200点5色 300点5色 400点5色
时间消耗 1,943ms 10,673ms 24,164ms 60,561ms


ef5669d18a1241f78491e758971af5b9.png


图的规模极大影响了搜索的规模,规模小的图要比规模大的图的搜索时间小的多。

通过上图,可以看到,当地图规模成线性级增长时,时间消耗大致成指数型增长。并且,随着数量级的增长,指数增长速率放缓,这是因为多种剪枝策略对大数据下优化作用更明显。

②合法涂色数对运行时间的影响:


200点5色 200点7色 200点9色
时间消耗 10.7s 74.6s 592.1s


3ffefba3f0e745aa9ee0b264434bec24.png


合法的颜色数极大的影响了搜索的规模,合法颜色少的图要比合法颜色多的图的搜索时间小的多。

通过分析可知,若对于某图涂色的存在可行解向量image.pngimage.png存在n ! 种等效可行解向量。因此,当合法颜色数增多时,可行解数量大致成阶乘型(指数)增加。

此外,随着合法颜色数的增长,指数增长速率增大,这是因为由于合法颜色数的增加导致可剪枝数降低,搜索树成指数级增长,剪枝策略的优化作用减弱。


③图的边密度对运行时间的影响(在400点5色下进行测试):


边密度5% 边密度7.5% 边密度10%
时间消耗 60,561ms 10,832ms 736ms


e77cd05e751b483a942464c55a47dc97.png

边密度影响了可行解的个数,也影响了剪枝的效率。边密度越大,剪枝效率越高。边密度越小,搜索树的每一支就会更深。

从上图以及上表中可以看出,随着边密度的提高,时间消耗指数级急速降低。这是剪枝效率大大提升的原因。

④图的连通分量对运行时间的影响(在400点5色下进行测试):


联通分量 1 3 5
时间消耗 60,561ms 54,831ms 48,265ms


b540bb6c91aa4dcea17c6337cb34e683.png

联通分量从一定程度上影响了搜索树的规模。当搜索过程中搜索完一个极大连通图后,搜素会从下一个极大连通图的最大度节点开始搜索,而不是从原节点上继续拓展,类似于重新从根节点伸展出一颗新的搜索树,降低了上一级的节点拓展。从一定程度上节省了时间。

通过分析上图以及上表,可以看到连通分量确实对图的运行时间有影响,并且连通分量越大,算法运行时间越短。


四、实验结论或体会


常规算法的算法效率往往较低,需要进行优化

对于回溯搜索算法,搜索时应优先选择限制较多的分支进行拓展搜索

可以通过数学方法对算法进行优化。例如本实验中借助图论中轮换的思想完成了优化。

除了算法本身,选择合适的数据结构也可以在一定程度上降低程序的运行时间。

算法优化过程中,算法的正确性验证必不可少。在本题中,每种优化策略都经过了理论和实际进行验证。


五、思考


轮换替换思想的推广:


1、算法描述:

通过对运行结果中可行解的分析以及对涂色逻辑的分析我们可以知道,对于m个合法颜色,规模为n的图的一个解向量如果为


image.png


也都为可行解。

image.png

此外,对于不可行的搜索节点也同理,当找到搜索树的某一枝无解后,可以利用轮换,将所有等效解全部剪枝剪掉,这大大降低了程序的运行时间。


2、运行并测试:


采用轮换进行剪枝的策略和上面几种已经介绍的剪枝策略,对450点5色地图进行涂色并计时,发现仅需要不到一秒的时间即可完成全部3840个点的搜索,与之前大约一分钟左右的时间相比,算法效率得到极大提升。


附录


完成本次实验后课余时间,尝试了一些其他的优化手段,并重新写了代码。算法效率得到极大提升,可以在50ms内求解出450个点的5种颜色的全部3840个可行解,将代码附上,供大家参考。


#include <iostream>
#include <time.h>
using namespace std;
#define COLOR 5    //颜色数
#define VERTEX 90     //点数
#define EDGE 500   //边数
class Vertex {
public:
    int color;
    int state[COLOR + 1];  //颜色状态,1为可选,非1为不可选
    int choice;  //可选颜色数,即state中1的数量
    int restrain;  //相邻数量
    Vertex() {
        color = 0;
        for (int i = 0; i < COLOR + 1; ++i) {
            state[i] = 1;
        }
        choice = COLOR;
        restrain = 0;
    }
};
int map2[VERTEX + 1][256];  //图的邻接表
long long int sum = 0;   //记录解的个数
long Start;
long End;
int ColorFirst(Vertex *S) {
    int maxRestrain = 0, maxIndex = 0;
    for (int i = 1; i <= VERTEX; ++i) {
        if (S[i].restrain > maxRestrain) {
            maxRestrain = S[i].restrain;
            maxIndex = i;
        }
    }
    S[maxIndex].color = 1;
    maxRestrain = 0;
    int next = 0;
    for (int k = 1; k <= map2[maxIndex][0]; ++k) {
        int j = map2[maxIndex][k];
        S[j].choice--;
        S[j].restrain--;
        S[j].state[1] = -maxIndex;
        if (S[j].restrain > maxRestrain) {
            maxRestrain = S[j].restrain;
            next = j;
        }
    }
    return next;
}
int getNext(Vertex *S) {
    int min = COLOR;
    int next = 0;
    for (int i = 1; i <= VERTEX; ++i) {
        if (S[i].color == 0) {
            if (S[i].choice == min) {
                if (S[i].restrain > S[next].restrain) {
                    min = S[i].choice;
                    next = i;
                }
            } else if (S[i].choice < min) {
                min = S[i].choice;
                next = i;
            }
        }
    }
    return next;
}
//搜索函数
int DFS(Vertex *S, int now, int count, int used) {
    if (count == VERTEX) //到达叶子,找到一个着色方案
    {
        sum += S[now].choice;
        /*if (sum > 1000000000) {
            End = clock();
            cout << End - Start << endl;
            exit(1);
        }*/
        return S[now].choice;
    } else {
        int s = 0;
        for (int i = 1; i <= COLOR; i++) {
            if (S[now].state[i] == 1) {
                int ss = 0;
                S[now].color = i;
                bool isNew = i > used;
                //剪枝
                for (int k = 1; k <= map2[now][0]; ++k) {
                    int j = map2[now][k];
                    if (S[j].color == 0 && S[j].state[i] == 1) {
                        /*S[j].state[i] = -now;
                        S[j].choice--;
                        S[j].restrain--;*/
                        if (S[j].choice == 1) {
                            goto BACK;
                        } else {
                            S[j].state[i] = -now;
                            S[j].choice--;
                            S[j].restrain++;
                        }
                    }
                }
                if (isNew)
                    ss = DFS(S, getNext(S), count + 1, used + 1);
                else
                    ss = DFS(S, getNext(S), count + 1, used);
                BACK:
                for (int k = 1; k <= map2[now][0]; ++k) {
                    int j = map2[now][k];
                    if (S[j].state[i] == -now) {
                        S[j].choice++;
                        S[j].restrain++;
                        S[j].state[i] = 1;
                    }
                }
                S[now].color = 0;
                if (isNew) {
                    s += ss * (COLOR - used);
                    sum += ss * (COLOR - used - 1);
                    break;
                }
                s += ss;
            }
        }
        if (sum > 100000000) {
            End = clock();
            cout << End - Start << endl;
            exit(1);
        } else
            return s;
    }
}
int main() {
    Vertex S[VERTEX + 1];
    FILE *fp;
    int command;
    cin >> command;
    if (command == 1) {
        if ((fp = fopen("G:\\MapData\\MapR_5_90_500.txt", "r")) == NULL)  //打开文件
        {
            printf("Can not open file!\n");
            exit(1);
        }
    } else if (command == 2) {
        if ((fp = fopen("G:\\MapData\\MapR_5_100_500.txt", "r")) == NULL)  //打开文件
        {
            printf("Can not open file!\n");
            exit(1);
        }
    } else if (command == 3) {
        if ((fp = fopen("G:\\MapData\\MapR_5_110_500.txt", "r")) == NULL)  //打开文件
        {
            printf("Can not open file!\n");
            exit(1);
        }
    } else if (command == 4) {
        if ((fp = fopen("G:\\MapData\\MapR_5_120_500.txt", "r")) == NULL)  //打开文件
        {
            printf("Can not open file!\n");
            exit(1);
        }
    }
    int u, v;
    char ch;
    for (int i = 1; i <= EDGE; i++) {
        fscanf(fp, "%c%d%d\n", &ch, &u, &v);
//        map[u][v] = map[v][u] = 1;
        map2[u][0]++;
        map2[u][map2[u][0]] = v;
        map2[v][0]++;
        map2[v][map2[v][0]] = u;
        S[u].restrain++;
        S[v].restrain++;
    }
    cout << "OK" << endl;
    fclose(fp);
    Start = clock();
    int a = DFS(S, getNext(S), 1, 0);
    End = clock();
    cout << "TIME:" << End - Start << endl;
    cout << "ANSWER:" << a << endl;
    return 0;
}
相关文章
|
6天前
|
算法 数据可视化
基于SSA奇异谱分析算法的时间序列趋势线提取matlab仿真
奇异谱分析(SSA)是一种基于奇异值分解(SVD)和轨迹矩阵的非线性、非参数时间序列分析方法,适用于提取趋势、周期性和噪声成分。本项目使用MATLAB 2022a版本实现从强干扰序列中提取趋势线,并通过可视化展示了原时间序列与提取的趋势分量。代码实现了滑动窗口下的奇异值分解和分组重构,适用于非线性和非平稳时间序列分析。此方法在气候变化、金融市场和生物医学信号处理等领域有广泛应用。
|
14天前
|
算法
算法设计与分析作业
这篇文章是关于算法设计与分析的作业,其中包含了两个算法实现:一个是使用分治算法实现的十进制大整数相乘(包括加法、减法和乘法函数),并进行了正确性和健壮性测试;另一个是使用快速排序思想实现的分治查找第K小元素的程序,并分析了其平均和最坏时间复杂度。
算法设计与分析作业
|
10天前
|
编解码 算法 图形学
同一路RTSP|RTMP流如何同时回调YUV和RGB数据实现渲染和算法分析
我们播放RTSP|RTMP流,如果需要同时做渲染和算法分析的话,特别是渲染在上层实现(比如Unity),算法是python这种情况,拉两路流,更耗费带宽和性能,拉一路流,同时回调YUV和RGB数据也可以,但是更灵活的是本文提到的按需转算法期望的RGB数据,然后做算法处理
|
14天前
|
人工智能 算法
第一周算法设计与分析 G : 排队援救
这篇文章介绍了解决算法问题"排队援救"的方法,通过使用队列和映射来模拟救援点的排队过程,并确定最终得到救援的人的顺序和编号。
|
14天前
|
算法 C++
第一周算法设计与分析 E : 构造回文串
这篇文章介绍了解决算法问题"构造回文串"的方法,即判断给定的整数N(视为字符串)是否可以通过在前面添加任意个0(或不添加)来构造一个回文串,并给出了相应的C++代码实现。
|
14天前
|
算法 C++
第一周算法设计与分析 H : 括号匹配
这篇文章介绍了解决算法问题"括号匹配"的方法,通过使用栈来检查给定字符串中的括号是否合法匹配,并提供了相应的C++代码实现。
|
14天前
|
算法 C++
第一周算法设计与分析 F : 模拟计算器
该文章 "第一周算法设计与分析 F : 模拟计算器" 的摘要或讨论。这篇文章介绍了如何设计一个程序来模拟一个基本的计算器,处理包含加、减、乘运算的表达式,并给出了相应的C++代码实现
|
6天前
|
算法 BI Serverless
基于鱼群算法的散热片形状优化matlab仿真
本研究利用浴盆曲线模拟空隙外形,并通过鱼群算法(FSA)优化浴盆曲线参数,以获得最佳孔隙度值及对应的R值。FSA通过模拟鱼群的聚群、避障和觅食行为,实现高效全局搜索。具体步骤包括初始化鱼群、计算适应度值、更新位置及判断终止条件。最终确定散热片的最佳形状参数。仿真结果显示该方法能显著提高优化效率。相关代码使用MATLAB 2022a实现。
|
29天前
|
算法
基于模糊控制算法的倒立摆控制系统matlab仿真
本项目构建了一个基于模糊控制算法的倒立摆控制系统,利用MATLAB 2022a实现了从不稳定到稳定状态的转变,并输出了相应的动画和收敛过程。模糊控制器通过对小车位置与摆的角度误差及其变化量进行模糊化处理,依据预设的模糊规则库进行模糊推理并最终去模糊化为精确的控制量,成功地使倒立摆维持在直立位置。该方法无需精确数学模型,适用于处理系统的非线性和不确定性。
基于模糊控制算法的倒立摆控制系统matlab仿真
|
7天前
|
资源调度 算法
基于迭代扩展卡尔曼滤波算法的倒立摆控制系统matlab仿真
本课题研究基于迭代扩展卡尔曼滤波算法的倒立摆控制系统,并对比UKF、EKF、迭代UKF和迭代EKF的控制效果。倒立摆作为典型的非线性系统,适用于评估不同滤波方法的性能。UKF采用无迹变换逼近非线性函数,避免了EKF中的截断误差;EKF则通过泰勒级数展开近似非线性函数;迭代EKF和迭代UKF通过多次迭代提高状态估计精度。系统使用MATLAB 2022a进行仿真和分析,结果显示UKF和迭代UKF在非线性强的系统中表现更佳,但计算复杂度较高;EKF和迭代EKF则更适合维数较高或计算受限的场景。