← Data structures & algorithms
Graphs Nodes, edges, direction, weight, and the questions they answer

Graph overview

Model who owes whom.

Your GitHub repositories already have a graph. GitHub calls it “a summary of the manifest and lock files stored in a repository”: repositories and packages are nodes, and each “depends on” is an edge with a direction. Follow the edges forwards and you get dependencies; follow them backwards and you get dependents. Splitwise’s “simplify debts” setting works on a graph too: it “restructures who owes whom within a group to minimize the total number of payments needed for everyone to settle up.” Let’s build one trip’s shared tab and ask it four different questions.

TypeScriptGoOne shared tab, two implementations.

01 / The idea

One tab, many questions.

A graph is a set of things and the pairs of them that are related. The things are nodes, also called vertices, and each related pair is an edge. That is the whole definition, which is why graphs turn up everywhere: who owes whom on a shared tab, which files import which, which roads meet at which junctions.

Storing one is rarely the hard part. The hard part is deciding what the graph is: what counts as a node, what counts as an edge, whether an edge points one way, whether it carries a number, and what happens when two edges join the same pair. Once those are decided, the question you are asking tells you which walk or which lesson to reach for.

On a trip tab the same graph answers very different questions. What does Ben owe Ana? Which friends are connected at all? Does money go round in a circle? And how few payments settle everyone? Each is a different way of reading the same edges.

02 / Name the rule

Name the nodes and edges before the structure.

  • Nodes. Here, people. An expense adds an edge from each person who shared it to the person who paid.
  • Direction. “Ben owes Ana” is not “Ana owes Ben.” A directed graph keeps each edge as an ordered pair. “Ben and Ana shared something” has no direction, and would be an undirected edge.
  • Weight. Each edge carries a number, here an amount in cents. A graph whose edges carry numbers is weighted.
  • Parallel edges. Two dinners make two edges between the same pair. A list of edges keeps both; an adjacency map merges them into one pair with the total.
  • Degree. Counting a node’s edges in and out gives its in-degree and out-degree. Adding their weights instead gives a balance: owed to you minus owed by you.
  • Paths, cycles, and groups. A path follows edges from node to node. A cycle is a path back to where it started. A connected group, or component, is every node you can reach when direction is ignored.

Choose those deliberately and the rest follows: groups ignore direction, circles of debt need it, and settling up needs each person’s weights in and out, and the groups to pay within.

Why does the tab keep two views of the same debts?Recorded and netted

The recorded graph keeps every share, so the tab can show its history: Ana owes Ben 30.00 for a taxi and Ben owes Ana 20.00 for dinner. The netted graph keeps only what is left of each pair: Ana owes Ben 10.00.

A circle of debt is a question about the netted graph. In the recorded one, any two people who owe each other anything already form a cycle of two, which says nothing useful.

03 / Follow one operation

A day out for five friends.

Five friends pay for dinner, a museum, a taxi, coffee, and a train. Then the tab asks the graph who is connected, whether money goes round, and how to settle up.

Before you watch, predict how many edges a dinner for three adds when one of the three paid, and whether the settle-up plan will need as many payments as there are debts. The animation replays what the TypeScript example recorded. Try it lets you run your own tab.

Graph overview

People are nodes. Debts are edges.

Graph · people are nodes, each debt is a directed, weighted edge

A shared tab for five friends

Ana0.00Ben0.00Chloe0.00Dev0.00Eli0.00
Showing debts as recorded, merged per pair. Balances: Ana 0.00, Ben 0.00, Chloe 0.00, Dev 0.00, Eli 0.00.

Showing debts as recorded, merged per pair

Edges 0 Pairs 0 Looked 0 Payments 0

01/ 08
Five people, no edges

Five people, no edges yet.

People are the nodes. Every share someone owes will become an edge pointing at the person who paid, weighted by the amount. A balance is what a person is owed minus what they owe.

Reduced motion: choose a scene to see its completed state.

Read this scene

People are the nodes. Every share someone owes will become an edge pointing at the person who paid, weighted by the amount. A balance is what a person is owed minus what they owe.

People are the nodes. Every share someone owes will become an edge pointing at the person who paid, weighted by the amount. A balance is what a person is owed minus what they owe.

Edges added so far: 0. Connections looked at: 0. Payments: 0.

Watch restarts when you return. Step through keeps your selected step. Try it starts from an empty tab each time you open it.

