背景


  继上一篇三角网格Dijkstra寻路算法之后,本篇将继续介绍一种更加智能,更具效率的寻路算法—A*算法,本文将首先介绍该算法的思想原理,再通过对比来说明二者之间的相同与不同之处,然后采用类似Dijkstra方式实现算法,算法利用了二叉堆数据结构,最后再通过一些小实验的效果展示其寻路效果。

搜索方法之启发式搜索


  我们知道之所以Dijkstra算法并不高效,即使采用了好的数据结构优化,原因在于访问的节点数量太多。而A*相比于Dijkstra的优势就在于利用了更多的信息、访问更少的节点。为了方便理解A*,我们先抛开最短路径不谈,首先来介绍一下图的搜寻。我们知道在所有能被抽象为Graph的数据结构上搜索一个特定节点,我们可以采用遍历的方式。BFS(Breath-First)遍历和DFS(Depth-First)遍历,这是所有有数据结构基础的人都了解的基本图遍历方法,当然在搜索问题中,大可不必遍历所有节点,只需在遍历到终点时跳出即可。为了实现搜索节点,我们可以采用BFS和DFS这两种方式。在图G上,给定一个起点S和一个终点E,图的BFS搜索方法主要逻辑如下:

 1 TravelSearch_BFS(G,S,E)
2 创建空队列容器Q
3 将S置为已访问并加入Q
4 While(Q不为空)
5 从Q中取出一个节点P
6 对P的所有邻居n
7 若n未被访问过
8 若n==E,则找到终点E,算法结束
9 将n置为已访问并加入Q
10 算法结束,未能找到终点E

DFS搜索方法(非递归式)主要逻辑如下:

 1 TravelSearch_DFS(G,S,E)
2 创建空栈容器Q
3 将S置为已访问并加入Q
4 While(Q不为空)
5 从Q中取出一个节点P
6 对P的所有邻居n
7 若n未被访问过
8 若n==E,则找到终点E,算法结束
9 将n置为已访问并加入Q
10 算法结束,未能找到终点E

  可以看出,DFS搜索和BFS搜索的关键区别在容器Q上,DFS采用LIFO的栈结构,而BFS则采用的是FIFO的队列结构。假如我们像下面这样做一个方格图,然后设置好起点和终点,规定好每个格子访问邻居的顺序为左→右→下→上的话,在这之上的DFS/BFS搜寻的演示动画已经寻路开销如下图所示:

DFS(Depth-First)寻找终点E动画 BFS(Breath-First)寻找终点E动画

  可以看出仅仅是容器不一样,寻找终点访问的节点数也不一样。也就是说,容器Q的进出方式可以影响到搜寻的开销。所以我们不难想到,如果一个更加“聪明”的容器Q能够按某种优先级去弹出节点,我们就有可能更早的找到需要的节点,从而避免访问过多其他节点。事实上在方格图G上这样的一个“智能”一点的容器完全可以设计出来,只需充分利用一下方格图的特点。我们令起始方格为(1,4),终止方格为(6,10),我们根据对方格图的先验了解可以知道,起点与终点的坐标暗含了他们的位置信息,例如从S(1,4)搜寻E(6,10),显然从S出发向着“东南”方向搜寻,发现终点的可能性更大。我们根据每一个方格节点的坐标,能够求出到E的欧式距离。这样,我们完全可以设计一个与无脑的Depth-First和Breath-First不同的搜索方式,我们称其为“Best-First”。这个Best-First搜索的代码结构和BFS/DFS差不多,但关键的区别是从容器中弹出元素不再是LIFO或是FIFO方式,而是选择一个离E欧式距离更近的节点。选择离E距离更近的节点弹出的原因是我们经验上认为这样使搜索“离找到E更近了一步”。能够判断远近是方格图的特点决定的,因为不是任何无向带权图都有所谓“距离”或者“节点坐标”这样的概念。我们利用方格图的先验信息设计了这样一个Best-First智能搜寻算法。支持这个算法的容器Q我们可以设计为一个二叉堆之类的堆容器,这样可以支持高效取最值操作。这个Q每次Pop出的元素都是Q中离终点E最近的,算法逻辑可用下面的伪代码表示:

 1 TravelSearch_BestFirst(G,S,E)
