【编程素质】数据结构+算法

1-1,线性表

1)顺序表

逻辑上相邻的两个元素物理位置上也相邻。
可以随机读取,但增删操作复杂。

2)单链表

读取麻烦,增删简单。

头指针和尾指针无法决定链表长度。

3)循环链表

表中最后一个结点的指针指向头结点,只有头结点是固定的。

4)双向链表

data *prior(直接前驱) *next(直接后继)

1-2,队列(queue)

只允许在队头(front)出,队尾(rear)进。
先进先出(FIFO,first in first out)

1)循环队列

循环队列充分利用向量空间,克服“假溢出”。

①插入删除

在循环队列中,队头指针和队尾指针的动态变化决定队列的长度。
front指定队首位置,删除一个元素就将front顺时针移动一位;
rear指向元素要插入的位置,插入一个元素就将rear顺时针移动一位;

②判断队满队空

方法一:设一布尔变量以区别队满队空。
方法二: 队满时:(rear+1)%n==front(n为队列长度)

1-3,栈(stack)

1)概念

仅在表尾进行增删的线性表。
栈顶(top)、栈底(bottom)、后进先出(LIFO,last in first out)。

2)操作

栈顶指针保持不变,栈顶指针的动态变化决定栈中元素的个数。有元素入栈,栈顶指针增加,有元素出栈,栈顶指针减少。

3)计算

①卡特兰数

若一序列进栈顺序为e1,e2,e3,e4,e5,问存在多少种可能的出栈序列?

设n个数出栈方式有f(n)种,入栈顺序确定,记下标1、2、…、n。
设最后一个出栈的是第k个数,则说明k之前的k-1个数要完成进栈出栈,有f(k-1)种方式;
对于k之后的n-k个数再完成进栈出栈,有f(n-k)种方式;
最后第k个数出栈,这时有f(k-1)*f(n-k)种方式。
注意:每个数都有可能是最后出栈的。
所以:f(n)=f(0)*f(n-1)+f(1)*f(n-2)+…+f(k-1)*f(n-k)+…+f(n-1)*f(0);

结果
即:f(5)=42种。

相关问题:已知前序遍历的顺序是xxxx求这棵树有多少种形状。

1-4,数组

数组一旦定义,其维数和维界就不再改变。
因此除了结构的初始化和销毁之外,数组只有存取元素和修改元素值的操作。

1-5,广义表

1-6,串

1)概念

字符位置:字符在序列中的序号(从1开始)
子串位置:第一个字符在主串的位置。

2)串的模式匹配

子串的定位操作通常称做模式匹配,是各种串处理系统中最重要的操作之一。

kmp算法

1-7,树

1)概念

结点的度:结点拥有的子树数。
堂兄弟:双亲在同一层结点。
层次:根为第一层,树的最大层次就是树的深度或高度。

2)二叉树的性质

性质1:二叉树的第i层至多有2^(i-1)个结点
性质2:深度为k的二叉树至多有(2^k)-1个结点。
性质3:若叶子结点为n0,度为2的结点数为n2,则n0 = n2 + 1

对于结点总数n,度为1的结点数n1,有:    n =  n0 + n1 + n2
算叶子结点角度出发:              n = n1 + 2n2 + 1
以上推出性质3.

性质4:具有n个结点的完全二叉树的深度为 logn + 1 (log n向下取整)
性质5:n个结点,按层序编号,对任一结点 i :
如果 i =1,则结点是根。
如果 i >1,则其双亲结点为 i/2 (向下取整)
如果 2i > n,则 i 无左孩子;否则其左孩子为结点 2i .
如果 2i + 1 > n,则 i 无有孩子;否则其右孩子为 2i+1 .

3)二叉树存储结构

①顺序存储

存储单元自上而下,自左至右一次存储完全二叉树的结点元素。(0表示不存在此结点)

②链式存储

二叉链表

含有两个指针域:结点、lchild、rchild

n个节点的二叉树有2n个链域,除了根节点没有被lchild和rchild指向,其余的节点必然会被指到.所以
空指针共有2n-(n-1)=n+1;
非空指针有2n-(n+1)=n-1;

三叉链表

含有三个指针域:结点、lchild、rchild、parent

线索链表和线索二叉树

