teratail header banner
teratail header banner
質問するログイン新規登録

質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.30%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

解決済

2回答

1241閲覧

乗換案内のようなアプリ開発において、特定の経路でスタックなどが発生してしまう。

Kamine

総合スコア8

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

0グッド

3クリップ

投稿2023/11/26 10:44

編集2023/11/27 11:18

0

3

実現したいこと

  • 乗換回数が最も少ない経路の表示(通る駅の数は問わない)
  • スタックの解消

前提

現在、都市開発ゲームの駅名に対応して、経路検索が出来るアプリの公開を目指して開発を進めている者です。
ソースコードはコピペしていただければそのまま動作します。
ヒントだけでも構いませんので、よろしければご回答よろしくお願いいたします。

以下、今回使用している路線図です。
赤……路線リストのline1
青……路線リストのline2
緑……路線リストのline3
に対応しています。
また、書かれた数字はコード内の駅番号リストと対応しています。

イメージ説明

発生している問題・エラーメッセージ

(最初の調査の際の情報です。コメント欄に調査し直した際の情報が書かれています)
・期待した結果
0と5……0,1,1,4,4,5の順で表示される。
1と5……1,4,4,5の順で表示される。
2と5……2,4,4,5の順で表示される。
3と5……3,1,1,4,4,5の順で表示される。
5と0……5,4,4,1,1,0の順で表示される。
5と1……5,4,4,1の順で表示される。
5と2……5,4,4,2の順で表示される。
5と3……5,4,4,1,1,3の順で表示される。

・実際の結果
0と5……System.IndexOutOfRangeException: インデックスが配列の境界外です。と表示される(337行目)
1と5……Process is terminated due to StackOverflowException.と表示される。
2と5……2,4,1と表示後、Process is terminated due to StackOverflowException.と表示される。
3と5……3,1と表示後、Process is terminated due to StackOverflowException.と表示される。
5と0……System.IndexOutOfRangeException: インデックスが配列の境界外です。と表示される(337行目)
5と1……System.IndexOutOfRangeException: インデックスが配列の境界外です。と表示される(337行目)
5と2……System.IndexOutOfRangeException: インデックスが配列の境界外です。と表示される(337行目)
5と3……System.IndexOutOfRangeException: インデックスが配列の境界外です。と表示される(337行目)

該当のソースコード

長文なので、リンクを貼らせていただきます。
リンク内容
お手数ですが、ソースコードのダウンロードをお願いします。

試したこと

for文やforeach文内で、路線番号や駅番号が若い順に処理されるようになっているので、if文を用いて適切な値を代入させようとしましたが、うまくいきませんでした。

補足情報(FW/ツールのバージョンなど)

Visual Studio 2022

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kamine

2023/11/26 11:47

大変申し訳ございません。調査内容を整理してまとめます。
ikedas

2023/11/26 12:50

「遠回り」、「スタック」、「無限ループ」とは、それぞれ ・どのような結果になると期待したのか。 ・実際にはどのような結果になったのか。 を、具体的に記してください。 なお、このコメント欄に書くのではなく、質問文を編集して書いてください。
Kamine

2023/11/26 13:19

わかりました。
Kamine

2023/11/26 13:24

申し訳ございません。調査し直した結果、遠回りとwhile文の無限ループは発生していなかったことがわかりました。
退会済みユーザー

退会済みユーザー

2023/11/26 14:11 編集

開始地点が3、終点が2の場合の期待する結果はどうなりますか。 3,1,1,4,4,2 なのか、3,1,1,2のどちらでしょうか? 編集前にあった「ただし距離の概念が実装されていないので移動する駅の数もしくは乗換回数がもっとも少なくなるようにしたい」という文章が、意図的にかどうかわかりませんが、削除されています。 前者は経由する駅の数が多いですが、乗り換え数は0回。 後者は経由する駅の数が一番少ないですが、乗り換え数は1回。 「移動する駅の数もしくは乗換回数がもっとも少なくなるようにしたい」という削除前の文を素直に受け取れば、乗り換え数が0回の前者が期待する結果とする考え方もありますが、そのような理解でいいですか? また、仮に、同じ数の”最短”経路が複数見つかった場合は、どのような結果を返せばよいでしょうか?複数の結果を返すべきなのか、どちらか一方の結果を任意に返してよいのでしょうか?
Kamine