2 创建空堆容器Q
3 将S置为已访问并加入Q
4 While(Q不为空)
5 从Q中取出一个节点P(即P是Q中距离E最近的格子)
6 对P的所有邻居n
7 若n未被访问过
8 若n==E,则找到终点E,算法结束
9 将n置为已访问并加入Q
10 算法结束,未能找到终点E

让我们看看他的执行效果:

  这一看我们设计的Best-First果然比DFS和BFS更直接了当,几乎是一路冲向终点,这样只遍历了很少的节点就找到终点。不过我们的方格图可没这么简单,一般来说是有点障碍物的,所以我们就设计一个有障碍物的方格图,用红叉方格表示,然后让这三种方法再去跑一遍,结果如下:

Depth-First Breath-First Best-First

  可以看出有障碍物的时候Best-First也能够巧妙的绕过去再冲向终点,也就是说,这个Best-First是一个方格图上寻路的好方法。一般都能比无脑DFS和BFS访问更少的节点。

  根据以上的叙述,我们引入一种对搜索方法的分类:一种叫做Uniformed Search,也就是我们前面提到的无脑搜索BFS/DFS,而Dijkstra算法也属于这一类方法。这种Search的特点是无脑暴力,但有很强的通用性,假如对图没有任何先验知识,例如除了是否访问到之外完全不知道终点的其他信息的话,就只能使用这样的搜寻;另一种叫做Informed Search,又叫启发式(heuristic)搜索,即有先验知识的搜索,例如Best-First。其特点是对终点的位置可以有一些启发的信息,根据这些信息可以有倾向性的去筛选可能的路径。而A*算法,也就属于这一类方法。

Uniform Search Informed Search
BFS搜寻 BestFirst搜寻
DFS搜寻 A*算法
Dijkstra算法  

在与Dijkstra算法的对比中理解A*算法


  在大致了解所谓启发式搜寻之后,再来了解A*算法的寻路思路。在介绍之中,将会与Dijkstra算法紧密结合来进行讲解,毕竟这两个算法关系密切,并且在代码结构变量使用上上高度相似,可以说是亲如父子。假如没有充分了解Dijkstra算法,可以先参考博文“三角网格上的寻路算法Part.1—Dijkstra算法”。

  A*与Dijkstra算法最大的不同在于,它采用了启发式估价函数f(n)=g(n)+h(n)。这个g(n)有点类似于distance数组,代表当前节点到n的实际路径长度,而h(n)表示节点n到终点的估算路径长度。在算法实现的时候,f(n)和g(n)也是使用数组来表示,f数组中f[n]表示从S经过n到E的路径当前总估价,g数组中g[n]表示S到n的实际路径长度。而h(n)是和先验信息有关的函数。在我们上面举例的方格图中,h(n)的计算方式就是计算n到E的欧式距离,所以h(n)可以不采用数组,而是保留函数的形式,需要的时候直接计算:

  我们知道Dijkstra算法中,distance数组是一个关键的变量。首先distance值的大小是从容器中取节点的标准,每次选择节点都是选择容器中distance值最小的节点。其次distance值还会在松弛操作中被更新为更小的值。也就是说Dijkstra算法是围绕着distance数组这个核心变量来运行的。而在A*算法中,核心变量则变为f数组,也就是说,A*算法中,我们的选择由distance最小变成了f最小。f数组成为容器的key,每次从容器中选择的是f值最小的节点。

  A*算法设置有一个开启列表和关闭列表,节点状态可分为在关闭列表中、在开启列表中与尚未访问到,开启列表与Dijkstra算法中的容器Q类似,而关闭列表也与其相似,能使用bool数组来实现。对节点类别的划分也可以与Dijkstra算法中的A,B,C三类节点分法一一对上号。A*算法的逻辑结构与Dijkstra算法相似度很高,我们可以从下面伪代码的对比中看出:

