@Iyatsu

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

第K最短路探索プログラムについて

Q&A

解決したいこと

C言語プログラムについて、座標上の線分から交差点を検出し、グラフを構築して第K最短経路を探索するプログラムを作成しているのですが、出力に問題があるように感じるため、プログラムに問題がないか確認してほしいです。(プログラム内にはこれに加えて橋の検出や新しい点のグラフ接続の機能もあります)

発生している問題・エラー

2点間の距離の出力についてはまだ十分な検証ができていない
経路の出力(例えば、始点と終点は必ず出力されるはずなのに出力されていない箇所がある、経路がループしている箇所がある)

該当するソースコード

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>

#define MAXN 1100  // 最大ノード数
#define MAXM 500   // 最大クエリ数
#define MAXQ 100   // 最大クエリ数
#define MAX_K 5    // 最多経路数
#define INF 1e18   // 無限大
#define EPS 1e-8   // 浮動小数点比較の誤差許容範囲

// 座標点構造体
typedef struct {
    double x, y;
} Point;

// 線分
typedef struct {
    int a, b;
} Segment;

// グラフの隣接リスト用エッジ構造体
typedef struct Edge {
    int to;
    double cost;
    struct Edge* next;
} Edge;

typedef struct {
    double cost;
    int path[MAXN];
    int length;
} Path;

// グローバル変数
int N,M;   // ノード数、クエリ数
Point points[MAXN];   // 座標点一覧
Segment segs[MAXM];   // 線分一覧
Edge* graph[MAXN];   // グラフの隣接リスト
Path paths[MAXQ * MAX_K];
int node_count = 0;   // ノードの総数
int cur_path[MAXN];
int cur_len = 0;
int vis[MAXN] = {0};
int path_count_total = 0;

// 2点が同じ座標かを比較(誤差考慮)
int equal(Point p1, Point p2) {
    return fabs(p1.x - p2.x) < EPS && fabs(p1.y - p2.y) < EPS;
}

// 2点間のユークリッド距離
double distance(Point p1, Point p2) {
    double dx = p1.x - p2.x, dy = p1.y - p2.y;
    return sqrt(dx * dx + dy * dy);
}

// 点pが線分ab上にあるか判定
int point_on_segment(Point p, Point a, Point b) {
    
    double dx1 = p.x - a.x;
    double dy1 = p.y - a.y;
    double dx2 = b.x - a.x;
    double dy2 = b.y - a.y;
  
    // 外積で一直線上にあるか判定し、内積で範囲内か確認
    if (fabs(dx1 * dy2 - dy1 * dx2) < EPS) {
        double dot = dx1 * dx2 + dy1 * dy2;
        double len_sq = dx2 * dx2 + dy2 * dy2;
        return (0 <= dot && dot <= len_sq) || fabs(dot) < EPS || fabs(dot - len_sq) < EPS;
    }
    return 0;
}

// 線分同士の交差判定と交点計算
int intersect(Point p1, Point q1, Point p2, Point q2, double* ix, double* iy) {
    
    // 端点同士が重なっているか確認
    if (point_on_segment(p1, p2, q2)) {
        *ix = p1.x;
        *iy = p1.y;
        return 1;
    }

    if (point_on_segment(q1, p2, q2)) {
        *ix = q1.x;
        *iy = q1.y;
        return 1;
    }

    if (point_on_segment(p2, p1, q1)) {
        *ix = p2.x;
        *iy = p2.y;
        return 1;
    }

    if (point_on_segment(q2, p1, q1)) {
        *ix = q2.x;
        *iy = q2.y;
        return 1;
    }

    // 行列式で交点計算
    double a = q1.x - p1.x;
    double b = -(q2.x - p2.x);
    double c = q1.y - p1.y;
    double d = -(q2.y - p2.y);
    double e = p2.x - p1.x;
    double f = p2.y - p1.y;
    double det = a * d - b * c;

    if (fabs(det) < EPS) return 0;   // 平行または一致

    double s = (d * e - b * f) / det;
    double t = (-c * e + a * f) / det;

    // 交差範囲のチェック
    if (s < EPS || s > 1 - EPS || t < EPS || t > 1 - EPS) return 0;

    *ix = p1.x + s * (q1.x - p1.x);
    *iy = p1.y + s * (q1.y - p1.y);

    // 既存点との重複回避
    for (int i = 0; i < node_count; i++) {
        if (equal((Point){*ix, *iy}, points[i])) {
            *ix = points[i].x;
            *iy = points[i].y;
            break;
        }
    }

    return 1;
}

