【0基礎チュートリアル】小学校算数レベルでわかる星Aの経路探索アルゴリズムを詳しく解説(Goコード付き)

I. 概要

A-Star (A-Star) 経路探索アルゴリズムは、キャラクターに移動コマンドを与えた後に開始点から終了点まで移動する方法や、NPC が 1 つの場所から移動するように制御する方法など、ゲーム プログラミングでよく使用されます。別のものなどに。

この記事は、Myopic Rhino の記事を参照しています:
https://www.gamedev.net/reference/articles/article2003.asp
中国語翻訳:
https://blog.csdn.net/weixin_44489823/article/details/89382502

原文では、アルゴリズムのアイデアのみが言及されていますが、具体的なコード実装は記載されていないため、具体的な実装にはまだいくつかの穴が埋められています。この記事では、最も単純な言語を使用して表現しようとしています。少し冗長ではありますが、誰でも理解できます。実際、A スター アルゴリズム自体にはあまり多くの数学的理論が含まれていないため、ほとんどの人にとってはまだ簡単だと思います理解する人々。

まず、A スター アルゴリズムは、グリッド システムに基づいた経路探索メカニズムです。地図を小さなグリッドに分割し、各グリッドの重みを計算することで経路の方向を決定します。よく知られたデジタルの 9 マスのグリッドを想像してください。中央の「5」キーの隣に 8 つの数字が囲まれています。 8 つの数字は 8 つの方向を表します。では、どの方向に移動するかを選択する必要がありますか? これが A-star アルゴリズムの機能です。

ここに画像の説明を挿入


2. 基本的な考え方

  • G、H、F

    A スター アルゴリズム全体は、G、H、F の 3 つのパラメーターを中心に動作します。それらの定義は非常に単純です。たとえば、以下の図には、開始グリッド、終了グリッド、およびグリッド P があります。

    G はP から開始点までの距離を表します。

    H はP から終点までの距離を表します。

    F は2 つの和、つまり G+H を表します。

ここに画像の説明を挿入


3. 詳細なアルゴリズム

ここに画像の説明を挿入

8*8の地図を例にします。Fは始点(From)、Tは終点(To)を表し、中央の赤い四角は「壁」、つまり場所です。次に、A スター アルゴリズムを段階的に使用して、F から T にどのように移行するかを見てみましょう。


最初のラウンド:

ここに画像の説明を挿入開始点 F (1, 3) から開始し、それに隣接する「隣接する正方形」が黄色で示されます。前述の 9 正方形のグリッドを覚えていますか? はい、A-star アルゴリズム全体が Jiugongge メソッドに従って動作します。

隣り合う黄色の四角には数字が3つあり、この3つの数字が上記のG、H、Fの値で、左下がG値(始点からの距離)、下がG値(始点からの距離)を表します。右隅はH値(終点からの距離)を表し、Fは非常に簡単でG+Hとなります。値の具体的な操作については、次のように規定します。

  • 1 グリッドの移動は 10 として記録されます。

  • 1マス斜めに移動する(つまり斜めに移動する)と14として記録されます。

数値 14 を選択する理由は、整数計算を行うために根数 2 の近似値を直接取得し、アルゴリズム内のアルゴリズムの平方根の実際の演算を回避し、効率が大幅に向上するためです。

たとえば、グリッド (2, 4) を例に挙げます。その G は 14 (開始点に到達するために 1 グリッド左に傾斜します)、その H は 50 です。これは、(2, 4) から終点 T (6, 3) 5 シフトが必要です。

注: H 値を計算するときは、いわゆる「マンハッタン距離」(マンハッタン距離) を使用します。これは、平行移動のみが考慮され、スキュー操作は実行されないことを意味します。(ニューヨークのマンハッタンでは、あるブロックから別のブロックへ歩くとき、その上をパンすることしかできず、建物の中を通過することはできないため、この名前が付けられました)