Function_AStar(图Graph,起点S,终点E)
初始化f数组为MAX。
初始化g数组为MAX。
初始化previus数组为-1。
初始化flagsmap_close数组为false。
初始化开启列表Q。
初始化g[S]=0。
初始化f[S]=h(S)。
将S加入Q。
While(Q不为空)算法结束,若进行到此步则说明未找到终点E
P=Q.ExtractMin(),即找到Q中f值最小的点P。
flagsmap_close[P]=true,即设置P为关闭状态(A类节点)。
若P==E
找到终点,算法结束。
对P的所有邻接点n
 若n为关闭状态(A类节点),即有flagsmap_close[n]==true
 continue继续循环。
  否则
 从S通过P到n的路径长度:
       distancePassP=g[P]+Weight(P,n)。
 若n为B类节点,即有f[n]!=MAX
 若distancePassP<g[n]
 将g[n]更新为distancePassP。
 将f[n]更新为distancePassP+h(n)。
 将previous[n]更新为P。
 若容器为堆则找到n在堆中的位置并调整之。
  否则n为C类节点,即f[n]==MAX
 将g[n]更新为distancePassP。
 将f[n]更新为distancePassP+h(n)。
 将previous[n]更新为P。
 将n加入Q。
算法结束,若进行到此步则说明未找到终点E
Function_Dijkstra(图Graph,起点S,终点E)
初始化distance数组为MAX。
初始化previus数组为-1。
初始化flagsmap_close数组为false。
初始化集合Q。
初始化distance[S]=0。
将S加入Q。
While(Q不为空)算法结束,若进行到此步则说明未找到终点E
P=Q.ExtractMin(),即找到Q中Distance值最小的点P。
flagsmap_close[p]=true,即设置P为A类节点。
若P==E
找到终点,算法结束。
对P的所有邻接点n
若n为A类节点,即有flagsmap_close[n]==true
continue继续循环。
否则
计算从S通过P到n的路径长度:
         distancePassP=distance[P]+Weight(P,n)。
若n为B类节点,即有distance[n]!=MAX
若distancePassP<distance[n]
将distance[n]更新为distancePassP。
将previous[n]更新为P。
若容器为堆则找到n在堆中的位置并调整之。
否则n为C类节点,即distance[n]==MAX
将distance[n]更新为distancePassP。
将previous[n]更新为P。
将n加入Q。
算法结束,若进行到此步则说明未找到终点E
A*算法伪代码 上篇博客中Dijkstra算法伪代码

  涂颜色的代码就是A*和Dijkstra算法不同的地方,可以看出A*与Djikstra有着很多相同的点,例如对节点的分类,对容器Q的使用等。Dijkstra算法是层层向外扩展搜索,而A*算法虽然也是一步步向外搜索,但其扩展的方向更加有倾向性。而正是h函数赋予了A*算法这样的倾向性,一般来说h(n)会设计成在n越与E接近时越小,直到h(E)=0。在A*迭代的过程中,每个节点的f值会越来越与实际路径总长度接近,而g值则起到类似Dijkstra算法中distance的作用。

其他的讨论


  关于A*算法正确性证明,博主也曾经想通过类似于Dijkstra算法那样简单的方式去证明,不过似乎是行不通的,经过一番搜索之后,发现一些有益的资源:

论文:Generalized Best-First Search Strategies and the Optimality of A*

Pdf:The Optimality of A*

  尤其是形式化的证明可以从初始论文中找到,证明过程比较复杂,至于你看没看懂,反正我是没有看懂 ╮(╯▽╰)╭,呵呵~

  关于A*算法伪代码中有一些比较权威的版本,例如维基百科的版本:

 function A*(start,goal)
closedset := the empty set //已经被估算的节点集合
openset := set containing the initial node //将要被估算的节点集合
came_from := empty map
g_score[start] := 0 //g(n)
h_score[start] := heuristic_estimate_of_distance(start, goal) //h(n)
f_score[start] := h_score[start] //f(n)=h(n)+g(n),由于g(n)=0,所以……
while openset is not empty //当将被估算的节点存在时,执行
x := the node in openset having the lowest f_score[] value //取x为将被估算的节点中f(x)最小的
if x = goal //若x为终点,执行
return reconstruct_path(came_from,goal) //返回到x的最佳路径
remove x from openset //将x节点从将被估算的节点中删除
add x to closedset //将x节点插入已经被估算的节点
foreach y in neighbor_nodes(x) //对于节点x附近的任意节点y,执行
if y in closedset //若y已被估值,跳过
continue
tentative_g_score := g_score[x] + dist_between(x,y) //从起点到节点y的距离 if y not in openset //若y不是将被估算的节点
add y to openset //将y插入将被估算的节点中
tentative_is_better := true
elseif tentative_g_score < g_score[y] //如果y的估值小于y的实际距离
tentative_is_better := true //暂时判断为更好
else
tentative_is_better := false //否则判断为更差
if tentative_is_better = true //如果判断为更好
came_from[y] := x //将y设为x的子节点
g_score[y] := tentative_g_score
h_score[y] := heuristic_estimate_of_distance(y, goal)
f_score[y] := g_score[y] + h_score[y]
return failure

  仔细分析不难得出这份伪代码的逻辑和本文的是一样的。不过需要指出,从网上搜索A*算法会有各种各样的版本,不过笔者发现所有的版本其逻辑可以归结为两类,其区别是对在关闭列表中节点的处理,例如下面的版本:

