前提・実現したいこと
タイトル通り、「複数の円が重ならずに入った正方形の中に、とある円が重ならずに入る場所があるかどうか」を、高速に判定するためのアルゴリズムを考えています。問題を図にすると下のような感じですかね。。(雑ですが)
(各円の中心座標と半径は既知とします。円の個数は任意です。)
もちろん、全ての点を探索すればいいのですが、非常に時間がかかると思います。そこで、比較的早そうな(証明できていなくてもいいので)アルゴリズムを探しています。
願わくばUnity2DでC#を使って実装できるものがいいです。(別にほかの言語でもOKです。なんとかします。)
###求める回答
解決策がなくても、ヒントになりそうな考え方やアルゴリズム、似た問題などがあれば、ぜひ教えていただきたいです。また、早くなりそうな方法であれば、全探索の改良などでも構いません(この問題における探索する順番の優先度など)。どうかよろしくお願いします。
###追記(2020/04/07)
返信遅れていますが、非常に興味深い回答が多数寄せられています。
誠にありがとうございます。
先日、Bongoさんが実験してくださったように、2次元の探索はさほど負荷がかからないことが分かりました。
そのため、悩んでいた事自体は解決したのですが、まだ展開がありそうなのでもう少し回答を募りたいと思います。
回答してくださった方には再度お礼申し上げます。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/04/05 04:43
回答8件
0
ベストアンサー
面白そうな問題に取り組んでいますね。すみませんが私も力技案しか思いつかなかったんですけれども、二次元空間ならば総当たりでも何とかなるんじゃないかと思います(立方体の空間に球体を詰めていくような3D版となると、このやり方ではきつそうですが...)。
話を単純化するために正方形のサイズは1.0×1.0とし、適当な大きさのテクスチャ(さしあたり2048×2048としました)の各ピクセルについてその位置に円を置けるかを判定することにしました。
各地点に円を置けるかどうかの判定は並列に行っても問題ないでしょうから、グラフィックス機能を使えば十分高速に処理できるだろうと見込んでのことです。
ロジックの中核となるスクリプトは下記のようにし...
C#
1using System; 2using System.Collections.Generic; 3using System.Linq; 4using UnityEngine; 5using Random = UnityEngine.Random; 6 7public class BoxAndBalls 8{ 9 private const int Level = 12; 10 private readonly List<Vector3> balls = new List<Vector3>(); 11 private readonly RenderTexture[] availabilityMaps; 12 private readonly Texture2D availabilityMapReader; 13 private readonly Texture2D ballTexture; 14 private Color[] ballTexturePixels; 15 private int ballTextureWidth = 1; 16 private int ballTextureHeight = 1; 17 18 private Material availablePositionFinder; 19 private readonly int RadiusProperty = Shader.PropertyToID("_Radius"); 20 private readonly int BallCountProperty = Shader.PropertyToID("_BallCount"); 21 22 public BoxAndBalls() 23 { 24 this.availabilityMaps = Enumerable.Range(0, Level).Select( 25 l => 26 { 27 var resolution = 1 << l; 28 return new RenderTexture(resolution, resolution, 0, RenderTextureFormat.R8); 29 }).ToArray(); 30 this.availabilityMapReader = new Texture2D(2, 2, TextureFormat.R8, false); 31 this.ballTexture = new Texture2D( 32 this.ballTextureWidth, 33 this.ballTextureHeight, 34 TextureFormat.RGBAFloat, 35 false) {filterMode = FilterMode.Point}; 36 this.ballTexturePixels = new Color[this.ballTextureWidth * this.ballTextureHeight]; 37 } 38 39 public Texture AvailabilityMap => this.availabilityMaps[Level - 1]; 40 public Texture BallTexture => this.ballTexture; 41 public int BallCount => this.balls.Count; 42 43 public bool TryAddBall(float x, float y, float radius) 44 { 45 if (!this.VerifyNextPosition(x, y, radius)) 46 { 47 Debug.Log($"({x}, {y})に半径{radius}のボールを置くことはできませんでした。"); 48 return false; 49 } 50 51 this.balls.Add(new Vector3(x, y, radius)); 52 this.UpdateBallTexture(); 53 Debug.Log($"({x}, {y})に半径{radius}のボールを置きました。"); 54 return true; 55 } 56 57 public bool TryGetRandomAvailablePosition(float radius, out float x, out float y) 58 { 59 if (this.availablePositionFinder == null) 60 { 61 this.availablePositionFinder = new Material(Shader.Find("Hidden/AvailablePositionFinder")); 62 } 63 64 // Levelに応じた解像度(12なら2048×2048)のテクスチャ上の全ピクセルについて 65 // 半径がradiusのボールを置けるかチェックし、結果を描画する 66 this.availablePositionFinder.SetFloat(this.RadiusProperty, radius); 67 this.availablePositionFinder.SetInt(this.BallCountProperty, this.balls.Count); 68 Graphics.Blit(this.ballTexture, this.availabilityMaps[Level - 1], this.availablePositionFinder, 0); 69 70 // 解像度を1/2、1/4、1/8...と半々に小さくしたテクスチャを用意しておき、 71 // 描画された結果を縦横1/2サイズのテクスチャに順次転写していく 72 // 2×2ピクセルずつ見ていき、4ピクセルのうちボールを置ける場所が1箇所以上あれば 73 // 次のテクスチャレベルの当該ピクセルは1となる 74 // 最終的に1×1ピクセルまで転写されたとき、そのテクスチャの色が1ならば 75 // どこかにまだボールを置ける場所が残っていることを意味する 76 for (var i = Level - 2; i >= 0; i--) 77 { 78 Graphics.Blit(this.availabilityMaps[i + 1], this.availabilityMaps[i], this.availablePositionFinder, 1); 79 } 80 81 // 結果を確認する 82 var currentActive = RenderTexture.active; 83 RenderTexture.active = this.availabilityMaps[0]; 84 this.availabilityMapReader.ReadPixels(new Rect(0, 0, 1, 1), 0, 0); 85 RenderTexture.active = currentActive; 86 if (this.availabilityMapReader.GetPixel(0, 0).r <= 0.0f) 87 { 88 Debug.Log($"もう半径{radius}のボールを置ける場所はなさそうです。"); 89 x = 0.0f; 90 y = 0.0f; 91 return false; 92 } 93 94 // もし置ける場所を取得したいのであれば、this.availabilityMaps[Level - 1]の全ピクセルを取得し 95 // 色が1の座標を抜き出して配列にでもして返せばいいだろうが、頻繁にやるには高コストと思われる 96 // どこかランダムに1箇所選ぶだけでいいなら、結果テクスチャを先ほどの転写順とは逆順に 97 // 調べていくことで、必要なピクセルのチェック数を節約できそう 98 // 今回は一人のプレイヤーがひたすらボールを詰めていくというデザインにしたが、たとえば 99 // コンピュータを相手に交互にボールを置いていくようなゲームデザインなら、敵の手番で 100 // ボールを置く場所を決定する際に、こういった機能が有用かもしれない 101 var offset = new Vector2Int(); 102 var availablePixelPositions = new List<Vector2Int>(); 103 var flipY = SystemInfo.graphicsUVStartsAtTop; 104 for (var i = 1; i < Level; i++) 105 { 106 RenderTexture.active = this.availabilityMaps[i]; 107 this.availabilityMapReader.ReadPixels( 108 new Rect(offset.x, flipY ? (1 << i) - 2 - offset.y : offset.y, 2, 2), 109 0, 110 0); 111 availablePixelPositions.Clear(); 112 for (var u = 0; u < 2; u++) 113 { 114 for (var v = 0; v < 2; v++) 115 { 116 var pixel = this.availabilityMapReader.GetPixel(u, v); 117 if (pixel.r > 0.0f) 118 { 119 availablePixelPositions.Add(new Vector2Int(u, v)); 120 } 121 } 122 } 123 124 offset = (offset + availablePixelPositions[Random.Range(0, availablePixelPositions.Count)]) * 2; 125 } 126 RenderTexture.active = currentActive; 127 var map = this.availabilityMaps[Level - 1]; 128 x = ((offset.x * 0.5f) + 0.5f) / map.width; 129 y = ((offset.y * 0.5f) + 0.5f) / map.height; 130 Debug.Log($"まだ({x}, {y})に半径{radius}のボールを置けそうです。"); 131 return true; 132 } 133 134 private void UpdateBallTexture() 135 { 136 var ballCount = this.balls.Count; 137 var capacity = this.ballTextureWidth * this.ballTextureHeight; 138 if (capacity < ballCount) 139 { 140 while (capacity < ballCount) 141 { 142 if (this.ballTextureWidth > this.ballTextureHeight) 143 { 144 this.ballTextureHeight <<= 1; 145 } 146 else 147 { 148 this.ballTextureWidth <<= 1; 149 } 150 151 capacity <<= 1; 152 } 153 154 this.ballTexture.Resize(this.ballTextureWidth, this.ballTextureHeight); 155 Array.Resize(ref this.ballTexturePixels, capacity); 156 } 157 158 for (var i = 0; i < ballCount; i++) 159 { 160 var ball = this.balls[i]; 161 this.ballTexturePixels[i] = new Color(ball.x, ball.y, ball.z); 162 } 163 164 this.ballTexture.SetPixels(this.ballTexturePixels); 165 this.ballTexture.Apply(false); 166 } 167 168 private bool VerifyNextPosition(float x, float y, float radius) 169 { 170 if ((x < radius) || (x > (1.0f - radius)) || (y < radius) || (y > (1.0f - radius))) 171 { 172 Debug.Log("壁に近すぎます。"); 173 return false; 174 } 175 176 foreach (var ball in this.balls) 177 { 178 var relativeX = ball.x - x; 179 var relativeY = ball.y - y; 180 var distance = Mathf.Sqrt((relativeX * relativeX) + (relativeY * relativeY)); 181 if (distance < (radius + ball.z)) 182 { 183 Debug.Log("他のボールに近すぎます。"); 184 return false; 185 } 186 } 187 188 return true; 189 } 190}
判定部分は下記のようなシェーダーコードとしました。
ShaderLab
1Shader "Hidden/AvailablePositionFinder" 2{ 3 Properties 4 { 5 _MainTex ("Texture", 2D) = "white" {} 6 _Radius ("Radius", Float) = 0.0 7 _BallCount ("Ball Count", Int) = 0 8 } 9 SubShader 10 { 11 CGINCLUDE 12 #include "UnityCG.cginc" 13 14 struct appdata 15 { 16 float4 vertex : POSITION; 17 float2 uv : TEXCOORD0; 18 }; 19 20 struct v2f 21 { 22 float2 uv : TEXCOORD0; 23 float4 vertex : SV_POSITION; 24 }; 25 26 v2f vert(appdata v) 27 { 28 v2f o; 29 o.vertex = UnityObjectToClipPos(v.vertex); 30 o.uv = v.uv; 31 return o; 32 } 33 34 sampler2D _MainTex; 35 ENDCG 36 37 Pass 38 { 39 Cull Off 40 ZWrite Off 41 ZTest Always 42 43 CGPROGRAM 44 #pragma vertex vert 45 #pragma fragment frag 46 47 float4 _MainTex_TexelSize; 48 float _Radius; 49 uint _BallCount; 50 51 fixed4 frag(v2f i) : SV_Target 52 { 53 if (any(max(max(_Radius - i.uv, i.uv - (1.0 - _Radius)), 0.0))) 54 { 55 // 壁に近すぎる 56 return 0.0; 57 } 58 59 uint ballTexWidth = (uint)_MainTex_TexelSize.z; 60 61 [loop] 62 for (uint index = 0; index < _BallCount; index++) 63 { 64 float2 uv = (float2(index % ballTexWidth, index / ballTexWidth) + 0.5) * _MainTex_TexelSize.xy; 65 float3 ball = tex2Dlod(_MainTex, float4(uv, 0.0, 0.0)).xyz; 66 67 if (distance(i.uv, ball.xy) < _Radius + ball.z) 68 { 69 // 他のボールに近すぎる 70 return 0.0; 71 } 72 } 73 74 return 1.0; 75 } 76 ENDCG 77 } 78 79 Pass 80 { 81 Cull Off 82 ZWrite Off 83 ZTest Always 84 85 CGPROGRAM 86 #pragma vertex vert 87 #pragma fragment frag 88 89 fixed4 frag(v2f i) : SV_Target 90 { 91 return sign(tex2D(_MainTex, i.uv)); 92 } 93 ENDCG 94 } 95 } 96}
この他、ゲームっぽい体裁にするためのBoxAndBalls
を使う側のスクリプトがあるのですが、本筋とは関係が薄いだろうということと字数節約のために省略しました。
動作させたところ、下図のような結果になりました。画面左上に表示されているのが次に置くべきボールのサイズ、箱の中に現れる青い×印はランダムに選択されたボールを置けるであろう場所です。
左下と右下のRawImage
はデバッグ目的のもので、左下はボールを置けると判断された領域、右下はシェーダー側へ既存ボールの位置・サイズを送り込むためにデータをテクスチャの形にしたものです。
今回は正方形内の個々の点についてそれぞれ他の円や壁との距離を調べる方法にしましたが、majiponiさんのおっしゃるように、既存の円や壁が発生させる配置禁止領域を重ね描きしていって禁止領域マップを作るというのもよさそうですね。
投稿2020/04/05 04:14
総合スコア10811
0
すべての点に対する探索でなく求められそうな気はします。
最適なポジションに置いた、追加できる最大の大きさの円と、それらに接する3つの既存の円(正方形の辺も円と扱う)を考えます。
既存の円の中心のうち2点を考えます。
追加円中心とその2点との距離の差は、対応する円の半径の差です。
2点からの距離の差が等しい点の集合は、双曲線です。
別の組み合わせで考えれば、複数の双曲線ができ、その交点に求める点はあります。
双曲線の交点は、ググったところどうやら解析的に求められるようです。
参考: https://yo-kun.hatenadiary.org/entry/20081130/p1
場所がわかればあとは既存の円の中心との距離とその半径の差が求める円の最大の半径です。
3つの円の組の求め方ですが、たぶん「円間の距離の短い順に線で結び、できた3角形分割」でいけるのではないかなという気がします。
ただ、これで計算できたとして、円が多くなると高々画面解像度程度の全探索(既存円半径+追加円半径で全既存円を塗って塗り残しを見る)とどちらが速いかは微妙なところなようにも思います。
投稿2020/04/05 05:12
総合スコア3047
0
各点で調べる、に近いですが…
入るか調べたい円の半径をR、各円の半径をr(n)、正方形の一辺をW、とします。まず、正方形内のすべての点に、0、とマークします。次に、各円に対し、中心からR+r(n)の半径の円を考え、その内部の点に、1、とマークします。最後に、座標が(R,R)-(W-R,W-R)の点の中に、0、とマークされている点がないか調べます。
投稿2020/04/04 21:49
総合スコア1722
0
既に存在する全ての図形に対して、正方形(円に外接、辺の長さ=円の直径)があるとする。
既に存在する全ての図形に対して、正方形(円に内接、対角線=円の直径)があるとする。
ここで想定する全ての正方形は、辺がx軸 or y軸に平行である。
- 全ての円を、円に外接する正方形であるとして判定
→1に該当ない場合
2-1, 全ての円は円に内接する正方形であるとして、次ステップで計算を行う座標をスクリーニング
2-2, 追加する円が既存の円と重複せずに存在できるか判定
(円と円の中心間距離d > 円1の半径r1 + 円2の半径r2)
もうちょっとスマートな解法ありそうですが...
個人的にも気になるので、考えついた方ぜひ解答ください!
投稿2020/04/04 15:39
総合スコア89
0
新たに配置したい円の半径をrとするとき,
既存の全ての円の半径をrだけ広げた世界において…
for( 円1 in 既存の円群 ) { for( 円2 in 既存の円群,ただし円1ではない ) { for( 交点 in 円1と円2の交点群 ) { if( この交点を含む円が存在しない ){ return 設置できる } } } if( 円1が全ての円と交点をもたない ){ return 設置できる } } return 設置できない
(上記は話を簡単にするため「円」だけで書いたが,外周も既存の円と同様にrだけ内側に狭めて適切に扱えばいい)
…みたいな話にならないでしょうか.
↓
ダメだ.ならないな.
円の弧を2交点の範囲で切り取っていって残るか(円周全体が他の円群によって包括されるかどうか)という判定じゃないとダメだ.
すなわち,円弧上の位置を1次元の媒介変数t=0~1で表すとしたら,円弧上の位置t=0~1の範囲すべてが他者によって含まれるかどうかをチェックするという作業になる.
(交点計算によって,他者に含まれるtの範囲を集めていけばいいと思う.
計算時間については,データ規模次第では交点計算対象を何らかの枝狩りで減らす工夫が必要かもしれない.)
処理の意味合いを図形的な観点で言えば…… 他で回答されているような「新しい円の中心を置けないエリア」のマスクを作ったとしたら,最終的にエリアの境界が1箇所でも残って見えていたら「置ける」という話.
(「置けない」場合には,空間の全域がマスクされるされるから境界が見えない)
投稿2020/04/07 09:48
編集2020/04/08 01:45総合スコア11996
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
[アナログな解]
入るかどうか調べる円(円X)の半径をRとします。
元々ある各円の中心から、その円の半径+Rの円を書きます。(円Xの中心が、その円の中に在ると、その円と円xは重なります)
外枠の正方形に接する幅Rの枠を書きます。(円Xの中心が、その枠の中に在ると、正方形と円Xは重なります)
正方形の中に、上で説明した円にも四角い枠にも入っていない部分(円でも枠でも塗りつぶされていない部分)が存在すれば、その部分に中心をもつ円Xは重ならずに置くことができます。
投稿2020/04/06 03:06
総合スコア6915
0
- 正方形内の任意の二つの円の間の空隙の最小値は、それぞれの中心点を結ぶ線分の長さ-(それぞれの円の半径を足したもの)に等しい
- ある円が他の円に重ならないための条件は、上記を正方形内のすべての円に対して計算した結果、すべての空隙が 0 以上であることを満たしているかどうかで判定可能(接するのがありかなしかで変わります)
- ただし、ある円が正方形内に収まるかどうかの判定も必要(これは中心点の座標と半径から判定可能)
ではいけないかなあ……
※もう少し絞るなら、新しい円の周辺の円のみで判定することも可能なような気がしますが……
って、問題は「そのような点があるか?」か……しらみつぶしでやるにしても面倒ですね。
すべての円から、それぞれの円の中心を頂点とする三角形で領域を分割してやると、それぞれの領域で入る最大の空隙はとりあえず分かります。これで大まかに入りそうな場所と確実に入らない場所を振り分けて、入りそうな場所に対してしらみつぶし、ですかねえ?
投稿2020/04/06 02:08
編集2020/04/06 02:12総合スコア13703
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。