目录
- 1 图介绍和表⽰⽅式
- 2 宽度优先搜索BFS
- 3 深度优先搜索DFS
- 4 排列组合问题
- 5 回溯算法,图论⾯试题实战
- 回溯
- 模式识别
- Backtracking的典型模板
- 括号生成 Parentheses
- N 皇后 N Queen
- 全排列模板
- 部分排列
- 有重复的全排列
- 返回所有的子集 Subsets
- 没有重复数的子集合 Unique Subsets
- 一共有多少个二叉搜索树 Unique Binary Search Trees
- 返回数字和为k的子序列 Combination Sum
- 找出 candidates 中所有可以使数字和为 target 的组合(每个数字能无限次)Combination Sum I
- 找出 candidates 中所有可以使数字和为 target 的组合(每个数字只能使用 一次)Combination Sum II
- 电话按键的组合可能性 Phone Number
- 单词到目标单词最少需要多少步 Word Ladder
- 找到所有回文数的可能性 Palindrome Partitioning
- 克隆图 Clone Graph
- 拓扑排序
- Conclusion
- 补充:A*算法
1 图介绍和表⽰⽅式
//类似与链表结构
//邻接点
struct AdjListNode {
int dest;
struct AdjListNode* next;
};
//pointer to head node of list
//邻接表头节点
struct AdjList {
struct AdjListNode* head;
};
//图结构
struct Graph {
int V;//顶点数
struct AdjList* array;
};
//a utility function to create a new adjacency list node
//节点初始化
struct AdjListNode* newAdjListNode(int dest) {
struct AdjListNode* newNode = (struct AdjListNode*)malloc(sizeof(struct AdjListNode));
newNode->dest = dest;
newNode->next = NULL;
return newNode;
};
//a utility function that create a graph of V vertices顶点
//图结构初始化
struct Graph* createGraph(int V) {
struct Graph* graph = (struct Graph*)malloc(sizeof(struct Graph));
graph->V = V;
//create an array of adjacency list. size of array will be V
graph->array = (struct AdjList*)malloc(V * sizeof(struct AdjList));
int i;
for (i = 0; i < V; ++i) {
graph->array[i].head = NULL;
}
return graph;
};
//adds an edges to an unfirected graph
//把节点加入初始化的图中
void addEdge(struct Graph* graph, int src, int dest) {
//Add an edge from src to dest. A new node is added to the adjacency
//list of src. The node is added at the beginning
struct AdjListNode* newNode = newAdjListNode(dest);
newNode->next = graph->array[src].head;//永远在邻接表的第一个来添加node,这里相当于接管后面的那一串链表
graph->array[src].head = newNode;//把这个node加入
//since graph is undirected,add an edge from dest to src also
//无向图,有重复
newNode = newAdjListNode(src);
newNode->next = graph->array[dest].head;
graph->array[dest].head = newNode;
}
2 宽度优先搜索BFS
BFS 适合⽤于『由近及 远』的搜索,⽐较适合⽤于求解最短路径、最少操作之类的问题。
void Graph_BFS(int s,int v) {
//标记所有的节点都没有被访问
bool* visited = new bool[v];
for (int i = 0; i < v; i++)
visited[i] = false;
queue<int> que;
visited[s] = true;
que.push(s);
list<int>::iterator i;
while (que.size()) {
s = que.front();
cout << s << endl;
que.pop();
//对当前节点的所有边对应的邻接节点做遍历
for (i = adj[s].begin(); i != adj[s].end(); ++i) {
if (!visited[*i]) {
visited[*i] = true;
que.push(*i);
}
}
}
}
3 深度优先搜索DFS
void Graph_DFSUtil( int v, bool visited[]) {
visited[v] = true;
cout << v << " ";
//对当前节点的所有边对应的邻接节点做遍历
list<int>::iterator i;
for (i = adj[v].begin(); i != adj[v].end(); ++i)
if (!visited[*i])
Graph_DFSUtil(*i, visited);
}
4 排列组合问题
单源最短路径问题
对于每条边都有⼀个权值的图来说,单源最短路径问题是指从某个节点出发,到其他节点的最短距离。该问题的常见算法有Bellman-Ford和Dijkstra算法。
如果每条边权值相同(⽆权图),由于从源开始访问图遇到节点的最⼩深度就等于到该节点的最短路径,因此 Priority Queue就退化成Queue,Dijkstra算法就退化成BFS。
Dijkstra 算法
贪心思想,剩下的节点中,取离原点最近的加入
DIJKSTRA(G, s)
S = EMPTY
Insert all vertexes into Q
While Q is not empty
u = Q.top
S.insert(u)
For each v is the neighbor of u
If d[v] > d[u] + weight(u, v)
d[v] = d[u] + weight(u, v) //总是取最小值
parent[v] = u //最后一直到所有的节点都加入集合
获得任意两点之间的最短距离(一般面试不写)
1 可以对所有的点进行dijkstra算法
2 Floyd算法
核心:动态规划:利用二维矩阵来存储i、j之间的最短距离,如果i、j不相连就是正无穷
FLOYD(G)
Distance(0) = Weight(G)
For k = 1 to n
For i = 1 to n
For j = 1 to n
//对于第k次做更新,比较i到j,次数小于k的中间节点
Distance(k)ij = min(Distance (k-1)ij, Distance (k-1)ik+ Distance (k-1)kj)
Return Distance(n)
5 回溯算法,图论⾯试题实战
回溯
DFS 通常从某个状态开始,根据特定的规则转移状态,直⾄⽆法转移(节点为空),然后回退到之前⼀步状态,继续按照指定规则转移状态,直⾄遍历完所有状态。
回溯法包含了多类问题,模板类似。排列组合模板->搜索问题(是否要排序,哪些情况要跳过)
使⽤回溯法的⼀般步骤:
确定所给问题的解空间:⾸先应明确定义问题的解空间,解空间中⾄少
包含问题的⼀个解。
确定结点的扩展搜索规则
以深度优先⽅式搜索解空间,并在搜索过程中⽤剪枝函数避免⽆效搜索(是否已经访问过了)。
模式识别
⽤Backtracking( Top-Down )解决发散结构问题
对于发散性问题(例如“所有组合” , “全部解”),可以选取其问题空间“收敛” 的⼀端作为起点,沿着节点发散的⽅向(或者说,当前节点的多种选择)进 ⾏递归,直到a.当前节点“不合法” 或 b.当前节点发散⽅向搜索完毕,才会return。
(backtracking选择尽量少的,尽可能到达最后,并不是搜索所有的发散方向,降低复杂度)
如果需要记录决策的路径,可以⽤vector &path沿着搜索的⽅向记 录,在满⾜胜利条件时记录当前path(通常是将path存⼊ vector> &paths)。
(找到符合条件的,还可能要继续往下走,比如找到一个符合条件的单词后,还可能存在以该单词为前缀的另外一个单词,推荐把胜利的判定和evaluator的判定分开) (传入path是引用的形式,相当于是全局的变量,在访问完一个节点之后,要恢复原有的状态,在return前,删除path中的节点)
Backtracking的典型模板
void backtracking( P node, vector<P> &path, vector<vector<P> >&paths ){
if(!node ) // invalid node
return;
path.push_back(node);
bool success = ; // condition for success
if( success )
paths.push_back( vector<P>(path.begin(),path.end()) ); // don't return here
for( P next: all directions )
backtracking( next, path, paths );
path.pop_back();
return;
}
括号生成 Parentheses
Given n pairs of parentheses, generate all valid combinations of parentheses. E.g. if n = 2, you should return ()(), (())
void parenthesesCombination(int leftRem, int rightRem, string& path, vector<string>& paths) {
if (leftRem < 0 || rightRem < 0)
return;
if (leftRem > 0) {
path.push_back('(');
parenthesesCombination(leftRem - 1, rightRem, path, paths);
path.pop_back();//((时,回溯到(,也就是在(时,确保(和((都能遍历到,没有的话,就只能时((情况
}
if (rightRem > leftRem) {
path.push_back(')');
if(rightRem==1)//边界条件设置
paths.push_back(path);
parenthesesCombination(leftRem, rightRem-1 , path, paths);
path.pop_back();
}
}
vector<string> generateParenthesis(int n) {
vector<string> res;
if (n <= 0)
return res;
string temp = "";
parenthesesCombination(n, n, temp, res);
return res;
}
N 皇后 N Queen
Please write a function to find all ways to place n queens on an n×n chessboard such that no two queens attack each other.
//检查新加入的row1和col1是否合法
bool checkValid(int row1, int col1, int* rowCol) {
for (int i= row1 - 1; i>= 0; i--) {
if (rowCol[i] == col1)
return false;
if (abs(row1 - i) == abs(col1 - rowCol[i]))
return false;
}
return true;
}
int GRID_SIZE;
void placeQ(int row, int rowCol[], vector<vector<int>>& res) {
if (row == GRID_SIZE) {
vector<int> p;
for (int i = 0; i < GRID_SIZE; i++)
p.push_back(rowCol[i]);
res.push_back(p);
}
for (int col = 0; col < GRID_SIZE; col++) {
if (checkValid(row, col, rowCol)) {
rowCol[row] = col;
placeQ(row + 1, rowCol, res);
rowCol[row] = -1000;
}
}
}
void Q(int n) {
GRID_SIZE = n;
const int p = n;
//rowcol[i]=j;第i行的第j个位置放皇后
int rowcol[1000];
memset(rowcol, 0, sizeof(rowcol));
vector<vector<int>> res;
placeQ(0,rowcol,res);
for (auto it1 = res.begin(); it1 != res.end(); ++it1) {
for (auto it2 = (*it1).begin(); it2 != (*it1).end(); ++it2) {
if (it2 != (*it1).end() - 1)
cout << (*it2) + 1 << " ";
else
cout << (*it2) + 1;
}
cout << endl;
}
cout << res.size() << endl;
}
全排列模板
从第一个位置(index)开始选,第一个数可以选从第一个开始的任何一个,第二个数可以选从第二个开始的任何一个,第n个数可以选从第n个开始的任何一个(因为选定的都swap到index-1里了)
void Permutation(string str, int index, int m, int size) {
if (index == m) {//m为size就是全排列方式
for (int i = 0; i < m; i++)
cout << str[i];
cout << endl;
}
else {
//从第⼀个数字起每个数分别与它后⾯出现的数字交换
for (int i = index; i < size; i++) {
swap(str[index], str[i]);
Permutation(str, index + 1, m, size);
swap(str[index], str[i]);
}
}
}
部分排列
问题:从0-9这⼗个数字中选取3个数字进⾏排列,打印所有的组合结果。
全排列是求出了所有字⺟的排列⽅式,该题是求出了部分字⺟排列的⽅法。只需要将结束的条件由if (index == size )改为 if (index == m)即可,其中m是指要进⾏全排列的字符个数
有重复的全排列
a, b, b
b, a, b
b, b, a
1.全排列就是从第⼀个数字起每个数分别与它后⾯的数字交换。
2.去重的全排列就是从第⼀个数字起每个数分别与它后⾯⾮重复出现的数字交换。
3.全排列的⾮递归就是由后向前找替换数和替换点,然后由后向前找第⼀个⽐替换数⼤的数与替换数交换,最后颠倒替换点后的所有数据。
有第一个数起,每个数和后面的非重复数进行交换,i与j进行交换,要求i到j中没有与j相等的数
void swap(vector<int>& num, int start, int end) {
int temp = num[start];
num[start] = num[end];
num[end]=temp;
}
void permuteHelper(int index, vector<int>& num, vector<vector<int>>& paths) {
if (index > num.size())
return;
if (index == num.size())
paths.push_back(num);
unordered_set<int> used;
for (int i = index; i < num.size(); i++) {
//从第1个数字起每个数分别与它后⾯⾮重复出现的数字交换
if (used.count(num[i]))
continue;
swap(num, index, i);
permuteHelper(index + 1, num, paths);
swap(num, index, i);
used.insert(num[i]);
}
}
返回所有的子集 Subsets
Given a set of distinct integers, return all possible subsets.
Note
Elements in a subset must be in non-descending order.
The solution set must not contain duplicate subsets.
Example If S = [1,2,3], a solution is: [ [3], [1], [2],[1,2,3], [1,3],[2,3], [1,2], []
解法一:回溯
void dfs(vector<int>& num, int pos, vector<int>& list,vector<vector<int>>& ret) {
ret.push_back(list);//处理了结尾为空的
//确定第pos位数取什么(一个排列,肯定是从第一个数起)
for (int i = pos; i < num.size(); i++) {
list.push_back(num[i]);
dfs(num, i + 1, list, ret);
list.pop_back();
}
}
解法二:位运算
使⽤位运算求组合问题
根据集合论知识,我们知道⼀个 包含N个元素的集合对应的所有⼦集个数为 2^N。举个例⼦,S=1,2,那么它的全⼦ 集=∅,{1},{2},{1,2} ⼦集的构造过程实际上是每个元素选与不选所构成的情况的汇总,因此 可以对 每个元素赋予⼀个0-1数值,将所有元素的选与不选与0-1对应起来,从⽽就可以 得到⼆进制数据, [0, 2^N−1]的⼆进制表⽰。
00 -> ∅ 01 -> {1} 10 -> {2} 11 -> {1,2}
void Combination(vector<int>& num) {
int len = num.size();
//列举出每位选与不选的所有情况
for (int cur = 0; cur < (1 << len); cur++) {
//依次确定每位的输出情况
for (int i = 0; i < len; i++) {
if (cur & (1 << i))
cout << num[i];
}
cout << endl;
}
}
没有重复数的子集合 Unique Subsets
- 与Subsets有关,先背下Subsets的模板 , 既然要求Unique的,就想办法排除掉重复的。
- 思考哪些情况会重复?如{1, 2(1), 2(2), 2(3)},规定{1, 2(1)}和{1, 2(2)}重复,{1, 2(1), 2(2)}和{1, 2(2), 2(3)}重复。观察规律。
- 得出规律:我们只关⼼取多少个2,不关⼼取哪⼏个。 规定必须从第⼀个2开始连续取(作为重复集合中的代表),如必 须是{1, 2(1)}不能是{1, 2{2})
- ⼀定要先排序
- 调⽤下⼀层递归时(ln 17),起始index ⼀定是i+1⽽不能错写成 start+1
void backtrack(vector<vector<int>>& ret, vector<int>& list, vector<int>& num,int pos) {
ret.push_back(list);
//确定第pos位数取什么
for (int i = pos; i < num.size(); i++) {
if (i>pos && num[i] == num[i - 1])//i>pos:第pos个数,相同的取值只娶一个。条件的提取妙!!!,
continue;
list.push_back(num[i]);
backtrack(ret, list, num, i + 1);
list.pop_back();
}
}
一共有多少个二叉搜索树 Unique Binary Search Trees
Given n, generate all structurally unique BST’s (binary search trees) that store values 1…n.
Example Given n = 3, your program should return all 5 unique BST’s shown below.
构造二叉搜索树,之前求可能的个数用dp;
想想dp怎么写?
https://blog.csdn.net/IS_MOKE/article/details/126256816
https://leetcode.cn/problems/unique-binary-search-trees/?favorite=2cktkvj
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
vector<TreeNode*> helper(int start, int end) {
vector<TreeNode*>result;
if (start > end) {
result.push_back(NULL);
return result;
}
for (int i = start; i <= end; i++) {
vector<TreeNode*> leftTree = helper(start, i - 1);
vector<TreeNode*> rightTree = helper(i + 1, end);
//link left and right sub tree to root(i)
for(int lsize=0;lsize<leftTree.size();lsize++)
for (int rsize = 0; rsize < rightTree.size(); rsize++) {
TreeNode* root = new TreeNode(i);
root->left = leftTree[lsize];
root->right = rightTree[rsize];
result.push_back(root);
}
}
return result;
}
返回数字和为k的子序列 Combination Sum
Given two integers n and k, return all possible combinations of k numbers out of 1,2,…,n.
void DFS(vector<vector<int>>& ret, vector<int> curr, int n, int k, int level) {
if (curr.size() == k) {
ret.push_back(curr);
return;
}
for (int i = level; i <= n; ++i) {
curr.push_back(i);
DFS(ret, curr, n, k, i + 1);
curr.pop_back();
}
}
找出 candidates 中所有可以使数字和为 target 的组合(每个数字能无限次)Combination Sum I
Given a set of candidate numbers © and a target number (T), find all unique combinations in C where the candidate numbers sums to T.
The same repeated number may be chosen from C unlimited number of times.
Note: All numbers (including target) will be positive integers. Elements in a combination (a1, a2, … , ak) must be in non-descending order. (ie, a1 ≤ a2 ≤ … ≤ ak). The solution set must not contain duplicate combinations.
void backtracking(vector<vector<int>>& ret, vector<int> curr, vector<int>candidates, int target, int level) {
if (target == 0) {
ret.push_back(curr);
return;
}else if (target<0){
return;
}
for (int i = level; i < candidates.size(); ++i) {
target -= candidates[i];
curr.push_back(candidates[i]);
backtracking(ret, curr, candidates, target, i);
curr.pop_back();
target += candidates[i];
}
}
找出 candidates 中所有可以使数字和为 target 的组合(每个数字只能使用 一次)Combination Sum II
Given a collection of candidate numbers © and a target number (T), find all unique combinations in C where the candidate numbers sums to T.
Each number in C may only be used once in the combination.
Note: All numbers (including target) will be positive integers. Elements in a combination (a1, a2, … , ak) must be in non-descending order. (ie, a1 ≤ a2 ≤ … ≤ ak). The solution set must not contain duplicate combinations.
void backtracking1(vector<vector<int>>& ret, vector<int> curr, vector<int> candidates, int target, int level) {
if (target == 0) {
ret.push_back(curr);
return;
}
else if (target < 0) {
return;
}
for (int i = level; i < candidates.size(); ++i) {
target -= candidates[i];
curr.push_back(candidates[i]);
backtracking1(ret, curr, candidates, target, i+1);
curr.pop_back();
target += candidates[i];
//add this while loop is to skip the duplication result
while ((i < candidates.size() - 1) && candidates[i] == candidates[i + 1])
++i;
}
}
电话按键的组合可能性 Phone Number
Given a digit string, return all possible letter combinations that the number could represent. A mapping of digit to letters (just like on the telephone buttons) is given as below:
void combination(vector<string>& ret, string curr, string digits, vector<string> dict, int level) {
if (level == digits.size()) {
ret.push_back(curr);
return;
}
if (level > digits.size())return;
int index = digits[level] - '0';
for (int i = 0; i < dict[index].size(); ++i) {
curr.push_back(dict[index][i]);
combination(ret, curr, digits, dict, level + 1);
curr.pop_back();
}
}
vector<string> letterCombination(string digits) {
vector<string> ret;
vector<string> dict;
string cur;
dict.push_back(" ");
dict.push_back(" ");
dict.push_back("abc");
dict.push_back("def");
dict.push_back("ghi");
dict.push_back("jkl");
dict.push_back("mno");
dict.push_back("pqrs");
dict.push_back("tuv");
dict.push_back("wxyz");
combination(ret, cur, digits, dict, 0);
for (auto i = ret.begin(); i != ret.end(); i++) {
for (auto j : (*i))
cout << j << " ";
cout << endl;
}
return ret;
}
单词到目标单词最少需要多少步 Word Ladder
Given two words (start and end), and a dictionary of words, find the length of shortest sequences of words from start to end, such that at each step only one letter is changed.
e.g: start word: hat stop word: dog dictionary{cat, dot, cot, fat}
sequence: hat->cat->cot->dot->dog
给某个单词,逐步用26个字母替换该单词,构成一个图(只有节点)
图节点的最短路径:BFS
int ladderLength(string start, string end, unordered_set<string>& dict) {
int cnt = 1;
//contains all of the words that is in the path,no duplicate
unordered_set<string> check;
queue<string> que;
que.push(start);
que.push("");//used to determine which level it is
string word;
string mid;//used to intermediate result
dict.insert(end);//保留答案,不然que会错过答案
while (!que.empty()){
word = que.front();
que.pop();
if (word == end) {
return cnt;
}
//used to determine which level it is
if (word.size() == 0&& !que.empty()) {
++cnt;
que.push("");
}
else if (!que.empty()) {
for (int i = 0; i < word.size(); ++i) {
mid = word;
for (int j = 'a'; j < 'z'; ++j) {
mid[i] = (char)j;
//end要加入dict,不然的话,end加不入que,就无法到达end
if (check.find(mid) == check.end()//不能有环路,一旦加入过check,就不能再入队了
&& dict.find(mid) != dict.end()) {//单词可变换的
que.push(mid);
check.insert(mid);
}
}
}
}
}
}
找到所有回文数的可能性 Palindrome Partitioning
Given a string s, partition s such that every substring of the partition is a palindrome. Return all possible palindrome partitioning of s. For example, given s = “aab”, Return [ [“aa”,“b”], [“a”,“a”,“b”] ]
动态规划中是找到回文串最小切分次数、这个是找到所有回文数的可能性
bool isValid(const string& str, int st, int ed) {
while (st < ed) {
if (str[st] != str[ed])
return false;
else {
++st; --ed;
}
}
return true;
}
//st为起点,i为结束点
void find(string s, int st, vector<string>& r, vector<vector<string>>& res) {
if (st >= s.size()) {
res.push_back(r);
return;
}
for (int i = st; i < s.size(); ++i) {
if (isValid(s, st, i)) {
r.push_back(s.substr(st, i - st + 1));
find(s, i + 1, r, res);
r.pop_back();
}
}
}
克隆图 Clone Graph
Clone an undirected graph. Each node in the graph contains a label and a list of its neighbors.
BFS
用queue来保存所有的neighbors,clone一个节点时,要clone他所有的neighbor,这些节点有些已经存在,有些没有存在,如何进行区分?hash_map,key用原来的node,value用新clone出来的node ,当一个node没有在map中,说明这个node还没有被clone。(就是用hash_map来充当visited数组)
struct Node {
int dest;
vector<Node*> neighbors;
};
//garph是起点
Node* clone(Node* graph) {
if (!graph)return NULL;
unordered_map<Node*, Node*>mp;
queue<Node*>q;
q.push(graph);
Node* graphCopy = new Node();
mp[graph] = graphCopy;
while (!q.empty()) {
//遍历当前node的所有的neighbors
Node* node = q.front();
q.pop();
int n = node->neighbors.size();
for (int i = 0; i < n; ++i) {
Node* neighbor = node->neighbors[i];
//no copy,这个邻居还没拷贝
if (mp.find(neighbor) == mp.end()) {
Node* p = new Node();
//把p拷贝到新节点(mp[node])的邻居上
mp[node]->neighbors.push_back(p);
//标记该节点已经复制过了
mp[neighbor] = p;
//接下来遍历这个节点的另据
q.push(neighbor);
}
else//a copy already exists:节点已经存在,就直接复制
mp[node]->neighbors.push_back(mp[neighbor]);
}
}
return graphCopy;
}
拓扑排序
拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列
(1)找到入度为0的点
(2)去除该点和该点的邻接边,继续(1)
拓扑排序的应用
拓扑排序通常用来“排序”具有依赖关系的任务。比如,如果用一个DAG图来表示一个工程,其中每个顶点表示工程中的一个任务,用有向边 表示在做任务 B 之前必须先完成任务 A。
class Graph {
int V;//顶点个数
list<int>* adj;//邻接表
queue<int> q;//维护一个入度为0顶点的集合
int* indegree;//记录每个顶点的入度
public:
Graph(int V);
~Graph();
void addEdge(int v, int w);//添加边
bool topological_sort();//拓扑排序
};
Graph::Graph(int v) {
this->V = v;
adj = new list<int>[v];
indegree = new int[v];
//入度全初始化为0
for (int i = 0; i < V; ++i)
indegree[i] = 0;
}
Graph::~Graph() {
delete[] adj;
delete[]indegree;
}
void Graph::addEdge(int v, int w) {
adj[v].push_back(w);
++indegree[w];
}
bool Graph::topological_sort() {
for (int i = 0; i < V; ++i) {
if (indegree[i] == 0)
q.push(i);
}
int count = 0;
while (!q.empty()) {
int v = q.front();
q.pop();
cout << v << " ";
++count;
//将v指向的所有的顶点的入度-1,并将入度减为0的顶点入栈
for (auto beg = adj[v].begin(); beg != adj[v].end(); ++beg) {
if (!(--indegree[*beg]))
q.push(*beg);
}
}
if (count < V)
return false;//没有输出全部的顶点,有向图中有回路
else
return true;
}
Conclusion
DFS (O(2^n), O(n!))
- Find all possible solutions
- Permutations / Subsets
BFS (O(m), O(n))
- Graph traversal (每个点只遍历⼀次)
- Find shortest path in a simple graph
补充:A*算法
f(n)=g(n)+h(n)
g(n)表示节点n到源节点的距离,h(n)表示节点n到目的节点之间的距离。
1)当h(n)=0时,A*算法就转换成了Dijkstra算法,即:每次只考虑到源节点最近的节点。
2)当g(n)=0时,A*算法就转化为了BFS算法,即:每次只考虑到目的节点最近的节点
A*算法模板