1:Put the start node, s, on a list called OPEN of unexpanded nodes
2:if |OPEN| = 0 then
3:  Exit—no solution exists
4:Remove a node n from OPEN, at which f = g + h is minimum and place it on a list called CLOSED
5:if n is a goal node then
6:  Exit with solution
7:Expand node n, generating all its successors with pointers back to n
8:for all successor n0of n do
9:  Calculate f(n0)
10:  if n0/ ∈ OPEN AND n0/ ∈ CLOSED then
11:    Add n0to OPEN
12:    Assign the newly computed f(n0) to node n0
13: else
14:  If new f(n0) value is smaller than the previous value, then update with the new value (and predecessor)
15:  If n0 was in CLOSED, move it back to OPEN
16:Go to (2) 

  总之就是部分版本对于close列表中的节点还会进行更新处理并重新加入开启列表,而另一部分版本是continue即什么都不做。咋一看这两者逻辑截然不同,那是不是有对于不对呢?其实这一区别主要和h函数有关。需要强调的是,A*算法的计算过程很大程度依赖于h函数的设计,一般来说h函数总会设计成h(n)<=Real(n),即n到E的实际距离。这样A*算法总会找到最短路径,这是存在证明的,论文里也提到过。假如h函数是一致单调的,例如对于两个节点n和m,若h(n)<h(m)能推出Real(n)<Real(m),则两个版本的伪代码是全部等价的。否则只有第二个版本是完备的,也就是需要重新加符合条件的closed节点入Open。Amitp在他的一篇关于A*的长篇文章中也指出这个h函数在consistant和admissible的时候,两份伪代码等价。通常我们将A*运用在实际模型的寻路上,例如三角网或者方格图,这些模型都是在欧式几何空间中很直观的模型,很多点和线都是具有实际几何意义的。在这类模型上,我们都知道两点之间线段最短的公理,因而采用欧式距离作为估计是很合理的。

  有很多关于A*的很详细的细节讨论可以从amitp的博客上获取,尤其是一些小应用很好玩,有兴趣的人可以尝试一下,这里贴出其中一个flash,不能运行的话可以刷新一下,注意这个起点需要鼠标拖动与终点分离。

实现代码


  A*算法的实现和上篇Dijkstra算法的实现是在同一个工程项目里,其测试用例都是用算法去求三角网格测地线。AbstractGraph,Mesh以及读取文件的类型可以参考上一篇博文。本文主要的关键是A*的逻辑。这里的类GeodeticCalculator_AStar是算法的执行主逻辑类,其中容器采用和Dijkstra算法一模一样的堆,这个堆唯一的不同就是key由从distance数组获取改为从f数组获取。

  Dijkstra算法与A*算法通用的二叉堆容器实现的代码:

#ifndef ASTAROPENSET_H
#define ASTAROPENSET_H
#include <vector>
#include <math.h>
#include "DijkstraSet.h"
class AStarSet_Heap:DijkstraSet
{
private:
std::vector<int> heapArray;
std::vector<float> *f_key;
std::vector<int> indexInHeap;// stores the index to heapArray for each vertexIndex, -1 if not exist
public:
AStarSet_Heap(int maxsize,std::vector<float> *key)
{
this->f_key=key;
this->indexInHeap.resize(maxsize,-1);
}
~AStarSet_Heap(){f_key=0;}
void Add(int pindex)
{
this->heapArray.push_back(pindex);
indexInHeap[pindex]=heapArray.size()-1;
ShiftUp(heapArray.size()-1);
}
int ExtractMin()
{
if(heapArray.size()==0)
return -1;
int pindex=heapArray[0];
Swap(0,heapArray.size()-1);
heapArray.pop_back();
ShiftDown(0);
indexInHeap[pindex]=-1;
return pindex;
}
bool IsEmpty()
{
return heapArray.size()==0;
}
void DecreaseKey(int pindex)
{
ShiftUp(indexInHeap[pindex]);
}
private:
int GetParent(int index)
{
return (index-1)/2;
}
int GetLeftChild(int index)
{
return 2*index+1;
}
int GetRightChild(int index)
{
return 2*index+2;
}
bool IsLessThan(int index0,int index1)
{
return (*f_key)[heapArray[index0]]<(*f_key)[heapArray[index1]];
}
void ShiftUp(int i)
{
if (i == 0)
return;
else
{
int parent = GetParent(i);
if (IsLessThan(i,parent))
{
Swap(i, parent);
ShiftUp(parent);
}
}
}
void ShiftDown(int i)
{
if (i >= heapArray.size()) return;
int min = i;
int lc = GetLeftChild(i);
int rc = GetRightChild(i);
if (lc < heapArray.size() && IsLessThan(lc,min))
min = lc;
if (rc < heapArray.size() && IsLessThan(rc,min))
min = rc;
if (min != i)
{
Swap(i, min);
ShiftDown(min);
}
}
void Swap(int i, int j)
{
int temp = heapArray[i];
heapArray[i] = heapArray[j];
heapArray[j] = temp;
indexInHeap[heapArray[i]]=i;//record new position
indexInHeap[heapArray[j]]=j;//record new position
}
}; #endif

算法主体:

#ifndef GEODETICCALCULATOR_ASTAR_H
#define GEODETICCALCULATOR_ASTAR_H
#include <vector>
#include <math.h>
#include "Mesh.h"
#include "AStarOpenSet.h"
class GeodeticCalculator_AStar
{
private:
AbstractGraph& graph;
int startIndex;
int endIndex;
std::vector<float> gMap;//current s-distances for each node
std::vector<float> fMap;//current f-distances for each node
std::vector<int> previus;//previus vertex on each vertex's s-path AStarSet_Heap* set_Open;//current involved vertices, every vertex in set has a path to start point with distance<MAX_DIS but may not be s-distance.
std::vector<bool> flagMap_Close;//indicates if the s-path is found std::vector<bool> visited;//record visited vertices;
std::vector<int> resultPath;//result path from start to end
public:
GeodeticCalculator_AStar(AbstractGraph& g,int vstIndex,int vedIndex):graph(g),startIndex(vstIndex),endIndex(vedIndex)
{
set_Open=0;
}
~GeodeticCalculator_AStar()
{
if(set_Open!=0) delete set_Open;
}
//core functions
bool Execute()//main function execute AStar, return true if the end point is reached,false if path to end not exist
{
visited.resize(graph.GetNodeCount(),false);
gMap.resize(graph.GetNodeCount(),MAX_DIS);
fMap.resize(graph.GetNodeCount(),MAX_DIS);
previus.resize(graph.GetNodeCount(),-1);
flagMap_Close.resize(graph.GetNodeCount(),false);
this->set_Open=new AStarSet_Heap(graph.GetNodeCount(),&fMap);
set_Open->Add(startIndex);
gMap[startIndex]=0;
fMap[startIndex]=GetH(startIndex);
while(!set_Open->IsEmpty())
{
int pindex=set_Open->ExtractMin();// vertex with index "pindex" found its s-path
flagMap_Close[pindex]=true;//mark it
if(pindex==endIndex)
return true;
UpdateNeighborMinDistance(pindex);// update its neighbor's s-distance
}
return false;
}
private:
//core functions
float GetH(int p1)
{
return graph.GetEvaDistance(p1,endIndex);
}//calculate h[p1] when needed, not necessary to create an array to store
void UpdateNeighborMinDistance(int pindex)
{
std::vector<int>& neightbourlist=graph.GetNeighbourList(pindex);
for(size_t i=0;i<neightbourlist.size();i++ )
{
int neighbourindex=neightbourlist[i];
visited[neighbourindex]=true;//just for recording , not necessary
if (flagMap_Close[neighbourindex])//if Close Nodes,Type A
{
continue;
}
else
{
float gPassp = gMap[pindex] + graph.GetWeight(pindex, neighbourindex);
if (fMap[neighbourindex]==MAX_DIS)//if unvisited nodes ,Type C
{
//same operation as in Dijkstra except the assignment of fMap
gMap[neighbourindex] = gPassp;
fMap[neighbourindex]=gPassp + GetH(neighbourindex);
previus[neighbourindex] = pindex;
set_Open->Add(neighbourindex);
}
else
{
//same operation as in Dijkstra except the assignment of fMap
if (gPassp < gMap[neighbourindex])//Type B
{
gMap[neighbourindex] = gPassp;
fMap[neighbourindex]=gPassp + GetH(neighbourindex);
previus[neighbourindex] = pindex;
set_Open->DecreaseKey(neighbourindex);
}
}
}
}
}// for neighbors of pindex ,execute relaxation operation
public:
//extra functions
std::vector<int>& GetPath()
{
int cur=endIndex;
while(cur!=startIndex)
{
resultPath.push_back(cur);
cur=previus[cur];
}
resultPath.push_back(startIndex);
std::reverse(resultPath.begin(),resultPath.end());
return resultPath;
}// reconstruct path from prev[]
float PathLength()
{
return gMap[endIndex];
}//return the length of the path form result path
int VisitedNodeCount()
{
return (int)std::count(visited.begin(),visited.end(),true);
}//return the visited nodes count
std::vector<bool>& GetVisitedFlags()
{
return visited;
}//return the visited flags of the nodes
};
#endif

  对比代码结构发现与Dijkstra算法简直不能再像,所以说先若是先了解了Dijkstra算法,那么只要做小的代码改动就能变成A*。