04 / Read the shape

One graph, one shared tab.

Basic form is Graph: a directed, weighted graph that keeps an edge list, merged adjacency maps in both directions, and running weights in and out of each node. It finds groups with a breadth-first walk and a cycle with a depth-first walk. In the wild wraps it in TripTab, which checks names and amounts, splits expenses in whole cents, records payments as edges back, and answers what one person owes another, balances, groups, a circle of net debts, and a settle-up plan.

A directed, weighted graph. It keeps every edge in the order added, merges edges between the same two nodes into adjacency maps, keeps running weights in and out of each node, finds groups with a breadth-first walk in both directions, and finds a cycle with a depth-first walk that follows direction. Every step can be recorded.

TypeScriptReading
trip.ts
export type Step =
	/** edge: an edge was added; total is the merged weight from `from` to `to` afterwards. */
	| { kind: 'edge'; from: string; to: string; weight: number; total: number }
	/** visit: a walk reached a node, which belongs to the numbered group. */
	| { kind: 'visit'; node: string; group: number }
	/** look: a walk examined the connection from `from` to `to`. */
	| { kind: 'look'; from: string; to: string }
	/** enter: a depth-first walk started exploring a node. */
	| { kind: 'enter'; node: string }
	/** leave: every edge out of the node has been explored. */
	| { kind: 'leave'; node: string }
	/** cycle: the walk reached a node it was still exploring. */
	| { kind: 'cycle'; nodes: string[] }
	/** pay: one payment in a settle-up plan. */
	| { kind: 'pay'; from: string; to: string; amount: number };

export type Edge = { from: string; to: string; weight: number };

const MAX_WEIGHT = 1_000_000_000;

// A directed, weighted graph. It keeps every edge in the order added, which is an edge list,
// and merges edges between the same two nodes into one entry in each node's adjacency maps,
// so "how much from A to B" and "what leaves A" are direct lookups.
export class Graph {
	#edges: Edge[] = [];
	#out = new Map<string, Map<string, number>>();
	#in = new Map<string, Map<string, number>>();
	#outTotal = new Map<string, number>();
	#inTotal = new Map<string, number>();

