ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

动态规划之背包(一)

2020-01-31 17:53:30  阅读:262  来源: 互联网

标签:50000 背包 int 智商 情商 奶牛 动态 规划 rightarrow



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

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\),有以下子集:
\[ \begin {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 \times 10^4, n ≤100\),所以最多只存在\(5\times 10^6\)的计算量,是非常划算的。过程变化为:每次的集合按照\(S = S \cup \{x~|~t + a_i, t \in S\}\)进行更新。例如:
\[ \begin {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\)的物品的最大价值总和。


现在我们可以再次尝试穷举:对于:
\[ \begin {bmatrix} w_1 = 1 \\ v_1 = 1 \end {bmatrix}, \begin {bmatrix} w_2 = 2 \\ v_2 = 1 \end {bmatrix}, \begin {bmatrix} w_3 = 2 \\ v_3 = 3 \end {bmatrix} \]
存在以下方案:其中\((a,b)\)表示价格\(a\)到重量\(b\)的映射,\(\{...\}\)中表示的是方案序号:
\[ \begin {align*} & \{\} \rightarrow (0, 0) \\ & \Downarrow \\ & \{\} \rightarrow (0, 0), \{1\} \rightarrow (1,1) \\ & \Downarrow \\ & \{\} \rightarrow (0, 0), \{1\} \rightarrow (1,1), \{2\} \rightarrow (2, 1), \{1, 2\} \rightarrow (3, 2) \\ & \Downarrow \\ & \{\} \rightarrow (0, 0), \{1\} \rightarrow (1,1), \{2\} \rightarrow (2, 1), \{1, 2\} \rightarrow (3, 2), \\ & \{3\} \rightarrow (2, 3), \{1, 3\} \rightarrow (3, 4), \{2, 3\} \rightarrow (4, 4), \{1, 2, 3\} \rightarrow (5, 5) \end {align*} \]
同样地,这样也存在很多冗余,由于这样的穷举事件复杂度如上题所说,可以优化。整个过程的表格可以表示为下列表格。其中\(x = 0\sim 5\)表示容量为\(0\sim 5\)的情况,其他值表示价值:

\(x:\) 0 1 2 3 4 5
\(f(x) \rightarrow 0:\) 0 0 0 0 0 0
\(f(x) \rightarrow 1:\) 0 1 1 1 1 1
\(f(x) \rightarrow 2:\) 0 1 1 2 2 2
\(f(x) \rightarrow 3:\) 0 1 3 4 4 5

\[ f(x) = \max \begin {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] \rightarrow f[-3 + base]\)。

  2. 对地址的变换:

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

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

    可以观察到,此时指针\(f\)已经位于\(\text{buffer}\)数组的中间。再进行\(f[-3]\)的操作就等于:\(*(f-3) = *(\text{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];
    }
}

标签:50000,背包,int,智商,情商,奶牛,动态,规划,rightarrow
来源: https://www.cnblogs.com/jeffersonqin/p/12246075.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有