タイルセットの境界の効率的なアルゴリズム


12

地図を形成する既知の有限サイズのタイルのグリッドがあります。マップ内の一部のタイルは、テリトリーと呼ばれるセットに配置されます。この領域はつながっていますが、その形状については何もわかっていません。ほとんどの場合、それはかなり規則的なブロブになりますが、一方向に非常に長くなる可能性があり、潜在的に穴がある可能性もあります。領土の(外側の)境界を見つけることに興味があります。

つまり、テリトリー内に存在することなく、テリトリー内のタイルのいずれかに触れるすべてのタイルのリストが必要です。これを見つける効率的な方法は何ですか?

難易度を上げるために、タイルがヘックスであることが起こりますが、これはそれほど大きな違いをもたらさないと思われます。各タイルにはまだ整数のxおよびy座標がラベル付けされており、タイルがあれば、隣接するタイルを簡単に見つけることができます。以下にいくつかの例を示します。黒は領土、青は探したい境界線です。 領土と国境の例 これ自体は難しい問題ではありません。擬似Pythonのこのための簡単なアルゴリズムの1つは次のとおりです。

def find_border_of_territory(territory):
    border = []
    for tile in territory:
        for neighbor in tile.neighbors():
            if neighbor not in territory and neighbor not in border:
                border.add(neighbor)

しかし、これは遅いので、もっと良いものが欲しいです。テリトリーでO(n)ループがあり、すべてのネイバーで別のループ(短いループですが、それでも)があります。次に、サイズがnである2つのリストのメンバーシップをチェックする必要があります。それはO(n ^ 2)のひどいスケーリングを与えます。ボーダーとテリトリーのリストの代わりにセットを使用して、メンバーシップをすばやくチェックできるようにすることで、それをO(n)に減らすことができますが、それでも素晴らしいことではありません。テリトリーは大きいが、単純な面積対線のスケーリングのために境界が小さい多くの場合があると予想しています。たとえば、領土が半径5のヘクスの場合、サイズは91ですが、境界線のサイズは36のみです。

誰かがもっと良いものを提案できますか?

編集:

以下の質問のいくつかに答えるため。領域のサイズは、約20〜100程度です。テリトリーを形成するタイルのセットはオブジェクトの属性であり、すべてのボーダータイルのセットを必要とするのはこのオブジェクトです。

最初、テリトリーはブロックとして作成され、その後、ほとんどの場合タイルが1つずつ増えます。この場合、最速の方法は、境界線のセットを保持し、取得したタイル上でのみ更新することです。領土に大きな変更が発生する場合があります-そのため、完全に再計算する必要があります。

私は、単純な境界検出アルゴリズムを実行することが最善の解決策であると考えています。これにより発生する唯一の追加の複雑性は、境界線が必要になるたびに再計算されるようにすることですが、それ以上ではありません。私は現在のフレームワークでこれを確実に行うことができると確信しています。

タイミングに関しては、現在のコードには、領土のすべてのタイルをチェックする必要があるいくつかのルーチンがあります。毎ターンではありませんが、作成時とその後は時々です。完全なプログラムのごく一部であるにもかかわらず、テストコードの私のスーツの実行時間の50%以上がかかります。したがって、繰り返しを最小限に抑えることに熱心でした。ただし、テストコードには、プログラムの通常の実行(当然)よりも多くのオブジェクトの作成が含まれるので、これはあまり重要ではないことがわかります。


10
形状について何も知られていない場合、O(N)アルゴリズムは妥当と思われます。より高速なものは、領域のすべての要素を見る必要はありません。これは、形状について何か知っている場合にのみ機能すると思います。
amitp

3
おそらくそれほど頻繁に行う必要はありません。また、nはあまり大きくなく、タイルの総数よりもはるかに少ないです。
トライラリオン

1
これらの領域はどのように作成/変更されますか?そして、どのくらいの頻度で変化しますか?それらがタイルごとに選択されている場合は、移動しながら隣人のリストを作成できます。頻繁に変更しない限り、テリトリーとその境界の配列を保存し、移動中に追加または削除できます常に再計算する必要があります)。
DaveMongoose