さらに、H の計算では障害物 (「壁」) の存在が無視されます。H は実際の値ではなく、残りの距離の推定値です。A スター アルゴリズムは本質的に終点への継続的なアプローチであり、最終的にはパス。

これら 3 つの数字に加えて、各黄色のグリッドには黒い矢印もあり、その方向はその「親正方形」が誰であるかを示します。明らかに、今は最初のラウンドなので、これら 8 つの黄色の正方形が親正方形 (または親ノード) になります。全部Fです。


第二ラウンド:

ここに画像の説明を挿入
現在 2 ラウンド目ですが、開始点の左側のマス (2、3) が青に変わり、前のラウンドの開始マスが黒に変わりました。この変更の内部ロジックを説明します。前のラウンドでは、最小の F 値 (F=50) を持つ黄色のセルは (2, 3) であったため、このラウンドの処理セルとして選択され、以降のすべてのラウンドで同様に処理されます。まず近傍 (例では黄色の四角) を決定し、次にそれらの GHF 値を計算します。最後に、すべての黄色のグリッドの中で F 値が最も小さいものを確認します。これはこのラウンドで生成された隣接グリッドではないことに注意してください。ただし、すべての黄色のグリッドの F 値を確認し、最も小さいものを処理グリッドとして見つけます。次の審査。

処理されたすべてのグリッドは後続の各ラウンドで使用されなくなるため、開始グリッド (1、3) は黒になります。したがって、 closeListという配列を維持し、処理されたすべてのグリッドを使用しないようにその配列に保存する必要があります。

同様に、すべての黄色のグリッドも維持するための配列が必要です。これをopenListと呼びます。各ラウンドで 1 つのグリッドだけが次のラウンドの処理グリッドとして選択され、選択されていないグリッドは一時ストレージとしてopenListに入れられます。

現在のグリッド (2、3) に注目してください。その中心にある 8 つのグリッドのうち左 3 つは赤い壁であり、無視されます。左側のグリッドは黒いグリッド (1、3) であり、closeList に追加されています。 、前述したように、 closeListに配置されたすべてのグリッドは処理されなくなるため、これらも無視されます。処理する必要があるのは、残りの 4 つの黄色のグリッドです: (1, 2)、(2, 2)、(1, 4) (2, 4)。実際、これら 4 つのグリッドは前のラウンドの openList に配置されていますここで行う必要があるのは、F 値が最も小さいものを次のラウンドの処理グリッドとして選択することです。このとき、F=64のグリッドが(2, 2)と(2, 4)の2つあるので違いが生じますが、どのように選べばよいのでしょうか?答えは、それを無視し、最後に追加されたものを標準として取り、画像を観察すると、F 値が等しいときにパスの分岐が発生することがわかります。A スターのアルゴリズムは、上に行くのが良いのか、下に行くのが良いのかを判断するほど高度な知能を持っておらず、どちらかを選択し、あとは近づき続けることしかできません。

もう一つ、特に議論しなければならない点があります。それは、 openListに含まれるグリッドを扱う場合、開始点との距離 (G 値) が 0.5 であるかどうかにも注意する必要があります。現在のグリッドによって生成された G 値よりも大きいこの文は非常にぎこちなく聞こえますが、それでも次のような状況を説明するために画像を使用します。

ここに画像の説明を挿入
上の図を見ると、右上隅の正方形の G 値は 14 です。つまり、1 つの正方形を移動するだけで開始点 (図の赤い矢印で示されている) に到達する必要があることを意味します。現在の四角形 (紫色の四角形) を経由する場合、開始点 (図の緑色の矢印で示されている) に到達するまでに 2 回移動する必要があるため、G 値は 20 になります。明らかに 14 < 20 であり、このグリッドが元のパスに従います。素晴らしいです。この場合は何もする必要はなく、無視してください。反例を見てみましょう:

ここに画像の説明を挿入

