File: cycles.go

package info (click to toggle)
golang-github-cue-lang-cue 0.12.0.-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 19,072 kB
  • sloc: sh: 57; makefile: 17
file content (153 lines) | stat: -rw-r--r-- 3,970 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// Copyright 2024 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package toposort

import "slices"

type ecNodeState struct {
	visitedIncoming []*ecNodeState
	blocked         bool
}

func (ecNode *ecNodeState) excluded() bool {
	return ecNode == nil
}

type ecFinderState struct {
	cycles []*Cycle
	stack  []*Node
}

type Cycle struct {
	Nodes Nodes
}

func (cycle *Cycle) RotateToStartAt(start *Node) {
	nodes := cycle.Nodes
	if start == nodes[0] {
		return
	}
	for i, node := range nodes {
		if start == node {
			prefix := slices.Clone(nodes[:i])
			copy(nodes, nodes[i:])
			copy(nodes[len(nodes)-i:], prefix)
			break
		}
	}
}

// Calculate the Elementary Cycles (EC) within the current Strongly
// Connected Component (SCC).
//
// If the component contains no cycles (by definition, this means the
// component contains only a single node), then the slice returned
// will be empty.
//
// In general:
//
//  1. If a component contains two or more nodes then it contains at
//     least one cycle.
//  2. A single node can be involved in many cycles.
//  3. This method finds all cycles within a component, but does not
//     include cycles that are merely rotations of each
//     other. I.e. every cycle is unique, ignoring rotations.
//  4. The cycles returned are unsorted: each cycle is itself in no
//     particular rotation, and the complete slice of cycles is
//     similarly unsorted.
//
// The complexity of this algorithm is O((n+e)*c) where
//   - n: number of nodes in the SCC
//   - e: number of edges between the nodes in the SCC
//   - c: number of cycles discovered
//
// Donald B Johnson: Finding All the Elementary Circuits of a Directed
// Graph. SIAM Journal on Computing. Volumne 4, Nr. 1 (1975),
// pp. 77-84.
func (scc *StronglyConnectedComponent) ElementaryCycles() []*Cycle {
	nodes := scc.Nodes
	nodeStates := make([]ecNodeState, len(nodes))
	for i, node := range nodes {
		node.ecNodeState = &nodeStates[i]
	}

	ec := &ecFinderState{}
	for i, node := range nodes {
		ec.findCycles(node, node)
		ec.unblockAll(nodes[i+1:])
		node.ecNodeState = nil
	}

	return ec.cycles
}

func (ec *ecFinderState) findCycles(origin, cur *Node) bool {
	stackLen := len(ec.stack)
	ec.stack = append(ec.stack, cur)

	curEc := cur.ecNodeState
	curEc.blocked = true

	cycleFound := false
	for _, next := range cur.Outgoing {
		if next.ecNodeState.excluded() {
			continue
		}
		if next == origin { // found cycle
			ec.cycles = append(ec.cycles, &Cycle{Nodes: slices.Clone(ec.stack)})
			cycleFound = true
		} else if !next.ecNodeState.blocked {
			if ec.findCycles(origin, next) {
				cycleFound = true
			}
		}
	}

	if cycleFound {
		ec.unblock(curEc)
	} else {
		for _, next := range cur.Outgoing {
			if next.ecNodeState.excluded() {
				continue
			}
			nextEc := next.ecNodeState
			nextEc.visitedIncoming = append(nextEc.visitedIncoming, curEc)
		}
	}

	if len(ec.stack) != stackLen+1 {
		panic("stack is unexpected height!")
	}
	ec.stack = ec.stack[:stackLen]
	return cycleFound
}

func (ec *ecFinderState) unblockAll(nodes Nodes) {
	for _, node := range nodes {
		nodeEc := node.ecNodeState
		nodeEc.blocked = false
		nodeEc.visitedIncoming = nodeEc.visitedIncoming[:0]
	}
}

func (ec *ecFinderState) unblock(nodeEc *ecNodeState) {
	nodeEc.blocked = false
	for _, previousEc := range nodeEc.visitedIncoming {
		if previousEc.blocked {
			ec.unblock(previousEc)
		}
	}
	nodeEc.visitedIncoming = nodeEc.visitedIncoming[:0]
}