分类 默认分类 下的文章

导语

作为程序员,时常在一些技术书中看到这些UML类图表示,大部分能看懂意思,但那些箭头和标记很容易混淆。其实UML类图是一门精准的语言,每个符号都是有着精确的程序语义的,在这篇文章中,通过自己整理的一个例子,举例说明,帮助自己加深理解。

UML类图例子

其实多看多用就熟悉了,举一个例子,来看这样一幅图,其中就包括了常见UML类图的基本图示法。
UML类图.png
首先看“交通工具”矩形框,它就代表一个类(Class)。类图分三层,第一层显示类的名称,如果是抽象类,则就用斜体显示。第二层是类的特性,通常就是字段和属性。第三层是类的操作,通常是方法或行为。注意前面的符号,‘+’表示public,‘-’表示private,‘#’表示protected。
然后注意左下角的“翻越”,它表示一个接口图,与类图的区别主要是顶端有<<interface>>显示。第一行是接口名称,第二行是接口方法。接口还有另一种表示方法,俗称棒棒糖表示法,比如图中的警车类就是实现了“鸣笛”的接口。

interface ICross {
    // 翻越
    void Cross();
}

interface IWhistle {
    // 鸣笛
    void Whistle();
}

接下来就可以看下类与类,类与接口之间的关系了。可以观察交通工具、车、轿车、卡车、越野车和警车之间的关系,他们都是继承关系,用空心三角形加上实线来表示。例子中的几种车中,越野车是可以翻越小山丘的,我让它实现了翻越接口,实现接口就用空心三角形加上虚线来表示。、

再看卡车和法规(这个例子可能不是特别恰当,但是我想不当更合适的),卡车一般都是运送货物,需要遵守政府的货运相关的法规,限重多少,和卡车能否上路行驶息息相关,卡车司机需要“知道”政府法规,当一个类‘知道’另一个类时,可以用关联(association)。关联关系用实线箭头来表示。

越野车队和越野车这两个类,生活中,开越野车的车友喜欢加入俱乐部,参加越野车活动,一个车队可以有多辆车,所以它们之间就满足聚合(Aggregation)关系。聚合表示一种弱的‘拥有’关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。聚合关系用空心的菱形+实线箭头来表示。

合成(Composition,也有翻译成‘组合’的)是一种强的‘拥有’关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样,在这里车和轮胎就是合成(组合)关系,因为它们是部分和整体的关系,并且轮胎和车的生命周期是相同的。合成关系用实心的菱形+实线箭头来表示。另外,你会注意到合成关系的连线两端还有一个数字‘1’和数字‘2’,这被称为基数。表明这一端的类可以有几个实例,很显然,一辆车应该有5个轮胎(算上备胎)。如果一个类可能有无数个实例,则就用‘n’来表示。关联关系、聚合关系也可以有基数的。

交通工具的几大特征,比如类型,路上、水上还是空中的交通工具,交通工具都需要燃料驱动,需要机油保养,他们之间是依赖关系(Dependency),用虚线箭头来表示。

abstract class Vehicle {
   // 增加能源
   void Fill(Energy energy);
   // 保养
   void Maintain(EngineOil oil);
}

结论

编程是一门技术,更加是一门艺术,不能只满足于写完代码运行结果正确就完事,时常考虑如何让代码更加简练,更加容易维护,容易扩展和复用,只有这样才可以真正得到提高。写出优雅的代码真的是一种很爽的事情。UML类图也不是一学就会的,需要有一个慢慢熟练的过程。所谓学无止境,其实这才是理解面向对象的开始呢。

导语

函数式编程的中非常重要的Map、Reduce、Filter的三种操作,这三种操作可以让我们非常方便灵活地进行一些数据处理——我们的程序中大多数情况下都是在到倒腾数据,尤其对于一些需要统计的业务场景,Map/Reduce/Filter是非常通用的玩法。

例子

Map示例
下面的程序代码中,我们写了两个Map函数,这两个函数需要两个参数:

  • 一个是字符串数组 []string,说明需要处理的数据一个字符串
  • 另一个是一个函数func(s string) string 或 func(s string) int
func MapStrToStr(arr []string, fn func(s string) string) []string {
    var newArray = []string{}
    for _, it := range arr {
        newArray = append(newArray, fn(it))
    }
    return newArray
}

func MapStrToInt(arr []string, fn func(s string) int) []int {
    var newArray = []int{}
    for _, it := range arr {
        newArray = append(newArray, fn(it))
    }
    return newArray
}

整个Map函数运行逻辑都很相似,函数体都是在遍历第一个参数的数组,然后,调用第二个参数的函数,然后把其值组合成另一个数组返回。
于是我们就可以这样使用这两个函数:

var list = []string{"Hao", "Chen", "MegaEase"}

x := MapStrToStr(list, func(s string) string {
    return strings.ToUpper(s)
})
fmt.Printf("%v\n", x)
//["HAO", "CHEN", "MEGAEASE"]

y := MapStrToInt(list, func(s string) int {
    return len(s)
})
fmt.Printf("%v\n", y)
//[3, 4, 8]

我们可以看到,我们给第一个 MapStrToStr() 传了函数做的是 转大写,于是出来的数组就成了全大写的,给MapStrToInt() 传的是算其长度,所以出来的数组是每个字符串的长度。
我们再来看一下Reduce和Filter的函数是什么样的。

Reduce示例

func Filter(arr []int, fn func(n int) bool) []int {
    var newArray = []int{}
    for _, it := range arr {
        if fn(it) {
            newArray = append(newArray, it)
        }
    }
    return newArray
}

var intset = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
out := Filter(intset, func(n int) bool {
   return n%2 == 1
})
fmt.Printf("%v\n", out)

out = Filter(intset, func(n int) bool {
    return n > 5
})
fmt.Printf("%v\n", out)

Filter示例

func Filter(arr []int, fn func(n int) bool) []int {
    var newArray = []int{}
    for _, it := range arr {
        if fn(it) {
            newArray = append(newArray, it)
        }
    }
    return newArray
}

var intset = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
out := Filter(intset, func(n int) bool {
   return n%2 == 1
})
fmt.Printf("%v\n", out)

out = Filter(intset, func(n int) bool {
    return n > 5
})
fmt.Printf("%v\n", out)

下图是一个比喻,其非常形象地说明了Map-Reduce是的业务语义,其在数据处理中非常有用。
map-reduce例子.png

业务示例
通过上面的一些示例,你可能有一些明白,Map/Reduce/Filter只是一种控制逻辑,真正的业务逻辑是在传给他们的数据和那个函数来定义的。是的,这是一个很经典的“业务逻辑”和“控制逻辑”分离解耦的编程模式。下面我们来看一个有业务意义的代码,来让大家强化理解一下什么叫“控制逻辑”与业务逻辑分离。

首先,我们一个员工对象,以及一些数据。

type Employee struct {
    Name     string
    Age      int
    Vacation int
    Salary   int
}

var list = []Employee{
    {"Hao", 44, 0, 8000},
    {"Bob", 34, 10, 5000},
    {"Alice", 23, 5, 9000},
    {"Jack", 26, 0, 4000},
    {"Tom", 48, 9, 7500},
    {"Marry", 29, 0, 6000},
    {"Mike", 32, 8, 4000},
}

相关的Reduce/Fitler函数:

func EmployeeCountIf(list []Employee, fn func(e *Employee) bool) int {
    count := 0
    for i, _ := range list {
        if fn(&list[i]) {
            count += 1
        }
    }
    return count
}

func EmployeeFilterIn(list []Employee, fn func(e *Employee) bool) []Employee {
    var newList []Employee
    for i, _ := range list {
        if fn(&list[i]) {
            newList = append(newList, list[i])
        }
    }
    return newList
}

func EmployeeSumIf(list []Employee, fn func(e *Employee) int) int {
    var sum = 0
    for i, _ := range list {
        sum += fn(&list[i])
    }
    return sum
}

简单说明一下:

  • EmployeeConutIf 和 EmployeeSumIf 分别用于统满足某个条件的个数或总数。它们都是Filter +
    Reduce的语义。
  • EmployeeFilterIn 就是按某种条件过虑。就是Fitler的语义。

于是我们就可以有如下的代码。

统计有多少员工大于40岁

old := EmployeeCountIf(list, func(e *Employee) bool {
    return e.Age > 40
})
fmt.Printf("old people: %d\n", old)