右上のマス目は変わりませんが、スタート地点が異なり、旧ルートをたどるとスタート地点に到達するまでに斜めに2回移動する必要があり、G値は28です。しかし、代わりに現在のグリッド (紫色のグリッド) を通過する場合、変換する必要があるのは 2 回だけであり、G 値は 20 です。明らかに 28 > 20 なので、図に示すように、親グリッド ポインターを変更して現在のグリッドを指すようにする必要があります。

ここに画像の説明を挿入
多くのコメントを見ましたが、この知識点について疑問を抱いている人も多いですが、これは理解の問題ではなく、この概念を言葉で表現するのが難しいということです。この 3 つの図を組み合わせると、まだ簡単だと思います。理解。早速、今回のケースに戻りましょう。


第 3 ラウンド:

ここに画像の説明を挿入

3 ラウンド目では、現在の処理グリッドは九公グリッドの中心である (2, 4) ですが、壁は処理する必要がなく、 closeListに配置されている 2 つの黒いグリッドも処理する必要はありません。その左 (1, 4) はすでにopenListに含まれるグリッドのG 値は 10 です。また、現在のグリッドを通るパスに変更すると、G 値は 10+14 = 24 になりますが、これも明らかに不要なので無視されます。その下では、openListに含まれていない 3 つの新しいグリッドが開かれており (色を意図的に少し異なるように変更していることに注意してください)、これらの新しく開かれたグリッドを扱うのは非常に簡単です。必要なのは、それらの G を計算することだけです。 、H、および F 値を個別に指定し、(図に示すように) 親グリッド ポインターを現在のグリッドにポイントします。


第 4 ラウンド:

ここに画像の説明を挿入

今ラウンドで新たに2つのグリッドがオープンすることを除けば、大きな変化はない。

第5ラウンド:

ここに画像の説明を挿入

このラウンドは G=70 のグリッド (0, 3) に到達しました。その左側が境界線であるため、新たに開くグリッドはなく、開いている周囲のグリッドの G 値を変更する必要はありません。 . 穏やかなラウンドです。


第6ラウンド:

ここに画像の説明を挿入

注: このラウンドでは、上記の openList に既に存在するグリッドの G 値が現在のグリッドの G 値よりも大きいため、このラウンドを注意深く分析してください。

現在のラウンドと最後のラウンドの間のグリッド (1, 1) の変化を観察すると、そのポインターが変化していることがわかります。なぜなら、(1, 1) から開始する場合は、元のパスに従って (2, 2) を通過して開始点 F に移動する必要があり、これには 2 つの斜めのシフトが必要です (G 値は 28)。ただし、(1, 2) 経由の場合、必要な変換は 2 つだけであるため (G 値は 20)、ポインタは現在のグリッドを指します。ここまでは、A-star アルゴリズムの経路探索プロセス中に起こり得る状況についての基本的な説明です。次に、全体像だけを載せます。

ここに画像の説明を挿入白文字はAスターアルゴリズムが開かれる順番を示しており、 openListに終点も配置されている場合は、始点から終点までのパスがあることを意味します。しかし、どうやってその道を決めるのでしょうか?それは非常に簡単で、各ノードの親ノードをエンドポイントから逆にできます (図の赤い矢印は逆プッシュプロセスを示します)。各ノードには明確な親ノードが 1 つだけあるため、パス探索プロセスで使用されたもののパスになれなかったノードはスキップされます。

すべてのパスをブロックするために壁を使用すると仮定すると、アルゴリズムは openList 内の次の可能なパスを探し続けます。openList 内のすべての要素が使い果たされると、それは進むべき道がないことを意味します。したがって、パス探索アルゴリズムには 2 つの終了条件があります。

1. openList にはエンドポイントが含まれますか? (パスが見つかりました)

2. openList は空ですか? (パスがありません)


4. コードの実装

