diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c68a973 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +# Description + +Please include a summary of the change and which issue is fixed. + +# How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. + +# Checklist: +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code (where needed) +- [ ] I have made corresponding changes to the documentation/README +- [ ] My changes generate no new warnings +- [ ] Tests are added for relevant functions +- [ ] New and existing unit tests pass locally with my changes diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..8c64b17 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,43 @@ +name: Test and lint +on: + push: + branches-ignore: + - main + +env: + GO_VERSION: 1.25.x + +jobs: + test: + name: Run tests + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Test + run: | + make test + + lint: + name: Run linter + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Lint + uses: golangci/golangci-lint-action@v9 + with: + version: v2.6 + args: --timeout=5m diff --git a/.gitignore b/.gitignore index aaadf73..ed20272 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,5 @@ go.work.sum .env # Editor/IDE -# .idea/ -# .vscode/ +.idea/ +.vscode/ diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..4795657 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,57 @@ +version: "2" +linters: + default: none + enable: + - bodyclose + - copyloopvar + - dogsled + - errcheck + - gochecknoinits + - goconst + - gocritic + - gocyclo + - goprintffuncname + - gosec + - govet + - ineffassign + - lll + - misspell + - nakedret + - revive + - staticcheck + - unconvert + - unparam + - unused + - whitespace + settings: + revive: + confidence: 0.9 + gocyclo: + min-complexity: 6 + lll: + line-length: 100 + gosec: + excludes: + - G505 + - G401 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/LICENSE b/LICENSE index a564624..7c8432a 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,8 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- +Third Party Licenses +The following dependencies include their own licenses. +- github.com/stretchr/testify (MIT License) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2e6d413 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.PHONY: lint +lint: + @golangci-lint run ./... + +.PHONY: test +test: + @go test -count=5 -race -cover ./... + + diff --git a/README.md b/README.md index 4cd6e25..62a0ded 100644 --- a/README.md +++ b/README.md @@ -1 +1,66 @@ -# flowmo \ No newline at end of file +# Flowmo + +A Go library for solving maximum flow problems in directed networks using Dinic's algorithm. + +## Installation + +```bash +go get github.com/oursimon/flowmo +``` + +## Usage + +```go +package main + +import ( + "fmt" + "github.com/oursimon/flowmo" +) + +func main() { + // Create a new flow network + f := flowmo.New() + + // Add directed edges with capacities + f.AddEdge("source", "a", 10) + f.AddEdge("source", "b", 5) + f.AddEdge("a", "sink", 7) + f.AddEdge("b", "sink", 8) + f.AddEdge("a", "b", 3) + + // Compute maximum flow + maxFlow, err := f.MaxFlow("source", "sink") + if err != nil { + panic(err) + } + + fmt.Printf("Maximum flow: %d\n", maxFlow) + + // Query node capacities + incoming, _ := f.IncomingCapacityByNode("sink") + fmt.Printf("Incoming capacity to sink: %d\n", incoming) +} +``` + +## API Reference + +### `New() *Flowmo` +Creates a new flow network instance. + +### `AddEdge(from, to Node, capacity int) error` +Adds a directed edge from `from` to `to` with the specified capacity. Nodes are created automatically if they don't exist. + +### `MaxFlow(source, sink Node) (int, error)` +Computes the maximum flow from the source node to the sink node using Dinic's algorithm. + +### `IncomingCapacityByNode(node Node) (int, error)` +Returns the total incoming flow capacity for the specified node. + +### `OutgoingCapacityByNode(node Node) (int, error)` +Returns the total outgoing flow capacity for the specified node. + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + diff --git a/examples/main.go b/examples/main.go new file mode 100644 index 0000000..b053a13 --- /dev/null +++ b/examples/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + + "github.com/oursimon/flowmo" +) + +func main() { + // Create a new flow network. + f := flowmo.New() + + // Add directed edges with capacities. + // Nodes are identified by string labels. + // If nodes do not exist, they will be created automatically. + _ = f.AddEdge("a", "b", 1) + _ = f.AddEdge("a", "c", 1) + _ = f.AddEdge("c", "b", 1) + + // Compute the maximum flow from source "a" to sink "b". + flow, err := f.MaxFlow("a", "b") + if err != nil { + panic(err) + } + + fmt.Printf("Max flow from 'a' to 'b': %d\n", flow) + + // Query incoming and outgoing capacities for specific nodes. + incoming, err := f.IncomingCapacityByNode("b") + if err != nil { + panic(err) + } + + fmt.Printf("Incoming capacity to 'b': %d\n", incoming) + + outgoing, err := f.OutgoingCapacityByNode("c") + if err != nil { + panic(err) + } + + fmt.Printf("Outgoing capacity from 'c': %d\n", outgoing) +} diff --git a/flowmo.go b/flowmo.go new file mode 100644 index 0000000..d69854b --- /dev/null +++ b/flowmo.go @@ -0,0 +1,136 @@ +package flowmo + +import ( + "fmt" + + "github.com/oursimon/flowmo/flowmoerrors" + "github.com/oursimon/flowmo/internal/network" +) + +type Node string + +// Flowmo represents a flow network that computes maximum flow using Dinic's algorithm. +// It maps string-based node identifiers to an internal graph representation. +type Flowmo struct { + indexByNode map[Node]int + network *network.Network +} + +func New() *Flowmo { + net := network.New() + return &Flowmo{ + indexByNode: make(map[Node]int), + network: net, + } +} + +// AddEdge adds a directed edge from the "from" node to the "to" node with the specified capacity. +// If either node does not exist, it will be created automatically. +// Multiple edges between the same pair of nodes are allowed. +func (f *Flowmo) AddEdge(from, to Node, capacity int) error { + fromIndex, err := f.addNode(from) + if err != nil { + return err + } + + toIndex, err := f.addNode(to) + if err != nil { + return err + } + + return f.network.AddEdge(fromIndex, toIndex, capacity) +} + +// MaxFlow computes the maximum flow from the source node +// to the sink node using Dinic's algorithm. +// The algorithm runs in O(V²E) time complexity where V is +// the number of nodes and E is the number of edges. +// +// Returns the maximum flow value and nil error on success. +// Returns 0 and an error if: +// - Source node does not exist (ErrNotFound) +// - Sink node does not exist (ErrNotFound) +// +// If source equals sink, returns 0 with no error (valid edge case). +// If no path exists from source to sink, returns 0 with no error. +func (f *Flowmo) MaxFlow(source, sink Node) (int, error) { + sourceIndex, exists := f.indexByNode[source] + if !exists { + return 0, fmt.Errorf( + "source node %q: %w", + source, + flowmoerrors.ErrNotFound, + ) + } + + sinkIndex, exists := f.indexByNode[sink] + if !exists { + return 0, fmt.Errorf( + "sink node %q: %w", + sink, + flowmoerrors.ErrNotFound, + ) + } + + return f.network.MaxFlow(sourceIndex, sinkIndex) +} + +// IncomingCapacityByNode returns the total incoming flow (used capacity) to the specified node. +// This represents the sum of all flow that has been pushed into the node across all incoming edges. +// This method should be called after MaxFlow has been computed. +// +// Returns the total incoming flow and nil error on success. +// Returns -1 and an error if: +// - Node does not exist (ErrNotFound) +func (f *Flowmo) IncomingCapacityByNode(node Node) (int, error) { + idx, exists := f.indexByNode[node] + if !exists { + return -1, fmt.Errorf( + "node %q: %w", + node, + flowmoerrors.ErrNotFound, + ) + } + + return f.network.IncomingFlowByNode(idx) +} + +// OutgoingCapacityByNode returns the total outgoing flow +// (used capacity) from the specified node. +// This represents the sum of all flow that has been pushed out of +// the node across all outgoing edges. +// This method should be called after MaxFlow has been computed. +// +// Returns the total outgoing flow and nil error on success. +// Returns -1 and an error if: +// - Node does not exist (ErrNotFound) +func (f *Flowmo) OutgoingCapacityByNode(node Node) (int, error) { + idx, exists := f.indexByNode[node] + if !exists { + return -1, fmt.Errorf( + "node %q: %w", + node, + flowmoerrors.ErrNotFound, + ) + } + + return f.network.OutgoingFlowByNode(idx) +} + +func (f *Flowmo) addNode(node Node) (int, error) { + if node == "" { + return -1, fmt.Errorf( + "node cannot be empty: %w", + flowmoerrors.ErrInvalidArgument, + ) + } + + if idx, exists := f.indexByNode[node]; exists { + return idx, nil + } + + idx := f.network.AddNode() + f.indexByNode[node] = idx + + return idx, nil +} diff --git a/flowmo_test.go b/flowmo_test.go new file mode 100644 index 0000000..1ac917a --- /dev/null +++ b/flowmo_test.go @@ -0,0 +1,166 @@ +package flowmo + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/oursimon/flowmo/flowmoerrors" +) + +func Test_New(t *testing.T) { + f := New() + assert.NotNil(t, f) + assert.NotNil(t, f.network) + assert.NotNil(t, f.indexByNode) + assert.Equal(t, 0, len(f.indexByNode)) +} + +func Test_AddEdge_basicFunctionality(t *testing.T) { + f := New() + + err := f.AddEdge("a", "b", 10) + assert.NoError(t, err) + + // Verify nodes were created + assert.Equal(t, 2, len(f.indexByNode)) + assert.Contains(t, f.indexByNode, Node("a")) + assert.Contains(t, f.indexByNode, Node("b")) +} + +func Test_AddEdge_sameNodesPair(t *testing.T) { + // Multiple edges between same nodes + f := New() + + err := f.AddEdge("a", "b", 10) + assert.NoError(t, err) + + err = f.AddEdge("a", "b", 5) + assert.NoError(t, err) + + // Should accumulate capacity + maxFlow, err := f.MaxFlow("a", "b") + assert.NoError(t, err) + assert.Equal(t, 15, maxFlow) +} + +func Test_AddEdge_negativeCapacity(t *testing.T) { + f := New() + + err := f.AddEdge("a", "b", -10) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} + +func Test_AddEdge_emptyFromNode(t *testing.T) { + f := New() + + err := f.AddEdge("", "b", 10) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} + +func Test_AddEdge_emptyToNode(t *testing.T) { + f := New() + + err := f.AddEdge("a", "", 10) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} + +func Test_AddEdge_zeroCapacity(t *testing.T) { + f := New() + + // Zero capacity should be allowed + err := f.AddEdge("a", "b", 0) + assert.NoError(t, err) + + maxFlow, err := f.MaxFlow("a", "b") + assert.NoError(t, err) + assert.Equal(t, 0, maxFlow) +} + +func Test_MaxFlow_simpleNetwork(t *testing.T) { + // a --10--> b + f := New() + + _ = f.AddEdge("a", "b", 10) + + maxFlow, err := f.MaxFlow("a", "b") + assert.NoError(t, err) + assert.Equal(t, 10, maxFlow) +} + +func Test_MaxFlow_sourceNotFound(t *testing.T) { + f := New() + + _ = f.AddEdge("a", "b", 10) + + _, err := f.MaxFlow("x", "b") + assert.ErrorIs(t, err, flowmoerrors.ErrNotFound) +} + +func Test_MaxFlow_sinkNotFound(t *testing.T) { + f := New() + + _ = f.AddEdge("a", "b", 10) + + _, err := f.MaxFlow("a", "x") + assert.ErrorIs(t, err, flowmoerrors.ErrNotFound) +} + +func Test_IncomingCapacityByNode_nodeNotFound(t *testing.T) { + f := New() + + _ = f.AddEdge("a", "b", 10) + + _, err := f.IncomingCapacityByNode("x") + assert.ErrorIs(t, err, flowmoerrors.ErrNotFound) +} + +func Test_OutgoingCapacityByNode_nodeNotFound(t *testing.T) { + f := New() + + _ = f.AddEdge("a", "b", 10) + + _, err := f.OutgoingCapacityByNode("x") + assert.ErrorIs(t, err, flowmoerrors.ErrNotFound) +} + +func Test_CompleteWorkflow(t *testing.T) { + // End-to-end test: create graph, compute max flow, query capacities + // 20 10 + // a -----> b -----> d + // | | ^ + // |10 |5 |15 + // v v | + // c ----------------> + f := New() + + _ = f.AddEdge("a", "b", 20) + _ = f.AddEdge("a", "c", 10) + _ = f.AddEdge("b", "c", 5) + _ = f.AddEdge("b", "d", 10) + _ = f.AddEdge("c", "d", 15) + + maxFlow, err := f.MaxFlow("a", "d") + assert.NoError(t, err) + assert.Equal(t, 25, maxFlow) + + // Verify incoming capacities + incomingB, _ := f.IncomingCapacityByNode("b") + assert.Equal(t, 15, incomingB) + + incomingC, _ := f.IncomingCapacityByNode("c") + assert.Equal(t, 15, incomingC) + + incomingD, _ := f.IncomingCapacityByNode("d") + assert.Equal(t, 25, incomingD) + + // Verify outgoing capacities + outgoingA, _ := f.OutgoingCapacityByNode("a") + assert.Equal(t, 25, outgoingA) + + outgoingB, _ := f.OutgoingCapacityByNode("b") + assert.Equal(t, 15, outgoingB) + + outgoingC, _ := f.OutgoingCapacityByNode("c") + assert.Equal(t, 15, outgoingC) +} diff --git a/flowmoerrors/errors.go b/flowmoerrors/errors.go new file mode 100644 index 0000000..117b48f --- /dev/null +++ b/flowmoerrors/errors.go @@ -0,0 +1,6 @@ +package flowmoerrors + +import "errors" + +var ErrNotFound = errors.New("not found") +var ErrInvalidArgument = errors.New("invalid argument") diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f097d9e --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/oursimon/flowmo + +go 1.25.4 + +require github.com/stretchr/testify v1.11.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c4c1710 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/network/dinic.go b/internal/network/dinic.go new file mode 100644 index 0000000..6817b08 --- /dev/null +++ b/internal/network/dinic.go @@ -0,0 +1,165 @@ +package network + +import ( + "fmt" + + "github.com/oursimon/flowmo/flowmoerrors" +) + +const infinity = 1 << 50 + +type dinic struct { + adj [][]*edge + nodeLevel []int + iterator []int + source int + sink int +} + +func maxFlow(net *Network, source, sink int) (int, error) { + if net == nil { + return 0, fmt.Errorf( + "network is nil: %w", + flowmoerrors.ErrInvalidArgument, + ) + } + + if source == sink { + return 0, nil + } + + d, err := newDinic(net.adj, source, sink) + if err != nil { + return 0, err + } + + return d.run() +} + +func newDinic(adj [][]*edge, source, sink int) (*dinic, error) { + nrOfNodes := len(adj) + invalidSource := source < 0 || source >= nrOfNodes + if invalidSource { + return nil, fmt.Errorf( + "source node %d: %w", + source, + flowmoerrors.ErrInvalidArgument, + ) + } + + invalidSink := sink < 0 || sink >= nrOfNodes + if invalidSink { + return nil, fmt.Errorf( + "sink node %d: %w", + sink, + flowmoerrors.ErrInvalidArgument, + ) + } + + return &dinic{ + adj: adj, + nodeLevel: make([]int, nrOfNodes), + iterator: make([]int, nrOfNodes), + source: source, + sink: sink, + }, nil +} + +func (d *dinic) run() (int, error) { + total := 0 + for d.buildLevelGraph() { + d.resetIterator() + for { + pushed := d.sendFlow(d.source, infinity) + if pushed == 0 { + break + } + + total += pushed + } + } + + return total, nil +} + +func (d *dinic) buildLevelGraph() bool { + d.resetLevel() + + queue := []int{d.source} + d.nodeLevel[d.source] = 0 + for len(queue) > 0 { + currentNode := queue[0] + queue = queue[1:] + + for _, e := range d.adj[currentNode] { + hasCapacity := e.capacity > 0 + isUnvisited := d.nodeLevel[e.to] < 0 + + if !hasCapacity || !isUnvisited { + continue + } + + d.nodeLevel[e.to] = d.nodeLevel[currentNode] + 1 + queue = append(queue, e.to) + } + } + + return d.nodeLevel[d.sink] >= 0 +} + +// nolint:gocyclo +func (d *dinic) sendFlow(currentNode, incomingFlow int) int { + // Base case: we reached the sink, so we can push everything we carried here. + if currentNode == d.sink { + return incomingFlow + } + + edges := d.adj[currentNode] + for d.iterator[currentNode] < len(edges) { + edgeIndex := d.iterator[currentNode] + e := edges[edgeIndex] + + d.iterator[currentNode]++ + + if e.capacity == 0 { + continue + } + + isOnNextLevel := d.nodeLevel[e.to] == d.nodeLevel[currentNode]+1 + if !isOnNextLevel { + continue + } + + flowLimit := incomingFlow + if e.capacity < flowLimit { + flowLimit = e.capacity + } + + // push it further down the path (dfs) + pushed := d.sendFlow(e.to, flowLimit) + if pushed == 0 { + continue + } + + e.capacity -= pushed + reverseEdge := d.adj[e.to][e.reverse] + reverseEdge.capacity += pushed + + return pushed + } + + // No augmenting path from this node. + return 0 +} + +func (d *dinic) resetLevel() { + for i := range d.nodeLevel { + d.nodeLevel[i] = -1 + } +} + +func (d *dinic) resetIterator() { + for i := range d.iterator { + d.iterator[i] = 0 + } +} diff --git a/internal/network/dinic_test.go b/internal/network/dinic_test.go new file mode 100644 index 0000000..5da1617 --- /dev/null +++ b/internal/network/dinic_test.go @@ -0,0 +1,243 @@ +package network + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/oursimon/flowmo/flowmoerrors" +) + +// Test_MaxFlow_singleEdge: +// +// (0) --10--> (1) +// +// maxFlow = 10 +func Test_MaxFlow_singleEdge(t *testing.T) { + net := New() + nodeZero := net.AddNode() + nodeOne := net.AddNode() + + _ = net.AddEdge(nodeZero, nodeOne, 10) + flow, err := maxFlow(net, nodeZero, nodeOne) + assert.NoError(t, err) + assert.Equal(t, 10, flow) +} + +// Test_MaxFlow_parallelPaths: +// +// 5 +// (0) ---> (1) +// \ \ +// \8 \5 +// \ \ +// > (2) ----> (3) +// 6 +// +// maxFlow = 5 + 6 = 11 +func Test_MaxFlow_parallelPaths(t *testing.T) { + net := New() + net.AddNode() + net.AddNode() + net.AddNode() + net.AddNode() + + _ = net.AddEdge(0, 1, 5) + _ = net.AddEdge(0, 2, 8) + _ = net.AddEdge(1, 3, 5) + _ = net.AddEdge(2, 3, 6) + + flow, err := maxFlow(net, 0, 3) + assert.NoError(t, err) + assert.Equal(t, 11, flow) +} + +// Test_MaxFlow_bottleneck: +// +// (0) --100--> (1) --1--> (2) --100--> (3) +// +// Bottleneck at edge 1->2 +// maxFlow = 1 +func Test_MaxFlow_bottleneck(t *testing.T) { + net := New() + net.AddNode() + net.AddNode() + net.AddNode() + net.AddNode() + + _ = net.AddEdge(0, 1, 100) + _ = net.AddEdge(1, 2, 1) + _ = net.AddEdge(2, 3, 100) + + flow, err := maxFlow(net, 0, 3) + assert.NoError(t, err) + assert.Equal(t, 1, flow) +} + +// Test_MaxFlow_MultiEdge: +// +// (0) --5--> (1) +// (0) --7--> (1) +// +// maxFlow = 5 + 7 = 12 +func Test_MaxFlow_multiEdge(t *testing.T) { + net := New() + + net.AddNode() + net.AddNode() + + _ = net.AddEdge(0, 1, 5) + _ = net.AddEdge(0, 1, 7) + + flow, err := maxFlow(net, 0, 1) + assert.NoError(t, err) + assert.Equal(t, 12, flow) +} + +// Test_MaxFlow_disjointPaths: +// +// (0) --3--> (1) --3--> (2) +// \ / +// \--------1----------/ +// +// maxFlow = 4 +func Test_MaxFlow_disjointPaths(t *testing.T) { + net := New() + net.AddNode() + net.AddNode() + net.AddNode() + + _ = net.AddEdge(0, 1, 3) + _ = net.AddEdge(1, 2, 3) + _ = net.AddEdge(0, 2, 1) + + flow, err := maxFlow(net, 0, 2) + assert.NoError(t, err) + assert.Equal(t, 4, flow) +} + +// Test_MaxFlow_cycle: +// +// (0) --5--> (1) --5--> (2) --4--> (3) +// ^---5-----| +// cycle +// +// maxFlow = 4 +func Test_MaxFlow_cycle(t *testing.T) { + net := New() + net.AddNode() + net.AddNode() + net.AddNode() + net.AddNode() + + _ = net.AddEdge(0, 1, 5) + _ = net.AddEdge(1, 2, 5) + _ = net.AddEdge(2, 3, 4) + _ = net.AddEdge(2, 1, 5) // cycle + + flow, err := maxFlow(net, 0, 3) + assert.NoError(t, err) + assert.Equal(t, 4, flow) +} + +// Test_MaxFlow_sourceEqualsSink: +// maxFlow = 0. +func Test_MaxFlow_sourceEqualsSink(t *testing.T) { + net := New() + net.AddNode() + + flow, err := maxFlow(net, 0, 0) + assert.NoError(t, err) + assert.Equal(t, 0, flow) +} + +// Test_MaxFlow_zeroCapacity: +// +// (0) --0--> (1) +// (0) --5--> (1) +// +// maxFlow = 5 +func Test_MaxFlow_zeroCapacity(t *testing.T) { + net := New() + net.AddNode() + net.AddNode() + + _ = net.AddEdge(0, 1, 0) + _ = net.AddEdge(0, 1, 5) + + flow, err := maxFlow(net, 0, 1) + assert.NoError(t, err) + assert.Equal(t, 5, flow) +} + +// Test_MaxFlow_internalSourceAndSink: +// +// (0) --100--> (1) +// / \ +// 3 / \ 2 +// v v +// (2) (3) +// \ / +// 2 \ / 3 +// v v +// (4) ------100 ------- (5) +// +// maxFlow from 1 to 4 = 2 + 2 = 4 +func Test_MaxFlow_internalSourceAndSink(t *testing.T) { + net := New() + net.AddNode() // 0 + net.AddNode() // 1 + net.AddNode() // 2 + net.AddNode() // 3 + net.AddNode() // 4 + net.AddNode() // 5 + + // Edges that shouldn't matter. + _ = net.AddEdge(0, 1, 100) + _ = net.AddEdge(4, 5, 100) + + // Internal subgraph where we measure the flow from 1 to 4. + _ = net.AddEdge(1, 2, 3) + _ = net.AddEdge(1, 3, 2) + _ = net.AddEdge(2, 4, 2) + _ = net.AddEdge(3, 4, 3) + + flow, err := maxFlow(net, 1, 4) + assert.NoError(t, err) + assert.Equal(t, 4, flow) +} + +func Test_newDinic_givenNilAdjacencyList(t *testing.T) { + _, err := newDinic(nil, 0, 1) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} + +func Test_newDinic_givenInvalidSink(t *testing.T) { + _, err := newDinic([][]*edge{}, -1, 1) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} + +func Test_newDinic_givenOutOfRangeSink(t *testing.T) { + net := New() + _ = net.AddNode() + _ = net.AddNode() + _ = net.AddEdge(0, 1, 100) + // source index 2 is out of range + _, err := newDinic(net.adj, 1, 2) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} + +func Test_newDinic_givenInvalidSource(t *testing.T) { + _, err := newDinic([][]*edge{}, -1, 1) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} + +func Test_newDinic_givenOutOfRangeSource(t *testing.T) { + net := New() + _ = net.AddNode() + _ = net.AddNode() + _ = net.AddEdge(0, 1, 100) + // source index 2 is out of range + _, err := newDinic(net.adj, 2, 1) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} diff --git a/internal/network/edge.go b/internal/network/edge.go new file mode 100644 index 0000000..06c019f --- /dev/null +++ b/internal/network/edge.go @@ -0,0 +1,12 @@ +package network + +type edge struct { + to int + capacity int + initialCapacity int + reverse int +} + +func (e *edge) isResidual() bool { + return e.initialCapacity == 0 +} diff --git a/internal/network/edge_test.go b/internal/network/edge_test.go new file mode 100644 index 0000000..83a7a03 --- /dev/null +++ b/internal/network/edge_test.go @@ -0,0 +1,31 @@ +package network + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_edge_isResidual(t *testing.T) { + tests := []struct { + name string + e edge + want bool + }{ + { + name: "residual edge default zero initial capacity", + e: edge{}, + want: true, + }, + { + name: "non-residual edge with positive initial capacity", + e: edge{initialCapacity: 10}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.e.isResidual()) + }) + } +} diff --git a/internal/network/network.go b/internal/network/network.go new file mode 100644 index 0000000..f44ca94 --- /dev/null +++ b/internal/network/network.go @@ -0,0 +1,135 @@ +package network + +import ( + "fmt" + + "github.com/oursimon/flowmo/flowmoerrors" +) + +type Network struct { + adj [][]*edge +} + +func New() *Network { + return &Network{ + adj: make([][]*edge, 0), + } +} + +func (net *Network) AddNode() int { + net.adj = append(net.adj, []*edge{}) + nodeIdx := len(net.adj) - 1 + + return nodeIdx +} + +func (net *Network) AddEdge(from, to, capacity int) error { + if capacity < 0 { + return fmt.Errorf( + "capacity must be non-negative %d: %w", + capacity, + flowmoerrors.ErrInvalidArgument) + } + + invalidFrom := from < 0 || from >= len(net.adj) + if invalidFrom { + return fmt.Errorf( + "from node %d: %w", + from, + flowmoerrors.ErrInvalidArgument) + } + + invalidTo := to < 0 || to >= len(net.adj) + if invalidTo { + return fmt.Errorf( + "to node %d: %w", + to, + flowmoerrors.ErrInvalidArgument, + ) + } + + net.addEdge(from, to, capacity) + net.addResidual(from, to) + + return nil +} + +func (net *Network) MaxFlow(source, sink int) (int, error) { + return maxFlow(net, source, sink) +} + +// nolint:gocyclo +func (net *Network) IncomingFlowByNode(node int) (int, error) { + invalidNode := node < 0 || node >= len(net.adj) + if invalidNode { + return -1, fmt.Errorf( + "node %d: %w", + node, + flowmoerrors.ErrInvalidArgument, + ) + } + + in := 0 + for _, edges := range net.adj { + for _, e := range edges { + if e.to != node || e.isResidual() { + continue + } + + in += e.initialCapacity - e.capacity + } + } + + return in, nil +} + +func (net *Network) OutgoingFlowByNode(node int) (int, error) { + invalidNode := node < 0 || node >= len(net.adj) + if invalidNode { + return -1, fmt.Errorf( + "node %d: %w", + node, + flowmoerrors.ErrInvalidArgument, + ) + } + + out := 0 + edges := net.adj[node] + for _, e := range edges { + if e.isResidual() { + continue + } + + out += e.initialCapacity - e.capacity + } + + return out, nil +} + +func (net *Network) addEdge(from, to, capacity int) { + toIndex := len(net.adj[to]) + forwardEdge := &edge{ + to: to, + capacity: capacity, + initialCapacity: capacity, + reverse: toIndex, + } + + net.adj[from] = append( + net.adj[from], + forwardEdge, + ) +} + +func (net *Network) addResidual(from, to int) { + fromIndex := len(net.adj[from]) + residualEdge := &edge{ + to: from, + capacity: 0, + reverse: fromIndex, + } + net.adj[to] = append( + net.adj[to], + residualEdge, + ) +} diff --git a/internal/network/network_test.go b/internal/network/network_test.go new file mode 100644 index 0000000..4055da1 --- /dev/null +++ b/internal/network/network_test.go @@ -0,0 +1,151 @@ +package network + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/oursimon/flowmo/flowmoerrors" +) + +func Test_AddEdge_invalidCapacity_shouldReturnError(t *testing.T) { + net := New() + err := net.AddEdge(0, 1, -1) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} + +func Test_AddEdge_outOfRangeFrom_shouldReturnError(t *testing.T) { + net := New() + net.AddNode() + err := net.AddEdge(1, 0, 10) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} + +func Test_AddEdge_outOfRangeTo_shouldReturnError(t *testing.T) { + net := New() + net.AddNode() + err := net.AddEdge(0, 1, 10) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} + +func Test_AddEdge_invalidFrom_shouldReturnError(t *testing.T) { + net := New() + net.AddNode() + err := net.AddEdge(-1, 0, 10) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} + +func Test_AddEdge_invalidTo_shouldReturnError(t *testing.T) { + net := New() + net.AddNode() + err := net.AddEdge(0, -1, 10) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) +} + +func Test_AddEdge_happyCase(t *testing.T) { + net := New() + from := net.AddNode() + to := net.AddNode() + capacity := 15 + + err := net.AddEdge(from, to, capacity) + assert.NoError(t, err) + + assert.Len(t, net.adj, 2) + assert.Len(t, net.adj[from], 1) + assert.Len(t, net.adj[to], 1) + + forwardEdge := net.adj[from][0] + assert.Equal(t, to, forwardEdge.to) + assert.Equal(t, capacity, forwardEdge.capacity) + assert.Equal(t, capacity, forwardEdge.initialCapacity) + + residualEdge := net.adj[to][0] + assert.Equal(t, from, residualEdge.to) + assert.Equal(t, 0, residualEdge.capacity) + assert.Equal(t, 0, residualEdge.initialCapacity) +} + +func Test_IncomingFlowByNode_beforeMaxFlow_shouldReturnZero(t *testing.T) { + net := New() + from := net.AddNode() + to := net.AddNode() + capacity := 10 + + _ = net.AddEdge(from, to, capacity) + + incoming, _ := net.IncomingFlowByNode(to) + assert.Equal(t, 0, incoming) +} + +func Test_IncomingFlowByNode_afterMaxFlow_shouldReturnFlow(t *testing.T) { + net := New() + from := net.AddNode() + to := net.AddNode() + capacity := 10 + + _ = net.AddEdge(from, to, capacity) + + _, _ = net.MaxFlow(from, to) + + incoming, _ := net.IncomingFlowByNode(to) + assert.Equal(t, capacity, incoming) +} + +func Test_OutgoingFlowByNode_beforeMaxFlow_shouldReturnZero(t *testing.T) { + net := New() + from := net.AddNode() + to := net.AddNode() + capacity := 10 + + _ = net.AddEdge(from, to, capacity) + + outgoing, _ := net.OutgoingFlowByNode(from) + assert.Equal(t, 0, outgoing) +} + +func Test_OutgoingFlowByNode_afterMaxFlow_shouldReturnFlow(t *testing.T) { + net := New() + from := net.AddNode() + to := net.AddNode() + capacity := 10 + + _ = net.AddEdge(from, to, capacity) + + _, _ = net.MaxFlow(from, to) + + outgoing, _ := net.OutgoingFlowByNode(from) + assert.Equal(t, capacity, outgoing) +} + +func Test_AddEdge_selfLoop_shouldWork(t *testing.T) { + // Self-loop: edge from node to itself + net := New() + node := net.AddNode() + + err := net.AddEdge(node, node, 10) + assert.NoError(t, err) + + // Self-loop should contribute 0 to max flow + flow, err := net.MaxFlow(node, node) + assert.NoError(t, err) + assert.Equal(t, 0, flow) +} + +func Test_IncomingFlowByNode_invalidNode_shouldReturnError(t *testing.T) { + net := New() + _ = net.AddNode() + + incoming, err := net.IncomingFlowByNode(5) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) + assert.Equal(t, -1, incoming) +} + +func Test_OutgoingFlowByNode_invalidNode_shouldReturnError(t *testing.T) { + net := New() + _ = net.AddNode() + + outgoing, err := net.OutgoingFlowByNode(5) + assert.ErrorIs(t, err, flowmoerrors.ErrInvalidArgument) + assert.Equal(t, -1, outgoing) +}