2023/11/26 14:27

混乱を招いてしまいましたら申し訳ございません。「ただし距離の~」の部分は自分で削除しました。理由として、実現したいことが多いと回答がつかなくなるのではと思ったためです。 「乗換回数が最も少なくなる」ことを優先したいです。修正し追記しておきます。 よって開始3、終点2の場合は、3,1,1,4,4,2を期待します。 最短経路が複数の場合は、そのうち1つの結果で構いません。
ikedas

2023/11/27 00:51

「実際の結果」に「と表示される(337行目)」とありますが、この「337行目」とはソースコードのどこのことですか。
Kamine

2023/11/27 10:29

申し訳ございません。 今日改めて確認しようとしましたところ、「ソースファイルがモジュールがビルドされたときのものと異なります。デバッガでこのファイルを使用しますか?」と表示されました。 そのため、一度クリーンをした所、表示されなくなりました。 その後念のため動作を確認し直しました所、以下のようになりましたので、こちらに訂正情報を書きます。
Kamine

2023/11/27 10:49

0と5……0,1と表示後、Process is terminated due to StackOverflowException.と表示される。 1と5……1,2と表示後。Process is terminated due to StackOverflowException.と表示される。 2と5……2,4,1、1,2と表示後、Process is terminated due to StackOverflowException.と表示される。 3と5……3,1、1,2と表示後、Process is terminated due to StackOverflowException.と表示される。 3と0……while内無限ループ 4と0……while内無限ループ 5と0……while内無限ループ 5と1……while内無限ループ 5と2……while内無限ループ 5と3……while内無限ループ 残りの組み合わせは問題ありませんでした。 また「System.IndexOutOfRangeException: インデックスが配列の境界外です。」の表示は出ませんでした。
luuguas

2023/11/27 11:12

コードを実行して出発駅0、到着駅5で試したところ、1回目の移動で駅0から駅1に移動する直前の passedDupStNum の中身は{4, 1, 2, 1}となっています。(1番目の4は219行目/2,3番目の1,2は261行目/4番目の1は 140行目のコードで追加) 通過する乗換駅は駅1だけなので、本来の中身は{1}であるべきです。 そのため、2回目の移動で駅1から駅4に移動したいのにも関わらず、DecideLine2 関数内で駅4(さらに駅2)が移動先の候補から外れてしまい、無限ループに陥っているようです。 おそらく他にもエラーの原因はあると思いますが、まずは passedDupStNum の中身が正しくなるようにコードを修正するとよいと思います。
Kamine

2023/11/27 13:21 編集

luuguas様、詳しい情報まで添えて下さりありがとうございます。
guest

回答2

0

ベストアンサー

Kamine さんのコードで発生している不具合に対する回答ではありませんが、乗り換え回数が最小となるような経路を求める問題に対して私なりに解き方を考えてみました。あくまで解法の1つですのでご了承ください。

路線図(グラフ)を工夫する

今回の問題で厄介なのは、「同じ路線上の駅を移動する」と「違う路線に乗り換える」の2種類の動き方があることです。

そのため、単純に路線図上を移動しようとしても「今どの路線にいるのか」や「同じ路線を移動したのか、それとも違う路線に乗り換えたのか」などの判断が難しいです。


そこで、複数の路線を1つの路線図で表すのではなく、路線ごとに別々の路線図で表すことにします。

そして、異なる路線図の同じ駅同士を線で繋げることで、「同じ路線上の駅を移動する」と「違う路線に乗り換える」の両方を「線で繋がっている別の駅に移動する」という1種類の動きで表すことができます。