// グラフに無向エッジを追加
void add_edge(int u, int v, double cost) {
    Edge* e = malloc(sizeof(Edge));
    e->to = v;
    e->cost = cost;
    e->next = graph[u];
    graph[u] = e;
}

// 点の比較関数(x優先 → y)
int cmp_point(const void* a, const void* b) {
    Point* p = (Point*)a;
    Point* q = (Point*)b;
    if (fabs(p->x - q->x) > EPS) return p->x < q->x ? -1 : 1;
    if (fabs(p->y - q->y) > EPS) return p->y < q->y ? -1 : 1;
    return 0;
}

// 既存ノードのインデックス取得(なければ追加)
int get_node_index(Point p) {
    for (int i = 0; i < node_count; i++) {
        if (equal(points[i], p)) return i;
    }
    points[node_count++] = p;
    return node_count - 1;
}

// 線分の交差を考慮しながらグラフ構築
void build_graph(int M) {

    // グラフ初期化
    for (int i = 0; i < MAXN; i++) {
        graph[i] = NULL;
    }
    
    for (int i = 0; i < M; i++) {
        Point p1 = points[segs[i].a];
        Point p2 = points[segs[i].b];
        Point seg_pts[MAXN];   // 分割された点列
        int count = 0;
        seg_pts[count++] = p1;
        seg_pts[count++] = p2;

        // 他の線分との交点を探す
        for (int j = 0; j < M; j++) {
            if (i == j) continue;
            double ix, iy;
            if (intersect(p1, p2, points[segs[j].a], points[segs[j].b], &ix, &iy)) {
                Point p = {ix, iy};
                int found = 0;
                for (int k = 0; k < count; k++) {
                    if (equal(p, seg_pts[k])) {
                        found = 1;
                        break;
                    }
                }  
                if (!found) {
                    seg_pts[count++] = p;
                }
            }
        }

        // 点をソートして順番に辺を張る
        qsort(seg_pts, count, sizeof(Point), cmp_point);

        // 重複を除去
        int unique_count = 1;

        for (int j = 1; j < count; j++) {
            if (!equal(seg_pts[j], seg_pts[unique_count-1])) {
                seg_pts[unique_count++] = seg_pts[j];
            }
        }

        count = unique_count;

        for (int k = 0; k < count - 1; k++) {
            int u = get_node_index(seg_pts[k]);
            int v = get_node_index(seg_pts[k + 1]);
            if (u == v) continue;  
            double d = distance(seg_pts[k], seg_pts[k + 1]);
            // 重複エッジチェック
            int edge_exists = 0;
            for (Edge* e = graph[u]; e; e = e->next) {
                if (e->to == v) {
                    edge_exists = 1;
                    break;
                }
            }
            if (!edge_exists) {
                add_edge(u, v, d);
                add_edge(v, u, d);   // 無向グラフ
            }
        }
    }
}

// 幹線道路検出用変数
int tin[MAXN],low[MAXN],visited[MAXN],timer = 0;

// 橋検出用DFS
void bridge_dfs(int v, int parent) {
    visited[v] = 1;
    tin[v] = low[v] = ++timer;

    for (Edge* e = graph[v]; e != NULL; e = e->next) {
        int to = e->to;
        if (to == parent) continue;

        if (visited[to]) {
            if (low[v] > tin[to]) low[v] = tin[to];
        } else {
            bridge_dfs(to, v);
            if (low[v] > low[to]) low[v] = low[to];

            if (low[to] > tin[v]) {
                printf("%d %d\n", v + 1, to + 1);  // 両端のノード番号を出力
            }
        }
    }
}


// ノード名(数値 or C+数値)からノードインデックスを取得
int parse_node(char* s, int N) {
    if (s[0] == 'C') {
        int id = atoi(s + 1);
        if (id < 1 || id > node_count - N) return -1;
        return N + id - 1;
    } else {
        int id = atoi(s);
        if (id < 1 || id > N) return -1;
        return id - 1;
    }
}

// 昇順にdoubleを比較するための関数(誤差考慮)
int comp_double(const void* A, const void* B) {
    double a = *(double*)A, b = *(double*)B;
    if (fabs(a - b) > EPS) return a < b ? -1 : 1;
    return 0;
}