これを辛抱強く見ていただければ、ほとんど手書きでコードを書くことができると思いますが、まだいくつかの小さな穴を埋める必要があります。1 つ目は G 値の計算です。この問題を説明する前に、距離の公式について見てみましょう。一般的に使用される距離の公式は 3 つあります。

  • マンハッタンの距離

    先ほど「マンハッタン距離」について触れましたが、これは上下左右にのみ移動でき、斜めには移動できない方法を指し、1手は1として記録されます。

  • チェビシェフ距離

    チェビシェフはマンハッタン距離に非常に似ており、概念的にはスラッシュの存在を許可しますが、1 平方メートルの直線を移動するか、スラッシュは 1 の距離に相当します。

  • ユークリッド距離:

    これは座標系内の 2 点間の実際の距離ですが、欠点は算術平方根演算を行う必要があることです。

    3 つの距離公式の違いは次のとおりです。

ここに画像の説明を挿入


次に問題が発生します。G を計算するときは、スラッシュを 14 として記録し、直線を 10 として記録する必要があります。これは、システムが平方根算術を実行することを回避する擬似的な「ユークリッド距離」です。2 点間の平行移動距離が 1 であるため、その対角距離は 2 の平方根 (値は 1.414) となり、浮動小数点数を避けるために、その近似値に基づいて 10 を乗算します。 1.4 の場合、それに対応して、平行移動もスケールアップする必要があります (1x10=10)。これが、距離定数として 10 と 14 を選択した理由です。さらに、疑似「ユークリッド距離」アルゴリズムを設計する必要もあります。コードは次のとおりです。

func calculate_G(p1 Point, p2 Point) int {
    
    
    dx := math.Abs(float64(p1.x - p2.x))
    dy := math.Abs(float64(p1.y - p2.y))
    straight := int(math.Abs(dx - dy))
    bias := int(math.Min(float64(dx), float64(dy)))
    distance := straight*10 + bias*14
    return distance
}

方法は、まず 2 点間のマンハッタン距離を取得し、小さい方の値を 14 倍し、dx と dy の差に 10 を乗算します。コードは Golang で書かれていますが、ロジックが非常に単純であるため、基本的に言語に依存しません。

2 番目は並べ替えの問題で、各ラウンドの終了時に、次のラウンドの処理マスとして openList から最小の F 値を持つマスを見つける必要があることをまだ覚えていますか? 並べ替えアルゴリズムの選択は最終結果には影響しませんが、効率は大きく異なる可能性があります。最初は手間を省くためにバブリング方式を使用していましたが、その後バイナリ ヒープに切り替えたところ、効率は少なくとも 5 倍高くなりました。バイナリ ヒープの並べ替えアルゴリズムは次のとおりです。

// 寻找list中的最小结点(二叉堆方法)
func findMin_Heap(tree []Node) Node {
    
    
    var n = len(tree) - 1
    // 建小根堆 (percolate_down)
    for i := (n - 1) / 2; i >= 0; i-- {
    
    
        percolate_down(tree, n, i)
    }
    return tree[0]
}
// 建堆(下滤)
func percolate_down(tree []Node, size int, parent_node int) {
    
    
    left_node := 2*parent_node + 1
    right_node := 2*parent_node + 2

    var max_node = parent_node
    if left_node <= size && tree[left_node].f < tree[parent_node].f {
    
    
        max_node = left_node
    }
    if right_node <= size && tree[right_node].f < tree[max_node].f {
    
    
        max_node = right_node
    }

    if max_node != parent_node {
    
    
        tree[parent_node], tree[max_node] = tree[max_node], tree[parent_node]
        percolate_down(tree, size, max_node)
    }
}

実際、バイナリ ヒープのソートには 2 つのステップが必要です。最初にヒープを構築し、次にソートします。しかし、このシナリオでは、実際にソートする必要はなく、最小値を取り出すことができれば十分です。バイナリ ヒープが構築されるときに、最小値がヒープの先頭にあることがすでに保証されています。ヒープ全体がまだ順序が狂っていても、はい。ただし、リンクされたリストの最初の値が最小値であることを保証でき、非常に効率的であるため、バイナリ ヒープと組み合わせた A スター アルゴリズムが最初の選択肢になります。

ただし、バイナリ ヒープの原理をよく理解していないと、数語で明確に説明することはできません。参考までに、私のブログ投稿を以下に示します。