2
重要:これは実際に診断およびプロファイルされたパフォーマンスの問題ですか?問題セットが小さければ(実際にはほんの数百の要素ですか?)、このO(n ^ 2)やO(n)が問題になるとは思いません。すべてのフレームで実行されるわけではないシステムの時期尚早な最適化のように聞こえます。
デリオス

1
最大6つの近傍があるため、単純なアルゴリズムはO(n)です。
エリック

回答:


11

通常、アルゴリズムを見つけるには、アルゴリズムを簡単にするデータ構造を使用するのが最適です。

この場合、あなたの領土。

領域は、順序付けられていない(O(1)ハッシュ)ボーダーと要素のセットである必要があります。

要素をテリトリーに追加するたびに、隣接するタイルを繰り返し処理し、それらが境界タイルであるかどうかを確認します。この場合、要素タイルでない場合、境界タイルです。

テリトリーから要素を減算するたびに、隣接するタイルがテリトリーにあることを確認し、自分がボーダータイルになるかどうかを確認します。これを高速にする必要がある場合は、境界タイルに「隣接カウント」を追跡させます。

これにより、テリトリーにタイルを追加したり、テリトリーからタイルを削除したりするたびに、O(1)が必要になります。国境を訪れるには、O(境界線の長さ)が必要です。テリトリーから要素を追加/削除するよりも頻繁に「境界とは何か」を知りたい限り、これが勝つはずです。


9

領域の中央にある穴のエッジも見つける必要がある場合、領域境界の領域での線形が最善です。内部のタイルは穴である可能性があるため、カウントする必要があるため、領土の輪郭で囲まれた領域内のすべてのタイルを少なくとも1回見て、すべての穴が見つかったことを確認する必要があります。

ただし、外側の境界(内側の穴ではない)を見つけることにのみ関心がある場合は、これをもう少し効率的に行うことができます。

  1. あなたの領土を分離する1つのエッジを見つけます。これを行うには...

    • (少なくとも1つのテリトリータイルを知っていて、マップ上に接続されたテリトリーブロブが1つしかないことを知っている場合)

      ...テリトリー内の任意のタイルから開始し、マップの最も近い端に向かって移動します。その際、テリトリータイルから非テリトリータイルに移行した最後のエッジを覚えておいてください。マップの端に到達すると、この記憶された端が開始端になります。

      このスキャンは、マップの直径で線形です。

    • または(テリトリータイルの場所が事前にわからない場合、またはマップに複数の接続されていないテリトリーが含まれる場合)

      ...マップの端から開始し、地形タイルに到達するまで各行に沿ってスキャンします。非地形から地形に渡った最後のエッジが開始エッジです。

      このスキャンは、マップの領域で最悪の線形(直径が2次)になる可能性がありますが、検索を制限する境界がある場合(たとえば、領域がほぼ常に中央の行を横切ることがわかっている場合)、この最悪の状況を改善できます-ケースの振る舞い。

  2. 手順1で見つかった開始エッジから開始して、地形の境界線の周りをたどり、開始エッジに戻るまで、外側の各非地形タイルを境界線コレクションに追加します。

    このエッジ追従ステップは、その領域ではなく、地形の輪郭の周囲で直線的です。欠点は、エッジが取ることができる各種類のターンを考慮する必要があり、インレットでの境界タイルの二重カウントを避ける必要があるため、コードがより複雑になることです。

あなたの例があなたの実際のデータサイズを数桁以内に代表している場合、私は単純な領域検索に行きます-それはまだこのような少数のタイルで非常に高速で、書くのが実質的に簡単です、理解し、維持します(通常、バグの減少につながります!)


7

注意:タイルが境界上にあるかどうかは、タイルとその近傍のみに依存します。

そのための:

  • このクエリを遅延的に実行するのは簡単です。たとえば、マップ全体で境界を検索する必要はなく、表示されているものだけを検索する必要があります。

  • このクエリを並行して実行するのは簡単です。実際、これを行うシェーダーコードをイメージできました。そして、視覚化以外の何かのためにそれが必要な場合は、テクスチャにレンダリングして使用できます。

  • タイルが変更されると、境界はローカルでのみ変更されるため、すべてを再度計算する必要はありません。

