init commit

This commit is contained in:
2026-08-04 06:58:30 -06:00
commit 2b551e1e3a
7 changed files with 865 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
public class App {
public static void main(String[] args) {
GUI.setup();
}
}
+114
View File
@@ -0,0 +1,114 @@
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import javax.swing.JPanel;
import javax.swing.text.Position;
import edu.princeton.cs.algs4.Edge;
public class DisplayPanel extends JPanel {
private static ST<Integer, Vertex> st;
private int n;
private static final int SIZE = 256;
private int a = SIZE / 2;
private int b = a;
private int r = 4 * SIZE / 5;
public DisplayPanel() {
super();
setPreferredSize(new Dimension(300, 300));
setSize(getPreferredSize());
}
@Override
protected void paintComponent(Graphics g) {
st = new ST<>();
EdgeWeightedGraph G = GraphFunctions.getGraph();
for(int v = 0; v < G.V(); v++)
st.put(v, new Vertex(v));
n = st.size();
// draw the entire graph first
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(Color.WHITE);
a = getWidth() / 2;
b = getHeight() / 2;
int m = Math.min(a, b);
r = 4 * m / 5;
int r2;
if(n > 0) r2 = Math.abs(m - r) / n;
else r2 = Math.abs(m - r) / 2;
for (int i = 0; i < n; i++) {
double t = 2 * Math.PI * i / n;
int x = (int) Math.round(a + r * Math.cos(t));
int y = (int) Math.round(b + r * Math.sin(t));
g2d.fillOval(x - r2, y - r2, 2 * r2, 2 * r2);
st.put(i, new Vertex(x, y));
}
g.getFont().deriveFont(Font.BOLD);
g2d.setColor(Color.ORANGE);
if(n < 50) {
for(int i = 0; i < n; i++) {
int x = st.get(i).getX();
int y = st.get(i).getY();
g.drawString(i + "", x - (2 * r2), y + (2 * r2));
}
}
drawLine(r2, g, g2d, G.edges(), Color.WHITE);
drawLine(r2, g, g2d, GraphFunctions.getMST(), Color.GREEN);
}
private void drawLine(int r2, Graphics g, Graphics2D g2d, Iterable<Edge> edges, Color color) {
g2d.setColor(color);
for (Edge e: edges) {
int v = e.either();
int w = e.other(v);
int vX = st.get(v).getX();
int wX = st.get(w).getX();
int vY = st.get(v).getY();
int wY = st.get(w).getY();
g.drawLine(vX, vY, wX, wY);
g.drawString(String.format("%.0f", e.weight()), ((vX + wX) / 2) + r2, ((vY + wY) / 2) + r2);
}
}
private class Vertex { // extends JComponent for @Override of paint()?
private int x;
private int y;
private int v;
public Vertex(int v) {
this.v = v;
}
public Vertex(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getV() {
return v;
}
}
}
+204
View File
@@ -0,0 +1,204 @@
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.Edge;
import edu.princeton.cs.algs4.StdRandom;
import java.util.NoSuchElementException;
import java.util.ArrayList;
/**
*
* Modification of Sedgewick and Wayne's EdgeWeightedGraph, to allow a dynamic number of vertices based upon user input.
*
* @author Robert Sedgewick
* @author Kevin Wayne
* @author Josh Ashton
*/
public class EdgeWeightedGraph {
private static final String NEWLINE = System.getProperty("line.separator");
private int V;
private int E;
private ArrayList<Bag<Edge>> adj;
/**
* Create a new EdgeWeightedGraph with no vertices or edges. These may be added dynamically by the user.
*/
public EdgeWeightedGraph() {
this.V = 0;
this.E = 0;
adj = new ArrayList<>();
}
public EdgeWeightedGraph(In in) {
if (in == null) throw new IllegalArgumentException("argument is null");
try {
V = in.readInt();
adj = new ArrayList<Bag<Edge>>();
for (int v = 0; v < V; v++) {
adj.add(new Bag<Edge>());
}
int E = in.readInt();
if (E < 0) throw new IllegalArgumentException("Number of edges must be non-negative");
for (int i = 0; i < E; i++) {
int v = in.readInt();
int w = in.readInt();
validateVertex(v);
validateVertex(w);
double weight = in.readDouble();
Edge e = new Edge(v, w, weight);
addEdge(e);
}
}
catch (NoSuchElementException e) {
throw new IllegalArgumentException("invalid input format in EdgeWeightedGraph constructor", e);
}
}
/**
* Initializes a random edge-weighted graph with {@code V} vertices and <em>E</em> edges.
*
* @param V the number of vertices
* @param E the number of edges
* @throws IllegalArgumentException if {@code V < 0}
* @throws IllegalArgumentException if {@code E < 0}
*/
public EdgeWeightedGraph(int V, int E) {
this.V = V;
adj = new ArrayList<Bag<Edge>>();
for (int v = 0; v < V; v++) {
adj.add(new Bag<Edge>());
}
if (E < 0) throw new IllegalArgumentException("Number of edges must be non-negative");
for (int i = 0; i < E; i++) {
int v = StdRandom.uniformInt(V);
int w = StdRandom.uniformInt(V);
int weight = StdRandom.uniformInt(0, 100);
Edge e = new Edge(v, w, (double) weight);
addEdge(e);
}
}
/**
* Adds a new vertex to the edge-weighted graph.
*/
public void addVertex() {
Bag<Edge> b = new Bag<>();
V++;
adj.add(b);
}
/**
* Adds the undirected edge {@code e} to this edge-weighted graph.
*
* @param e the edge
* @throws IllegalArgumentException unless both endpoints are between {@code 0} and {@code V-1}
*/
public void addEdge(Edge e) {
int v = e.either();
int w = e.other(v);
validateVertex(v);
validateVertex(w);
adj.get(v).add(e);
adj.get(w).add(e);
E++;
}
/**
* Returns the number of vertices in this edge-weighted graph.
*
* @return the number of vertices in this edge-weighted graph
*/
public int V() {
return V;
}
/**
* Returns the number of edges in this edge-weighted graph.
*
* @return the number of edges in this edge-weighted graph
*/
public int E() {
return E;
}
// throw an IllegalArgumentException unless {@code 0 <= v < V}
private void validateVertex(int v) {
if (v < 0 || v >= V)
throw new IllegalArgumentException("vertex " + v + " is not between 0 and " + (V-1));
}
/**
* Returns the edges incident on vertex {@code v}.
*
* @param v the vertex
* @return the edges incident on vertex {@code v} as an Iterable
* @throws IllegalArgumentException unless {@code 0 <= v < V}
*/
public Iterable<Edge> adj(int v) {
validateVertex(v);
return adj.get(v);
}
/**
* Returns the degree of vertex {@code v}.
*
* @param v the vertex
* @return the degree of vertex {@code v}
* @throws IllegalArgumentException unless {@code 0 <= v < V}
*/
public int degree(int v) {
validateVertex(v);
return adj.get(v).size();
}
/**
* Returns all edges in this edge-weighted graph.
* To iterate over the edges in this edge-weighted graph, use foreach notation:
* {@code for (Edge e : G.edges())}.
*
* @return all edges in this edge-weighted graph, as an iterable
*/
public Iterable<Edge> edges() {
Bag<Edge> list = new Bag<Edge>();
for (int v = 0; v < V; v++) {
int selfLoops = 0;
for (Edge e : adj(v)) {
if (e.other(v) > v) {
list.add(e);
}
// add only one copy of each self loop (self loops will be consecutive)
else if (e.other(v) == v) {
if (selfLoops % 2 == 0) list.add(e);
selfLoops++;
}
}
}
return list;
}
/**
* Returns a string representation of the edge-weighted graph.
* This method takes time proportional to <em>E</em> + <em>V</em>.
*
* @return the number of vertices <em>V</em>, followed by the number of edges <em>E</em>,
* followed by the <em>V</em> adjacency lists of edges
*/
public String toString() {
StringBuilder s = new StringBuilder();
s.append("vertices: " + V + " || edges: " + E + NEWLINE);
for (int v = 0; v < V; v++) {
s.append("vertex " + v + " edges: ");
for (Edge e : adj.get(v)) {
s.append(e + " ");
}
s.append(NEWLINE);
}
return s.toString();
}
}
+237
View File
@@ -0,0 +1,237 @@
import edu.princeton.cs.algs4.StdRandom;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseListener;
import java.io.File;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenuBar;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.border.BevelBorder;
public class GUI {
private final static Color bg = Color.DARK_GRAY;
private final static Color fg = Color.WHITE;
private static JFrame frame;
private static JPanel contentPane;
public static void setup() {
frame = new JFrame("Graph");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setPreferredSize(new Dimension(1000, 1000));
frame.setSize(frame.getPreferredSize());
frame.setResizable(false);
try {
UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
} catch (Exception e) {
System.out.println("Error with cross platform look/feel in frameSetup().");
}
GraphFunctions.init();
frame.add(createContentPane());
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private static JPanel createContentPane() {
contentPane = new JPanel(new BorderLayout());
contentPane.add(new DisplayPanel(), BorderLayout.CENTER);
contentPane.add(createButtonPane(), BorderLayout.NORTH);
contentPane.setForeground(fg);
contentPane.setBackground(bg);
return contentPane;
}
private static JPanel createButtonPane() {
JPanel pane = new JPanel();
pane.setForeground(fg);
pane.setBackground(bg);
pane.setAlignmentX(0.5f);
pane.setLayout(new BoxLayout(pane, BoxLayout.X_AXIS));
pane.setBorder(BorderFactory.createLineBorder(Color.BLACK));
pane.add(createAddVPanel());
JTextField vertex1 = new JTextField();
JTextField vertex2 = new JTextField();
JTextField eWeight = new JTextField();
vertex1.setForeground(fg);
vertex1.setBackground(bg);
vertex2.setForeground(fg);
vertex2.setBackground(bg);
eWeight.setForeground(fg);
eWeight.setBackground(bg);
pane.add(vertex1);
pane.add(vertex2);
pane.add(eWeight);
JButton b = new JButton("Add an edge");
b.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
if(vertex1.getText().equals("") || vertex2.getText().equals("") || eWeight.getText().equals("")) {
int v = StdRandom.uniformInt(GraphFunctions.getGraph().V());
int w = StdRandom.uniformInt(GraphFunctions.getGraph().V());
double weight = StdRandom.uniformInt(0, 100);
GraphFunctions.addEdge(v, w, weight);
}
else {
GraphFunctions.addEdge(Integer.parseInt(vertex1.getText()),
Integer.parseInt(vertex2.getText()),
Double.parseDouble(eWeight.getText()));
}
contentPane.removeAll();
frame.setContentPane(createContentPane());
contentPane.repaint();
contentPane.revalidate();
}
});
b.setFocusPainted(false);
b.setFocusable(false);
b.setForeground(fg);
b.setBackground(bg);
pane.add(b);
JPanel p = new JPanel(new GridLayout(1, 2));
JPanel subPanel = new JPanel(new GridLayout(2, 1));
b = new JButton("Reset graph");
b.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
GraphFunctions.reset();
contentPane.removeAll();
frame.setContentPane(createContentPane());
contentPane.repaint();
contentPane.revalidate();
}
});
b.setFocusPainted(false);
b.setFocusable(false);
b.setForeground(fg);
b.setBackground(bg);
subPanel.add(b);
b = new JButton("Load graph");
b.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
JFileChooser fc = new JFileChooser("~");
int returnVal = fc.showOpenDialog(frame);
if (returnVal == JFileChooser.APPROVE_OPTION) {
GraphFunctions.loadGraph(fc.getSelectedFile());
contentPane.removeAll();
frame.setContentPane(createContentPane());
contentPane.repaint();
contentPane.revalidate();
}
}
});
b.setFocusPainted(false);
b.setFocusable(false);
b.setForeground(fg);
b.setBackground(bg);
subPanel.add(b);
p.add(subPanel);
p.add(createRandomPanel());
contentPane.add(p, BorderLayout.SOUTH);
return pane;
}
private static JPanel createAddVPanel() {
JPanel vertPnl = new JPanel(new GridLayout(1, 3));
vertPnl.setForeground(fg);
vertPnl.setBackground(bg);
JLabel verticesLbl = new JLabel("Number of vertices to add: ");
verticesLbl.setForeground(fg);
verticesLbl.setBackground(bg);
vertPnl.add(verticesLbl);
JTextField addVertices = new JTextField();
addVertices.setForeground(fg);
addVertices.setBackground(bg);
vertPnl.add(addVertices);
JButton b = new JButton("Add vertices");
b.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
if(addVertices.getText().equals("")) GraphFunctions.addVertex();
else {
for(int i = 0; i < Integer.parseInt(addVertices.getText()); i++)
GraphFunctions.addVertex();
}
contentPane.removeAll();
frame.setContentPane(createContentPane());
contentPane.repaint();
contentPane.revalidate();
}
});
b.setFocusPainted(false);
b.setFocusable(false);
b.setForeground(fg);
b.setBackground(bg);
vertPnl.add(b);
return vertPnl;
}
private static JPanel createRandomPanel() {
JPanel randomPanel = new JPanel(new GridLayout(2, 1));
randomPanel.setForeground(fg);
randomPanel.setBackground(bg);
JPanel subPanel = new JPanel(new GridLayout(2, 2));
subPanel.setForeground(fg);
subPanel.setBackground(bg);
JLabel veLabel = new JLabel("Number of Vertices:");
veLabel.setForeground(fg);
JTextField vertexCount = new JTextField();
vertexCount.setForeground(fg);
vertexCount.setBackground(bg);
JLabel edLabel = new JLabel("Number of edges:");
edLabel.setForeground(fg);
JTextField edgesCount = new JTextField();
edgesCount.setForeground(fg);
edgesCount.setBackground(bg);
subPanel.add(veLabel);
subPanel.add(vertexCount);
subPanel.add(edLabel);
subPanel.add(edgesCount);
randomPanel.add(subPanel);
JButton b = new JButton("Random graph");
b.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
if(vertexCount.getText().equals("") || edgesCount.getText().equals(""))GraphFunctions.random(10, 20);
else GraphFunctions.random(Integer.parseInt(vertexCount.getText()), Integer.parseInt(edgesCount.getText()));
contentPane.removeAll();
frame.setContentPane(createContentPane());
contentPane.repaint();
contentPane.revalidate();
}
});
b.setFocusPainted(false);
b.setFocusable(false);
b.setForeground(fg);
b.setBackground(bg);
randomPanel.add(b);
return randomPanel;
}
}
+76
View File
@@ -0,0 +1,76 @@
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.KruskalMST;
import java.io.File;
import java.util.Random;
import edu.princeton.cs.algs4.Edge;
/**
* @author Josh Ashton
*/
public class GraphFunctions {
private static KruskalMST mst;
private static EdgeWeightedGraph g = new EdgeWeightedGraph(); // TODO: Convert to a DirectedEdgeWeightedGraph at some point.
public static void init() {
String fileName = "./resources/GraphInternet.txt";
g = new EdgeWeightedGraph(new In(fileName));
edu.princeton.cs.algs4.EdgeWeightedGraph G = new edu.princeton.cs.algs4.EdgeWeightedGraph(g.V());
for (Edge e : g.edges()) {
G.addEdge(e);
}
mst = new KruskalMST(G);
}
public static void random(int V, int E) {
g = new EdgeWeightedGraph(V, E);
edu.princeton.cs.algs4.EdgeWeightedGraph G = new edu.princeton.cs.algs4.EdgeWeightedGraph(g.V());
for (Edge e : g.edges()) {
G.addEdge(e);
}
mst = new KruskalMST(G);
}
public static void reset() {
g = new EdgeWeightedGraph();
edu.princeton.cs.algs4.EdgeWeightedGraph G = new edu.princeton.cs.algs4.EdgeWeightedGraph(g.V());
for (Edge e : g.edges()) {
G.addEdge(e);
}
mst = new KruskalMST(G);
}
public static void loadGraph(File f) {
g = new EdgeWeightedGraph(new In(f));
edu.princeton.cs.algs4.EdgeWeightedGraph G = new edu.princeton.cs.algs4.EdgeWeightedGraph(g.V());
for (Edge e : g.edges()) {
G.addEdge(e);
}
mst = new KruskalMST(G);
}
public static void addVertex() {
g.addVertex();
}
public static void addEdge(int v, int w, double cost) {
g.addEdge(new Edge(v, w, cost));
}
public static Iterable<Edge> getMST() {
edu.princeton.cs.algs4.EdgeWeightedGraph G = new edu.princeton.cs.algs4.EdgeWeightedGraph(g.V());
for (Edge e : g.edges()) {
G.addEdge(e);
}
return new KruskalMST(G).edges();
}
public static EdgeWeightedGraph getGraph() {
return g;
}
}
+206
View File
@@ -0,0 +1,206 @@
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.TreeMap;
/**
* For additional documentation, see
* <a href="https://algs4.cs.princeton.edu/35applications">Section 3.5</a> of
* <i>Algorithms, 4th Edition</i> by Robert Sedgewick and Kevin Wayne.
*
* @author Robert Sedgewick
* @author Kevin Wayne
*
* @param <Key> the generic type of keys in this symbol table
* @param <Value> the generic type of values in this symbol table
*/
public class ST<Key extends Comparable<Key>, Value> implements Iterable<Key> {
private TreeMap<Key, Value> st;
/**
* Initializes an empty symbol table.
*/
public ST() {
st = new TreeMap<Key, Value>();
}
/**
* Returns the value associated with the given key in this symbol table.
*
* @param key the key
* @return the value associated with the given key if the key is in this symbol table;
* {@code null} if the key is not in this symbol table
* @throws IllegalArgumentException if {@code key} is {@code null}
*/
public Value get(Key key) {
if (key == null) throw new IllegalArgumentException("calls get() with null key");
return st.get(key);
}
/**
* Inserts the specified key-value pair into the symbol table, overwriting the old
* value with the new value if the symbol table already contains the specified key.
* Deletes the specified key (and its associated value) from this symbol table
* if the specified value is {@code null}.
*
* @param key the key
* @param val the value
* @throws IllegalArgumentException if {@code key} is {@code null}
*/
public void put(Key key, Value val) {
if (key == null) throw new IllegalArgumentException("calls put() with null key");
if (val == null) st.remove(key);
else st.put(key, val);
}
/**
* Removes the specified key and its associated value from this symbol table
* (if the key is in this symbol table).
* This is equivalent to {@code remove()}, but we plan to deprecate {@code delete()}.
*
* @param key the key
* @throws IllegalArgumentException if {@code key} is {@code null}
*/
public void delete(Key key) {
if (key == null) throw new IllegalArgumentException("calls delete() with null key");
st.remove(key);
}
/**
* Removes the specified key and its associated value from this symbol table
* (if the key is in this symbol table).
* This is equivalent to {@code delete()}, but we plan to deprecate {@code delete()}.
*
* @param key the key
* @throws IllegalArgumentException if {@code key} is {@code null}
*/
public void remove(Key key) {
if (key == null) throw new IllegalArgumentException("calls remove() with null key");
st.remove(key);
}
/**
* Returns true if this symbol table contain the given key.
*
* @param key the key
* @return {@code true} if this symbol table contains {@code key} and
* {@code false} otherwise
* @throws IllegalArgumentException if {@code key} is {@code null}
*/
public boolean contains(Key key) {
if (key == null) throw new IllegalArgumentException("calls contains() with null key");
return st.containsKey(key);
}
/**
* Returns the number of key-value pairs in this symbol table.
*
* @return the number of key-value pairs in this symbol table
*/
public int size() {
return st.size();
}
/**
* Returns true if this symbol table is empty.
*
* @return {@code true} if this symbol table is empty and {@code false} otherwise
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* Returns all keys in this symbol table in ascending order,
* as an {@code Iterable}.
* <p>
* To iterate over all of the keys in the symbol table named {@code st},
* use the foreach notation: {@code for (Key key : st.keys())}.
*
* @return all keys in this symbol table in ascending order
*/
public Iterable<Key> keys() {
return st.keySet();
}
/**
* Returns all keys in this symbol table in ascending order.
* To iterate over all of the keys in a symbol table named {@code st}, use the
* foreach notation: {@code for (Key key : st)}.
* <p>
* This method is provided for backward compatibility with the version from
* <em>Introduction to Programming in Java: An Interdisciplinary Approach.</em>
*
* @return all keys in this symbol table in ascending order
* @deprecated Replaced by {@link #keys()}.
*/
@Deprecated
public Iterator<Key> iterator() {
return st.keySet().iterator();
}
/**
* Returns the smallest key in this symbol table.
*
* @return the smallest key in this symbol table
* @throws NoSuchElementException if this symbol table is empty
*/
public Key min() {
if (isEmpty()) throw new NoSuchElementException("calls min() with empty symbol table");
return st.firstKey();
}
/**
* Returns the largest key in this symbol table.
*
* @return the largest key in this symbol table
* @throws NoSuchElementException if this symbol table is empty
*/
public Key max() {
if (isEmpty()) throw new NoSuchElementException("calls max() with empty symbol table");
return st.lastKey();
}
/**
* Returns the smallest key in this symbol table greater than or equal to {@code key}.
*
* @param key the key
* @return the smallest key in this symbol table greater than or equal to {@code key}
* @throws NoSuchElementException if there is no such key
* @throws IllegalArgumentException if {@code key} is {@code null}
*/
public Key ceiling(Key key) {
if (key == null) throw new IllegalArgumentException("argument to ceiling() is null");
Key k = st.ceilingKey(key);
if (k == null) throw new NoSuchElementException("argument to ceiling() is too large");
return k;
}
/**
* Returns the largest key in this symbol table less than or equal to {@code key}.
*
* @param key the key
* @return the largest key in this symbol table less than or equal to {@code key}
* @throws NoSuchElementException if there is no such key
* @throws IllegalArgumentException if {@code key} is {@code null}
*/
public Key floor(Key key) {
if (key == null) throw new IllegalArgumentException("argument to floor() is null");
Key k = st.floorKey(key);
if (k == null) throw new NoSuchElementException("argument to floor() is too small");
return k;
}
public static void main(String[] args) {
ST<String, Integer> st = new ST<String, Integer>();
for (int i = 0; !StdIn.isEmpty(); i++) {
String key = StdIn.readString();
st.put(key, i);
}
for (String s : st.keys())
StdOut.println(s + " " + st.get(s));
}
}
@@ -0,0 +1,23 @@
9
21
0 1 38
0 3 48
0 5 35
0 8 40
1 2 50
1 3 36
1 7 42
1 8 48
2 4 25
2 5 40
2 7 43
2 8 30
3 5 30
3 6 65
3 8 35
4 5 20
4 8 28
5 8 27
6 7 60
6 8 55
7 8 52