バイナリ ヒープとヒープ ソートの詳細なナニー レベルのチュートリアルは少し冗長ですが、理解できることは保証されています_バイナリ ヒープ ソート_rockage のブログ - CSDN ブログ

コードについては他に言うことはありません。重要なことは、openListcloseListの 2 つの配列を維持することです。パスファインディングの各ラウンドでは、これら 2 つの配列が追加、削除、または変更される可能性があり、これがアルゴリズム全体の核心です。全体的な操作プロセスは次のとおりです。

  1. 開始点をopenListに置き、それを最初の「現在のグリッド」にし、経路探索を開始します

  2. 現在のグリッドをcloseListに入れて、次のラウンドが処理されないようにします。

  3. 次の場合、現在のグリッドを中心として、それを囲む 8 つの隣接するグリッドの G、H、F を計算します。

    • マップ境界を越えた近隣 -> 無視

    • 隣人は障害物 (壁) の上にいます -> 無視します

    • 隣接セルがすでにcloseListに存在します-> 無視します

    • 隣接セルは新しいセルであり、openListにありません。

      • G、H、F 値を計算し、その親グリッドを現在のグリッドとして設定します。
    • 近傍は新しい正方形ではなく、既にopenListに存在します。

      • 開始点までの G 値を計算します (現在のセルによって生成された G 値より小さい場合 -> 無視します)

      • 現在のセルによって生成された G 値より大きい場合:

        • この隣接グリッドの親グリッドを現在のグリッドに変更し、G 値と F 値を更新します。
  4. すべての適格な隣接セルを処理し、openListに保存します。

  5. 8 方向すべてが検出および計算されたら、openListから最小の F 値を持つグリッドを選択し、現在のラウンドが終了します

  6. 前のラウンドで選択した最小の F 値を持つグリッドを新しいラウンドの「現在のグリッド」として設定します

  7. 手順 2 ~ 6 を繰り返します

  8. ループエンド判定

    パスファインディングの各ラウンドの後、openList を判断する必要があります。

    1. 終点がopenListに含まれており、パスが見つかったことを示します。終点から開始して、親ノードに従ってパスが反転され、プログラムが終了します。

    2. openListの要素は0 であり、パスが見つからないことを示し、プログラムは終了します。


package main

import (
	"fmt"
	"math"
	"time"
)

type Point struct {
    
    
	x int
	y int
}

type Node struct {
    
    
	coordinate Point
	parent     *Node
	f, g, h    int
}

// 地图大小
const cols = 8
const rows = 8

func main() {
    
    

	// 创建起点和终点 (F=From T=To)
	F := Point{
    
    1, 3}
	T := Point{
    
    6, 3}

	var obstacle []Point
	// 创建障碍
	obstacle = append(obstacle, Point{
    
    3, 1})
	obstacle = append(obstacle, Point{
    
    3, 2})
	obstacle = append(obstacle, Point{
    
    3, 3})
	obstacle = append(obstacle, Point{
    
    3, 4})

	// 创建地图
	var preMap [cols][rows]byte
	for y := 0; y <= rows-1; y++ {
    
    
		for x := 0; x <= cols-1; x++ {
    
    
			preMap[x][y] = 46 // 用字符 . 表示
		}
	}

	// 在地图上标记起点与终点
	preMap[F.x][F.y] = 70 // 字符 F = From
	preMap[T.x][T.y] = 84 // 字符 T = To

	// 在地图上标记障碍
	for _, v := range obstacle {
    
    
		preMap[v.x][v.y] = 88 // 用字符 X 表示
	}

	// 打印初始地图
	for y := 0; y <= rows-1; y++ {
    
    
		for x := 0; x <= cols-1; x++ {
    
    
			fmt.Printf("%c", preMap[x][y])
		}
		fmt.Printf("\n")
	}

	path := A_Star(preMap, F, T) // 开始寻径

	if path != nil {
    
     // 如找到路径则再次打印它:
		fmt.Println()
		fmt.Println("The path is as follow: ")
		// 在地图上标记障碍
		for _, v := range path {
    
    
			preMap[v.x][v.y] = 42 // 用字符 * 表示
		}
		for y := 0; y <= rows-1; y++ {
    
    
			for x := 0; x <= cols-1; x++ {
    
    
				fmt.Printf("%c", preMap[x][y])
			}
			fmt.Printf("\n")
		}
	}
}

