-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathlis.go
224 lines (200 loc) · 7.36 KB
/
lis.go
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
package slice
import (
"cmp"
"slices"
)
// Editorial note: "longest increasing subsequence" and "longest
// non-decreasing subsequence" are a mouthful, so in this file
// "subsequence" and "longest subsequence" imply the ordering, unless
// explicitly specified otherwise.
// LNDS computes a longest non-decreasing subsequence of vs.
//
// This implementation takes O(P·log(n)) time and O(n) space for
// inputs of length n = len(vs) and longest subsequence length P. If
// the longest subsequence is the entire input, it takes O(n) time and
// O(n) space.
func LNDS[T cmp.Ordered, Slice ~[]T](vs Slice) Slice {
return LNDSFunc(vs, cmp.Compare)
}
// LNDSFunc computes a longest non-decreasing subsequence of vs, in
// the order determined by the cmp function. cmp must return a
// negative number when a < b, a positive number when a > b, and zero
// when a == b.
//
// This implementation takes O(P·log(n)) time and O(n) space for
// inputs of length n = len(vs) and longest subsequence length P. If
// the longest subsequence is the entire input, it takes O(n) time and
// O(n) space.
func LNDSFunc[T any, Slice ~[]T](vs Slice, cmp func(a, b T) int) Slice {
if len(vs) == 0 {
return vs
}
// At its core, the algorithm considers every possible
// non-decreasing subsequence of vs, and picks one of the
// longest. The naive implementation is quadratic in either time
// or space (see lis_test.go for the former). Thankfully, four
// optimizations let us achieve O(n·log(n)):
//
// - Each element only gets to participate in creating the
// longest subsequences it can, we discard all shorter
// options. It might still participate in many subsequences of
// that length, but that brings us to...
// - We only need to remember one subsequence of every length,
// the one whose final element is the smallest. This is the
// tails array, and means that every new element will
// contribute to exactly 0 or 1 subsequence.
// - Elements always appear in the tails array in non-decreasing
// order, so we can use a binary search to find the one
// subsequence that a new element might contribute to. This
// gets us O(log(n)) time per element, instead of O(n).
// - Successive non-decreasing new elements always contribute to
// the longest currently known subsequence, which is the final
// entry of tails. If we check for this trivial case before
// embarking on the binary search, elements that appear in
// non-decreasing order can be processed in O(1) time rather
// than O(log(n)).
//
// There are truly marvelous proofs of these optimizations, which
// this comment is too short to contain.
var (
// tails[L] is the index into vs for the final element of a
// subsequence of length L. If several such subsequences
// exist, tails keeps whichever has the smallest final
// element, according to cmp.
tails = make([]int, 1, len(vs))
// prev[i] is the index into vs for the element that comes
// before vs[i], in some subsequence tracked by tails. If
// vs[i] is the first element of that subsequence, prev[i] is
// -1.
//
// It's effectively the pointers of linked lists whose heads
// are tracked in tails.
prev = make([]int, len(vs))
)
// The loop is cleaner if it can assume that at least 1 element
// has already been processed. Run the first iteration directly.
prev[0] = -1
tails[0] = 0
for i := range vs[1:] {
// While the loop is cleaner if we lift out the first
// iteration, it's confusing to humans to have i be off-by-one
// from vs's natural indexing. Correct that here so the rest
// of the loop is easier to follow.
i++
idxOfBestTail := tails[len(tails)-1]
if cmp(vs[i], vs[idxOfBestTail]) >= 0 {
// Fast path: the i-th element extends the currently known
// longest subsequence.
prev[i] = idxOfBestTail
tails = append(tails, i)
continue
}
// Otherwise, the i-th element _must_ be an improvement over a
// shorter subsequence currently being tracked in tails. Find
// and replace it.
//
// Note we run the search over tails minus its final element,
// which avoids repeating the fast path's compare during the
// search. It doesn't change the outcome since the fast path
// eliminated the "beyond the end of tails" edge case.
replaceIdx := bisectRight(tails[:len(tails)-1], vs[i], func(idx int, target T) int {
return cmp(vs[idx], target)
})
// The new element is extending the subsequence tracked in
// replaceIdx-1. If we're replacing the singleton subsequence,
// we have to avoid the out of bounds read of tails.
if replaceIdx == 0 {
prev[i] = -1
} else {
prev[i] = tails[replaceIdx-1]
}
tails[replaceIdx] = i
}
// We can now iterate back through the longest subsequence and
// partition the input.
ret := make([]T, len(tails))
seqIdx := tails[len(tails)-1] // current longest subsequence element
for i := range ret {
ret[len(ret)-1-i] = vs[seqIdx]
seqIdx = prev[seqIdx]
}
return ret
}
// LIS computes a longest strictly increasing subsequence of vs.
//
// This implementation takes O(P·log(n)) time and O(n) space for
// inputs of length n = len(vs) and longest subsequence length P. If
// the longest subsequence is the entire input, it takes O(n) time and
// O(n) space.
func LIS[T cmp.Ordered, Slice ~[]T](vs Slice) Slice {
return LISFunc(vs, cmp.Compare)
}
// LISFunc computes a longest strictly increasing subsequence of vs,
// in the order determined by the cmp function. cmp must return a
// negative number when a < b, a positive number when a > b, and zero
// when a == b.
//
// This implementation takes O(P·log(n)) time and O(n) space for
// inputs of length n = len(vs) and longest subsequence length P. If
// the longest subsequence is the entire input, it takes O(n) time and
// O(n) space.
func LISFunc[T any, Slice ~[]T](vs Slice, cmp func(a, b T) int) Slice {
// LISFunc is almost exactly the same as LNDSFunc, except that
// comparisons are > instead of >= and the binary search leans
// left. Further comments have been omitted for brevity.
if len(vs) == 0 {
return vs
}
var (
tails = make([]int, 1, len(vs))
prev = make([]int, len(vs))
)
prev[0] = -1
tails[0] = 0
for i := range vs[1:] {
i++
idxOfBestTail := tails[len(tails)-1]
if cmp(vs[i], vs[idxOfBestTail]) > 0 {
prev[i] = idxOfBestTail
tails = append(tails, i)
continue
}
replaceIdx, _ := slices.BinarySearchFunc(tails[:len(tails)-1], vs[i], func(idx int, target T) int {
return cmp(vs[idx], target)
})
if replaceIdx == 0 {
prev[i] = -1
} else {
prev[i] = tails[replaceIdx-1]
}
tails[replaceIdx] = i
}
ret := make([]T, len(tails))
seqIdx := tails[len(tails)-1]
for i := range ret {
ret[len(ret)-1-i] = vs[seqIdx]
seqIdx = prev[seqIdx]
}
return ret
}
// bisectRight returns the position where target should be inserted in
// a sorted slice. If target is already present in the slice, the
// returned position is one past the final existing occurrence.
//
// This is effectively a right-leaning variant of
// slices.BinarySearch. It doesn't return a found bool, since by
// definition it will never return an index equivalent to target.
func bisectRight[T, U any, Slice ~[]T](vs Slice, target U, cmp func(T, U) int) (idx int) {
ln := len(vs)
low, high := uint(0), uint(ln)
for low < high {
mid := (low + high) / 2
if cmp(vs[mid], target) > 0 {
high = mid
} else {
low = mid + 1
}
}
ret := int(low)
return ret
}