算法效果


  因为我们始终强调A*改进了Djikstra算法的访问节点次数,所以我们可以使用两个模型的运行实际例子来说明。首先是计算测地线距离,上篇文章中我们就已经算出了Dijkstra的最短路径,其中访问过的节点都涂成绿色了,这次采用A*算法,可以看出A*算法比起Djikstra算法的确减少了节点的访问。

Dijkstra算法寻找的测地线以及访问过的节点
A*算法寻找的测地线以及访问过的节点

  我们再换一个方格图的模型如下,在这个模型上我们再做一些试验:

8个邻域其权值 邻接关系,ok表示邻接no表示不邻接X表示障碍物

  这个小软件PathSeeker是博主使用WPF写的一个算法可视化小工具,可以用鼠标设置方格图大小以及起点终点和障碍物,还能够选择BFS,Dijkstra算法,A*算法来计算路径和显示参数。

WPF小工具PathSeeker下载地址:https://files.cnblogs.com/chnhideyoshi/PathSeeker.rar

  PathSeeker可以显示A*与Djikstra算法对每个访问到的节点所设置的distance值、g值、f值、h值等。方格寻路时能够寻找八邻域方格,八个领域方格的边权值不一样,而且不容许从角落穿出去。

Dijkstra算法寻路结果,方块右下角为distance值 A*算法寻路结果,方块左上、右上和右下分别对应f值、h值和g值

  从上述结果的确可以看出A*相对Dijkstra的改善,值得注意的是A*与Dijkstra都能找到最短,如果最短路径不止一条两个算法的最短路径不完全是一模一样的,但是路径长度会是一样的。

  最后提供一下所有源代码的维护地址:

计算三角网近似测地线代码工程: https://github.com/chnhideyoshi/SeededGrow2d/tree/master/MeshGeodetic

PathSeeker代码工程:https://github.com/chnhideyoshi/SeededGrow2d/tree/master/PathSeeker

  转载本文注明出处啊:http://www.cnblogs.com/chnhideyoshi/p/AStar.html