さらに、この図を使うと「乗り換え回数のカウント」についても上手く考えることができます。

乗り換え回数を「目的の駅に到着するまでにかかるコスト」と言い換えることで、

  • 同じ路線上を移動する場合は 0 のコストがかかる(=乗り換え回数は増えない)
  • 別の路線に乗り換える場合は 1 のコストがかかる(=乗り換え回数が1増える)

と表現することができ、それぞれの線を通るときに対応するコストがかかると考えることができます。

複数の路線図を使って表現する図

ちなみに、複数の点(今回は各駅のこと)とそれらを繋ぐ線からなる構造のことを「グラフ」といい、グラフにおける点のことを「頂点」、線のことを「辺」といいます。さらに、それぞれの辺がコスト(重み)を持っているグラフのことを「重み付きグラフ」といいます。(「グラフ理論」で調べるとよいです)

コストを最小化する問題を解く

上の図を使うと、「乗り換え回数を最小化する」という問題は「通過した辺のコストの合計を最小化する」問題に言い換えられます。

例えば「0番目の駅から出発して、4番目の駅に到着するまでに必要な最小の乗り換え回数」を求めるには、上の図において「(0番または6番または12番)の駅から出発して、(4番または10番または16番)の駅に到着するまでに必要な最小のコスト」を求めればよいです。この答えとなる経路の1つは 0番→1番→7番→10番 で、かかるコストは1です。


肝心の経路をどうやって求めるかについては、様々なグラフ探索の方法があります。その中でも特に有効そうな方法を2つ紹介します。

1. ダイクストラ法

色んなグラフ探索の手法のなかで「重み付きグラフ上の2つの頂点を繋ぐ経路のうち最小のコストを求める」のに適しているのが「ダイクストラ法」というアルゴリズムです。

ダイクストラ法では「出発地点を固定したとき、すべての頂点について出発地点からの経路のうち最小のコストを求める」ことができます。

また、経路における所要時間(または所要距離)と乗り換え回数の優先度が変わった場合にも、辺の重みを調整することで同様に解くことができます。

2. 深さ優先探索

今回の問題では考慮すべき条件が乗り換え回数だけなので上記のダイクストラ法で上手くいきますが、条件がさらに増えた場合や複数の経路を求めたい場合などになると、ダイクストラ法では探索に限度があります。

そこで、グラフ探索の基礎である深さ優先探索を使うことをおすすめします。

深さ優先探索は「移動できる頂点がなくなるまで移動することを繰り返し、行き止まりに当たったら1歩戻る」というシンプルな方法ですが、深さ優先探索を理解できれば、条件が複雑になっても柔軟に対応することができます。(特に再帰関数を使った深さ優先探索は様々な探索に応用できます)

まずは出発地点と到着地点を結ぶ全ての経路を調べるところから始めて、そこから乗り換え回数や路線などの情報を追加したり、探索範囲を絞ったりすることを段階的におこなっていくと良いと思います。(探索範囲を絞る方法についてはビームサーチや焼きなましなど色々)

補足

グラフ探索について解説しているサイトをまとめました。かなり分量がありますが、理解に役立つと思います。

深さ優先探索について

幅優先探索について

幅優先探索は深さ優先探索とセットで出てくることが多いです。

ダイクストラ法について

必ずしもダイクストラ法を使う必要はないので見なくても良いです。

投稿2023/11/27 13:28

luuguas

総合スコア501

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kamine

2023/11/28 14:31

詳しいご回答ありがとうございます。ご提示下さったコードは問題なく動作しました。
guest

0

ではヒントだけ。

まず、「スタックオーバーフロー」がどういうことなのかを、プログラミングの参考書などで調べてみてください。また「インデックスが配列の境界外」の意味も調べてください。

