Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
acomagu committed Oct 27, 2019
0 parents commit 041ae87
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# mindfa: The implementation of Hopcroft's algorithm in Go

The implementation of [DFA minimization](https://en.wikipedia.org/wiki/DFA_minimization) using Hopcroft's algorithm in Go. The time complexity is O(n log n) and the memory complexity is O(n)(n is the number of the states of the input DFA).
136 changes: 136 additions & 0 deletions dfa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Package mindfa implements DFA minimization using Hopcroft's algorithm.
package mindfa

import (
"fmt"
"sort"
)

// Minimize takes a DFA representation, minimize it and returns the groups of
// the states. The states in a group has same behavior.
//
// The all of arguments represents a single DFA(before minimization). It has
// 0..nState states, 0..nSymbol input symbols and transitions as transition function.
//
// transition is a function takes state and symbol and returns the destination
// state. The states which is included in finals are accepting state. The order
// of the resulting partitions is not defined, but the order of the states in
// a partition is ascending.
//
// It uses Hopcroft's algorithm. The memory complexity is O(nState).
//
// The all numbers in finals must be in [0, nState), and two same values must not
// appear.
func Minimize(nState, nSymbol int, finals []int, transition func(state, symbol int) int) [][]int {
if len(finals) > nState {
panic(fmt.Sprintf("len(finals) should be less than or equal to nState: len(finals) = %d, nState = %d", len(finals), nState))
}

whole := make([]int, nState+max(len(finals), nState-len(finals)))
copy(whole, finals)
sort.Ints(whole[:len(finals)])

for i := 0; i < len(finals)-1; i++ {
if whole[i] == whole[i+1] {
panic(fmt.Sprintf("finals contains same two value: %d", whole[i]))
}
}

cmpl(whole[len(finals):nState], whole[:len(finals)], nState)

buf := whole[nState:]

partitions := [][]int{whole[:len(finals)], whole[len(finals):nState]}
// works is a set of the partition which has never tried to be split.
works := [][]int{whole[:len(finals)], whole[len(finals):nState]}

for len(works) > 0 {
for c := 0; c < nSymbol; c++ {
for ip, pFrom := range partitions {
ip1, ip2 := 0, len(buf)-1
for _, state := range pFrom {
if includes(works[0], transition(state, c)) {
buf[ip1] = state
ip1++
} else {
buf[ip2] = state
ip2--
}
}

if ip1 == 0 || ip2 == len(buf)-1 {
continue
}

p1 := pFrom[:ip1]
copy(p1, buf[:ip1])

p2 := pFrom[ip1:]
for i := range p2 {
p2[i] = buf[len(buf)-1-i]
}

var split bool
for i, w := range works {
if &w[0] != &pFrom[0] {
continue
}

// Split works[i].
works = append(works, p2)
works[i] = p1
split = true
break
}

if !split {
if len(p1) < len(p2) {
works = append(works, p1)
} else {
works = append(works, p2)
}
}
partitions[ip] = p1
partitions = append(partitions, p2) // Don't worry, p2 is not iterated in the current loop.
}
}
// pseudo-shift
works[0] = works[len(works)-1]
works = works[:len(works)-1]
}
return partitions
}

// cmpl returns the complement set of a in (0..upper).
func cmpl(dst, a []int, upper int) {
var n, i int
for _, u := range a {
for ; n < u; n++ {
dst[i] = n
i++
}
n++
}
for ; n < upper; n++ {
dst[i] = n
i++
}
}

func includes(a []int, e int) bool {
if len(a) < 100 {
var i int
for ; i < len(a) && a[i] < e; i++ {
}
return i < len(a) && a[i] == e
}
i := sort.SearchInts(a, e)
return i < len(a) && a[i] == e
}

func max(a, b int) int {
if a > b {
return a
}
return b
}
92 changes: 92 additions & 0 deletions dfa_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package mindfa_test

import (
"fmt"
"sort"

"github.com/acomagu/mindfa"
)

// Minimize the DFA as: https://commons.wikimedia.org/wiki/File%3ADFA_to_be_minimized.jpg#/media/File%3ADFA_to_be_minimized.jpg
// The result becomes: https://commons.wikimedia.org/wiki/File%3AMinimized_DFA.jpg#/media/File%3AMinimized_DFA.jpg .
func ExampleMinimize() {
nState := 6
nSymbol := 2
finals := []int{2, 3, 4}
transitions := [][]int{
// 0 1
0: {1, 2}, // a
1: {0, 3}, // b
2: {4, 5}, // c
3: {4, 5}, // d
4: {4, 5}, // e
5: {5, 5}, // f
}
transitionFunc := func(state, symbol int) int { return transitions[state][symbol] }

partitions := mindfa.Minimize(nState, nSymbol, finals, transitionFunc)
fmt.Println(partitions) // Output: [[2 3 4] [0 1] [5]]
}

// This example creates the minimum DFA inputs the digits of year(like 2 -> 0 -> 2 -> 1)
// and accepts if the year is a leap year.
func ExampleMinimize_determiningLeapYear() {
nSymbol := 10
nState := 400

// finals becomes the list of leap years up to 400.
var finals []int
for s := 0; s < nState; s++ {
if s == 0 || (s%100 != 0 && s%4 == 0) {
finals = append(finals, s)
}
}

// e.g. 2 -> 20 -> 202 -> 21
transitions := func(state, symbol int) int {
return (state*10 + symbol) % nState
}

partitions := mindfa.Minimize(nState, nSymbol, finals, transitions)

// Mark years belonging to the same partition with identical number.
classes := make([]int, nState)
for _, p := range partitions {
for _, s := range p {
classes[s] = p[0]
}
}

checkLeapYear := func(year int) {
state := classes[0]
ds := digits(year) // digits(n) returns the slice of the digits of n.
for i := len(ds) - 1; i >= 0; i-- {
state = transitions(classes[state], ds[i])
}

// If the state is acceptable, it is a leap year.
if u := sort.SearchInts(finals, state); u < len(finals) && finals[u] == state {
fmt.Printf("%d is a leap year.\n", year)
} else {
fmt.Printf("%d is not a leap year.\n", year)
}
}

checkLeapYear(2019)
checkLeapYear(2020)
checkLeapYear(2021)
// Output:
// 2019 is not a leap year.
// 2020 is a leap year.
// 2021 is not a leap year.
}

// digits returns the slice of the digits of n.
func digits(n int) []int {
var ans []int
for n > 0 {
ans = append(ans, n%10)
n /= 10
}
return ans
}
115 changes: 115 additions & 0 deletions dfa_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package mindfa

import (
"testing"

"github.com/matryer/is"
)

func TestMinimize2(t *testing.T) {
is := is.New(t)

nState := 5
nSymbol := 3
finals := []int{1, 3}
var transitions [5][3]int
transitions[0][0] = 3
transitions[0][1] = 4
transitions[0][2] = 3
transitions[1][0] = 4
transitions[1][1] = 2
transitions[1][2] = 3
transitions[2][0] = 3
transitions[2][1] = 4
transitions[2][2] = 3
transitions[3][0] = 2
transitions[3][1] = 4
transitions[3][2] = 4
transitions[4][0] = 0
transitions[4][1] = 3
transitions[4][2] = 3

partitions := Minimize(nState, nSymbol, finals, func(s, c int) int { return transitions[s][c] })
is.Equal(len(partitions), 4)
}

func TestMinimize1(t *testing.T) {
is := is.New(t)

nSymbol := 10
nState := 400

var finals []int
for s := 0; s < nState; s++ {
if s == 0 || (s%100 != 0 && s%4 == 0) {
finals = append(finals, s)
}
}

transitions := make([][]int, 0, nState)
for i := 0; i < nState; i++ {
t := make([]int, 0, nSymbol)
for c := 0; c < nSymbol; c++ {
t = append(t, (i*nSymbol+c)%nState)
}

transitions = append(transitions, t)
}

partitions := Minimize(nState, nSymbol, finals, func(s, c int) int { return transitions[s][c] })
is.Equal(len(partitions), 7)

classes := make([]int, nState)
for _, p := range partitions {
for _, s := range p {
classes[s] = p[0]
}
}

for n := 0; n < 5000; n++ {
cur := classes[0]
ds := digits(n)
for i := len(ds) - 1; i >= 0; i-- {
cur = transitions[classes[cur]][ds[i]]
}
is.Equal(includes(finals, cur), n%4 == 0 && (n%100 != 0 || n%400 == 0))
}
}

var _unused interface{}

func BenchmarkMinimize(b *testing.B) {
nSymbol := 10
nState := 400

var finals []int
for s := 0; s < nState; s++ {
if s == 0 || (s%100 != 0 && s%4 == 0) {
finals = append(finals, s)
}
}

transitions := make([][]int, 0, nState)
for i := 0; i < nState; i++ {
t := make([]int, 0, nSymbol)
for c := 0; c < nSymbol; c++ {
t = append(t, (i*nSymbol+c)%nState)
}

transitions = append(transitions, t)
}

transition := func(s, c int) int { return transitions[s][c] }
for i := 0; i < b.N; i++ {
_unused = Minimize(nState, nSymbol, finals, transition)
}
}

func digits(n int) []int {
var ans []int
for n > 0 {
ans = append(ans, n%10)
n /= 10
}
return ans
}
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/acomagu/mindfa

go 1.13

require (
github.com/matryer/is v1.2.0
github.com/theodesp/unionfind v0.0.0-20181009090329-54e28c9f081e // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/theodesp/unionfind v0.0.0-20181009090329-54e28c9f081e h1:mMI3DfT0woqtLW0fkZHqa+JAh4+IYd1L3pndbUzAOpg=
github.com/theodesp/unionfind v0.0.0-20181009090329-54e28c9f081e/go.mod h1:1YbOQ/RED6w5UOYsaWLb346ayQhuDN5xE5Tv31r1n38=

0 comments on commit 041ae87

Please sign in to comment.