统计有多少员工薪水大于6000

high_pay := EmployeeCountIf(list, func(e *Employee) bool {
    return e.Salary >= 6000
})
fmt.Printf("High Salary people: %d\n", high_pay)
//High Salary people: 4

列出有没有休假的员工

no_vacation := EmployeeFilterIn(list, func(e *Employee) bool {
    return e.Vacation == 0
})
fmt.Printf("People no vacation: %v\n", no_vacation)
//People no vacation: [{Hao 44 0 8000} {Jack 26 0 4000} {Marry 29 0 6000}]

统计所有员工的薪资总和

total_pay := EmployeeSumIf(list, func(e *Employee) int {
    return e.Salary
})

fmt.Printf("Total Salary: %d\n", total_pay)
//Total Salary: 43500

统计30岁以下员工的薪资总和

younger_pay := EmployeeSumIf(list, func(e *Employee) int {
if e.Age < 30 {
    return e.Salary
} 
return 0 })

泛型Map-Reduce

我们可以看到,上面的Map-Reduce都因为要处理数据的类型不同而需要写出不同版本的Map-Reduce,虽然他们的代码看上去是很类似的。所以,这里就要带出来泛型编程了,Go语言在本文写作的时候还不支持泛型(注:Go开发团队技术负责人Russ Cox在2012年11月21golang-dev上的mail确认了Go泛型(type parameter)将在Go 1.18版本落地,即2022.2月份)。

简单版 Generic Map
所以,目前的Go语言的泛型只能用 interface{} + reflect来完成,interface{} 可以理解为C中的 void*,Java中的 Object ,reflect是Go的反射机制包,用于在运行时检查类型。
下面我们来看一下一个非常简单不作任何类型检查的泛型的Map函数怎么写。

func Map(data interface{}, fn interface{}) []interface{} {
    vfn := reflect.ValueOf(fn)
    vdata := reflect.ValueOf(data)
    result := make([]interface{}, vdata.Len())

    for i := 0; i < vdata.Len(); i++ {
        result[i] = vfn.Call([]reflect.Value{vdata.Index(i)})[0].Interface()
    }
    return result
}

上面的代码中

  • 通过 reflect.ValueOf() 来获得 interface{} 的值,其中一个是数据 vdata,另一个是函数 vfn;
  • 然后通过 vfn.Call() 方法来调用函数,通过 []refelct.Value{vdata.Index(i)}来获得数据。

Go语言中的反射的语法还是有点令人费解的,但是简单看一下手册还是能够读懂的。我这篇文章不讲反射,所以相关的基础知识还请大家自行Google相关的教程。
于是,我们就可以有下面的代码——不同类型的数据可以使用相同逻辑的Map()代码。

square := func(x int) int {
  return x * x
}
nums := []int{1, 2, 3, 4}

squared_arr := Map(nums,square)
fmt.Println(squared_arr)
//[1 4 9 16]

upcase := func(s string) string {
  return strings.ToUpper(s)
}
strs := []string{"Hao", "Chen", "MegaEase"}
upstrs := Map(strs, upcase);
fmt.Println(upstrs)
//[HAO CHEN MEGAEASE]

但是因为反射是运行时的事,所以,如果类型什么出问题的话,就会有运行时的错误。比如:

x := Map(5, 5)
fmt.Println(x)

上面的代码可以很轻松的编译通过,但是在运行时就出问题了,还是panic错误……

panic: reflect: call of reflect.Value.Len on int Value

goroutine 1 [running]:
reflect.Value.Len(0x10b5240, 0x10eeb58, 0x82, 0x10716bc)
        /usr/local/Cellar/go/1.15.3/libexec/src/reflect/value.go:1162 +0x185
main.Map(0x10b5240, 0x10eeb58, 0x10b5240, 0x10eeb60, 0x1, 0x14, 0x0)
        /Users/chenhao/.../map.go:12 +0x16b
main.main()
        /Users/chenhao/.../map.go:42 +0x465
exit status 2

健壮版的Generic Map

所以,如果要写一个健壮的程序,对于这种用interface{} 的“过度泛型”,就需要我们自己来做类型检查。下面是一个有类型检查的Map代码:

func Transform(slice, function interface{}) interface{} {
  return transform(slice, function, false)
}

func TransformInPlace(slice, function interface{}) interface{} {
  return transform(slice, function, true)
}

func transform(slice, function interface{}, inPlace bool) interface{} {
 
  //check the <code data-enlighter-language="raw" class="EnlighterJSRAW">slice</code> type is Slice
  sliceInType := reflect.ValueOf(slice)
  if sliceInType.Kind() != reflect.Slice {
    panic("transform: not slice")
  }

  //check the function signature
  fn := reflect.ValueOf(function)
  elemType := sliceInType.Type().Elem()
  if !verifyFuncSignature(fn, elemType, nil) {
    panic("trasform: function must be of type func(" + sliceInType.Type().Elem().String() + ") outputElemType")
  }

  sliceOutType := sliceInType
  if !inPlace {
    sliceOutType = reflect.MakeSlice(reflect.SliceOf(fn.Type().Out(0)), sliceInType.Len(), sliceInType.Len())
  }
  for i := 0; i < sliceInType.Len(); i++ {
    sliceOutType.Index(i).Set(fn.Call([]reflect.Value{sliceInType.Index(i)})[0])
  }
  return sliceOutType.Interface()

}

func verifyFuncSignature(fn reflect.Value, types ...reflect.Type) bool {

  //Check it is a funciton
  if fn.Kind() != reflect.Func {
    return false
  }
  // NumIn() - returns a function type's input parameter count.
  // NumOut() - returns a function type's output parameter count.
  if (fn.Type().NumIn() != len(types)-1) || (fn.Type().NumOut() != 1) {
    return false
  }
  // In() - returns the type of a function type's i'th input parameter.
  for i := 0; i < len(types)-1; i++ {
    if fn.Type().In(i) != types[i] {
      return false
    }
  }
  // Out() - returns the type of a function type's i'th output parameter.
  outType := types[len(types)-1]
  if outType != nil && fn.Type().Out(0) != outType {
    return false
  }
  return true
}

上面的代码一下子就复杂起来了,可见,复杂的代码都是在处理异常的地方。我不打算Walk through 所有的代码,别看代码多,但是还是可以读懂的,下面列几个代码中的要点:

  • 代码中没有使用Map函数,因为和数据结构和关键有含义冲突的问题,所以使用Transform,这个来源于 C++ STL库中的命名。
  • 有两个版本的函数,一个是返回一个全新的数组 – Transform(),一个是“就地完成” – TransformInPlace();
  • 在主函数中,用 Kind() 方法检查了数据类型是不是 Slice,函数类型是不是Func;
  • 检查函数的参数和返回类型是通过 verifyFuncSignature() 来完成的,其中:NumIn() – 用来检查函数的“入参”,NumOut() 用来检查函数的“返回值”;
  • 如果需要新生成一个Slice,会使用 reflect.MakeSlice() 来完成。
    好了,有了上面的这段代码,我们的代码就很可以很开心的使用了:

可以用于字符串数组:

list := []string{"1", "2", "3", "4", "5", "6"}
result := Transform(list, func(a string) string{
    return a +a +a
})
//{"111","222","333","444","555","666"}

可以用于整形数组:

list := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
TransformInPlace(list, func (a int) int {
  return a*3
})
//{3, 6, 9, 12, 15, 18, 21, 24, 27}

可以用于结构体:

var list = []Employee{
    {"Hao", 44, 0, 8000},
    {"Bob", 34, 10, 5000},
    {"Alice", 23, 5, 9000},
    {"Jack", 26, 0, 4000},
    {"Tom", 48, 9, 7500},
}

result := TransformInPlace(list, func(e Employee) Employee {
    e.Salary += 1000
    e.Age += 1
    return e
})

健壮版的 Generic Reduce
同样,泛型版的 Reduce 代码如下:

func Reduce(slice, pairFunc, zero interface{}) interface{} {
  sliceInType := reflect.ValueOf(slice)
  if sliceInType.Kind() != reflect.Slice {
    panic("reduce: wrong type, not slice")
  }

  len := sliceInType.Len()
  if len == 0 {
    return zero
  } else if len == 1 {
    return sliceInType.Index(0)
  }

  elemType := sliceInType.Type().Elem()
  fn := reflect.ValueOf(pairFunc)
  if !verifyFuncSignature(fn, elemType, elemType, elemType) {
    t := elemType.String()
    panic("reduce: function must be of type func(" + t + ", " + t + ") " + t)
  }

  var ins [2]reflect.Value
  ins[0] = sliceInType.Index(0)
  ins[1] = sliceInType.Index(1)
  out := fn.Call(ins[:])[0]

  for i := 2; i < len; i++ {
    ins[0] = out
    ins[1] = sliceInType.Index(i)
    out = fn.Call(ins[:])[0]
  }
  return out.Interface()
}

健壮版的 Generic Filter

同样,泛型版的 Filter 代码如下(同样分是否“就地计算”的两个版本):
func Filter(slice, function interface{}) interface{} {
  result, _ := filter(slice, function, false)
  return result
}

func FilterInPlace(slicePtr, function interface{}) {
  in := reflect.ValueOf(slicePtr)
  if in.Kind() != reflect.Ptr {
    panic("FilterInPlace: wrong type, " +
      "not a pointer to slice")
  }
  _, n := filter(in.Elem().Interface(), function, true)
  in.Elem().SetLen(n)
}

var boolType = reflect.ValueOf(true).Type()

func filter(slice, function interface{}, inPlace bool) (interface{}, int) {

  sliceInType := reflect.ValueOf(slice)
  if sliceInType.Kind() != reflect.Slice {
    panic("filter: wrong type, not a slice")
  }

  fn := reflect.ValueOf(function)
  elemType := sliceInType.Type().Elem()
  if !verifyFuncSignature(fn, elemType, boolType) {
    panic("filter: function must be of type func(" + elemType.String() + ") bool")
  }

  var which []int
  for i := 0; i < sliceInType.Len(); i++ {
    if fn.Call([]reflect.Value{sliceInType.Index(i)})[0].Bool() {
      which = append(which, i)
    }
  }

  out := sliceInType

  if !inPlace {
    out = reflect.MakeSlice(sliceInType.Type(), len(which), len(which))
  }
  for i := range which {
    out.Index(i).Set(sliceInType.Index(which[i]))
  }

  return out.Interface(), len(which)
}

后记

还有几个未尽事宜:

  1. 使用反射来做这些东西,会有一个问题,那就是代码的性能会很差。所以,上面的代码不能用于你需要高性能的地方。怎么解决这个问题,我们会在本系列文章的下一篇文章中讨论。
  2. 上面的代码大量的参考了 Rob Pike的版本,他的代码在 https://github.com/robpike/filter
  3. 其实,在全世界范围内,有大量的程序员都在问Go语言官方什么时候在标准库中支持 Map/Reduce,Rob Pike说,这种东西难写吗?还要我们官方来帮你们写么?这种代码我多少年前就写过了,但是,我从来一次都没有用过,我还是喜欢用“For循环”,我觉得你最好也跟我一起用“For循环”。

我个人觉得,Map/Reduce在数据处理的时候还是很有用的,Rob Pike可能平时也不怎么写“业务逻辑”的代码,所以,对他来说可能也不太了解业务的变化有多么的频繁……

当然,好还是不好,由你来判断,但多学一些编程模式是对自己的帮助也是很有帮助的。

导语

有了之前两篇关于背包问题文章的积累,《一文搞懂经典0-1背包问题》《背包问题的变形一:无限制背包(Unbounded Knapsack)》,今天讨论的背包问题另一个变形问题,限制背包(Bounded Knapsack)就容易理解得多了。限制背包的意思是每样物品不是只有一件,也不是无限数量,给定一个限制数量,求取装满背包获得的最大价值。那么这个问题利用之前积累的知识很容易解决。背包问题可以由浅入深,而且层层递进,经过这次的学习,我发现迷上了这个经典的NPC问题。

限制背包(Bounded Knapsack Problem)

最容易想到的还是把每件物品按照数量限制(naive),一件件取出,然后更新之前定义的dp数组(见《一文搞懂经典0-1背包问题》),OK,源代码修改下之前无限制背包问题的,也很简单:

func knapsackBounded1(w []int, v []int, m []int, W int) int {
    n := len(w)
    dp := make([]int, W+1)
    for i := 0; i < n; i++ {
        for j := 0; j < m[i]; j++ {
            knapsack01(dp, w[i], v[i])
        }
    }

    r := 0
    for i := 0; i <= W; i++ {
        if dp[i] > r {
            r = dp[i]
        }
    }

    return r
}

数组m给出的是物品的数量限制。算法时间复杂度O(NW*Sigma(m[i])),空间复杂度O(W)(已经通过滚动数组优化)。

如果要进行优化,自然想到了之前smarter的方式,按2的幂次方去组合出虚拟物品来,这样考虑的物品数量大大降低了,但是恶心的是,不像无限制背包,我们每件物品都可以随便取,直到超过重量上限。这里,我们需要按两种方式考虑,如果m[i]w[i] >= W,OK,那直接使用knapsackComplete更新dp数组即可,如果m[i]w[i] < W,那么我们尝试尽可能拆分虚拟物品,比如m[i] = 19,那么可以拆出的虚拟物品组合为(1, 2, 4, 8)倍原来的物品,剩下19-(1+2+4+8)=4倍物品,我们单独考虑。代码如下,稍微复杂些,仔细看,应该不难理解:

func knapsackBounded2(w []int, v []int, m []int, W int) int {
    n := len(w)
    dp := make([]int, W+1)
    for i := 0; i < n; i++ {
        if m[i] * w[i] >= W {
            knapsackComplete(dp, w[i], v[i])
            continue
        }

        left := m[i]
        for k := 0; k < math.Ilogb(float64(m[i])); k++ {
            knapsack01(dp, w[i] * 1<<k, v[i] * 1<<k)
            left -= 1<<k
        }

        for j := 0; j < left; j++ {
            knapsack01(dp, w[i], v[i])
        }
    }

    r := 0
    for i := 0; i <= W; i++ {
        if dp[i] > r {
            r = dp[i]
        }
    }

    return r
}

算法时间复杂度O(NW*Sigma(m[i])),空间复杂度O(W)(已经通过滚动数组优化)。

性能测评

参数设置,W=100000,w取值[11, 220],长度10001,v取值[7, 33],m取值[5, 100],测试性能如下:

knapsackUnbounded3=281800, time_cost=778.899197ms
knapsackBounded2=195626, time_cost=16.464975377s
knapsackBounded1=195626, time_cost=1m27.138384055s

看上去这个参数下,基本上算法耗时都很高了,相当于有10w件不同的商品,虚拟一下,knapsackBounded2的商品数量达到了200w左右,然后通过动态规划计算(重复子问题其实没那么高),如果可以大部分进入到m[i] * w[i] >= W,性能会比较好些,试了下面这组参数,W=100000,w取值[1000, 2000],长度10001,v取值[7, 33],m取值[1000, 2000],性能结果如下:

knapsackUnbounded3=3168, time_cost=769.803021ms
knapsackBounded2=3168, time_cost=1.718676383s
knapsackBounded1废了,算不出结果

到此为止,背包问题的讨论蛮多了,我感觉自己又重新上了一遍大学的课程,温故而知新,慢慢来,学习是长跑,不追求一蹴而就,追求的是细水长流。

导语

之前写过一篇《一文搞懂经典0-1背包问题》,n个物品放入包中,每件只能选择放一次,放过之后就不能再放了。当时分析了三种算法,一个是递归搜索(在n<20,W特别大时适用),一种是记忆化递归,最后是动态规划算法,记忆化递归和动态规划都依赖于子问题有重复,否则,最糟糕的情况下,每件物品都是不同的2的幂次方重量,就会退化到递归搜索。今天要再深入讨论两个变形题目,一个是无限制的背包问题,一个是有限制的背包问题,今天先讨论下无限制的背包问题。

无限制背包(Unbounded Knapsack Problem)

简单说每件物品没有数量限制,随便放入包中。关于这个算法,有三种解法。基本思路都是虚拟出所有的物品,因为有总重量限制,每件物品放入背包中的数量总是有上限的。
先看下0-1背包问题动态规划实现方法的源码:

func knapsack01D(w []int, v []int, W int) int {
    n := len(w)
    dp := make([]int, W+1)
    for i := 0; i < n; i++ {
        for j := W; j >= w[i]; j-- {
            dp[j] = max(dp[j-w[i]]+v[i], dp[j])
        }
    }

    r := 0
    for i := 0; i <= W; i++ {
        if dp[i] > r {
            r = dp[i]
        }
    }

    return r
}

这里把更新dp数组的部分,包装成一个函数knapsack01,如下:

func knapsack01(dp []int, w int, v int) {
    W := len(dp)
    for j := W-1; j >= w; j-- {
        dp[j] = max(dp[j-w]+v, dp[j])
    }
}

第一种方法,是把每件东西可以选择数量都虚拟出来(naive),比如第i件重量w[i],那么第i件东西最多有W/w[i]件,然后就变成每件虚拟的物品扔给knapsack01更新dp数组的部分。

func knapsackUnbounded1(w []int, v []int, W int) int {
    n := len(w)
    dp := make([]int, W+1)
    for i := 0; i < n; i++ {
        m := W/w[i]
        for j := 0; j < m; j++ {
            knapsack01(dp, w[i], v[i])
        }
    }

    r := 0
    for i := 0; i <= W; i++ {
        if dp[i] > r {
            r = dp[i]
        }
    }

    return r
}

算法时间复杂度为O(NW^2),空间复杂度O(W)(已经通过滚动数组优化)。

第二种方法,控制虚拟出的物品数量更少(smart),以不同的2的幂次方虚拟出物品,对于第i件物品,最多能放W/w[i]件,第i件物品的虚拟物品为,w[i]2^0、w[i]2^1、w[i]2^2……w[i]2^logW/w[i],代码如下:

func knapsackUnbounded2(w []int, v []int, W int) int {
    n := len(w)
    dp := make([]int, W+1)
    for i := 0; i < n; i++ {
        m := math.Ilogb(float64(W)/float64(w[i]))
        for k := 0; k <= m; k++ {
            knapsack01(dp, w[i] << k, v[i] << k)
        }
    }

    r := 0
    for i := 0; i <= W; i++ {
        if dp[i] > r {
            r = dp[i]
        }
    }

    return r
}

算法时间复杂度为O(NWlogW),空间复杂度O(W)(已经通过滚动数组优化)。

第三种方法,实现上是最简单的,但也是最难想到的,原先我们做dp更新时,故意将j的迭代顺序从W最大值逆向迭代到当前物品重量w,其实我们只需要改为顺向迭代即可,更新dp函数knapsack01改为knapsackComplete,代码如下:

func knapsackComplete(dp []int, w int, v int) {
    W := len(dp)
    for j := w; j < W; j++ {
        dp[j] = max(dp[j-w]+v, dp[j])
    }
}

整体的背包问题代码变为:

func knapsackUnbounded3(w []int, v []int, W int) int {
    n := len(w)
    dp := make([]int, W+1)
    for i := 0; i < n; i++ {
        knapsackComplete(dp, w[i], v[i])
    }

    r := 0
    for i := 0; i <= W; i++ {
        if dp[i] > r {
            r = dp[i]
        }
    }

    return r
}

算法时间复杂度为O(NW),空间复杂度O(W)(已经通过滚动数组优化)。

可以这么优化的原因,我通过下面这个简单例子说明:
无限制背包问题.png

红色的箭头就是表示,我同一件物品可以反复取,直到即将超过W最大重量为止,比如在i=2时,计算dp[5],从dp[3]变化过来的,dp[3]此时已经使用过w[2]了,但没关系。因为此时第2件物品可以反复使用。(自己去想想,理解这个点,是无限制背包问题里面最难的)。

性能测试

上面进行了理论时间复杂度的分析,我跑了下性能,设置W=10000,w物品数量1001,w取值[11,22],v取值[7,33],测试下来的性能符合理论分析,knapsackUnbounded3 优于 knapsackUnbounded2 优于 knapsackUnbounded1,并且knapsackUnbounded1基本上无法work了,在W=10000、w取值[11,22]时,耗时大概已经达到11s。

knapsackUnbounded3=29088, time_cost=9.429946ms
knapsackUnbounded2=29088, time_cost=151.371282ms
knapsackUnbounded1=29088, time_cost=11.718217767s

下面有时间再分析下有限制的背包问题,这个问题是背包变形问题中较为复杂的。