回溯算法四:子集树、排列树以及组合优化

目录

一、子集树

1、子集树:若一个组合问题的解释给定集合的子集,则解向量<x1, x2,...,xn>可以表示为分量取值为{0,1}的比特串,解空间可以组成一颗完全二叉树,称这棵搜索树为一棵子集树;

2、由于解向量的每个分量均取0或1,因此可以省略解集合处理过程;

3、子集问题示例,可以参考:回溯算法三:经典问题实现(m-着色、n-皇后、Hamilton回路、子集和)

二、排列树

1、把解向量是n个元素排列的组合问题的搜索树,称为排列树,将这类组合问题成为排列树问题;

2、由于排列树问题的特殊性,可以将n-叉完全二叉树简化,以提高效率;

3、简化步骤:

EXPLORE(P, K)
0 n = length(x)
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
8 for i=(k,...,n)                    // 对当前第k个分量,逐一检测各种可能的取值
10    do x[k] <=> x[i]               // 遍历x[k]的有效取值(非完全遍历),且交换后,维持x的有效性       
11        if IS-PARTIAL(x, k)        // 确定是否为部分解
12            then 	EXPLORE(P, k+1)  // 继续递归下一步探索过程
13    x[k] <=> x[i]                  // 还原x,继续进行for中的下一个排列

4、n-皇后问题示例如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// x[i]表示每行棋子的列位置,k表示最新添加的棋子索引,即当前处理的第k行的棋子位置问题(k从0开始)
int isPartial(int n, int *x, int k)
{
    int diff;
    for (int i = 0; i < k; i++) {
        // 判断新增的位置与历史位置是否满足规则:不同行、不同列、不同斜线
        // 遍历判断新增棋子与之前棋子的位置差异
        diff = x[i] - x[k];
        // 列位置相同——同列;列号之差等于行号之差——对角线;遍历处理[0,k-1]行,不可能同行
        if (diff == 0 || (diff == i - k) || (diff == k - i)) {
            return 0;
        }
    }
    return 1;
}

void swap(int *x, int k, int i)
{
    int temp = x[k];
    x[k] = x[i];
    x[i] = temp; 
}
// 递归过程,x与k同时变化,其他均为定值
void nQueensPlus(int n, int *x, int k)
{
    int i;
    // 完全解判断:k为当前解长度,n为完整解的最大长度
    if (k >= n) {
        for (int i = 0; i < n; i++) {
            printf("%d ", x[i]);
        }
        printf("\n");
        return;
    }

    // 递归遍历,回溯过程
    for (int i = k; i < n; i++) {
        // 针对x[k]的遍历值,取值范围为x[k,,,n],有效减小搜索空间
        swap(x, k, i);
        // 判断部分解逻辑复杂,建议抽取函数
        if (isPartial(n, x, k)) {
            nQueensPlus(n, x, k + 1);
        }
        // 恢复x,进行下一轮
        swap(x, i, k);
    }
}

int main(void)
{
    // n为棋盘规模,x为解向量空间,k为当前解的个数[0, n-1]
    int n = 4;
    int k = 0;
    // 初始化解向量
    int *x = (int*)malloc(n * sizeof(int));
    for (int i = 0; i < n; i++) {
        x[i] = i;
    }
    
    // n为棋盘规模,locations为棋盘位置集合(列),x为解向量空间,k为当前解的个数[0, n-1]
    nQueensPlus(n, x, k);
    while(1);
    return 0;
}

三、组合优化

1、简要说明

(1)组合优化问题需要在解空间中找到最优者:在回溯过程中,需要跟踪目前为止最优合法解的目标值most,与当前搜索到的合法解current的目标值进行比较,确认是否需要更新most。

(2)可以利用当前最优值most,对不可能成为最优解的子树进行剪枝(isPartial步骤)。

2、旅行商问题

(1)问题描述:商人从n个城市中的一个出发,希望走遍每个城市且每个城市只经过一次,然后回到出发的城市。如果有这样的路径,要求找到里程最短的路径。(最短的Hamilton回路)

输入:带权无向图G=<V, E>,权函数w:E->R,起始点s。

输出:如果存在Hamilton回路,输出权值最小的路径,否则输出无解信息。

(2)分析过程:

(3)算法实现:

(4)测试结果:

上一篇:什么是虚拟DOM?(vue,react)什么是diff算法?


下一篇:谈谈差分数组