# 作業內容
在 200x200 的正方形裡面畫出 5x5=25 個點,模擬道路隨機連通每個點,然後在道路上隨機布置一些點,最後輸入點的 id 還有 range 看看點可以到的範圍
# 程式實作
# 定義資料結構
# 點的 class
static class Point { | |
int id; | |
int x; | |
int y; | |
int xIndex; | |
int yIndex; | |
Point(int id, int x, int y, int xIndex, int yIndex) { | |
this.id = id; | |
this.x = x; | |
this.y = y; | |
this.xIndex = xIndex; | |
this.yIndex = yIndex; | |
} | |
// 計算兩點間的歐幾里得距離 | |
double distanceTo(Point other) { | |
return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2)); | |
} | |
} |
# 邊的 class
static class Edge { | |
Point start; | |
Point end; | |
double weight; | |
Edge(Point start, Point end) { | |
this.start = start; | |
this.end = end; | |
this.weight = start.distanceTo(end); | |
} | |
} |
# 隨機生成點
這邊限縮了一些範圍,使點比較集中在中心
int q_x = (i * seq + 10) + (int)(Math.random() * (seq - 20));
static void rand_generated_point(int xRange, int yRange, int seq) throws IOException { | |
List<String> lines = new ArrayList<>(); | |
int k = 0; | |
for (int i = 0; i < 5; i++) { | |
for (int j = 0; j < 5; j++) { | |
int q_x = (i * seq + 10) + (int)(Math.random() * (seq - 20)); | |
int q_y = (j * seq + 10) + (int)(Math.random() * (seq - 20)); | |
Point p = new Point(k, q_x, q_y, i, j); | |
points.add(p); | |
lines.add((k+1) + " " + q_x + " " + q_y); | |
// System.out.println("(" + q_x + ", " + q_y + ")"); | |
k++; | |
} | |
} | |
try (FileWriter fw = new FileWriter("rand_point.txt")) { | |
for (String line : lines) { | |
fw.write(line + "\r\n"); | |
} | |
} | |
String gnuplotPath = "C:\\Program Files\\gnuplot\\bin\\gnuplot.exe"; | |
String pltfile = "rand_point.plt"; | |
String pltFilePath = new File(pltfile).getAbsolutePath(); | |
ProcessBuilder pb = new ProcessBuilder(gnuplotPath, pltFilePath); | |
pb.start(); | |
} |
繪圖的 plt 檔案:
set term pngcairo font "AR PL UKai TW"
set output "rand_point.png"
set xlabel "x"
set ylabel "y"
unset key
set xrange [0:205]
set yrange [0:205]
set xtics 0, 40, 200
set ytics 0, 40, 200
set size square
plot "rand_point.txt" using 2:3:1 with labels point pointtype 7
# 隨機生成邊
這邊是我認為最困難的地方,要隨機路段又要確保都有連通,我最後採取了先生成最小生成樹,然後再隨機生成一些線段,我在思考應該使用 Prim演算法 還是 Kruskal演算法 ,最後我選擇了我幾乎沒寫過的 Kruskal演算法 。
static class DSU { | |
int[] parent; | |
DSU(int n) { | |
parent = new int[n]; | |
for (int i = 0; i < n; i++) parent[i] = i; | |
} | |
int find(int x) { | |
if (parent[x] != x) parent[x] = find(parent[x]); | |
return parent[x]; | |
} | |
void union(int x, int y) { | |
parent[find(x)] = find(y); | |
} | |
} | |
static void rand_generated_edges() throws IOException { | |
int gridSize = 5; | |
int n = gridSize * gridSize; | |
edges.clear(); | |
// Step 1: 建構所有合法邊(上下左右) | |
List<Edge> allEdges = new ArrayList<>(); | |
for (int i = 0; i < gridSize; i++) { | |
for (int j = 0; j < gridSize; j++) { | |
int idx = i * gridSize + j; | |
Point p = points.get(idx); | |
if (j < gridSize - 1) { // 右邊 | |
Point right = points.get(idx + 1); | |
allEdges.add(new Edge(p, right)); | |
} | |
if (i < gridSize - 1) { // 下邊 | |
Point down = points.get(idx + gridSize); | |
allEdges.add(new Edge(p, down)); | |
} | |
} | |
} | |
// Step 2: Kruskal's algorithm -> 最小生成樹,確保連通 | |
Collections.shuffle(allEdges); // 隨機順序,保證隨機生成樹 | |
DSU dsu = new DSU(n); | |
List<Edge> resultEdges = new ArrayList<>(); | |
for (Edge e : allEdges) { | |
int u = points.indexOf(e.start); | |
int v = points.indexOf(e.end); | |
if (dsu.find(u) != dsu.find(v)) { | |
dsu.union(u, v); | |
resultEdges.add(e); | |
} | |
} | |
// Step 3: 剩下的邊以一定機率加入,讓圖更密但仍保連通 | |
for (Edge e : allEdges) { | |
if (!resultEdges.contains(e) && Math.random() < 0.3) { | |
resultEdges.add(e); | |
} | |
} | |
edges.addAll(resultEdges); | |
// Step 4: 寫入檔案 | |
try (FileWriter fw = new FileWriter("rand_line.txt")) { | |
for (Edge e : edges) { | |
fw.write(e.start.x + " " + e.start.y + " " + e.end.x + " " + e.end.y + "\r\n"); | |
} | |
} | |
String gnuplotPath = "C:\\Program Files\\gnuplot\\bin\\gnuplot.exe"; | |
String pltfile = "rand_line.plt"; | |
String pltFilePath = new File(pltfile).getAbsolutePath(); | |
ProcessBuilder pb = new ProcessBuilder(gnuplotPath, pltFilePath); | |
pb.start(); | |
} |
繪圖的 plt 檔案:
set term pngcairo font "AR PL UKai TW"
set output "rand_line.png"
set xlabel "x"
set ylabel "y"
unset key
set xrange [0:205]
set yrange [0:205]
set xtics 0, 40, 200
set ytics 0, 40, 200
set size square
# 畫線:從 rand_line.txt 讀入,每行格式為 x1 y1 x2 y2
plot \
"rand_line.txt" using 1:2:($3-$1):($4-$2) with vectors nohead lt rgb "gray", \
"rand_point.txt" using 2:3:1 with labels point pointtype 7 font ",10" offset char 0,0.7
# 隨機生成垃圾在線段上
這邊使用了一些數學,根據隨機的 t 來計算垃圾的相對位置
P = A + t * (B - A) //2D 向量插值公式
int x = (int) Math.round (e.start.x + t * (e.end.x - e.start.x));
int y = (int)Math.round(e.start.y + t * (e.end.y - e.start.y));
static void rand_generated_trash() throws IOException { | |
int k = 0; | |
for (Edge e : edges){ | |
if (Math.random() > 0.33) continue; | |
double t = Math.random(); | |
int x = (int)Math.round(e.start.x + t * (e.end.x - e.start.x)); | |
int y = (int)Math.round(e.start.y + t * (e.end.y - e.start.y)); | |
Point trashPoint = new Point(k++,x,y,-1,-1); | |
trash.add(trashPoint); | |
} | |
try (FileWriter fw = new FileWriter("rand_trash.txt")) { | |
for (Point p : trash) { | |
fw.write(p.x + " " + p.y + "\r\n"); | |
} | |
} | |
String gnuplotPath = "C:\\Program Files\\gnuplot\\bin\\gnuplot.exe"; | |
String pltfile = "rand_trash.plt"; | |
String pltFilePath = new File(pltfile).getAbsolutePath(); | |
ProcessBuilder pb = new ProcessBuilder(gnuplotPath, pltFilePath); | |
pb.start(); | |
} |
繪圖的 plt 檔案:
set term pngcairo font "AR PL UKai TW"
set output "rand_trash.png"
set xlabel "x"
set ylabel "y"
unset key
set xrange [0:205]
set yrange [0:205]
set xtics 0, 40, 200
set ytics 0, 40, 200
set size square
plot \
"rand_line.txt" using 1:2:($3-$1):($4-$2) with vectors nohead lt rgb "gray", \
"rand_point.txt" using 2:3:1 with labels point pointtype 7 font ",10" offset char 0,0.7, \
"rand_trash.txt" using 1:2 with points pointtype 6 lc rgb "red" pointsize 1.2
# 輸入點的 id ,並且計算並劃出可以到的線段範圍
雖然我的強項是圖論,不過真的很少寫 java 還是有點吃力。
// BFS 用的內部類別 | |
static class State { | |
Point current; | |
double remaining; | |
State(Point current, double remaining) { | |
this.current = current; | |
this.remaining = remaining; | |
} | |
} | |
static void generated_range_line() throws IOException { | |
Scanner scanner = new Scanner(System.in); | |
System.out.print("請輸入起點 ID:"); | |
int id = scanner.nextInt(); | |
System.out.print("請輸入最大延伸距離:"); | |
double range = scanner.nextDouble(); | |
scanner.close(); | |
// 將有向圖改成無向圖 | |
List<Edge> originalEdges = new ArrayList<>(edges); | |
for (Edge e : originalEdges) { | |
edges.add(new Edge(e.end, e.start)); | |
} | |
// 建立鄰接表 | |
Map<Point, List<Edge>> graph = new HashMap<>(); | |
for (Edge e : edges) { | |
graph.computeIfAbsent(e.start, k -> new ArrayList<>()).add(e); | |
} | |
// 取得起始點 | |
Point startPoint = null; | |
for (Point p : points) { | |
if (p.id == id-1) { | |
startPoint = p; | |
break; | |
} | |
} | |
if (startPoint == null) { | |
System.err.println("起點 ID 不存在!"); | |
return; | |
} | |
// BFS 結構 | |
Set<Point> visited = new HashSet<>(); | |
Queue<State> queue = new LinkedList<>(); | |
List<String> outputLines = new ArrayList<>(); | |
queue.offer(new State(startPoint, range)); | |
visited.add(startPoint); | |
while (!queue.isEmpty()) { | |
State state = queue.poll(); | |
Point current = state.current; | |
double remaining = state.remaining; | |
if (!graph.containsKey(current)) continue; | |
for (Edge edge : graph.get(current)) { | |
if (visited.contains(edge.end)) continue; | |
double cost = edge.weight; | |
if (cost <= remaining) { | |
// 可以走完整段 | |
outputLines.add(edge.start.x + " " + edge.start.y + " " + edge.end.x + " " + edge.end.y); | |
queue.offer(new State(edge.end, remaining - cost)); | |
visited.add(edge.end); | |
} else { | |
// 無法走完整段,但還是要畫出部分線段 | |
double t = remaining / cost; | |
int midX = (int)Math.round(edge.start.x + t * (edge.end.x - edge.start.x)); | |
int midY = (int)Math.round(edge.start.y + t * (edge.end.y - edge.start.y)); | |
outputLines.add(edge.start.x + " " + edge.start.y + " " + midX + " " + midY); | |
} | |
} | |
} | |
// 輸出結果到 range_line.txt | |
try (FileWriter fw = new FileWriter("range_line.txt")) { | |
for (String line : outputLines) { | |
fw.write(line + "\r\n"); | |
} | |
} | |
String gnuplotPath = "C:\\Program Files\\gnuplot\\bin\\gnuplot.exe"; | |
String pltfile = "range_line.plt"; | |
String pltFilePath = new File(pltfile).getAbsolutePath(); | |
ProcessBuilder pb = new ProcessBuilder(gnuplotPath, pltFilePath); | |
pb.start(); | |
} |
繪圖的 plt 檔案:
set term pngcairo font "AR PL UKai TW"
set output "range_line.png"
set xlabel "x"
set ylabel "y"
unset key
set xrange [0:205]
set yrange [0:205]
set xtics 0, 40, 200
set ytics 0, 40, 200
set size square
plot \
"rand_line.txt" using 1:2:($3-$1):($4-$2) with vectors nohead lt rgb "gray", \
"range_line.txt" using 1:2:($3-$1):($4-$2) with vectors nohead lt rgb "blue" lw 2, \
"rand_point.txt" using 2:3:1 with labels point pointtype 7 font ",10" offset char 0,0.7, \
"rand_trash.txt" using 1:2 with points pointtype 6 lc rgb "red" pointsize 1.2
# 總結
# 流程
按照順序查看四張圖,可以一步一步看出程式碼做了什麼:
- rand_point.png
- rand_line.png
- rand_trash.png
- range_line.png
# 完整程式碼
import java.io.File; | |
import java.io.FileWriter; | |
import java.io.IOException; | |
import java.util.*; | |
public class GridRoadSimulator { | |
static List<Point> points = new ArrayList<>(); | |
static List<Edge> edges = new ArrayList<>(); | |
static List<Point> trash = new ArrayList<>(); | |
// 點的資料結構 | |
static class Point { | |
int id; | |
int x; | |
int y; | |
int xIndex; | |
int yIndex; | |
Point(int id, int x, int y, int xIndex, int yIndex) { | |
this.id = id; | |
this.x = x; | |
this.y = y; | |
this.xIndex = xIndex; | |
this.yIndex = yIndex; | |
} | |
// 計算兩點間的歐幾里得距離 | |
double distanceTo(Point other) { | |
return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2)); | |
} | |
} | |
static class Edge { | |
Point start; | |
Point end; | |
double weight; | |
Edge(Point start, Point end) { | |
this.start = start; | |
this.end = end; | |
this.weight = start.distanceTo(end); | |
} | |
} | |
static class DSU { | |
int[] parent; | |
DSU(int n) { | |
parent = new int[n]; | |
for (int i = 0; i < n; i++) parent[i] = i; | |
} | |
int find(int x) { | |
if (parent[x] != x) parent[x] = find(parent[x]); | |
return parent[x]; | |
} | |
void union(int x, int y) { | |
parent[find(x)] = find(y); | |
} | |
} | |
static void rand_generated_point(int xRange, int yRange, int seq) throws IOException { | |
List<String> lines = new ArrayList<>(); | |
int k = 0; | |
for (int i = 0; i < 5; i++) { | |
for (int j = 0; j < 5; j++) { | |
int q_x = (i * seq + 10) + (int)(Math.random() * (seq - 20)); | |
int q_y = (j * seq + 10) + (int)(Math.random() * (seq - 20)); | |
Point p = new Point(k, q_x, q_y, i, j); | |
points.add(p); | |
lines.add((k+1) + " " + q_x + " " + q_y); | |
// System.out.println("(" + q_x + ", " + q_y + ")"); | |
k++; | |
} | |
} | |
try (FileWriter fw = new FileWriter("rand_point.txt")) { | |
for (String line : lines) { | |
fw.write(line + "\r\n"); | |
} | |
} | |
String gnuplotPath = "C:\\Program Files\\gnuplot\\bin\\gnuplot.exe"; | |
String pltfile = "rand_point.plt"; | |
String pltFilePath = new File(pltfile).getAbsolutePath(); | |
ProcessBuilder pb = new ProcessBuilder(gnuplotPath, pltFilePath); | |
pb.start(); | |
} | |
static void rand_generated_edges() throws IOException { | |
int gridSize = 5; | |
int n = gridSize * gridSize; | |
edges.clear(); | |
// Step 1: 建構所有合法邊(上下左右) | |
List<Edge> allEdges = new ArrayList<>(); | |
for (int i = 0; i < gridSize; i++) { | |
for (int j = 0; j < gridSize; j++) { | |
int idx = i * gridSize + j; | |
Point p = points.get(idx); | |
if (j < gridSize - 1) { // 右邊 | |
Point right = points.get(idx + 1); | |
allEdges.add(new Edge(p, right)); | |
} | |
if (i < gridSize - 1) { // 下邊 | |
Point down = points.get(idx + gridSize); | |
allEdges.add(new Edge(p, down)); | |
} | |
} | |
} | |
// Step 2: Kruskal's algorithm -> 最小生成樹,確保連通 | |
Collections.shuffle(allEdges); // 隨機順序,保證隨機生成樹 | |
DSU dsu = new DSU(n); | |
List<Edge> resultEdges = new ArrayList<>(); | |
for (Edge e : allEdges) { | |
int u = points.indexOf(e.start); | |
int v = points.indexOf(e.end); | |
if (dsu.find(u) != dsu.find(v)) { | |
dsu.union(u, v); | |
resultEdges.add(e); | |
} | |
} | |
// Step 3: 剩下的邊以一定機率加入,讓圖更密但仍保連通 | |
for (Edge e : allEdges) { | |
if (!resultEdges.contains(e) && Math.random() < 0.3) { | |
resultEdges.add(e); | |
} | |
} | |
edges.addAll(resultEdges); | |
// Step 4: 寫入檔案 | |
try (FileWriter fw = new FileWriter("rand_line.txt")) { | |
for (Edge e : edges) { | |
fw.write(e.start.x + " " + e.start.y + " " + e.end.x + " " + e.end.y + "\r\n"); | |
} | |
} | |
String gnuplotPath = "C:\\Program Files\\gnuplot\\bin\\gnuplot.exe"; | |
String pltfile = "rand_line.plt"; | |
String pltFilePath = new File(pltfile).getAbsolutePath(); | |
ProcessBuilder pb = new ProcessBuilder(gnuplotPath, pltFilePath); | |
pb.start(); | |
} | |
static void rand_generated_trash() throws IOException { | |
int k = 0; | |
for (Edge e : edges){ | |
if (Math.random() > 0.33) continue; | |
double t = Math.random(); | |
int x = (int)Math.round(e.start.x + t * (e.end.x - e.start.x)); | |
int y = (int)Math.round(e.start.y + t * (e.end.y - e.start.y)); | |
Point trashPoint = new Point(k++,x,y,-1,-1); | |
trash.add(trashPoint); | |
} | |
try (FileWriter fw = new FileWriter("rand_trash.txt")) { | |
for (Point p : trash) { | |
fw.write(p.x + " " + p.y + "\r\n"); | |
} | |
} | |
String gnuplotPath = "C:\\Program Files\\gnuplot\\bin\\gnuplot.exe"; | |
String pltfile = "rand_trash.plt"; | |
String pltFilePath = new File(pltfile).getAbsolutePath(); | |
ProcessBuilder pb = new ProcessBuilder(gnuplotPath, pltFilePath); | |
pb.start(); | |
} | |
static void generated_range_line() throws IOException { | |
Scanner scanner = new Scanner(System.in); | |
System.out.print("請輸入起點 ID:"); | |
int id = scanner.nextInt(); | |
System.out.print("請輸入最大延伸距離:"); | |
double range = scanner.nextDouble(); | |
scanner.close(); | |
// 將有向圖改成無向圖 | |
List<Edge> originalEdges = new ArrayList<>(edges); | |
for (Edge e : originalEdges) { | |
edges.add(new Edge(e.end, e.start)); | |
} | |
// 建立鄰接表 | |
Map<Point, List<Edge>> graph = new HashMap<>(); | |
for (Edge e : edges) { | |
graph.computeIfAbsent(e.start, k -> new ArrayList<>()).add(e); | |
} | |
// 取得起始點 | |
Point startPoint = null; | |
for (Point p : points) { | |
if (p.id == id-1) { | |
startPoint = p; | |
break; | |
} | |
} | |
if (startPoint == null) { | |
System.err.println("起點 ID 不存在!"); | |
return; | |
} | |
// BFS 結構 | |
Set<Point> visited = new HashSet<>(); | |
Queue<State> queue = new LinkedList<>(); | |
List<String> outputLines = new ArrayList<>(); | |
queue.offer(new State(startPoint, range)); | |
visited.add(startPoint); | |
while (!queue.isEmpty()) { | |
State state = queue.poll(); | |
Point current = state.current; | |
double remaining = state.remaining; | |
if (!graph.containsKey(current)) continue; | |
for (Edge edge : graph.get(current)) { | |
if (visited.contains(edge.end)) continue; | |
double cost = edge.weight; | |
if (cost <= remaining) { | |
// 可以走完整段 | |
outputLines.add(edge.start.x + " " + edge.start.y + " " + edge.end.x + " " + edge.end.y); | |
queue.offer(new State(edge.end, remaining - cost)); | |
visited.add(edge.end); | |
} else { | |
// 無法走完整段,但還是要畫出部分線段 | |
double t = remaining / cost; | |
int midX = (int)Math.round(edge.start.x + t * (edge.end.x - edge.start.x)); | |
int midY = (int)Math.round(edge.start.y + t * (edge.end.y - edge.start.y)); | |
outputLines.add(edge.start.x + " " + edge.start.y + " " + midX + " " + midY); | |
} | |
} | |
} | |
// 輸出結果到 range_line.txt | |
try (FileWriter fw = new FileWriter("range_line.txt")) { | |
for (String line : outputLines) { | |
fw.write(line + "\r\n"); | |
} | |
} | |
String gnuplotPath = "C:\\Program Files\\gnuplot\\bin\\gnuplot.exe"; | |
String pltfile = "range_line.plt"; | |
String pltFilePath = new File(pltfile).getAbsolutePath(); | |
ProcessBuilder pb = new ProcessBuilder(gnuplotPath, pltFilePath); | |
pb.start(); | |
} | |
// BFS 用的內部類別 | |
static class State { | |
Point current; | |
double remaining; | |
State(Point current, double remaining) { | |
this.current = current; | |
this.remaining = remaining; | |
} | |
} | |
public static void main(String[] args) { | |
int xRange = 200; | |
int yRange = 200; | |
int numPoints = 25; | |
try { | |
rand_generated_point(xRange, yRange, 40); | |
} catch (IOException e) { | |
System.err.println("An error occurred: " + e.getMessage()); | |
} | |
// for (Point p : points) { | |
// System.out.println("Point ID: " + p.id + ", Coordinates: (" + p.x + ", " + p.y + ")"); | |
// } | |
try { | |
rand_generated_edges(); | |
} catch (Exception e) { | |
System.err.println("An error occurred while generating edges: " + e.getMessage()); | |
} | |
// for (Edge e : edges) { | |
// System.out.println(e.start.x + " " + e.start.y + " " + e.end.x + " " + e.end.y + "\r\n"); | |
// } | |
try { | |
rand_generated_trash(); | |
} catch (Exception e) { | |
System.err.println("An error occurred while generating trash: " + e.getMessage()); | |
} | |
try { | |
generated_range_line(); | |
} catch (Exception e) { | |
System.err.println("An error occurred while generating range lines: " + e.getMessage()); | |
} | |
} | |
} |
# 結果圖片

# 心得
我其實有發現我有地方與教授的不太一樣,不過我真的太晚開始寫了,希望教授不會發現,不過應該大致一樣,我蠻好奇別人生成邊的地方別人都怎麼寫,那邊先隨機然後判斷再加上邊的寫法感覺會很複雜。