	/** Adds a node with no edges; returns false if it was already there. */
	addNode(node: string): boolean {
		if (this.#out.has(node)) return false;
		this.#out.set(node, new Map());
		this.#in.set(node, new Map());
		this.#outTotal.set(node, 0);
		this.#inTotal.set(node, 0);
		return true;
	}

	/** Adds an edge, and its nodes if they are new. A second edge between the same nodes merges. */
	addEdge(from: string, to: string, weight: number, steps?: Step[]): void {
		if (from === to) throw new RangeError('an edge joins two different nodes');
		if (!Number.isInteger(weight) || weight < 1 || weight > MAX_WEIGHT)
			throw new RangeError('a weight is a whole number from 1 to 1000000000');
		this.addNode(from);
		this.addNode(to);
		this.#edges.push({ from, to, weight });
		const total = this.weight(from, to) + weight;
		this.#out.get(from)!.set(to, total);
		this.#in.get(to)!.set(from, total);
		this.#outTotal.set(from, this.#outTotal.get(from)! + weight);
		this.#inTotal.set(to, this.#inTotal.get(to)! + weight);
		steps?.push({ kind: 'edge', from, to, weight, total });
	}

	nodes(): string[] {
		return [...this.#out.keys()];
	}

	/** Every edge in the order added, before merging. */
	edges(): Edge[] {
		return this.#edges.map((edge) => ({ ...edge }));
	}

	/** How many merged entries the adjacency maps hold: one per ordered pair with an edge. */
	get pairs(): number {
		let pairs = 0;
		for (const targets of this.#out.values()) pairs += targets.size;
		return pairs;
	}

	/** The merged weight from `from` to `to`, or 0. */
	weight(from: string, to: string): number {
		return this.#out.get(from)?.get(to) ?? 0;
	}

	/** Nodes an edge leads to from `node`, with merged weights, in the order first connected. */
	successors(node: string): [string, number][] {
		return [...(this.#out.get(node) ?? [])];
	}

	/** Nodes with an edge into `node`, with merged weights, in the order first connected. */
	predecessors(node: string): [string, number][] {
		return [...(this.#in.get(node) ?? [])];
	}

	weightOut(node: string): number {
		return this.#outTotal.get(node) ?? 0;
	}

	weightIn(node: string): number {
		return this.#inTotal.get(node) ?? 0;
	}

	// Groups of nodes joined by edges in either direction. A breadth-first walk starts at each
	// node not yet reached, in node order, and looks along successors, then predecessors.
	components(steps?: Step[]): string[][] {
		const groups: string[][] = [];
		const reached = new Set<string>();
		for (const start of this.#out.keys()) {
			if (reached.has(start)) continue;
			const group = [start];
			reached.add(start);
			steps?.push({ kind: 'visit', node: start, group: groups.length });
			for (let i = 0; i < group.length; i++) {
				const node = group[i];
				for (const next of [...this.#out.get(node)!.keys(), ...this.#in.get(node)!.keys()]) {
					steps?.push({ kind: 'look', from: node, to: next });
					if (reached.has(next)) continue;
					reached.add(next);
					group.push(next);
					steps?.push({ kind: 'visit', node: next, group: groups.length });
				}
			}
			groups.push(group);
		}
		return groups;
	}

	// A cycle that follows edge directions, or null. A depth-first walk starts at each node not
	// yet explored, in node order; reaching a node still on the walk's path closes a cycle.
	// The walk keeps its own stack, so a long chain cannot overflow the call stack.
	findCycle(steps?: Step[]): string[] | null {
		const done = new Set<string>();
		const onPath = new Set<string>();
		for (const root of this.#out.keys()) {
			if (done.has(root)) continue;
			const path = [root];
			// Each frame holds a node's neighbors and a cursor, so taking the next one is O(1).
			const pending = [{ list: [...this.#out.get(root)!.keys()], at: 0 }];
			onPath.add(root);
			steps?.push({ kind: 'enter', node: root });
			while (path.length) {
				const node = path.at(-1)!;
				const frame = pending.at(-1)!;
				const next = frame.at < frame.list.length ? frame.list[frame.at++] : undefined;
				if (next === undefined) {
					done.add(node);
					onPath.delete(node);
					steps?.push({ kind: 'leave', node });
					path.pop();
					pending.pop();
					continue;
				}
				steps?.push({ kind: 'look', from: node, to: next });
				if (onPath.has(next)) {
					const cycle = path.slice(path.indexOf(next));
					steps?.push({ kind: 'cycle', nodes: [...cycle] });
					return cycle;
				}
				if (done.has(next)) continue;
				onPath.add(next);
				steps?.push({ kind: 'enter', node: next });
				path.push(next);
				pending.push({ list: [...this.#out.get(next)!.keys()], at: 0 });
			}
		}
		return null;
	}
}
GoAlongside
trip.go
// Step is one recorded event. Kind is edge, visit, look, enter, leave, cycle, or pay; the other
// fields that kind uses are set.
type Step struct {
	Kind   string
	From   string
	To     string
	Node   string
	Weight int
	Total  int
	Group  int
	Nodes  []string
	Amount int
}

type Edge struct {
	From, To string
	Weight   int
}

// Neighbor is a node at the other end of merged edges, with their total weight.
type Neighbor struct {
	Node   string
	Weight int
}

const maxWeight = 1_000_000_000

func record(steps *[]Step, step Step) {
	if steps != nil {
		*steps = append(*steps, step)
	}
}

// Graph is a directed, weighted graph. It keeps every edge in the order added, which is an edge
// list, and merges edges between the same two nodes into one entry in each node's adjacency
// maps, so "how much from A to B" and "what leaves A" are direct lookups. The order slices
// remember when each neighbor was first connected, because Go maps have no order.
type Graph struct {
	nodes             []string
	edges             []Edge
	out, in           map[string]map[string]int
	outOrder, inOrder map[string][]string
	outTotal, inTotal map[string]int
}

func NewGraph() *Graph {
	return &Graph{
		out:      map[string]map[string]int{},
		in:       map[string]map[string]int{},
		outOrder: map[string][]string{},
		inOrder:  map[string][]string{},
		outTotal: map[string]int{},
		inTotal:  map[string]int{},
	}
}

// AddNode adds a node with no edges and reports false if it was already there.
func (g *Graph) AddNode(node string) bool {
	if _, ok := g.out[node]; ok {
		return false
	}
	g.nodes = append(g.nodes, node)
	g.out[node] = map[string]int{}
	g.in[node] = map[string]int{}
	return true
}

// AddEdge adds an edge, and its nodes if they are new. A second edge between the same nodes merges.
func (g *Graph) AddEdge(from, to string, weight int, steps *[]Step) error {
	if from == to {
		return errors.New("an edge joins two different nodes")
	}
	if weight < 1 || weight > maxWeight {
		return errors.New("a weight is a whole number from 1 to 1000000000")
	}
	g.AddNode(from)
	g.AddNode(to)
	g.edges = append(g.edges, Edge{from, to, weight})
	if _, ok := g.out[from][to]; !ok {
		g.outOrder[from] = append(g.outOrder[from], to)
		g.inOrder[to] = append(g.inOrder[to], from)
	}
	total := g.out[from][to] + weight
	g.out[from][to] = total
	g.in[to][from] = total
	g.outTotal[from] += weight
	g.inTotal[to] += weight
	record(steps, Step{Kind: "edge", From: from, To: to, Weight: weight, Total: total})
	return nil
}

func (g *Graph) Nodes() []string { return slices.Clone(g.nodes) }

// Edges returns every edge in the order added, before merging.
func (g *Graph) Edges() []Edge { return slices.Clone(g.edges) }

// Pairs counts merged entries: one per ordered pair of nodes with an edge.
func (g *Graph) Pairs() int {
	pairs := 0
	for _, targets := range g.outOrder {
		pairs += len(targets)
	}
	return pairs
}

// Weight returns the merged weight from one node to another, or 0.
func (g *Graph) Weight(from, to string) int { return g.out[from][to] }

// Successors lists nodes an edge leads to, with merged weights, in the order first connected.
func (g *Graph) Successors(node string) []Neighbor {
	var list []Neighbor
	for _, to := range g.outOrder[node] {
		list = append(list, Neighbor{to, g.out[node][to]})
	}
	return list
}

// Predecessors lists nodes with an edge into this one, with merged weights, in the order first connected.
func (g *Graph) Predecessors(node string) []Neighbor {
	var list []Neighbor
	for _, from := range g.inOrder[node] {
		list = append(list, Neighbor{from, g.in[node][from]})
	}
	return list
}

func (g *Graph) WeightOut(node string) int { return g.outTotal[node] }

func (g *Graph) WeightIn(node string) int { return g.inTotal[node] }

// Components returns groups of nodes joined by edges in either direction. A breadth-first walk
// starts at each node not yet reached, in node order, and looks along successors, then predecessors.
func (g *Graph) Components(steps *[]Step) [][]string {
	var groups [][]string
	reached := map[string]bool{}
	for _, start := range g.nodes {
		if reached[start] {
			continue
		}
		group := []string{start}
		reached[start] = true
		record(steps, Step{Kind: "visit", Node: start, Group: len(groups)})
		for i := 0; i < len(group); i++ {
			node := group[i]
			for _, next := range slices.Concat(g.outOrder[node], g.inOrder[node]) {
				record(steps, Step{Kind: "look", From: node, To: next})
				if reached[next] {
					continue
				}
				reached[next] = true
				group = append(group, next)
				record(steps, Step{Kind: "visit", Node: next, Group: len(groups)})
			}
		}
		groups = append(groups, group)
	}
	return groups
}

// FindCycle returns a cycle that follows edge directions, or nil. A depth-first walk starts at
// each node not yet explored, in node order; reaching a node still on the walk's path closes a
// cycle. The walk keeps its own stack, so a long chain cannot overflow the call stack.
func (g *Graph) FindCycle(steps *[]Step) []string {
	done := map[string]bool{}
	onPath := map[string]bool{}
	for _, root := range g.nodes {
		if done[root] {
			continue
		}
		path := []string{root}
		next := []int{0}
		onPath[root] = true
		record(steps, Step{Kind: "enter", Node: root})
		for len(path) > 0 {
			top := len(path) - 1
			node := path[top]
			if next[top] == len(g.outOrder[node]) {
				done[node] = true
				delete(onPath, node)
				record(steps, Step{Kind: "leave", Node: node})
				path, next = path[:top], next[:top]
				continue
			}
			to := g.outOrder[node][next[top]]
			next[top]++
			record(steps, Step{Kind: "look", From: node, To: to})
			if onPath[to] {
				cycle := slices.Clone(path[slices.Index(path, to):])
				record(steps, Step{Kind: "cycle", Nodes: slices.Clone(cycle)})
				return cycle
			}
			if done[to] {
				continue
			}
			onPath[to] = true
			record(steps, Step{Kind: "enter", Node: to})
			path = append(path, to)
			next = append(next, 0)
		}
	}
	return nil
}
Reading the TypeScriptMaps keep insertion order

A Map iterates in the order keys were first added, and setting an existing key keeps its place. That is why the walks visit neighbors in the order people first became connected, and why the recorded steps are the same every run.

findCycle keeps its own stack of pending neighbors and a set of nodes on the path, so a chain of 100,000 edges, which the tests build, cannot overflow the call stack.

Reading the GoMaps have no order

Go maps iterate in no particular order, so Graph keeps slices of neighbors beside its maps, appended the first time a pair appears. The walks read the slices, and the recorded steps match TypeScript’s exactly.

slices.Concat joins successors and predecessors for the group walk, and the built-in min picks each payment’s amount.

What would I normally use in application code?Maps first, a library for big graphs

Neither TypeScript nor Go ships a general graph type. For a tab, a map of maps like this one is the usual shape. For large graphs, or questions such as shortest routes across millions of edges, reach for a graph library or a database built for graph queries.

Whatever you use, keep money in whole cents and decide in writing where leftover cents go.

05 / Try a decision

A circle of debts.

Debts, balances, and payments are easy to confuse. Decide which one settles this tab before the feedback tells you.

On a three-person tab, Ana owes Ben 20.00, Ben owes Chloe 20.00, and Chloe owes Ana 20.00. How many payments settle the tab?

06 / Follow the cost

Three ways to store it.

Here is every operation at a glance, for V people, E recorded edges, and P merged pairs. The rest of this section measures what those letters come to.

Shared tab: time and extra space
OperationTimeExtra spaceWhat it assumes
Record a share or paymentO(1) averageO(1)Append to the edge list, add into one merged pair, and update two running totals.
What A owes BO(1) averageO(1)Two lookups, one per direction, then subtract.
A balanceO(1) averageO(1)The running weight in minus the running weight out.
Everyone A owesO(out-degree)O(out-degree)Read one adjacency map: one entry per person A has an edge to.
Find groupsO(V + P)O(V)V people and P merged pairs; each pair is looked at from both ends.
Find a circleO(V + P)O(V)Build the net graph, then one depth-first walk with its own stack.
Plan settling upO(V + P + V²)O(V)Groups first; then at most g − 1 payments in a group of g, each found by scanning it.
Store the tab—O(E + V + P)E edges in the list for history, plus merged maps. A matrix would take V² cells instead.

In the animation, six edges merged into five pairs, four net debts remained, and three payments settled everyone. At a larger size, from the lesson’s TypeScript tab: 20 people and 2,000 random expenses recorded 12,808 edges, which merged into 380 pairs. That is every ordered pair of 20 people, so a 20 by 20 matrix of 400 cells would have done just as well. A tab among friends is small and dense. Netting left 190 debts, one for every pair, and the plan settled all of them in 19 payments. The group walk looked at 760 connections, each pair from both ends.

The plan is quick, but not always the fewest payments. On 1,000 random tabs of six friends, it used 4,982 payments where the fewest possible was 4,980: one extra payment, in 2 of the tabs. One small group shows why. Its balances are +5.00, +4.00, −4.00, −3.00, and −2.00. The plan pays the largest debt to the largest credit first and needs four payments. Three are enough: −4.00 to +4.00, and the other two to +5.00. These are counts from the lesson’s TypeScript example, not timings.

Edge list, adjacency map, or matrix?It depends on the question

An edge list is the history: cheap to append, but finding what Ben owes Ana means reading all of it. Adjacency maps answer “what leaves this node?” and “how much from A to B?” directly, and take space for pairs that exist. A matrix answers any pair in one cell but takes V² cells, which is fine for 20 friends and wasteful for a million accounts that each know a few hundred.

The adjacency list lesson measures that trade on a sparse airline network.

07 / Give it a real job

Settle a real trip.

TripTab takes 2 to 20 people with checked names. An expense of whole cents is split evenly, and leftover cents go one each to the first people listed, so 100.00 for three is 33.34, 33.33, and 33.33. A payment never deletes an edge; it adds one back, so the tab keeps its history and nets it on demand.

Two cautions. First, the plan can ask someone to pay a friend they never owed directly; Splitwise’s own example has Anna, who owes Bob, pay Charlie. Say so in the interface, or people will think the app made a mistake. Second, do not promise the fewest payments. Anton Cao shows that “the decision variant of this problem is NP-complete,” and notes that Splitwise’s greedy approach “does not always produce the optimal answer.” A plan that is never worse than n − 1 payments per group is a promise you can keep.

Build UIs?The day your task board’s “blocked by” links become a graph you have to keep honest.

When you have to own it

Your team’s task board gets “blocked by” links. Each card can be blocked by other cards, so every link is an edge from the blocker to the card it holds up. Keep both directions, the way Graph does: a card’s incoming list is what it waits on, and its outgoing list is what it holds up.

Then the rules arrive. A card can move to Done only when every card in its incoming list is Done. Finishing a card should say what it unblocks: the cards in its outgoing list whose other blockers are all Done already. Each rule reads one map, which is why you keep two.

The tricky one is adding a link. If Deploy is blocked by Review, and Review is blocked by Tests, then blocking Tests on Deploy means none of the three can ever finish. Before you save, look for a path from the new blocker back to the card along “blocked by” links. A breadth-first walk finds the shortest one. If it exists, refuse and name the loop by card title: “Tests would wait on Deploy, which waits on Review, which waits on Tests.” Otherwise show the link at once, save it, and take it back out if the server says no.

A card’s Move to Done button. Its incoming list says whether every blocker is Done; its outgoing list says which cards finishing it unblocks.

ReactAlready in your code
FinishCard.tsx
import type { Board, Card } from './board';

const titles = (cards: Card[]) => cards.map((card) => card.title).join(', ');

// A card's incoming list is everything it waits on. It can move to Done when none are left.
export function waitingOn(board: Board, id: string): Card[] {
	return [...(board.blockedBy.get(id) ?? [])]
		.map((blocker) => board.cards.get(blocker)!)
		.filter((blocker) => !blocker.done);
}

// A card's outgoing list is everything it holds up. Finishing it unblocks the open ones whose
// only unfinished blocker is this card. Once the card is Done, the same call says what it freed.
export function unblocks(board: Board, id: string): Card[] {
	return [...(board.blocks.get(id) ?? [])]
		.map((next) => board.cards.get(next)!)
		.filter((next) => !next.done && waitingOn(board, next.id).every((b) => b.id === id));
}

export function FinishCard({
	board,
	card,
	onFinish
}: {
	board: Board;
	card: string;
	/** Asks the owner of the board to mark the card Done. */
	onFinish: (card: string) => void;
}) {
	const { title, done } = board.cards.get(card)!;
	const waiting = waitingOn(board, card);
	const freed = unblocks(board, card);

	if (done) {
		return (
			<p>
				{freed.length ? `${title} is Done. Ready to start: ${titles(freed)}.` : `${title} is Done.`}
			</p>
		);
	}

	return (
		<div>
			{/* Say why in words, not only with a grayed-out button. */}
			{waiting.length > 0 && <p>Waiting on {titles(waiting)}.</p>}
			<button type="button" disabled={waiting.length > 0} onClick={() => onFinish(card)}>
				Move to Done
			</button>
			{waiting.length === 0 && freed.length > 0 && <p>Finishing this unblocks {titles(freed)}.</p>}
		</div>
	);
}

08 / Make the call

Let the question pick the lesson.

And sometimes you do not need a graph. If the only question is who is up and who is down, a map from each person to a running balance is enough. The graph earns its place when the tab must show who owes whom, which friends are connected, or where money goes round.

09 / Take the idea with you

Explain it without saying “graph.”

“I write down every debt as an arrow from the person who owes to the person who paid, with the amount on it. Two arrows between the same people add up. To see who is connected I follow arrows either way; to find money going round I follow them only forwards; to settle up I add each person’s arrows in and out and pay off the biggest debts to the biggest credits.” That describes the model. The name is what you call it in a review.

Before moving on, explain three things without the word: why a dinner for three adds only two edges, why a circle of equal debts needs no payment, and why groups ignore direction while circles do not. Then find a list of relationships in your own code, and name its nodes, its edges, and whether they point one way.

Connections to follow nextRelated lessons

Take the tab into your editor. Add a trip of 12 friends and 50 expenses, compare the edge list, merged pairs, and net debts, then change the plan to pay within each circle first and count whether it ever saves a payment.

Back to data structures & algorithms →