// A*寻路主函数 preMap=地图 F=起点 T=终点
func A_Star(preMap [cols][rows]byte, F Point, T Point) []Point {
    
    
	var openList []Node
	var closeList []Node

	findPath := func() {
    
    
		//遍历 open list ,查找 F 值最小的节点,把它作为当前要处理的节点。
		curNode := findMin_Heap(openList)

		closeList = append(closeList, curNode)   // 将当前结点放入 closeList 中
		openList = deleteNode(openList, curNode) // 将 当前结点 从 openList 中删除

		// 遍历检测相邻节点的8个方向:NW N NE / W E / SW S SE
		direction := []Point{
    
    {
    
    -1, -1}, {
    
    0, -1}, {
    
    1, -1}, {
    
    -1, 0}, {
    
    1, 0}, {
    
    -1, 1}, {
    
    0, 1}, {
    
    1, 1}}

		for _, v := range direction {
    
     // 遍历基于父节点的8个方向
			var neighbour Node
			neighbour.coordinate.x = curNode.coordinate.x + v.x
			neighbour.coordinate.y = curNode.coordinate.y + v.y

			// 1. 是否超越地图边界?
			if (neighbour.coordinate.x < 0 || neighbour.coordinate.x >= cols) ||
				(neighbour.coordinate.y < 0 || neighbour.coordinate.y >= rows) {
    
    
				continue
			}

			// 2. 是否障碍物?
			if preMap[neighbour.coordinate.x][neighbour.coordinate.y] == 88 {
    
     // 88 = 字符 'X'
				continue
			}

			// 3. 自身是否已经存在于 closeList 中?
			if existNode(neighbour, closeList) != nil {
    
    
				continue
			}

			checkNode := existNode(neighbour, openList)
			// 这个邻居结点不在 openList 中
			if checkNode == nil {
    
    
				neighbour.parent = &curNode                                 // 当前结点设置为它的父结点
				d1 := curNode.g                                             // g = 当前结点到起点的距离
				d2 := calculate_G(neighbour.coordinate, curNode.coordinate) // 该邻居结点与当前结点的距离
				neighbour.g = d1 + d2
				neighbour.h = calculate_H(neighbour.coordinate, T) // h = 该邻居节点到终点的距离
				neighbour.f = neighbour.g + neighbour.h            // f = g + h
				openList = append(openList, neighbour)             // 把它加入 open list

			} else {
    
    
				// 该结点在 openList 中
				d1 := curNode.g + calculate_G(checkNode.coordinate, curNode.coordinate)
				d2 := checkNode.g

				if d1 < d2 {
    
     // 如果经由 curNode的路径更短,则将这个邻居的父节点指向 curNode 并更新 g,f
					// 在 Go 中,不允许使用指针直接修改切片元素, 需要遍历元素的下标
					index := 0
					for i, v := range openList {
    
    
						if neighbour.coordinate == v.coordinate {
    
    
							index = i
						}
					}
					openList[index].parent = &curNode
					openList[index].g = d1
					openList[index].f = neighbour.g + neighbour.h
				}
			}
		}
		// 观察每一轮结束后 openList 和 closeList 的变化:

	}

	start := time.Now() // 计时开始

	var fNode Node
	fNode.f = 0                        // 起点的优先级为0(最高)
	fNode.coordinate = F               // 起点坐标
	openList = append(openList, fNode) // 将起点装入 openList 中
	var tNode *Node
	var path []Point
	found := false

	for {
    
    
		if found {
    
     // 找到路径
			// 从终点指针开始反推路径:

			for {
    
    
				path = append(path, tNode.coordinate)
				if tNode.parent != nil {
    
    
					tNode = tNode.parent
				} else {
    
    
					break
				}
			}
			// 反转:从终点到起点,改为从起点到终点
			for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
    
    
				path[i], path[j] = path[j], path[i]
			}
			break
		}

		findPath() // 开始寻路

		for _, v := range openList {
    
     // 如果终点被包含在openList中,说明找到路径了
			if v.coordinate == T {
    
    
				tNode = &v
				found = true
				break
			}
		}

		if len(openList) == 0 {
    
     // openList耗尽,表示找不到路径
			found = false
			break
		}

	}
	duration := time.Since(start) // 计时结束
	fmt.Println("Running time:", duration)

	if found {
    
    
		fmt.Println("Path finding success!")
		return path
	} else {
    
    
		fmt.Println("No path was found!")
		return nil
	}

}

