github.com/qiuhoude/go-web@v0.0.0-20220223060959-ab545e78f20d/algorithm/datastructures/dp/knapsack_problem_test.go (about)

     1  package dp
     2  
     3  import "testing"
     4  
     5  //背包问题9讲 https://www.kancloud.cn/kancloud/pack/70125
     6  
     7  /*
     8  0-1 背包问题 求总总量最大
     9  现在我们有 n 个物品,每个物品的重量不等,并且不可分割。我们现在期望选择几件物
    10  品,装载到背包中。在不超过背包所能装载重量的前提下,如何让背包中物品的总重量最大
    11  
    12  我们可以把物品依次排列,整个问题就分解为了 n 个阶段,每个阶段
    13  对应一个物品怎么选择。先对第一个物品进行处理,选择装进去或者不装进去,然后再递归地处
    14  理剩下的物品
    15  
    16  使用回溯方式
    17  i 表示考察到哪个物品了
    18  curW 当前背包的重量
    19  limitW 重量限制
    20  weight 物品列表
    21  */
    22  func bag01Problem(i, cw, limitW int, weight []int, ret *int) {
    23  	al1Cnt++
    24  	if cw == limitW || // 表示装满了
    25  		i == len(weight) { // 表示已经考察完所有的物品
    26  		if cw > *ret { //找到了
    27  			*ret = cw
    28  		}
    29  		return
    30  	}
    31  
    32  	bag01Problem(i+1, cw, limitW, weight, ret) // 选择不装第 i 个物品
    33  	if cw+weight[i] <= limitW {                // 已经超过可以背包承受的重量的时候,就不要再装了
    34  		bag01Problem(i+1, cw+weight[i], limitW, weight, ret) //选择 第 i 个物品
    35  	}
    36  }
    37  
    38  /*
    39  使用回溯方式记忆化搜索的方式,将f(i,cw) 重复的递归调用去除掉
    40  f(i,cw) 建立 i,cw 的状态数组
    41  */
    42  func bag01ProblemRS(limitW int, weight []int, ret *int) {
    43  	// 初始化state
    44  	state := make([][]bool, len(weight))
    45  	for i := range state {
    46  		state[i] = make([]bool, limitW+1)
    47  		for j := range state[i] {
    48  			state[i][j] = false
    49  		}
    50  	}
    51  	bag01RememberSearch(0, 0, limitW, weight, ret, state)
    52  }
    53  
    54  func bag01RememberSearch(i, cw, limitW int, weight []int, ret *int, state [][]bool) {
    55  	al2Cnt++
    56  	if cw == limitW || // 表示装满了
    57  		i == len(weight) { // 表示已经考察完所有的物品
    58  		if cw > *ret { //找到了
    59  			*ret = cw
    60  		}
    61  		return
    62  	}
    63  	if state[i][cw] { //之前已经访问过
    64  		return
    65  	}
    66  	state[i][cw] = true                                      // 已经访问过
    67  	bag01RememberSearch(i+1, cw, limitW, weight, ret, state) // 不选择i
    68  	if cw+weight[i] <= limitW {
    69  		bag01RememberSearch(i+1, cw+weight[i], limitW, weight, ret, state)
    70  	}
    71  }
    72  
    73  /*
    74  动态规划的方式
    75  
    76  把整个求解过程分为 n 个阶段,每个阶段会决策一个物品是否放到背包中。每个物品决策
    77  (放入或者不放入背包)完之后,背包中的物品的重量会有多种情况,也就是说,会达到多种不
    78  同的状态,对应到递归树中,就是有很多不同的节点
    79  
    80  把每一层重复的状态(节点)合并,只记录不同的状态,然后基于上一层的状态集合,来推
    81  导下一层的状态集合。我们可以通过合并每一层重复的状态,这样就保证每一层不同状态的个数
    82  都不会超过 w 个(w 表示背包的承载重量)
    83  
    84  用一个二维数组 states[n][w+1],来记录每层可以达到的不同状态
    85  
    86  第 0 个(下标从 0 开始编号)物品的重量是 2,要么装入背包,要么不装入背包,决策完之
    87  后,会对应背包的两种状态,背包中物品的总重量是 0 或者 2。我们用 states[0][0]=true 和
    88  states[0][2]=true 来表示这两种状态
    89  
    90  第 1 个物品的重量也是 2,基于之前的背包状态,在这个物品决策完之后,不同的状态有 3
    91  个,背包中物品总重量分别是 0(0+0),2(0+2 or 2+0),4(2+2)。我们用 states[1][0]=true,
    92  states[1][2]=true,states[1][4]=true 来表示这三种状态
    93  
    94  以此类推,直到考察完所有的物品后,整个 states 状态数组就都计算好了。我把整个计算的过
    95  程画了出来,你可以看看。图中 0 表示 false,1 表示 true。我们只需要在最后一层,找一个值
    96  为 true 的最接近 w(这里是 9)的值,就是背包中物品总重量的最大值。
    97  
    98  然后通过当前阶段的状态集合,来推导下一个阶段的状态集合,动态地往前推进
    99  
   100  */
   101  func knapsack01ProblemDP1(w int, weight []int) int {
   102  	n := len(weight)
   103  	// 初始化状态
   104  	states := make([][]bool, n)
   105  	for i := range states {
   106  		states[i] = make([]bool, w+1)
   107  		for j := range states[i] {
   108  			states[i][j] = false
   109  		}
   110  	}
   111  	// 第0个weight 的两种状态
   112  	states[0][0] = true         //不放0个物品, 第一行的数据要特殊处理,可以利用哨兵优化
   113  	states[0][weight[0]] = true // 放第0个物品
   114  
   115  	// 动态规划状态转移
   116  	for i := 1; i < n; i++ {
   117  		for j := 0; j <= w; j++ { // 不把第 i 个物品放入背包
   118  			if states[i-1][j] { // 前一个物品已经放入
   119  				states[i][j] = states[i-1][j]
   120  			}
   121  		}
   122  		for j := 0; j <= w-weight[i]; j++ { //  第 i 个物品放入背包
   123  			if states[i-1][j] {
   124  				states[i][j+weight[i]] = true
   125  			}
   126  		}
   127  	}
   128  	// 结果
   129  	for i := w; i >= 0; i-- {
   130  		if states[n-1][i] {
   131  			return i
   132  		}
   133  	}
   134  	return 0
   135  }
   136  
   137  /*
   138  使用 1维数组来做
   139  */
   140  func knapsack01ProblemDP2(w int, weight []int) int {
   141  	n := len(weight)
   142  	// 初始化状态
   143  	states := make([]bool, w+1)
   144  	for i := range states {
   145  		states[i] = false
   146  	}
   147  	states[0] = true
   148  	states[weight[0]] = true
   149  	// 动态规划状态转移,
   150  	for i := 1; i < n; i++ {
   151  		//j 需要从大到小来处理。如果我们按照 j 从小到大处理的话,会出现 for 循环重复计算的问题
   152  		for j := w - weight[i]; j >= 0; j-- { //  第 i 个物品放入背包
   153  			if states[j] {
   154  				states[j+weight[i]] = true
   155  			}
   156  		}
   157  	}
   158  	// 结果
   159  	for i := w; i >= 0; i-- {
   160  		if states[i] {
   161  			return i
   162  		}
   163  	}
   164  	return 0
   165  }
   166  
   167  // 带价值的背包问,不能使用记忆化搜索的方式解答
   168  
   169  // 使用回溯的方式
   170  func knapsack02(i, cw, cv, limitW int, weight, value []int, ret *int) {
   171  	al1Cnt++
   172  	if cw == limitW || // 表示装满了
   173  		i == len(weight) { // 表示已经考察完所有的物品
   174  		if cv > *ret { //找到了
   175  			*ret = cv
   176  		}
   177  		return
   178  	}
   179  
   180  	knapsack02(i+1, cw, cv, limitW, weight, value, ret) // 选择不装第 i 个物品
   181  	if cw+weight[i] <= limitW {                         // 已经超过可以背包承受的重量的时候,就不要再装了
   182  		knapsack02(i+1, cw+weight[i], cv+value[i], limitW, weight, value, ret) //选择 第 i 个物品
   183  	}
   184  }
   185  
   186  // 使用动态规划的方式
   187  func knapsack02Dp(w int, weight, value []int) int {
   188  	n := len(weight)
   189  	// f(i,cw,cv) cv 就是存储的值
   190  	// 初始化states
   191  	states := make([][]int, n)
   192  	for i := range states {
   193  		states[i] = make([]int, w+1)
   194  		for j := range states[i] {
   195  			states[i][j] = -1
   196  		}
   197  	}
   198  	states[0][0] = 0                // 第物品放不放进背包
   199  	states[0][weight[0]] = value[0] // 第一放
   200  	for i := 1; i < n; i++ {
   201  		for j := 0; j <= w; j++ { // 不把第 i 个物品放入背包
   202  			if states[i-1][j] >= 0 { // 前一个物品已经放入
   203  				states[i][j] = states[i-1][j]
   204  			}
   205  		}
   206  		for j := 0; j <= w-weight[i]; j++ { //  第 i 个物品放入背包
   207  			if states[i-1][j] >= 0 {
   208  				// 上一val放入背包的值 + 本次背包的值
   209  				v := states[i-1][j] + value[i]
   210  				if v > states[i][j+weight[i]] { // 相同总量下选取 value值大的
   211  					states[i][j+weight[i]] = v
   212  				}
   213  			}
   214  		}
   215  	}
   216  	// 在最后找出最大值
   217  	maxV := 0
   218  	for _, v := range states[n-1] {
   219  		if v > maxV {
   220  			maxV = v
   221  		}
   222  	}
   223  	return maxV
   224  }
   225  
   226  var al1Cnt int = 0
   227  var al2Cnt int = 0
   228  
   229  func TestBagProblem(t *testing.T) {
   230  	weight := []int{2, 2, 4, 6, 3}
   231  	limit := 9
   232  
   233  	ret1 := 0
   234  	bag01Problem(0, 0, limit, weight, &ret1)
   235  	t.Log("maxW ret1:", ret1, "al1Cnt:", al1Cnt)
   236  
   237  	ret2 := 0
   238  	bag01ProblemRS(limit, weight, &ret2)
   239  	t.Log("maxW ret2:", ret2, "al2Cnt:", al2Cnt)
   240  
   241  	ret3 := knapsack01ProblemDP1(limit, weight)
   242  	t.Log("maxW ret3:", ret3)
   243  
   244  	ret4 := knapsack01ProblemDP2(limit, weight)
   245  	t.Log("maxW ret4:", ret4)
   246  
   247  	// 带价值的背包问题
   248  	value := []int{3, 4, 8, 9, 6}
   249  	ret5 := 0
   250  	knapsack02(0, 0, 0, limit, weight, value, &ret5)
   251  	t.Log("maxV ret5:", ret5)
   252  
   253  	ret6 := knapsack02Dp(limit, weight, value)
   254  	t.Log("maxV ret6:", ret6)
   255  
   256  }