// 深さ優先探索で全経路を探索し、目的地までの距離と経路を記録
void dfs(int u, int goal, double cost) {
    cur_path[cur_len++] = u;

    if (u == goal) {
        paths[path_count_total].cost = cost;
        paths[path_count_total].length = cur_len;
        for (int i = 0; i < cur_len; i++) {
            paths[path_count_total].path[i] = cur_path[i];
        }
        path_count_total++;
        cur_len--;
        return;
    }

    for (Edge* e = graph[u]; e; e = e->next) {
        int v = e->to;
        if (vis[v]) continue;
        vis[v] = 1;
        dfs(v, goal, cost + e->cost);
        vis[v] = 0;
    }

    cur_len--;
}

// 経路を距離の昇順にソート
int compare_path(const void* a, const void* b) {
    double d1 = ((Path*)a)->cost;
    double d2 = ((Path*)b)->cost;
    if (fabs(d1 - d2) > EPS) return d1 < d2 ? -1 : 1;
    return 0;
}

// DFSを呼び出し、結果をソートして準備
void solve(int start, int goal, int k, int output_path) {
    memset(vis, 0, sizeof(vis));
    path_count_total = 0;
    cur_len = 0;

    vis[start] = 1;
    dfs(start, goal, 0);
    vis[start] = 0;

    qsort(paths, path_count_total, sizeof(Path), compare_path);

    for (int i = 0; i < k && i < path_count_total; i++) {
        printf("%.5f\n", paths[i].cost);
        if (output_path) {
            for (int j = 0; j < paths[i].length; j++) {
                int node = paths[i].path[j];
                // 数字ノードとCラベルノードの区別
                if (node < N) {
                    printf("%d ", node + 1);   // ノード1〜Nはそのまま出力
                } else {
                    printf("C%d ", node - N + 1);   // ノードN以降は "C" + 番号で出力
                }
            }
            printf("\n");
        }
    }

    // 経路が存在しない場合
    if (path_count_total == 0) {
        printf("NA\n");
    }
}

void connect_new_points(int P) {
    for (int i = 0; i < P; i++) {
        Point p;
        scanf("%lf %lf", &p.x, &p.y);

        double min_dist = INF;
        Point best_point;

        for (int j = 0; j < M; j++) {
            Point a = points[segs[j].a];
            Point b = points[segs[j].b];

            double dx = b.x - a.x;
            double dy = b.y - a.y;
            double t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / (dx * dx + dy * dy);
            if (t < 0) t = 0;
            if (t > 1) t = 1;

            Point proj = { a.x + t * dx, a.y + t * dy };
            double d = distance(p, proj);

            if (d + EPS < min_dist) {
                min_dist = d;
                best_point = proj;
            }
        }

        // 出力
        printf("%.5f %.5f\n", best_point.x, best_point.y);

        // グラフに接続
        int pi = get_node_index(p);
        int ci = get_node_index(best_point);
        double d = distance(p, best_point);
        add_edge(pi, ci, d);
        add_edge(ci, pi, d);

        // 線分にも記録(順番どおりにMを増やす)
        segs[M].a = pi;
        segs[M].b = ci;
        M++;
    }
}

// 入力読み込みと各クエリに対する出力
int main() {
    int P, Q;

    scanf("%d %d %d %d", &N, &M, &P, &Q);   // 点数、線分数、新たに加えられる点の数、クエリ数
    
    for (int i = 0; i < N; i++) {
        scanf("%lf %lf", &points[i].x, &points[i].y);
    }

    node_count = N;

    for (int i = 0; i < M; i++) {
        scanf("%d %d", &segs[i].a, &segs[i].b);
        segs[i].a--; segs[i].b--;   // 0-indexed に変換
    }

    build_graph(M);   // グラフ構築

    memset(visited, 0, sizeof(visited));   // 幹線道路検出
    timer = 0;

    for (int i = 0; i < node_count; i++) {
        if (!visited[i]) {
        bridge_dfs(i, -1);
        }
    }

    for (int q = 0; q < Q; q++) {
        char s1[10], s2[10];
        int k;
        scanf("%s %s %d", s1, s2, &k);
        int start = parse_node(s1, N);
        int goal = parse_node(s2, N);
        if (start == -1 || goal == -1) {
            printf("NA\n");
            continue;
        }

        solve(start, goal,k,1);   
        
    }

    connect_new_points(P);   

    return 0;
}

自分で試したこと

テストケースとして以下の内容で実行してみました。
入力
8 8 0 2
0 5
1 7
4 9
8 8
10 4
7 1
5 1
2 2
1 4
4 7
7 2
2 5
5 8
8 3
3 6
6 1
C3 4 5
8 5 5