以下気づいた点を述べます。

  1. ご質問のコードはResult2()がいろいろな関数を介してMove()を呼び出し、Move()はResult2()を呼び出すという形になっています。「相互再帰」という、再帰呼び出しの一種です。
    再帰呼び出しを使ったコードでよくある失敗が、再帰の終了条件を明確にしていないというものです。そのため再帰呼び出しが終了せず、スタックオーバーフローが発生します。
    このような失敗を避けるためには、再帰呼び出しする関数では最初に「これこれの条件が成立したらそれ以上再帰せず関数を終了する」というコードを必ず記述するとよいです。
    今回の場合だと、質問に明示されていない暗黙の条件として「一度通った駅は二度と通らない」というものがあるはずです。ですから再帰の終了条件として「すでに調べた駅に到達したら、それ以上調べずに関数を終了する」というコードが関連の関数に必要ですが、それが抜けている (書くべき場所に書かれていない) と思われます。

  2. 経路の探索と乗り換え回数のカウントの両方を一度にやろうとしているため、ミスが混入しやすいです。今後経由駅数などほかの条件を適用したい場合にコードの変更も難しいです。
    まずは一つの関数で、「出発駅と到着駅が与えられたときに、あり得る経路をすべて列挙する」(乗り換え数などの条件は加味しない) というコードを書いてみてはどうですか。もちろん再帰呼び出しを使ってもかまいません。

  3. 一般の場合の経路探索では「組み合わせ爆発」が起きる可能性があります (参考: 「フカシギの数え方」)。都市開発ゲームということなので、プレイヤーがそのようなことをひき起こす路線を敷いてしまうこともありえますね。
    そのため、2. で実装した単純な経路列挙アルゴリズムは改良の余地があります。経路探索の途中で、あきらかに最良ではない経路 (すでにみつかった経路よりも乗り換え数が多くなる、など) がでてきたら探索を打ち切ってほかの候補を探す、などの工夫が必要です。

今はいきなり3. をやろうとしている節もありますが、2.のコードを書いてから3.の改良をやればコードの見通しもよくなり、うまくいくのではないでしょうか。

【追記】ちなみに、手元で書いてみたら少なくとも2.はできました。さらに3.の改良をするのも簡単そうです。

コード

なんかコードを載せた回答が出てしまったのでもういいや。2.のコードで私が書いたものも載せときますね。

go