境界を事前に計算することもできます。つまり、ヘクスに居住している場合、その瞬間にタイルが境界であるかどうかを判断できます。つまり:

  • ループを使用してグリッドにデータを入力し、それが境界とは何かを決定するために使用するのと同じ場合。
  • 空のグリッドから始めて、変更するタイルを選択すると、境界をローカルで更新できます。

境界にリストを使用しないでください。本当に必要な場合はセットを使用します(境界を何にしたいのかわかりません)。ただし、タイルが境界であるか、タイルの属性ではないものを作成する場合は、別のデータ構造に移動して確認する必要はありません。


2

エリアを1タイル上に移動し、次に右上、次に右下などに移動します。その後、元のエリアを削除します。

6つのセットすべてをマージするには、O(n)、O(n.log(n))、ソートO(n)をソートする必要があります。元のタイルが何らかのソート形式で保存されている場合、マージされたセットはO(n)でもソートできます。

各タイルに少なくとも1回アクセスする必要があるため、O(n)未満のアルゴリズムはないと思います。


1

これを行う方法についてのブログ記事を書きました。これは、@ DMGregoryがエッジセルから開始して境界の周りを行進するという最初の方法を使用します。PythonではなくC#ですが、簡単に適応できるはずです。

https://dillonshook.com/hex-city-borders/


0

元のポスト:

このサイトにコメントすることはできませんので、擬似コードアルゴリズムで答えてみます。

すべての地域に、境界の一部である最大6つの隣人がいることを知っています。領域内のすべてのタイルについて、6つの隣接するタイルを潜在的な境界リストに追加します。次に、境界線から領土内のすべてのタイルを引くと、境界線タイルだけが残ります。順序付けられていないセットを使用して各リストを保存すると、最適に機能します。お役に立てば幸いです。

編集単純な反復よりもはるかに効果的な方法があります。以下の(現在削除されている)回答で述べようとしたが、最良の場合はO(1)を、最悪の場合はO(n)を達​​成できます。

テリトリーへのタイルの追加O(1)-O(N):

隣人がいない場合は、単に新しい領域を作成します。

隣人が1人の場合、新しいタイルを既存のテリトリーに追加します。

5つまたは6つの隣人の場合、すべてが接続されていることがわかっているため、新しいタイルを既存のテリトリーに追加します。これらはすべてO(1)操作であり、新しい境界領域の更新もO(1)です。これは、あるセットと別のセットの単純なマージであるためです。

2、3、または4つの隣接地域の場合、最大3つの固有の地域を統合する必要があります。これは、結合されたテリトリーサイズのO(N)です。

テリトリーO(1)-O(N)からタイルを削除する:

隣人がゼロの場合、領土は消去されます。O(1)

1人の隣人が領土からタイルを削除します。O(1)

2つ以上の近隣地域では、最大3つの新しい地域を作成できます。これはO(N)です。

ここ数週間、空き時間を使って、単純なヘックスベースのテリトリーゲームであるデモプログラムを開発しました。領土を隣同士に配置して収入を増やしましょう。赤、緑、青の3人のプレイヤーが、限られたゲームフィールドに戦略的にタイルを配置することで、最大の収益を上げるために競います。

ここからゲームをダウンロードできます(.7z形式) hex.7z

シンプルなマウスコントロールLMBはタイルを配置します(ホバーで強調表示された場所にのみ配置できます)。上が得点、下が収入。効果的な戦略を考え出すことができるかどうかを確認してください。

コードはここにあります:

Eagle / EagleTest

ソースコードからビルドするには、EagleとAllegro 5が必要です。どちらもcmakeでビルドします。現在、16進ゲームはCBプロジェクトでビルドされています。

それらの反対票を逆さまにします。:)


これは基本的にOPのアルゴリズムで行われますが、最後にすべてを削除するよりも、含める前に隣接タイルをチェックする方がわずかに高速です。
ScienceSnake

基本的には同じですが、それらを差し引いたほうが効率的です
BugSquasher

回答を完全に更新し、以下の無関係な回答を削除しました。
BugSquasher
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.