software_test.png

出力は以下のようになりました。
5.02588
C9 C10 C14 C5 C6 C15 C4 4
7.66932
C9 C10 C14 C5 C6 C15 C4 C13 C8 4
10.08468
C9 C10 C14 C5 C6 C15 C4 C13 C7 C8 4
11.52546
C9 C10 C14 C5 C6 C15 C2 C9 C10 C14 C6 C7 C8 4
11.64414
C9 C10 C14 C5 C6 C15 C2 C9 C10 C11 C14 C6 C7 C8 4
12.97830
8 C16 C10 C14 C5 6 C15 5
13.39433
C3 C13 C8 5
13.43799
C8 C7 C6 C8 5
14.24758
C3 C4 C13 C8 5
14.29125
C8 C7 C4 C13 C8 5

0 likes

1Answer

私の方の環境で実行したところ、C3 4 5をやるとpath_count_totalが9952になって、

Path paths[MAXQ * MAX_K];

ここに収まらずにbus errorになります。

// 深さ優先探索で全経路を探索し、目的地までの距離と経路を記録
void dfs(int u, int goal, double cost, int k) {
    if (path_count_total >= k && paths[k - 1].cost < cost) {
        return;
    }

    cur_path[cur_len++] = u;

    if (u == goal) {
        if (path_count_total >= k) {
            path_count_total--;
        }
        paths[path_count_total].cost = cost;
        paths[path_count_total].length = cur_len;
        for (int i = 0; i < cur_len; i++) {
            paths[path_count_total].path[i] = cur_path[i];
        }
        path_count_total++;
        qsort(paths, path_count_total, sizeof(Path), compare_path);
        cur_len--;
        return;
    }

    for (Edge* e = graph[u]; e; e = e->next) {
        int v = e->to;
        if (vis[v]) continue;
        vis[v] = 1;
        dfs(v, goal, cost + e->cost, k);
        vis[v] = 0;
    }

    cur_len--;
}

上のように上位k位までに入れない経路は足切りしちゃえば、無駄にpathsに保存することなくbus errorを回避できて、下記のようなそれっぽい出力が出ましたよ。

5.02588
C3 C4 4 
6.81441
C3 C13 C4 4 
6.81606
C3 C13 C8 4 
7.66932
C3 C4 C13 C8 4 
8.66463
C3 C12 3 C4 4 
8.24621
8 C11 C14 C6 C15 5 
8.88220
8 C11 C14 C6 C7 C15 5 
8.95958
8 C11 C10 C14 C6 C15 5 
9.05655
8 C11 C14 C5 C6 C15 5 
9.59557
8 C11 C10 C14 C6 C7 C15 5 
0Like