1package main 2 3import ( 4 "bufio" 5 "fmt" 6 "os" 7) 8 9var stations = []string{ 10 "駅0", 11 "駅1", 12 "駅2", 13 "駅3", 14 "駅4", 15 "駅5", 16} 17 18var lines = [][]int{ 19 {0, 1, 2}, 20 {3, 1, 4, 2}, 21 {4, 5}, 22} 23 24var nextStations = [][]int{} 25 26var results = [][]int{} 27 28func main() { 29 // 準備: 隣接駅のリストを作っておく 30 for s := 0; s < len(stations); s++ { 31 nextStations = append(nextStations, listNextStations(s)) 32 } 33 34 // 出発駅と到着駅を入力 35 scanner := bufio.NewScanner(os.Stdin) 36 37 fmt.Print("出発駅……") 38 scanner.Scan() 39 start := scanner.Text() 40 fmt.Print("到着駅……") 41 scanner.Scan() 42 goal := scanner.Text() 43 44 s := -1 45 g := -1 46 for i, v := range stations { 47 switch v { 48 case start: 49 s = i 50 case goal: 51 g = i 52 } 53 if 0 <= s && 0 <= g { 54 break 55 } 56 } 57 58 // 結果を得る 59 findResults(s, g, []int{}) 60 61 fmt.Println(len(results), " routes") 62 fmt.Println(results) 63} 64 65// 駅 s に隣接するすべての駅を列挙する 66func listNextStations(s int) []int { 67 var next = []int{} 68 69 for _, line := range lines { 70 for i, station := range line { 71 if s == station { 72 if 0 < i { 73 next = append(next, line[i-1]) 74 } 75 if i < len(line)-1 { 76 next = append(next, line[i+1]) 77 } 78 } 79 } 80 } 81 82 return next 83} 84 85// 駅 s から最終目的の駅 g までの経路を探索し、 86// 見つかったものを配列 results に加える。 87// visited はこれまでに通ってきた駅の配列 (s が出発駅なら空)。 88func findResults(s, g int, visited []int) { 89 // 経路を一駅進める 90 visited = append(visited, s) 91 92 // 隣接する駅をすべて調べる 93 for _, n := range nextStations[s] { 94 // ゴールなら経路を記録して、その先はもう調べない 95 if n == g { 96 result := make([]int, len(visited)+1) 97 copy(result, append(visited, n)) 98 results = append(results, result) 99 continue 100 } 101 102 // すでに通った駅ならもう調べない 103 var passed bool 104 for _, v := range visited { 105 if n == v { 106 passed = true 107 break 108 } 109 } 110 if passed { 111 continue 112 } 113 114 // 上記のいずれでもなければ、次の駅に進んで探索を続行 115 findResults(n, g, visited) 116 } 117 // すべての隣接駅を調べ終わったら (再帰) 関数の呼び出しから戻る 118 119 // 経路を一駅戻す 120 visited = visited[:len(visited)-1] 121}

言語はGoです。経路探索の主要部であるfindResults()関数では、luuguasさんも言及されている「深さ優先探索」を、再帰呼び出しを用いて行っています。

実行結果:

$ go run main.go 出発駅……駅0 到着駅……駅5 2 routes [[0 1 2 4 5] [0 1 4 5]] $ go run main.go 出発駅……駅1 到着駅……駅5 2 routes [[1 2 4 5] [1 4 5]] $ go run main.go 出発駅……駅2 到着駅……駅5 2 routes [[2 1 4 5] [2 4 5]] $ go run main.go 出発駅……駅3 到着駅……駅5 2 routes [[3 1 2 4 5] [3 1 4 5]] $ go run main.go 出発駅……駅3 到着駅……駅0 1 routes [[3 1 0]] $ go run main.go 出発駅……駅4 到着駅……駅0 2 routes [[4 1 0] [4 2 1 0]] # 以後は上記の逆順の結果になるだけなので省略。

投稿2023/11/27 02:01

編集2023/11/27 22:57
ikedas

総合スコア4441

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kamine

2023/11/27 10:51

ご回答のほど、ありがとうございます。書かれたことを参考にプログラムを組み直してみることにします。
Kamine

2023/11/27 11:42 編集

すみません。回答の2.のコードで出た通りは何通りでしたか。また、2.の処理部分は何行のコードを書いたか教えていただければ幸いです。
ikedas

2023/11/27 12:05

「何通り」とはどういうことを言っていますか。またコードの行数をお知りになりたい理由は何でしょうか。
Kamine

2023/11/27 13:20 編集

すみません、「出発駅と到着駅が与えられたときに、あり得る経路をすべて列挙する」を「あり得る経路をすべて列挙する」の部分だけで解釈してしまっていました。あり得る経路をすべて列挙したら何通りか聞きたかったのですが、そもそも解釈違いでしたので無視して構いません。 コードの行数につきましては、どのくらいのコードを書けば期待する動作が実現できるか、単純に気になったためです。 今後は文章をよく理解してから返信します。すみません。
ikedas

2023/11/27 14:18 編集

回答中のリンク先の動画はご覧になりましたか。あれで駅数が25になる路線図にあたるもの (動画中で「4 × 4」といっているもの) で試したところ、正しく8512通りとなりました。 2.のコードの行数は、駅名や路線データなどの値をセットしている分などもふくめて全体で100行くらい、そのうち実際に探索をする関数は30行たらずです (いずれもコメント行や空行は除く)。言語はC#ではないですが、C#で書いたとしてもそう大きくは変わらないでしょう。
ikedas

2023/11/27 22:18

コードを載せました。
Kamine

2023/11/28 14:29

ご回答とコードありがとうございます。リンク先の動画は見ました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.30%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問