在二叉链表中的空链域中存储新的指针:fwd和bkwd,分别指向结点在某次遍历时得到的前驱结点、后继节点。

4)遍历二叉树

①中序遍历

前缀表示(波兰式)

②先序遍历

中缀表示。

③后序遍历

后缀表示(逆波兰式)。
后序遍历有些结点是需要存储的,否则找不到,必须要用栈保存。

5)树的存储结构

①双亲表示法

data、parent(指向双亲结点)

②孩子表示法

多重链表(多个指针域指向孩子们:data child1 child2 ……):这样有多个空指针域。
改进:data degree child1 ……childd ( child指针有degree个,节省空间,单身不方便操作)

③孩子兄弟表示法

data *firstchild(第一个孩子) *nextsibling(下一个兄弟)

6)森林和二叉树的转换

森林转换为二叉树

对于森林F,二叉树B。
F为空,则B为空树;
若F非空,B的根为F第一棵树的根,F的孩子放在新结点的左子树,F的兄弟为右子树。

森林的第二棵树可以是根结点的右子树。

综上所述,任何一颗树树对应的二叉树,其右子树必为空。

二叉树转换为森林

7)二叉树应用

①平衡二叉树(Balanced Binary Tree、AVL树)

它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
作用:插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能(O(log(n)))。
常用实现方法有:a.红黑树(Red Black Tree) b.AVLc.替罪羊树 d.Treap e.伸展树

②二叉排序树(Binary Sort Tree,二叉查找树,二叉搜索树、B树)

若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉搜索树。

③满二叉树

除最后一层无任何子节点外,每一层上的所有结点都有两个子结点二叉树。

④完全二叉树

只有最下面的一层结点度能够小于2,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。
堆是一种完全二叉树或者近似完全二叉树,所以效率极高。

⑤B-树(B_树、平衡的多叉树)

B-树是一种多路搜索树(并不一定是二叉的)。主要用作文件的索引。

m阶B树定义:

a.每个结点至多有m个子结点;
b.除根节点和叶结点外,其它每个结点至少有(m/2上整)个子结点;
c.根结点至少有两个子结点(唯一例外的是根结点就是叶结点时没有子结点,此时B树只有一个结点);
d.所有叶结点在同一层,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空);
e.有k个子结点的非根结点恰好包含k-1个关键码。

查找

B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点。
B-树

⑥B+树

B-树的变形,为叶子结点增加链表指针(从大到小顺序链接)。
每个树上有2个头指针,一个指向根结点,另一个指向关键字最小的叶子结点。因此对B+树可以进行2种查找运算:一种是从最小关键字起顺序查找,另一种是从根结点开始随机查找。
B+树

⑦B*树

B+树的变形,为非叶子结点也增加链表指针。在B+树的非根和非叶子结点再增加指向兄弟的指针。

⑧Huffman树(最优树)

给定带权路径长度最短的树。
应用:赫夫曼编码。

⑨笛卡尔树

笛卡尔树是一棵二叉树,树的每个节点有两个值,一个为key,一个为value。
i>对于key,笛卡尔树是一棵二叉搜索树,每个节点的左子树的key都比它小,右子树都比它大。
ii>对于value,所有结点的value满足优先队列(不妨设为最小堆)的顺序要求,即该结点的value值比其子树中所有结点的value值小。

1-8,图

1)概念

定点(Vertex)
弧(Arc)、边(Edge)
有向图、无向图
完全图:有n(n-1)/2条边的无向图。
有向完全图:有n(n-1)条弧的有向图。
稀疏图:有很少条边或弧的图。反之为稠密图。
入度、出度。
连通图:图中任意两个结点都是连通的(有相互到达的路径)。
连通分量:无向图中的极大连通子图。
强连通图:任意两顶点直接存在路径的有向图(不一定直接相连)。
强连通分量:有向图中的极大强连通子图。

稀疏矩阵

矩阵中,若数值为0的元素数目远远多于非0元素的数目,并且非0元素分布没有规律时,则称该矩阵为稀疏矩阵;与之相反,若非0元素数目占大多数时,则称该矩阵为稠密矩阵。

稀疏矩阵压缩的存储方法是:三元组、十字链表。
三元组:将非零元素所在的行、列以及它的值构成一个三元组(i,j,v),然后再按某种规律存储这些三元组,这种方法可以节约存储空间。但是当涉及到矩阵运算时,要大量移动元素。
十字链表:十字链表表示法可以避免大量移动元素。
节点结构如下: down指向同列下一个节点,right指向同行下一个节点。
节点
矩阵部分表示:
矩阵部分表示

2)图转换为树

3)图的存储结构

①数组表示法

②多重链表

一个结点加多个指向其邻接点的指针。

③邻接矩阵

④邻接表

⑤十字链表

⑥邻接多重表

4)图的遍历

①深度优先搜索

②广度优先搜索

5)最短路径

Prim算法、Kruskal算法、Dijkstra算法。

6)拓扑排序

对一个**有向无环图**G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序的序列,简称拓扑序列。

7)

1-9,堆

1)概念

堆的定义

定义一

小(大)根堆:完全二叉树中任意结点小于(大于)其孩子。

定义二

n个元素的序列{k1,k2,…,kn}满足以下关系时,称为堆。
堆

2)堆排序

是对树型选择排序占用空间多的改进。

①算法思想

对于小根堆,输出对顶最小值,使得剩余n-1个元素的序列重新建成一个堆,则得到n个元素的次小值。如此反复执行,可以得到一个有序序列。

输出对顶元素后,调整剩余元素成为一个新堆
调整堆

将无序序列建成一个堆:
对于无序序列{49,38,65,97,76,13,27,49},依次放入二叉树。自叶子结点到堆顶筛选,如图所示。
新建堆

②时间复杂度

O(nlogn)

③heap和stack区别

对于数据结构来说:
栈是一种线形集合,遵循LIFO(Last In First Out,后进先出)。可基于数组或者链表来实现。
堆是一个有序集合,一般通过二叉树来实现。

对于存储来说:
基本数据类型存储在栈里面.而一个对象,他的实体存储在堆里面,他的引用存储在栈里面。
i>stack的空间由操作系统自动分配和释放,heap的空间是手动申请和释放的
ii>heap常用new关键字来分配。
iii>stack空间有限,heap的空间是很大的自由区。
iv>在Java中,若只是声明一个对象,则先在栈内存中为其分配地址空间,若再new一下,实例化它,则在堆内存中为其分配地址。

1-10查找

1)Bloom Filter

点击查看Bloom Filter

2)折半查找

有序表查找。

3)二叉排序树

4)平衡二叉树

5)B-树

6)B+树

7)键树(数字查找树)

8)哈希表

①构造哈希函数方法

哈希函数跟查找速度密切相关。对于要查找的字段A,哈希函数的选择跟A以及A的类型关系密切。
常见方法:直接定址法、数字分析法、平方取中法、折叠发、保留余数法、随机数法

②处理冲突的方法

拉链式哈希表最坏情况是所有记录的散列值都冲突,这样就退化为线性查找,时间复杂度为O(n)。

i>开放定址法

冲突后,依次放后续空位。

ii>再哈希法

iii>链地址法

链地址法
平均查找长度ASL = (1 * 6 + 2 * 4 + 3+4)/12

iv>建立一个公共溢出区

只要发生冲突,都填入溢出表。

④大量记录的哈希表

对于很多的记录,组成的哈希表长度不可能很长,因此要解决散列冲突,不能在常数时间内找到记录。
可以吧哈希表映射到文件中,分级查找。

1-11,排序

2-1,算法

1)概念

算法是指令的集合,是为了解决特定问题而规定的一些列操作。

①主要功能

对输入特定的运算产生期望的输出。

②算法结果

输入数据、处理数据、输出结果。

③影响因素

硬件层面:计算机执行每条指令的速度
软件层面:编译产生的代码质量
算法策略:算法的好坏
问题规模

2)算法与程序区别

①算法

性质:
(1)输入:由外部提供的量作为算法的输入。
(2)输出:算法产生至少一个量作为输出。
(3)确定性:组成算法的每条指令是清晰,无歧义的。
(4)有限性:算法中每条指令的执行次数是有限的,执行每条指令的时间也是有限的。

②程序

程序是算法用某种程序设计语言的具体实现。
程序可以不满足算法的性质(4)。
例如操作系统,是一个在无限循环中执行的程序,因而不是一个算法。

3)复杂度

算法复杂度,即算法在编写成可执行程序后,运行时所需要的资源,资源包括时间资源和内存资源。

①时间复杂度

指算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示。
一般取主要工作语句的耗费时间作为算法的时间复杂性。

②空间复杂度

包括程序本身的存储和它所使用的工作单元存储。

③常用公式

a.等比数列求和等比数列求和公式
b.常见复杂度阶排序
多项式:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3)
指数阶:O(2^n) < O(n!) < O(n^n)
c.其他
3=2^(log3)

2-2,递归与分治策略

1)概念

①递归

直接或间接地调用自身的算法称为递归算法。

递归条件:
a.要解决的问题可以转换为一个新问题,且与原问题的解决方案相同。
b.这个转换可以使问题得得解决
c.有一个明确的结束递归条件。

优缺点:
可读性强,容易用数学归纳法证明算法的正确性;
运行效率低(时间和空间都低)。

递归算法都可以通过采用一个用户定义的栈来模拟系统的递归调用工作栈,从而改为非递归算法。

②分治

基本思想:将一个规模为n的问题分解为k个规模较小的子问题,这些子问题相互独立且与原问题相同。
递归是分治法的一种方法。

2)场景

①二叉树

②阶乘n!

int factorial(int n){
    if( 0==n) return 1;
    return n*factorial(n-1);
}

复杂度:T(n) = T(n-1) +1 = O(n)

③Fibonacci数列

int fibonacci(){
    if(n<=1) return 1;
    return fibonacci(n-1) + fibonacci(n-2);
}

复杂度:T(n) = T(n-1) + T(n-2) + 1 = 2T(n-1) + 1 (n趋于无穷大)= 1+2+2^2 + …+2^n = O(2^n) (等比数列)

④排列问题

void Perm(Type list[], int k, int m){
    if( k==m ){//只剩下一个元素
        for(int i = 0; i<=m; i++) cout<<list[i];
        cout<<endl;
    }else{
        for{
            Swap(list[k],list[i]);
            Perm(list,k+1,m);
            Swap(list[k],list[i]);//恢复交换前的状态
        }
    }
}

复杂度:T(n) = nT(n-1) + 1 = n! + n = O(n!)

⑤整数划分

⑥Hanoi塔问题

分析:
假设移动n个圆盘需要f(n)次移动。
一个圆盘,只需一步就可以了 f(1)=1……①
n个圆盘,假设开始圆盘在A柱,可以先把A柱的上面n-1个圆盘移到B,再将A剩下的一个移到C,最后将B的n-1个移到C。总共需要f(n)=2f(n-1)+1……②
根据①②两式,可求出f(n)=2^n-1 所以O(n)=2^n

/**
 * 汉诺塔算法(递归)
 * @author luo
 *1.有三根杆子A,B,C。A杆上有若干碟子  (最大的一个在底下,其余一个比一个小,依次叠上去)
 *2.每次移动一块碟子,小的只能叠在大的上面  
 *3.把所有碟子从A杆全部移到C杆上
 *
 */
public class Hanoi{
    public static void main(String[] args) throws NumberFormatException, IOException{
        int n;
        BufferedReader buf = new BufferedReader(new InputStreamReader(System.in));
        System.out.print("请输入碟子盘数:");
        n = Integer.parseInt(buf.readLine());
        Hanoi hanoi = new Hanoi();
        hanoi.move(n, 'A', 'B', 'C');
    }

    public void move(int n, char a, char b, char c) {
        if (n == 1){
            System.out.println("盘 " + n + " 由 " + a + " 移至 " + c);
        }else {
            move(n - 1, a, c, b);
            System.out.println("盘 " + n + " 由 " + a + " 移至 " + c);
            move(n - 1, b, a, c);
        }
    }
}

复杂度:T(n) = 2T(n-1) + 1 = O(2^n)
时间复杂度:O(2^n) 。

⑦二分搜索

点击查看源代码:递归算法和非递归算法

⑧合并排序

点击查看源代码:递归算法和非递归算法

⑨快速排序

点击查看源代码:递归算法

⑩大整数乘法

问题:
X和Y都是n位二进制整数,计算它们的乘积XY。

解决办法:
a.小学方法,效率低。
b.改进:
复杂度分析

⑪Strassen矩阵乘法

a.8次乘法4次加法
T(n)=O(n^3)
b.改进:7次乘法18次加法
T(n)=O(n^log7)

⑫棋盘覆盖

点击查看源代码:递归算法

⑬线性时间选择

a.问题描述:给定n个元素(无序),找出第K小的元素。
当k=1,则要找的数为最小元素。k=n为最大元素。k=(n+1)/2,表示中位数。
b.基本思想:类似快速排序。

⑭快速傅里叶变换

基于大整数乘法。
a.算法
b.复杂度:O(nlogn)

2-3,动态规划

1)概念

①基本思想

将问题分解成若干子问题求解,再从子问题得到原问题的解。

②与分治法区别

动态规划的子问题不是相互独立的,再计算过程中保存已解决子问题的答案,在后续计算中使用,避免重复计算。
自底向上计算,是用空间换取时间。

③适用场景

求解最优问题

④设计步骤

a.找出最优解的性质和结构特征
b.递归的定义最优值
c.自底向上计算最优值
d.构造最优解

2)demo

①独立任务最优调度问题(双机调度问题)

算法-独立任务最优调度问题(双机调度问题)

②矩阵连乘问题

【编程素质】算法-矩阵连乘问题(枚举法、备忘录法、动态规划)

③ 寻找一条从左上角(arr[0][0])到右下角(arr[m-1][n-1])的路线, 使得沿途经过的数组中的整数和最小。

package luo.main;

import luo.minPath.minPathArr;

public class Main {

    public static void main(String[] args){
        int[][] arr = {{1,4,3},{8,7,5},{2,1,5}};
        System.out.println("路径:");
        System.out.println("最小值为:" + minPathArr.getMinPathArr(arr));        
    }
}
package luo.minPath;
/**
 * 寻找一条从左上角(arr[0][0])到右下角(arr[m-1][n-1])的路线,
 * 使得沿途经过的数组中的整数和最小。
 * @author luo
 *
 *分析:
 *从右下角开始倒着分析:
 *最后一步到达arr[m-1][n-1]只有两条路,
 *即通过arr[m-2][n-1]或arr[m-1][n-2]到达,
 *假设从arr[0][0]到arr[m-2][n-1]沿途数组最小值为minPath=f(m-2,n-1)
 *因此最后一步选择的路线为min{f(m-2,n-1),f(m-1,n-2)}
 *同理可推其他点的路径
 *由此可推广到一般情况:
 *假设arr[i-1][j]与arr[i][j-1]的minPath的和为f(i-1,j)和f(i,j-1);
 *那么到达arr[i][j]的路径上所有数字和的min为
 *f(i,j)=min{f(i-1,j),f(i,j-1)}+arr[i][j]。
 *
 *方法总结:
 *1,递归法:逆向求解。效率低。改进:把每次计算到的f(i-1,j-1)缓存起来避免多余的计算,即使用动态规划算法
 *2,动态规划算法:正向求解。空间换时间的算法,通过缓存计算的中间值,从而减少重复计算的次数,提高算法效率。
 */
public class minPathArr{

    public static int getMinPathArr(int[][] arr){

        if(null == arr || 0 == arr.length){
            return 0;
        }
        int row = arr.length;
        int col = arr[0].length;
        //用来保存计算的中间值
        int[][] cache = new int[row][col];
        cache[0][0] = arr[0][0];
        for(int i=1; i<col; i++){
            cache[0][i] = cache[0][i-1] + arr[0][i];
        }
        for(int j=1; j<row; j++){
            cache[j][0] = cache[j-1][0] + arr[j][0];
        }
        //在遍历二维数组的过程中不断把计算结果保存到cache中
        for(int i=1; i<row; i++){
            for(int j=1; j<col; j++){
                //可以确定选择的路线为arr[i][j-1]
                if (cache[i-1][j] > cache[i][j-1]) {
                    cache[i][j] = cache[i][j-1] + arr[i][j];
                    System.out.println("[" + i + "," + (j-1) + "] ");
                }else{
                    //可以确定选择的路线为arr[i-1][j]
                    cache[i][j] = cache[i-1][j] + arr[i][j];
                    System.out.println("[" + (i-1) + "," + j + "] ");
                }
            }
        }
        System.out.println("[" + (row-1) + "," + (col-1) + "] ");
        return cache[row-1][col-1];
    }
}

