236 lines
6.4 KiB
Go
Executable File
236 lines
6.4 KiB
Go
Executable File
/*
|
|
Copyright 2023 The Karmada 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 spreadconstraint
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"k8s.io/klog/v2"
|
|
)
|
|
|
|
// GroupInfo indicate the group info.
|
|
type GroupInfo struct {
|
|
name string
|
|
value int
|
|
weight int64
|
|
}
|
|
|
|
type dfsPath struct {
|
|
id int
|
|
groups []*GroupInfo
|
|
weight int64
|
|
value int
|
|
}
|
|
|
|
func (path *dfsPath) next() *dfsPath {
|
|
path.id++
|
|
result := new(dfsPath)
|
|
*result = *path
|
|
if path.groups != nil {
|
|
result.groups = make([]*GroupInfo, len(path.groups))
|
|
copy(result.groups, path.groups)
|
|
}
|
|
for _, group := range result.groups {
|
|
result.weight += group.weight
|
|
result.value += group.value
|
|
}
|
|
result.sortGroups()
|
|
return result
|
|
}
|
|
|
|
func (path *dfsPath) enqueue(group *GroupInfo) {
|
|
path.groups = append(path.groups, group)
|
|
}
|
|
|
|
func (path *dfsPath) popLast() {
|
|
path.groups = path.groups[:path.length()-1]
|
|
}
|
|
|
|
func (path *dfsPath) length() int {
|
|
return len(path.groups)
|
|
}
|
|
|
|
func (path *dfsPath) sortGroups() {
|
|
sort.Slice(path.groups, func(i, j int) bool {
|
|
if path.groups[i].weight != path.groups[j].weight {
|
|
return path.groups[i].weight > path.groups[j].weight
|
|
}
|
|
|
|
return path.groups[i].name < path.groups[j].name
|
|
})
|
|
}
|
|
|
|
func (path *dfsPath) matchSubPath(subPath *dfsPath) bool {
|
|
if subPath.length() >= path.length() {
|
|
return false
|
|
}
|
|
|
|
for i, group := range subPath.groups {
|
|
if path.groups[i].name != group.name {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (path *dfsPath) String() string {
|
|
var groupNames []string
|
|
for _, group := range path.groups {
|
|
groupNames = append(groupNames, group.name)
|
|
}
|
|
return fmt.Sprintf("[%s]", strings.Join(groupNames, "->"))
|
|
}
|
|
|
|
// selectGroups select best groups in all groups.
|
|
func selectGroups(groups []*GroupInfo, minConstraint, maxConstraint, target int) []*GroupInfo {
|
|
if len(groups) == 0 {
|
|
return nil
|
|
}
|
|
|
|
feasiblePaths := findFeasiblePaths(groups, minConstraint, maxConstraint, target)
|
|
if len(feasiblePaths) == 0 {
|
|
return nil
|
|
}
|
|
klog.V(4).Infof("find feasible paths: %v", feasiblePaths)
|
|
|
|
return prioritizePaths(feasiblePaths).groups
|
|
}
|
|
|
|
// findFeasiblePaths find all feasible path based on DFS.
|
|
// We give an example of a tree diagram for easy understanding, assuming: target=7, groups: [2,3,6,7].
|
|
// Note: groups is only briefly represented by the value.
|
|
//
|
|
// +---------------------------------+
|
|
// | target=7 |
|
|
// +---------------+-------------+---+
|
|
// / | | \
|
|
// 2/ 3| 6| \7
|
|
// / | | \
|
|
// +---+ +-+-+ +-+-+ +---+
|
|
// | 5 | | 4 | | 1 | | 0 |
|
|
// +-+-+ +---+ +-+-+ +---+
|
|
// / | \ / \ |
|
|
// 3/ 6| \7 6/ \7 7|
|
|
// / | \ / \ |
|
|
// +---+ +-+-+ +---+ +---+ +---+ +-+-+
|
|
// | 2 | |-1 | |-2 | |-2 | |-3 | |-6 |
|
|
// +---+ +---+ +---+ +---+ +---+ +---+
|
|
// / \
|
|
// 6/ \7
|
|
// / \
|
|
// +---+ +---+
|
|
// |-4 | |-5 |
|
|
// +---+ +---+
|
|
//
|
|
// The path from the root path to all leaf nodes whose value is less than or equal to 0 is a feasible combination.
|
|
// minConstraint and maxConstraint are used to limit the length of the path. Therefore, we can exclude some
|
|
// combinations whose path is too long or too short during the search process.
|
|
// If we set minConstraint=2 and maxConstraint=2, so the feasible paths is: [2,6] [2,7] [3,6] [3,7] [6,7]
|
|
func findFeasiblePaths(groups []*GroupInfo, minConstraint, maxConstraint, target int) (paths []*dfsPath) {
|
|
if len(groups) > 1 {
|
|
sort.Slice(groups, func(i, j int) bool {
|
|
if groups[i].value != groups[j].value {
|
|
return groups[i].value < groups[j].value
|
|
}
|
|
|
|
if groups[i].weight != groups[j].weight {
|
|
return groups[i].weight > groups[j].weight
|
|
}
|
|
|
|
return groups[i].name < groups[j].name
|
|
})
|
|
}
|
|
|
|
if len(groups) < minConstraint {
|
|
return
|
|
}
|
|
|
|
if len(groups) == minConstraint {
|
|
var sum int
|
|
rootPath := new(dfsPath)
|
|
for _, i := range groups {
|
|
sum += i.value
|
|
rootPath.enqueue(i)
|
|
}
|
|
if sum >= target {
|
|
paths = append(paths, rootPath.next())
|
|
}
|
|
return
|
|
}
|
|
|
|
rootPath := new(dfsPath)
|
|
var dfsFunc func(int, int)
|
|
dfsFunc = func(sum, begin int) {
|
|
if sum >= target && rootPath.length() >= minConstraint && rootPath.length() <= maxConstraint {
|
|
paths = append(paths, rootPath.next())
|
|
return
|
|
}
|
|
|
|
// pruning makes DFS faster
|
|
if rootPath.length() >= maxConstraint {
|
|
return
|
|
}
|
|
|
|
for i := begin; i < len(groups); i++ {
|
|
sum += groups[i].value
|
|
rootPath.enqueue(groups[i])
|
|
dfsFunc(sum, i+1)
|
|
sum -= groups[i].value
|
|
rootPath.popLast()
|
|
}
|
|
}
|
|
dfsFunc(0, 0)
|
|
return
|
|
}
|
|
|
|
// prioritizePaths select a best path from feasible paths based on a multi-dimensional prioritization strategy.
|
|
// That's: weight > value > id(just for predictable result).
|
|
// But for the existence of subpaths, we should give priority to subpaths.
|
|
// For example:
|
|
// G1 -> G2 -> G3
|
|
// G1 -> G2
|
|
// G1 -> G3
|
|
// The best path is [G1 -> G2 -> G3], but [G1 -> G2] is its subpath. So we should choose [G1 -> G2].
|
|
func prioritizePaths(feasiblePaths []*dfsPath) *dfsPath {
|
|
if len(feasiblePaths) == 1 {
|
|
return feasiblePaths[0]
|
|
}
|
|
|
|
sort.Slice(feasiblePaths, func(i, j int) bool {
|
|
if feasiblePaths[i].weight != feasiblePaths[j].weight {
|
|
return feasiblePaths[i].weight > feasiblePaths[j].weight
|
|
}
|
|
|
|
if feasiblePaths[i].value != feasiblePaths[j].value {
|
|
return feasiblePaths[i].value > feasiblePaths[j].value
|
|
}
|
|
|
|
return feasiblePaths[i].id < feasiblePaths[j].id
|
|
})
|
|
|
|
finalPath := feasiblePaths[0]
|
|
for i := 1; i < len(feasiblePaths); i++ {
|
|
if finalPath.matchSubPath(feasiblePaths[i]) {
|
|
finalPath = feasiblePaths[i]
|
|
}
|
|
}
|
|
return finalPath
|
|
}
|