Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ go.work.sum
.env

# Editor/IDE
# .idea/
# .vscode/
.idea/
.vscode/
57 changes: 57 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -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$
5 changes: 5 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.PHONY: lint
lint:
@golangci-lint run ./...

.PHONY: test
test:
@go test -count=5 -race -cover ./...


67 changes: 66 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,66 @@
# flowmo
# 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.

42 changes: 42 additions & 0 deletions examples/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
136 changes: 136 additions & 0 deletions flowmo.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading