薄弱处总结

🗨️字数统计=4.1k字 ⏳阅读时长≈14分钟

小阶段沉淀及总结

近期发现了一些自己知识点的薄弱之处,利用加班的工作之余零碎的时间提升自己,顺便将其总结为本篇文章。(连续上半个月的班,每天还加班到深夜甚至凌晨,脑瓜子真的是嗡嗡的!!!)

大纲
1.算法总结
2.四叉树

(一)五大算法总结

1)分治算法

基本思想

分而治之,把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

需要满足的条件

①该问题缩小到一定程度就可以容易的解决。

②该问题可以分解为若干规模较小的相同问题,即该问题具有最优子结构。

③利用该问题分解出的子问题的解可以合并为该问题的解。

④该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。

因为问题的计算复杂性一般是随着问题规模的增加而增加,所以①是满足的。②是应用分治法的前提,也是递归思想的应用。是否应用分治法完全取决于③,若不满足该条件,则可以考虑用贪心法或动态规划法。④涉及到分治法的效率,如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。

基本的步骤

step1分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;

step2解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;

step3合并:将各个子问题的解合并为原问题的解。

思维过程(类似于数学归纳法)

1、一定是先找到最小问题规模时的求解方法

2、然后考虑随着问题规模增大时的求解方法

3、找到求解的递归函数式后(各种规模或因子),设计递归程序即可。

2)动态规划

基本思想

其与分治法最大的差别:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。

适用的情况

①最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

②无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响,也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

③有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

基本的步骤

动态规划的设计都有着一定的模式,一般要经历以下几个步骤:

初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
①划分阶段:按照问题的时间特征,把问题分为若干个阶段,在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

②确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来,当然,状态的选择要满足无后效性。

③确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。

④寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

思维过程

一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程。实际应用中可以按以下几个简化的步骤进行设计:
①分析最优解的性质,并刻画其结构特征。

②递归的定义最优解。

③以自底向上或自顶向下的记忆化方式(备忘录法 dptable)计算出最优值。

④根据计算最优值时得到的信息,构造问题的最优解。

注意

使用动态规划求解问题,最重要的就是确定动态规划三要素:
①问题的阶段

②每个阶段的状态

③从前一个阶段转化到后一个阶段之间的递推关系。

递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。

确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。

3)贪心算法

基本思想

贪心算法不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性(某个状态以后的过程不会影响以前的状态,只与当前状态有关。)

基本思路

①建立数学模型来描述问题。

②把求解的问题分成若干个子问题。

③对每一子问题求解,得到子问题的局部最优解。

④把子问题的解局部最优解合成原来解问题的一个解。

需要满足的条件

局部最优策略能导致产生全局最优解。一定要注意判断问题是否适合采用贪心算法策略,找到的解是否一定是问题的最优解。

例题分析——背包问题

有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。

物品 A B C D E F G
重量 35 30 60 50 40 10 25
价值 10 40 30 50 35 40 30

分析:

目标函数: ∑pi最大约束条件是装入的物品总重量不超过背包容量:∑wi<=M( M=150)

(1)根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?

(2)每次挑选所占重量最小的物品装入是否能得到最优解?

(3)每次选取单位重量价值最大的物品,成为解本题的策略。

值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。
可惜的是,它需要证明后才能真正运用到题目的算法中。一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。对于例题中的3种贪心策略,都是无法成立(无法被证明)的,解释如下:

(1)贪心策略:选取价值最大者。反例:

W=30   物品:A B C      重量:28 12 12      价值:30 20 20

根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。

(2)贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。

(3)贪心策略:选取单位重量价值最大的物品。反例:

W=30   物品:A B C      重量:28 20 10      价值:28 20 10

根据策略,三种物品单位重量价值一样,程序无法依据现有策略作出判断,如果选择A,则答案错误。

四)回溯法(DFS)

基本思想

在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。而满足回溯条件的某个状态的点称为“回溯点”。若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

基本思路