2-4,贪心算法

1)概念

①基本思想

通过一系列的选择来得到问题的解,它所做的每一个选择都是当前状态下局部最好选择,即贪心选择。
例子:
有3种硬币:1.1角、0.5角、0.1角。给顾客找钱1.5角,最少拿几个硬币。
贪心算法:
选出一个不超过1.5的最大面值:1.1
选出一个不超过0.4的最大面值:0.1*4
其实最优解为3*0.5
所以:贪心算法不是从整体最优上考虑,而是做出某种意义上的局部最优选择,在范围广的许多问题中能获得整体最优解。

②与动态规划的区别

在贪心算法中,仅在当前状态下做出最好选择(贪心选择),即局部最优选择,然后再去解做出这个选择后产生的子问题。动态规划算法是自底向上的方式解问题,贪心算法是自顶向下的解问题,以迭代的方式做出相继的贪心选择。

2)使用场景

a.背包问题

①0-1背包问题
给定n种物品和一个背包。
物品i的重量是W[i],价值为v[i],背包容量为c。
问应如何选择装入背包的物品,使得装入背包的物品总价值最大?

要求:对每种物品i只有2种选择:装入或不装入。不能装入多次,也不能部分装入。

不可以用贪心算法求解。因为贪心算法无法保证最终能将背包装满。
②背包问题
与0-1背包类似,不同的是在选择物品i装入背包时,可以选择装入部分物品。

可以用贪心算法求解:
①计算物品单位重量价值v[i]/w[i]
②贪心选择策略:将尽可能多的单位重量价值最高的物品装入。若装入后未满,继续依次装入背包。

b.轮船的最优装载问题

贪心选择策略:优先装重量轻的物品。

c.哈夫曼编码

哈夫曼算法:
将每一字符的频率放入队列Q。
从Q中取出具有最小频率的两棵树x和y,将它们合并为一颗新树。并把新书的频率放入队列Q中。
重复合并。合并n-1次后,优先队列中只剩下一棵树,即哈夫曼树。

d.单源最短路径(Dijkstra算法)

基本思想:
顶点集合S中,仅含有源。用dist[i]记录每个顶点u[i]与源的最短路径长度(没有路径记为无穷大)。
从dist中选出第i个节点为最短路径,则在S中加入顶点u,并对dist大小做修改(每个顶点与源的最短路径,可以经过新加入的结点)
一旦S包含了所有V中顶点,dist就记录了从源到所有其他顶点直接的最短路径长度。
带权有向图
迭代过程

如果还要求出相应的最短路径,则增加prev数组,记录最短路径上i的前一个顶点。
迭代过程

e.最小生成树(Prim算法、Kruskal算法)

应用:设计通信网络,建立城市之间的通信线路。

prim算法贪心选择:顶点集S={1},选取与顶点集中各顶点边最小的顶点,加入顶点集S。重复迭代上述步骤。

kruskal算法贪心选择:
①按边的权值从小到大排序。
②由小到大取边(直到生成n-1条边为止。注意:取边时,两个顶点分别在不同的连通分支上。)
最小生成树

f.活动安排问题(会场安排问题、图着色问题)

活动安排问题(会场安排问题、图着色问题)

g.NP完全问题(多机调度问题)

贪心选择策略:优先采用最长处理时间作业。
例子:
设7个独立作业{1,2,3,4,5,6,7}由3台机器M[1],M[2],M[3]来加工处理,各作业所需处理时间为{2,14,4,16,6,5,3}。
求解:对每个作业根据处理时间从大到小排序:
NP问题

2-5,回溯法

以深度优先方式系统搜索问题解的算法称为回溯法,适用于解组合数较大的问题。
“通用的解题法”;可以系统的搜索一个问题的所有解或任一解。

算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。能避免不必要搜索的穷举式搜索法。

1)基本思想

①针对所给问题,定义问题的解空间;

②确定易于搜索的解空间结构;

③以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。