// 从 list 中删除一个结点
func deleteNode(list []Node, target Node) []Node {
    
    
	var newChain []Node
	for indexToRemove, v := range list {
    
    
		if v == target {
    
    
			newChain = append(list[:indexToRemove], list[indexToRemove+1:]...) // 从 openList中 移除 target
			break
		}
	}
	return newChain
}

// 判断节点是否存在于list中
func existNode(target Node, list []Node) *Node {
    
    
	for _, element := range list {
    
    
		if element.coordinate == target.coordinate {
    
     // 用XY值来判定唯一性
			return &element
		}
	}
	return nil
}

// 计算到终点的距离(H值)
func calculate_H(p1 Point, p2 Point) int {
    
    
	const D = 10 // 距离系数
	// 采用"曼哈顿距离(Manhattan distance)" :
	dx := int(math.Abs(float64(p1.x - p2.x)))
	dy := int(math.Abs(float64(p1.y - p2.y)))
	distance := (dx + dy) * D
	/*
		// 采用"切比雪夫距离(Chebyshev distance)":
		dx := math.Abs(float64(p1.x - p2.x))
		dy := math.Abs(float64(p1.y - p2.y))
		distance := int(math.Max(float64(dx), float64(dy)) * D)
	*/
	return distance
}

// 计算到起点的距离(G值)
func calculate_G(p1 Point, p2 Point) int {
    
    
	dx := math.Abs(float64(p1.x - p2.x))
	dy := math.Abs(float64(p1.y - p2.y))
	straight := int(math.Abs(dx - dy))
	bias := int(math.Min(float64(dx), float64(dy)))
	distance := straight*10 + bias*14
	return distance
}

// 寻找list中的最小结点(使用二叉堆方法)
func findMin_Heap(tree []Node) Node {
    
    
	var n = len(tree) - 1
	// 建小根堆 (percolate_down)
	for i := (n - 1) / 2; i >= 0; i-- {
    
    
		percolate_down(tree, n, i)
	}
	return tree[0]
}

// 建堆(下滤)
func percolate_down(tree []Node, size int, parent_node int) {
    
    
	left_node := 2*parent_node + 1
	right_node := 2*parent_node + 2

	var max_node = parent_node
	if left_node <= size && tree[left_node].f < tree[parent_node].f {
    
    
		max_node = left_node
	}
	if right_node <= size && tree[right_node].f < tree[max_node].f {
    
    
		max_node = right_node
	}

	if max_node != parent_node {
    
    
		tree[parent_node], tree[max_node] = tree[max_node], tree[parent_node]
		percolate_down(tree, size, max_node)
	}
}


5. 追記

同じ文章ですが、この記事は入門的なアイデアのみを目的としており、導入としてのみ機能します。実際に A スター アルゴリズムをビジネス プログラムに適用するには、さらなる学習が必要です。たとえば、1280*720 のゲーム マップを最初に 128*72 に縮小し、次に A スター パスファインディングの効率がはるかに高くなる前に 2 値化することができます。

おすすめ

転載: blog.csdn.net/rockage/article/details/131773004