回溯算法二:算法框架与实现

一、算法框架分析

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



posted @ 2021-04-09 01:12  Pangolin2  阅读(3)  评论(0编辑  收藏