确定了解空间的组织节后后,回溯法从开始结点(根节点)出发,以深度优先方式搜索整个解空间。
这个开始结点称为活结点,也是当前的扩展结点。
在当前扩展结点处,搜索深一层的新结点,这个新结点称为活结点,并成为当前扩展结点。
此时,往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点。

回溯法以这种工作方式递归地在解空间中搜索,直到找到所要求的解或解空间中已无活结点为止。

2)场景

需要求满足某些约束条件的最佳解。

3)基本概念

①问题的解向量

回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…,xn)的形式。

②显约束

对分量xi的取值限定。

③隐约束

为满足问题的解而对不同分量之间施加的约束。

④解空间

对于问题的一个实例,解向量满足显式约束条件的所有多元组,构成了该实例的一个解空间。
问题的解空间至少应包含问题的一个最优解。

⑤扩展结点

一个正在产生儿子的结点称为扩展结点

⑥活结点

一个自身已生成但其儿子还没有全部生成的节点称做活结点

⑦死结点

一个所有儿子已经产生的结点称做死结点

⑧深度优先的问题状态生成法

如果对一个扩展结点R,一旦产生了它的一个儿子C,就把C当做新的扩展结点。在完成对子树C(以C为根的子树)的穷尽搜索之后,将R重新变成扩展结点,继续生成R的下一个儿子(如果存在)

⑨宽度优先的问题状态生成法

在一个扩展结点变成死结点之前, 它一直是扩展结点

⑩限界函数

法为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(bounding function)来处死那些实际上不可能产生所需解的活结点,以减少问题的计算量。
具有限界函数的深度优先生成法称为回溯法。

⑪常用剪枝函数

用约束函数在扩展结点处剪去不满足约束的子树;
用限界函数剪去得不到最优解的子树。

⑫解空间树:子集树和排列树

子集树:所给的问题是从n个元素的集合S中找出满足某种性质的子集。
排列树:所给的问题是从n个元素的集合S中找出满足某种性质的排列。

4)复杂度分析

①空间复杂度

如果解空间树中从根结点到叶结点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n))。而显式地存储整个解空间则需要O(2h(n))或O(h(n)!)内存空间。

②时间复杂度

遍历子集树需要O(2^n)计算时间。
遍历排列树需要O(n!)计算时间。

5)举例

①0-1背包问题

解空间树
w=[16,15,15],p=[45,25,25],c=30
1,根结点是唯一活结点,也是当前扩展结点。A可以沿纵深方向到B或C。假设先到B。
2,A、B为活结点,B是当前扩展结点。选取了w1,故在B处剩余背包容量r=14,获取价值p=45。
3,B可以到D或E,选择D,需要r>=15,不可行,故选择E(不需要装入背包)。
4,E成为新的扩展结点,A、B、E是活结点。E可以到J或K。J不可行,K可行。K为新的扩展结点。得到一个可行解x=(1,0,0),此时K不能再纵深扩展,成为死结点。故回溯至E。
5,E也没有可扩展的结点,故E为死结点。回溯至B。
6,B也为死结点。回溯至A。此时r=30,p=0.
7,C成为扩展结点,r=30,p=0.
8,选择F为扩展结点,此时A、C、F为活结点。r=15,p=25。
9,选择L为扩展结点,r=0,p=50。找到一个解(0,1,1)
10…….

2-6,分支限界法

1)基本思想

以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。
每一个活结点只有一次成为扩展结点。活结点一旦成为扩展结点,就一次性产生所有儿子的结点。废弃导致不可行解或导致非最优解的儿子结点,其余儿子结点加入活结点表中。一直重复此过程直到找到所需的解或活结点表为空。

2)与回溯法区别

分支限界法类似回溯法,也是在问题的解空间上搜索问题解的算法。但是求解目标不同。
回溯法的求解目标是找出解空间中满足约束条件的所有解,而分支限界法的求解目标是找出满足约束条件的一个解或最优解。
回溯法以深度优先的方式搜索解空间,分支限界法以广度优先或最小耗费优先的方式搜索解空间。
他们对当前扩展结点所采取的扩展方式不同。

2-7

猜你喜欢

转载自blog.csdn.net/sunshinetan/article/details/70832801