Comments

  1. @Iyatsu

    Questioner

    修正したところ提示していただいた結果になりましたが、不可解な点があったので確認していただきたいです。
    交差地点を検出したところ、発見された順で
    Intersection at: 1.86667 5.70000
    Intersection at: 3.29412 6.23529
    Intersection at: 3.20000 6.20000
    Intersection at: 4.82192 6.80822
    Intersection at: 6.75000 5.08333
    Intersection at: 5.84000 2.96000
    Intersection at: 6.06667 3.48889
    Intersection at: 5.39344 1.91803
    Intersection at: 4.00000 2.50000
    Intersection at: 2.70000 4.45000
    Intersection at: 3.76923 2.84615
    Intersection at: 3.21739 6.26087
    Intersection at: 5.28571 5.57143
    Intersection at: 6.22857 3.05714
    Intersection at: 4.26087 2.56522
    Intersection at: 2.45614 3.59649
    のようになったのですが、交差地点番号が発見順でつけられているとすると、例えばC3から4への経路は本来ならばC3->C2->C4->4になるんじゃないかと思います。

  2. // 点をソートして順番に辺を張る
    qsort(seg_pts, count, sizeof(Point), cmp_point);
    

    build_graph関数で線分ごとに発見した交点を全部seg_ptsに入れた後に、上記でX座標値の昇順でソートしてからget_node_indexをしていますから、1-4の間の交点は下記の通りに採番されています。

    C1=(1.86667,5.70000)
    C2=(3.20000,6.20000)
    C3=(3.29412,6.23529)
    C4=(4.82192,6.80822)
    

    Edgeを作成するときに、1-C1, C1-C2, C2-C3, C3-C4, C4-4 でX座標値の小さい順に2つずつの組で接続するんですから、この順番になっていないといけません。
    なので、C3(points[10])からならC3->C4->4が最短経路です。


    ちなみに、build_graphの後にpointsの中身を全部出力すると

        build_graph(M);   // グラフ構築
        for (int i = 0; i < node_count; i++) {
            printf("P[%d]=(%.5f, %.5f)\n", i, points[i].x, points[i].y);
        }
    
    P[0]=(0.00000, 5.00000)
    P[1]=(1.00000, 7.00000)
    P[2]=(4.00000, 9.00000)
    P[3]=(8.00000, 8.00000)
    P[4]=(10.00000, 4.00000)
    P[5]=(7.00000, 1.00000)
    P[6]=(5.00000, 1.00000)
    P[7]=(2.00000, 2.00000)
    P[8]=(1.86667, 5.70000)
    P[9]=(3.20000, 6.20000)
    P[10]=(3.29412, 6.23529)
    P[11]=(4.82192, 6.80822)
    P[12]=(5.39344, 1.91803)
    P[13]=(5.84000, 2.96000)
    P[14]=(6.06667, 3.48889)
    P[15]=(6.75000, 5.08333)
    P[16]=(2.70000, 4.45000)
    P[17]=(3.76923, 2.84615)
    P[18]=(4.00000, 2.50000)
    P[19]=(3.21739, 6.26087)
    P[20]=(5.28571, 5.57143)
    P[21]=(4.26087, 2.56522)
    P[22]=(6.22857, 3.05714)
    P[23]=(2.45614, 3.59649)
    

    こんな感じになってます。

  3. ついでにいうと、このソート部分はちょっと問題があって、

    5 3 0 1
    0 10000000
    1 0
    0 9000000
    2 9000000
    2 8999999
    1 2
    3 4
    3 5
    C1 2 5
    

    みたいな極端な数値の場合に、

    P[0]=(0.0000000000, 10000000.0000000000)
    P[1]=(1.0000000000, 0.0000000000)
    P[2]=(0.0000000000, 9000000.0000000000)
    P[3]=(2.0000000000, 9000000.0000000000)
    P[4]=(2.0000000000, 8999999.0000000000)
    P[5]=(0.1000000050, 8999999.9499999974)
    P[6]=(0.1000000000, 9000000.0000000000)
    

    こんな感じで局所的にX軸方向でのソートを諦めてY軸方向でのソートにしてしまうので、順番がおかしくなります。
    なので、X軸方向の差分とY軸方向の差分を比較してより大きな方向でソートするとかした方がいいと思います。

  4. @Iyatsu

    Questioner

    ご指摘いただいたソートの箇所を以下のように変更しました。

    Segment current_seg;
    Point current_a, current_b;
    
    int cmp_point_along_segment(const void *a, const void *b) {
        const Point *pa = (const Point *)a;
        const Point *pb = (const Point *)b;
    
        double dx = current_b.a - current_a.a;
        double dy = current_b.b - current_a.b;
    
        double da = (pa->a - current_a.a) * dx + (pa->b - current_a.b) * dy;
        double db = (pb->a - current_a.a) * dx + (pb->b - current_a.b) * dy;
    
        if (da < db) return -1;
        if (da > db) return 1;
        return 0;
    }
    
            // build_graph関数内
            current_a = a;
            current_b = b;
            // 点をソートして順番に辺を張る
            qsort(seg_pts, count, sizeof(Point), cmp_point_along_segment);
    
    

    この修正に対して最初の図と同様のテストをしたところ、以下のようになりました。

    5.02588
    C3 C4 4 
    6.81441
    C3 C13 C4 4 
    6.81606
    C3 C13 C5 4 
    7.66932
    C3 C4 C13 C5 4 
    8.66463
    C3 C12 3 C4 4 
    8.24621
    8 C9 C15 C7 C14 5 
    8.88220
    8 C9 C15 C7 C6 C14 5 
    8.95958
    8 C9 C10 C15 C7 C14 5 
    9.05655
    8 C9 C15 C8 C7 C14 5 
    9.59557
    8 C9 C10 C15 C7 C6 C14 5 
    

    経路について修正前と異なっている箇所がありますが、正しくできているか確認していただきたいです。
    また、ご提示いただいた極端な数値のテストケースにおいては以下のような出力になりました。

    9000000.00000
    C1 C2 2 
    9000000.16180
    C1 3 C2 2 
    

Your answer might help someone💌