①针对所给问题,确定问题的解空间:首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解;

②确定结点的扩展搜索规则;

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

基本框架

设问题的解是一个n维向量(a1,a2,………,an),约束条件是ai(i=1,2,3,…..,n)之间满足某种条件,记为f(ai)。

非递归回溯框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  int a[n],i;
初始化数组a[];
i = 1;
while (i>0(有路可走) and (未达到目标)) // 还未回溯到头
{
if(i > n) // 搜索到叶结点
{
搜索到一个解,输出;
}
else // 处理第i个元素
{
a[i]第一个可能的值;
while(a[i]在不满足约束条件且在搜索空间内)
{
a[i]下一个可能的值;
}
if(a[i]在搜索空间内)
{
标识占用的资源;
i = i+1; // 扩展下一个结点
}
else
{
清理所占的状态空间; // 回溯
i = i –1;
}
}
}
递归的算法框架

回溯法是对解空间的深度优先搜索,在一般情况下使用递归函数来实现回溯法比较简单,其中i为搜索的深度,框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int a[n];
try(int i)
{
if(i>n)
输出结果;
else
{
for(j = 下界; j <= 上界; j=j+1) // 枚举i所有可能的路径
{
if(fun(j)) // 满足限界函数和约束条件
{
a[i] = j;
... // 其他操作,如回溯前的清理工作(如a[i]置空值等);
try(i+1);

}
}
}
}

五)分支限界法(BFS)

基本思想

在问题的解空间树T上搜索问题解的算法,分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。

基本思路

分支限界法的搜索策略:在扩展结点处,先生成其所有的儿子结点(分支),然后再从当前的活结点表中选择下一个扩展对点。为了有效地选择下一扩展结点,以加速搜索的进程,在每一活结点处,计算一个函数值(限界),并根据这些已计算出的函数值,从当前活结点表中选择一个最有利的结点作为扩展结点,使搜索朝着解空间树上有最优解的分支推进,以便尽快地找出一个最优解。在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,那些导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被子加入活结点表中。此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所求的解或活结点表为空时为止。

回溯法和分支限界法的区别

回溯法深度优先搜索堆栈活结点的所有可行子结点被遍历后才被从栈中弹出找出满足约束条件的所有解。

分支限界法广度优先或最小消耗优先搜索队列、优先队列每个结点只有一次成为活结点的机会找出满足约束条件的一个解或特定意义下的最优解。

(二)四叉树

四叉树及其变种常用在碰撞检测以及地图块加载的案例中。近期看了一个技术分享,一款MMO开放大世界的手游,在玩家跳转场景时,把跳转的时间压制在1秒,大大的减少了玩家等待的时间。他们是怎么做到的呢?首先是根据玩家身上的跨场景任务进行预加载,其次,他们把场景地图分为一个个的小块tile,通过四叉树,同步加载玩家坐标周边的地块以及NPC,异步加载场景中的其它物体。这样玩家的就不会长时间进行等待,而是一秒后便可以进入场景,游戏体验极好。

插入、更新、删除、查询的过程

插入
1
2
3
插入一个物体,插入时分割空间直到能容纳改物体的最小空间,插入该物体到结点
if 这个节点是能容纳该物体的最小节点,放入这个节点
else 分割这个节点,找到合适的子节点执行插入操作
删除
1
2
3
找到该物体的所属的结点,删除该物体,如果结点不再包含物体并且不含有叶子节点,删除该节点
获取给定物体在节点中的空间划分
获取给定物体在当前节点的空间信息,即4象限信息
查询

查询是否包含给定物体

更新
1
2
3
当物体的位置、大小发生变化,更新4叉树中该物体的信息
一般是删除该物体后重新插入
获取某节点下所有物体

单个空间节点为什么要设置成超过8个元素才往下继续划分空间?

因为要平衡查询、与更新的性能

为什么要设置一个looseness松散值让空间节点之间有重合?

因为要防止边缘元素频繁进更换空间节点,加大更新的性能消耗

分享到