回溯算法二:算法框架与实现
一、算法框架分析
1、刻画组合各自特性的动态属性统一表示如下:
(1)MAKE-ITEMS:生成当前节点的取值集合;
(2)IS-PARTIAL:判断部分解;
(3)IS-COMPLETE:判断完整解;
(4)PRINT-SOLUTION:打印一个合法解,输出结果;
静态属性:
(5)问题的解向量长度:n
(6)问题的解向量x
2、组合问题抽象描述如下:
输入:解向量的最大长度n,解向量x,产生解向量第k个分量取值集合items={items1,items2,...,itemsm}的过程MAKE-ITEMS,判断部分解规则IS-PARTIAL,判断完整解的规则IS-COMPLETE,打印合法解的方法PRINT-SOLUTION的组合问题P。
输出:如果问题P有合法解,输出所有合法解,否则输出无解信息。
3、算法框架如下:
设解向量的分量取值集合items有m个元素,解向量的维数为n,则解空间可以组织为高度为n的m叉完全树。
回溯算法框架:
BACKTRACK(P)
1 flag = false // 是否有解标志
2 为解向量x分配存储空间 // malloc
3 EXPLORE(P, 1) // 探索过程
4 if flag == false // 判断解标志
5 then print_error "no solution" // 打印无解信息
探索过程EXPLORE:
EXPLORE(P, K)
1 if IS-COMPLATE(X) // 判断解向量是否完全
2 then flag = true // 若为完全解,则置解标志,输出解信息,并返回
3 PRINT-SOLUTION(X)
4 return // 需要分析,是否需要输出所有的完整解
5 if k > n // k为当前解向量长度,n为解向量的最大长度
6 then return // 若k>n,表示当前分支遍历完全且无解,直接返回
7 items = MAKE-ITEMS(K) // 生成当前节点的取值集合
8 m = length(items) // 集合长度
9 for i=(1,...,m) // 对当前第k个分量,逐一检测各种可能的取值
10 do x[k] = items[i]
11 if IS-PARTIAL(x, k) // 确定是否为部分解
12 then EXPLORE(P, k+1) // 继续递归下一步探索过程
二、 框架代码实现
结构体及变量定义:
// 单链表定义
struct List {
void *data;
struct List *next;
};
typedef struct List List;
// 变量定义
void *x; // 解向量
int n; // 解向量长度
int elesize; // 解向量元素存储长度
int flag = 0; // 解标志
int (*isComplete)(void *x, int k); // 判断完整合法解
void (*printSolution)(void *x, int k); // 打印解向量
List (*makeItems)(int k); // 生成第k个分量的选项表
int (*isPartial)(void *x, int k); // 判断部分合法解
算法框架实现:
void backtrack(void(*explore)(int))
{
explore(0);
if (!flag) {
printf("no solution.\n");
}
}
void generalExplore(int k)
{
int i;
// step1: 完整解判断
if (isComplete(x, k)) {
flag = 1;
printSolution(x, k);
return;
}
// step2: 无解退出
if (k >= n) {
return;
}
// step3: 处理下一个节点
// step3.1: 确定第k个节点的取值集合
List *iterms = makeIterms(k);
List *q = iterms;
// step3.2: 遍历该节点的取值集合iterms
while (q != NULL) {
// step4: 针对某个取值,将该节点的值加入解空间,确认是否为部分解
memcpy(x + k * elesize, q->data, elesize);
if (isPartial(x, k)) {
// step5: 若是部分解则处理下一个节点,直到完全解结束,或者无解退出
generalExplore(k + 1);
} // step5.1: 此处省略的else语句,表明该取值不满足部分解,放弃该方案,不进行后续节点彷徨
// 步骤4、5可以处理一种取值方案, 遍历第k个节点的下一种取值方案,回溯的味道尽在于此
// 本质上是,深度搜索所有解空间,然后在递归过程中,根据部分解要求及时剪枝处理;
// 剪枝完成后回到上一层,再继续向后进行,即出现回溯;
// 若是没有剪枝动作,兼职就是深度搜索,暴力解决
q = q->next;
}
// step6: 完成第k个节点的处理
listClear(iterms);
free(iterms);
iterms = NULL;
}
三、 m-着色问题:代码实现
以下完整代码包括:全局变量定义,单链表操作实现,回溯法框架,着色问题具体实现,测试代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/**************************************part1: 单链表处理**********************************************/
// 单链表定义
struct List {
void *data;
struct List *next;
};
typedef struct List List;
// 链表新增节点:e为节点数据,l为链表头指针,将新增节点添加在链表头
void listPushBegin(List **l, void *e)
{
// 节点内存申请
List *p = (List*)malloc(sizeof(List));
// 节点数据赋值
p->data = e;
// 将新节点插入链表头
p->next = *l;
// 更新链表头指针l
*l = p;
// printf("l= %d\n", (*l)->data);
}
// 链表新增节点,并将节点添加到链表尾部,l为链表尾指针(单链表,需要另外保存头节点)
void listPushBack(List **l, void *e)
{
// 节点内存申请
List *p = (List*)malloc(sizeof(List));
// 节点数据赋值,并将尾指针置空
p->data = e;
p->next = NULL;
// 若表非空,则将新节点添加到表尾
if (*l != NULL) {
(*l)->next = p;
}
// 更新表尾指针
*l = p;
}
// 删除链表头节点,l为链表头指针
void *listDeleteBegin(List **l)
{
void *e = NULL;
if (*l != NULL) {
// 取出链表头数据
e = (*l)->data;
// 更新链表头指针,向后移位
*l = (*l)->next;
}
// 将原链表头数据返回
return e;
}
// 清除整个链表,l为链表头指针
void listClear(List *l)
{
// 清理数据域,释放内存后置空
if (l->data != NULL) {
free(l->data);
l->data = NULL;
}
// 若链表无后继,则表明已经清理到链表尾部
if (l->next == NULL) {
return;
}
// 递归清理l的后继
listClear(l->next);
// 释放l的指针域,并置空
free(l->next);
l->next = NULL;
}
/**************************************part2: 变量定义**********************************************/
// 变量定义
int *x; // 解向量:类型需要根据问题确定
int n; // 解向量长度
int elesize; // 解向量元素存储长度:用于数据赋值
int flag; // 解标志
// int (*isComplete)(void *x, int k); // 判断完整合法解
// void (*printSolution)(void *x, int k); // 打印解向量
// List (*makeItems)(int k); // 生成第k个分量的选项表
// int (*isPartial)(void *x, int k); // 判断部分合法解
/**************************************part3: 着色问题**********************************************/
// m为颜色种数,G为图的邻接矩阵(按行优先)
int m;
int *G;
// 完全解判断:k为当前解长度,n为完整解的最大长度
int isComplete(int *x, int k)
{
return k >= n;
}
// 打印完整合法解:x为解向量,n为完整解长度
void printSolution(int *x, int k)
{
int i;
printf("solution: ");
for (i = 0; i < n; i++) {
printf("%d ", x[i]);
}
printf("\n\n");
}
// 创建解向量的的第k个分量的取值集合:根据具体问题确定,着色问题取值固定,可以简化
List *makeIterms(int k)
{
List *iterms = NULL;
int i;
// 将各节点的颜色取值,插入集合iterms,颜色固定,可以简化
for (i = 0; i < m; i++) {
int *e = (int *)malloc(sizeof(int));
// 插入链表时,是从头部插入,因此颜色插入顺序为3、2、1,则链表中最终顺序为1、2、3
// 详细分析问题后,可以简化处理
*e = m - i;
listPushBegin(&iterms, e);
}
return iterms;
}
// 判断部分合法解:
int isPartial(int *x, int k)
{
int i;
for (i = 0; i < k; i++) {
// 根据邻接表判断两个节点之间是否相邻,再进一步判断其配色是否相同,x中按顺序保存各节点的配色
// 按行优先邻接矩阵,针对k列,按行i遍历时,若G[i][k]==1,表示第i个节点和第j个节点相邻;若x中对应的配色相同,则不满足解要求
if ((G[i * n + k] == 1) && (x[i] == x[k])) {
return 0;
}
}
// 遍历完全:第k个节点的着色方案x[k]满足部分解要求
return 1;
}
/**************************************part4: 基本框架**********************************************/
void backtrack(void(*explore)(int))
{
explore(0);
if (!flag) {
printf("no solution.\n");
}
}
void generalExplore(int k)
{
int i;
// 判断当前解是否完整,若是则输出
if (isComplete(x, k)) {
flag = 1;
printSolution(x, k);
return;
}
if (k >= n) {
return;
}
// 继续确定第k个节点的解
printf("drawing k=%d\n", k);
// 确定第k个节点的所有解的取值
List *iterms = makeIterms(k);
List *q = iterms;
while (q != NULL) {
// 遍历取值集合,判断x[k]加入解空间后,是否满足部分解要求
memcpy(x + k * elesize, q->data, elesize);
if (isPartial(x, k)) {
// 若满足要求,则继续进行确定下一个节点,递归处理
printf("x[%d] = %d is partial\n", k, x[k]);
generalExplore(k + 1);
} else {
printf("x[%d] = %d is not partial\n", k, x[k]);
}
// 遍历第k个节点的下一种取值方案,回溯的味道尽在于此
// 本质上是,深度搜索所有解空间,然后在递归过程中,根据部分解要求及时剪枝处理;
// 剪枝完成后回到上一层,再继续向后进行,即出现回溯;
// 若是没有剪枝动作,兼职就是深度搜索,暴力解决
q = q->next;
}
listClear(iterms);
free(iterms);
iterms = NULL;
}
/**************************************测试程序**********************************************/
int main(int argc, char ** argv)
{
// 按行优先邻接矩阵
int a[25] = {0, 1, 1, 0, 0,
1, 0, 0, 1, 1,
1, 0, 0, 1, 1,
0, 1, 1, 0, 1,
0, 1, 1, 1, 0};
// 以下变量均为全局变量,G-邻接矩阵,n-节点数,m-颜色种类,flag-解标志,elesize-解元素大小,x-解向量
G = a;
n = 5;
m = 3;
flag = 0;
// elesize = sizeof(int),会出现异常,很奇怪,可能与calloc有关???
elesize = 1;
// calloc(元素个数,单个元素长度-字节)
x = (int*)calloc(5, sizeof(int));
backtrack(generalExplore);
while(1);
return (EXIT_SUCCESS);
}
四、测试结果
1、测试结果
2、回溯过程分析
drawing k=0
x[0] = 1 is partial
drawing k=1
x[1] = 1 is not partial
x[1] = 2 is partial
drawing k=2
x[2] = 1 is not partial
x[2] = 2 is partial
drawing k=3
x[3] = 1 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is not partial
x[4] = 3 is partial
solution: 1 2 2 1 3
x[3] = 2 is not partial
x[3] = 3 is partial
drawing k=4
x[4] = 1 is partial
solution: 1 2 2 3 1
x[4] = 2 is not partial
x[4] = 3 is not partial
x[2] = 3 is partial
drawing k=3
x[3] = 1 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is not partial
x[4] = 3 is not partial
x[3] = 2 is not partial
x[3] = 3 is not partial
x[1] = 3 is partial
drawing k=2
x[2] = 1 is not partial
x[2] = 2 is partial
drawing k=3
x[3] = 1 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is not partial
x[4] = 3 is not partial
x[3] = 2 is not partial
x[3] = 3 is not partial
x[2] = 3 is partial
drawing k=3
x[3] = 1 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is partial
solution: 1 3 3 1 2
x[4] = 3 is not partial
x[3] = 2 is partial
drawing k=4
x[4] = 1 is partial
solution: 1 3 3 2 1
x[4] = 2 is not partial
x[4] = 3 is not partial
x[3] = 3 is not partial
x[0] = 2 is partial
drawing k=1
x[1] = 1 is partial
drawing k=2
x[2] = 1 is partial
drawing k=3
x[3] = 1 is not partial
x[3] = 2 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is not partial
x[4] = 3 is partial
solution: 2 1 1 2 3
x[3] = 3 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is partial
solution: 2 1 1 3 2
x[4] = 3 is not partial
x[2] = 2 is not partial
x[2] = 3 is partial
drawing k=3
x[3] = 1 is not partial
x[3] = 2 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is not partial
x[4] = 3 is not partial
x[3] = 3 is not partial
x[1] = 2 is not partial
x[1] = 3 is partial
drawing k=2
x[2] = 1 is partial
drawing k=3
x[3] = 1 is not partial
x[3] = 2 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is not partial
x[4] = 3 is not partial
x[3] = 3 is not partial
x[2] = 2 is not partial
x[2] = 3 is partial
drawing k=3
x[3] = 1 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is partial
solution: 2 3 3 1 2
x[4] = 3 is not partial
x[3] = 2 is partial
drawing k=4
x[4] = 1 is partial
solution: 2 3 3 2 1
x[4] = 2 is not partial
x[4] = 3 is not partial
x[3] = 3 is not partial
x[0] = 3 is partial
drawing k=1
x[1] = 1 is partial
drawing k=2
x[2] = 1 is partial
drawing k=3
x[3] = 1 is not partial
x[3] = 2 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is not partial
x[4] = 3 is partial
solution: 3 1 1 2 3
x[3] = 3 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is partial
solution: 3 1 1 3 2
x[4] = 3 is not partial
x[2] = 2 is partial
drawing k=3
x[3] = 1 is not partial
x[3] = 2 is not partial
x[3] = 3 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is not partial
x[4] = 3 is not partial
x[2] = 3 is not partial
x[1] = 2 is partial
drawing k=2
x[2] = 1 is partial
drawing k=3
x[3] = 1 is not partial
x[3] = 2 is not partial
x[3] = 3 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is not partial
x[4] = 3 is not partial
x[2] = 2 is partial
drawing k=3
x[3] = 1 is partial
drawing k=4
x[4] = 1 is not partial
x[4] = 2 is not partial
x[4] = 3 is partial
solution: 3 2 2 1 3
x[3] = 2 is not partial
x[3] = 3 is partial
drawing k=4
x[4] = 1 is partial
solution: 3 2 2 3 1
x[4] = 2 is not partial
x[4] = 3 is not partial
x[2] = 3 is not partial
x[1] = 3 is not partial