三角网格上的寻路算法Part.2—A*算法的更多相关文章

  1. 三角网格上的寻路算法Part.1—Dijkstra算法

    背景 最近在研究中产生了这样的需求:在三角网格(Mesh)表示的地形图上给出两个点,求得这两个点之间的地面距离,这条距离又叫做"测地线距离(Geodesic)".计算三角网格模型表 ...

  2. 三角网格上的寻路算法Part.1—Dijkstra算法 等

    http://www.cnblogs.com/chnhideyoshi/p/AStar.html

  3. unity A*寻路 (三)A*算法

    这里我就不解释A*算法 如果你还不知道A*算法 网上有很多简单易懂的例子 我发几个我看过的链接 http://www.cnblogs.com/lipan/archive/2010/07/01/1769 ...

  4. 三角网格(Triangle Mesh)的理解

    最简单的情形,多边形网格不过是一个多边形列表:三角网格就是全部由三角形组成的多边形网格.多边形和三角网格在图形学和建模中广泛使用,用来模拟复杂物体的表面,如建筑.车辆.人体,当然还有茶壶等.图14.1 ...

  5. bullet物理引擎与OpenGL结合 导入3D模型进行碰撞检测 以及画三角网格的坑

    原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/11681069.html 一.初始化世界以及模型 /// 冲突配置包含内存的默认设置,冲突设置. ...

  6. 一步一步写算法(之prim算法 上)

    原文:一步一步写算法(之prim算法 上) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 前面我们讨论了图的创建.添加.删除和保存等问题.今 ...

  7. 昇腾CANN论文上榜CVPR,全景图像生成算法交互性再增强!

    摘要:近日,CVPR 2022放榜,基于CANN的AI论文<Interactive Image Synthesis with Panoptic Layout Generation>强势上榜 ...

  8. Python使用DDA算法和中点Bresenham算法画直线

    title: "Python使用DDA算法和中点Bresenham算法画直线" date: 2018-06-11T19:28:02+08:00 tags: ["图形学&q ...

  9. 第三十节,目标检测算法之Fast R-CNN算法详解

    Girshick, Ross. “Fast r-cnn.” Proceedings of the IEEE International Conference on Computer Vision. 2 ...

随机推荐

  1. Mybatis-运行原理

    一.mybatis分层图 二.运行流程 根据全局配置文件创建sqlSessionFactory对象 根据全局配置文件的io流来构建SqlSessionFactoryBuilder对象: 解析(XmlC ...

  2. html标签设置contenteditable时,去除粘贴文本自带样式

    在一个div标签里面加了可编辑的属性,从别的地方复制了一串文本,只想把文本内容存到接口里面,结果发现文本自带的标签和样式都会存进去. $(".session-new-name"). ...

  3. java中的++i是线程安全的吗?

    java中的++i是线程安全的吗?为什么?怎么使它线程安全呢? 先说答案: 非线程安全 先说下为什么是非线程安全的? 从Java内存模型说起 Java内存模型规定了所有的便利都存储在主内存中,每个线程 ...

  4. 启动Springboot 报错 Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Sat Jan 12 15:50:25 CST 2019 There was an unexpected error (type=Not

    解决方案:http://www.cnblogs.com/michaelShao/p/6675186.html

  5. 【kafka学习笔记】合理安排broker、partition、consumer数量

    broker的数量最好大于等于partition数量 一个partition最好对应一个硬盘,这样能最大限度发挥顺序写的优势. broker如果免得是多个partition,需要随机分发,顺序IO会退 ...

  6. gitlab 集成openldap

    Setting up LDAP sign-in If you have an LDAP directory service such as Active Directory, you can conf ...

  7. Jenkins多分支构建

    目录 一.创建多分支pipeline 二.根据分支部署 gitlab触发与多分支 Generic Webhook多分支 一.创建多分支pipeline 在实际中,需要多分支同时进行开发.如果每个分支都 ...

  8. Log4j漏洞源码分析

    Log4j漏洞源码分析 这几天Log4j的问题消息满天飞,今天我们就一起来看看从源码角度看看这个漏洞是如何产生的. 大家都知道这次问题主要是由于Log4j中提供的jndi的功能. 具体涉及到的入口类是 ...

  9. 2020腾讯犀牛鸟网络安全T-Star高校挑战赛writeup

    签到 浏览器禁用js,在www目录下有 key 命令执行基础 使用 | 可以执行其他命令,在 ../目录下有 key.php cat 一下读出来 你能爆破吗 一开始随便输了个账号的时候不是这个页面,抓 ...

  10. CF652B z-sort 题解

    Content 定义一个数组是 \(\text{Z}\) 排序过的,当且仅当: 对于所有的 \(i=2k+1(k\in\mathbb Z)\),\(a_i\leqslant a_{i-1}\). 对于 ...