• 动态规划之背包(一)



    动态规划的大部分问题可以化为:有限资源获得最大收益

    1. 子集和

    对于一列数,每个数都是非负数

    [a_1, a_2, a_3, cdots, a_n,~~a_i ≥ 0 ]

    挑出一个子集使得子集的和等于某一个目标(T)


    这时通常会产生一种错误的贪心,就是从最大的数开始选择,反例(t = 100,~a_1 = 99, ~a_2 = 50, ~a_3 = 50)

    所以此时开始思考穷举:对于数列(1, 2, 3, 4),有以下子集:

    [egin {align*} &{} \ &Downarrow\ &{}, {1} \ &Downarrow\ &{}, {1}, {2}, {1, 2} \ &Downarrow\ &{}, {1}, {2}, {1, 2}, {3}, {1, 3}, {2, 3}, {1, 2, 3} \ &Downarrow\ &{}, {1}, {2}, {1, 2}, {3}, {1, 3}, {2, 3}, {1, 2, 3}, {4}, {1, 4}, {2, 4}, {1, 2, 4}, {3, 4}, {1, 3, 4}, {2, 3, 4}, {1, 2, 3, 4} end {align*} ]

    但是复杂度太高对于(n)个数,会产生(2^n)个子集,没有可行性。

    但是可以发现之前穷举的表格是可以压缩的。因为观察到(1 + 2 = 3)(1 + 3 = 4)(cdots),每次只记录和的值。我们可以发现,通常这类题目中会存在一个至关重要的条件:(sum_i a_i ≤ 5 imes 10^4, n ≤100),所以最多只存在(5 imes 10^6)的计算量,是非常划算的。过程变化为:每次的集合按照(S = S cup {x~|~t + a_i, t in S})进行更新。例如:

    [egin {align*} &0 ~( + 1)\ Rightarrow~ &0,~1~(+2) \ Rightarrow~ &0,~1,~2,~3 ~(+3) \ Rightarrow~ &0,~1,~2,~3,~4,~5,~6 ~( + 4) \ Rightarrow~ &0,~1,~2,~3,~4,~5,~6,~7,~8,~9,10 end {align*} ]

    要完成以上操作,需要一种数据结构,由于(sum_i a_i)的限制,所以可以使用桶的数据结构,其对于取非常方便:

    1 2 3 4 5 6 7 8 9
    S × × × × ×
    S + 2
    (cup) × × × ×

    伪代码:

    S[0] = true; S[1 ... 50000] = false;
    for (int i = 1; i <= n; i ++) {
    		T[0 ... a[i - 1]] = false; // 非常重要,不加可能产生问题
    		for (x = 0; x <= 50000; x ++)	T[x + a[i]] = S[x];
    				T[x + a[i]] = S[x];
    		for (x = 0; x <= 50000; x ++) S[x] = S[x] || T[x];
    }
    

    但是我们不需要T数组的存在。

    注意:以下代码为错误代码:

    // 错误代码:
    for (int i = 1; i <= n; i ++)
    		for (int x = 0; x ≤ 50000; x ++)
    				S[x] = S[x] || S[x - a[i]];
    

    原因:

    我们所希望的算法为:(S_x’ = S_x ~or ~S_{x - a_i}),但是实际上:(S_x’ = S_x~or~S_{x - a_i} ')

    所以改变思路:倒着算:从(S_n)算到(S_1),伪代码(正确算法):

    // 正确代码:
    for (int i = 1; i <= n; i ++)
    		for (int x = 50000; x >= a[i]; x --)
    				S[x] = S[x] || S[x - a[i]];
    

    但是这仍然可以被优化。由于我们所需要计算(T)是否可以被组成,所以只需将内循环的上界换为(T)即可:

    for (int i = 1; i <= n; i ++)
    		for (int x = T; x >= a[i]; x --)
    				S[x] = S[x] || S[x - a[i]];
    cout << S[T] << endl;
    

    2. 01背包

    (n, n ≤100)个物品,每个物品有两个属性:

    • 价值:第(i)个物品的价值为(v_i)
    • 重量:第(i)个物品的重量为(w_i)

    每个背包有一个容量上界(c)(c ≤ 50000)。要求求出放入背包重量不超过(c)的物品的最大价值总和。


    现在我们可以再次尝试穷举:对于:

    [egin {bmatrix} w_1 = 1 \ v_1 = 1 end {bmatrix}, egin {bmatrix} w_2 = 2 \ v_2 = 1 end {bmatrix}, egin {bmatrix} w_3 = 2 \ v_3 = 3 end {bmatrix} ]

    存在以下方案:其中((a,b))表示价格(a)到重量(b)的映射,({...})中表示的是方案序号:

    [egin {align*} & {} ightarrow (0, 0) \ & Downarrow \ & {} ightarrow (0, 0), {1} ightarrow (1,1) \ & Downarrow \ & {} ightarrow (0, 0), {1} ightarrow (1,1), {2} ightarrow (2, 1), {1, 2} ightarrow (3, 2) \ & Downarrow \ & {} ightarrow (0, 0), {1} ightarrow (1,1), {2} ightarrow (2, 1), {1, 2} ightarrow (3, 2), \ & {3} ightarrow (2, 3), {1, 3} ightarrow (3, 4), {2, 3} ightarrow (4, 4), {1, 2, 3} ightarrow (5, 5) end {align*} ]

    同样地,这样也存在很多冗余,由于这样的穷举事件复杂度如上题所说,可以优化。整个过程的表格可以表示为下列表格。其中(x = 0sim 5)表示容量为(0sim 5)的情况,其他值表示价值:

    (x:) 0 1 2 3 4 5
    (f(x) ightarrow 0:) 0 0 0 0 0 0
    (f(x) ightarrow 1:) 0 1 1 1 1 1
    (f(x) ightarrow 2:) 0 1 1 2 2 2
    (f(x) ightarrow 3:) 0 1 3 4 4 5

    [f(x) = max egin {cases} f(x) \ f(x - w[i]) + v[i] end {cases} ]

    如同上道题差不多,代码:

    for (int i = 1; i <= n; i ++) {
    		for (int x = C; x >= w[i]; x --) 
    				f[x] = max(f[x], f[x - w[i]] + v[i]);
    }
    cout << f[C];
    

    3. 奶牛博览会

    题目

    奶牛想证明它们是聪明而风趣的。为此,贝西筹备了一个奶牛博览会,她已经对(n)头奶牛进行了面试,确定了每头奶牛的智商和情商(可能是负数)。

    贝西有权决定让哪些奶牛参加展览。她希望出展奶牛的智商与情商之和越大越好,而且这些奶牛的智商之和要大于等于零,情商之和也要大于等于零。请帮助贝西设计一组方案,使得智商加情商的和最大。如果选不出一组符合要求的奶牛,则输出(0)

    数据范围

    (1≤n≤100)
    (−1000≤a_i,~b_i≤1000)


    题解:

    这道题的解决思路和先前差不多,就是将情商与智商,进行映射,即一个智商对应一个情商值(一个情商值对应一个智商值亦可)。而更新时,以某一个数据(情商或智商)作为基准,开始更新。

    但是这个过程中会产生众多问题:

    (一)在运算过程中,难免产生负的情商或者智商,而对于一个数组,形如(f[-3])的写法是不被允许的,因为其越界了。

    解决负数下标的方法:

    1. 使用一个base,由于所以值的绝对值都不大于(50000),所以对于每一次使用数组中的值时,序号前都加上base,且在定义时改为int f[100002];(f[-3] ightarrow f[-3 + base])

    2. 对地址的变换:

      首先定义一个倍长的数组:( ext{buffer}[100002])。进行如下操作:

      int buffer[100002];
      int *f = buffer + 50001;
      

      可以观察到,此时指针(f)已经位于( ext{buffer})数组的中间。再进行(f[-3])的操作就等于:(*(f-3) = *( ext{buffer} + 50001 - 3)),并没有越界。

    (二)数据更新的顺序存在问题,应该对数据进行正负区分更新:

    解决数据中正负同时存在的问题:

    for (int i = 1; i <= n; i ++) {
    		if (s[i] >= 0)
          	for (x = 50000; x >= -50000; x --) {
              	f[x + s[i]] = max(f[x + s[i]], f[x] + t[i]);
            }
      	else
          	for (x = -50000; x <= 50000; x ++) {
              	f[x + s[i]] = max(f[x + s[i]], f[x] + t[i]);
            }
    }
    

    但是这仍然是错误的,因为没有考虑初值的问题:

    int inf = 50000;
    for (int i = -50000; i <= 50000; i ++) f[i] = -inf;
    

    其实这里只需要设置0…50000即可。

    接下来考虑答案是多少:

    int ans = 0;
    for (int x = 0; x <= 50000; i ++)
    		if (f[x] >= 0) ans = max(ans, x + f[x]);
    

    但是,这仍然存在隐患,可以被优化:

    int ub = 0, lb = 0;
    for (int i = 1; i <= n; i ++) {
    		if (s[i] >= 0) {
          	for (x = ub; x >= lb; x --)
              	f[x + s[i]] = max(f[x + s[i]], f[x] + t[i]);
            ub += s[i];
        }
      	else {
          	for (x = lb; x <= ub; x ++)
              	f[x + s[i]] = max(f[x + s[i]], f[x] + t[i]);
            lb += s[i];
       	}
    }
    
  • 相关阅读:
    20155210——20155233信息安全系统设计基础实验一
    # 2017-2018-1 20155210 《信息安全系统设计基础》第四周学习总结
    2017-2018-1 20155210 《信息安全系统设计基础》第3周学习总结
    第二周作业 20155210 潘滢昊
    20155210 实验五
    20155210 2016-2017-2《Java程序设计》课程总结
    20155210第四次实验
    2017-2018-1 20155208 实验四 外设驱动程序设计
    2017-2018-1 20155208 课堂测试(ch06)(补做)
    2017-2018-1 20155208 实验三 实时系统
  • 原文地址:https://www.cnblogs.com/jeffersonqin/p/12246075.html
Copyright © 2020-2023  润新知