0/1背包问题

来源:互联网 发布:圆形水池计算软件 编辑:程序博客网 时间:2024/05/17 18:12

问题:输入两个整数n 和m,从数列1, 2, 3, ... , n 中随意取几个数,使其和等于m,要求将其中所有的可能组合列出来。

1. 双指针思路

    我在网上google一下,关于这个问题的解法和程序很多,基本都是参考0/1背包问题的思路。由于我并不熟悉背包问题,因此刚看到这个问题的时候并没有想到可以借鉴背包问题的思路,而是马上想到了不久前看到过的双指针的思路。

    解题的主要思想是:在原数列中找到两个数a、b的组合,使a + b = m。接下来,把a拆分为两个数x、y的和,即x + y = a,则x + y + b = m是满足条件的一个组合。再接下来,把x拆分为两个数i、j的和,即i + j = x,则i + j + y + b = m也是满足要求的一个组合 ,按着这种思路一直拆分下去直到不能拆为止。对于b,同理也可以按照处理a的方法拆分,直到不能拆为止。这样全部拆分完以后,就得到了所有源自于组合a、b的和等于m的组合。然后,在原数列中找下一组两个数c、d的组合,使c + d = m。同样按照前面的思路递归的拆分c和d,直到不能再拆分为止。只要把原数列中两个数的和等于m的所有组合全部拆分完毕,也就得到了数列中和为m的所有可能组合。

    双指针实现思路为:设l(ow),u(p)为两个指针(在此问题中l和u其实是整型索引值),分别指向数列的第1个和最后1个元素,l只能往右移动,u只能往左移动。然后把则l + u和m的关系有以下三种情况:


    1)l + u > m  由于数列是递增的,若要使l + u有可能等于m必须减少u所指的值,u至少往前移一个位置,即u--;

    2)l + u < m  同理,若要使l + u有可能等于m必须增加l所指的值,l至少往后移一个位置,即l++;

    3)l + u = m  说明找到两个数的和等于m,输出l和u所指的值。

         (3.1) 拆分l  只能把l拆分为两个小于l的数的和,此时子数列为[1, 2, ... , l-1],令l = 1, u = l -1, m = m - u,按照1)、2)、3)的步骤递归拆分下去,直到不能拆为止;

         (3.2) 拆分u 同理,子数列应为[l+1, ... , u -1],令l = l + 1, u = u -1, m = m - l,按照1)、2)、3)的步骤递归拆分下去,直到不能拆为止;

         (3.3) l++, u--  在原数列[1, 2, ..., n]中递归寻找下一个两个数的和等于m的组合,然后回到步骤1)继续递归,直到找到两个数的和等于m的所有组合为止。

C/C++程序如下:

// 输入两个整数n和m,从数列1,2,3,……n中随意取几个数,使其和等于m。// 要求将所有的可能组合列出来vector<int> bag_vect;vector<int>::iterator it;void syAllGroup(int _min, int _max, int m){// 递归终止条件if (_min < 1 || m < 1 || _min >= _max)return;int l = _min, u = _max;if (u > m){// 一个数的组合printf("%d\n", m);u = m - 1;}if(l+u > m){u--;assert(l <= u);}else if (l+u < m){l++;assert(l <= u);}else// l + u == m{// 输出组合结果for(it = bag_vect.begin(); it != bag_vect.end(); ++it)printf("%d ", *it);printf("%d %d\n", l, u);// 组合中包含u,拆分l=i+j,即i+j+u=mbag_vect.push_back(u);syAllGroup(_min, l-1, l);bag_vect.pop_back();// 组合中包含l,拆分u=x+y,即l+x+y=mbag_vect.push_back(l);syAllGroup(l+1, u-1, u);bag_vect.pop_back();l++;u--;}// 递归在原数列中下一组合l,u使l+u=msyAllGroup(l, u, m);}

2. 背包问题

    google了一下有关背包问题的知识,发现背包问题包括0/1背包问题、有界背包问题、无界背包问题等好多种衍生类型。0/1背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,其他类型的背包问题往往也可以转换成01背包问题求解。

    问题描述[1]:

    有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

    基本思路:

    这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。

    用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:

            f[i][v] = max{ f[i-1][v], f[i-1][v-c[i]] + w[i] }

    这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为f[i-1][v];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f[i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。

   把上面的问题转换为:有n件物品和一个容量为m的背包,第i件物品的大小为i,求恰把背包装满的所有物品组合。转换后的问题与原问题是等价的,因而可以借鉴0/1背包问题的思路:假设问题的解为F(n, m),可分解为两个子问题 F(n-1, m-n)和F(n-1, m)。对这两个问题递归求解,求解过程中,如果找到了恰能装满背包的物品组合,则打印出来[2]。

(1)F(n-1, m-n)表示背包中包含n,此时背包的剩余容量为m - n,则只需在[1...n-1]中找一个大小为k = m - n的物品就恰能装满背包,即k + n = m是满足要求的一个组合。然后令n = k , m = k,问题F(k, k)又可分解为两个子问题求解, 一直递归下去。

2)F(n-1, m)表示背包中不包含n,此时背包的剩余容量仍为m,则只需在[1...n-1]中找一个大小为s = m的物品就恰能装满背包,则s = m也是满足要求的一个组合。然后令n = s, m = s,问题F(s, s)又可分解为两个子问题求解,一直递归下去。

C/C++程序如下:

// 思路参考01背包问题:定义函数F(n,m)来求解这个问题,// 那么F(n,m)可以分解为两个子问题F(n-1,m)和F(n-1,m-n).vector<int> zerone_vect;vector<int>::iterator it;void syZeroneKnapsack(int n, int m){// 递归终止条件if (n < 1 || m < 1)return;if (n > m)n = m;// 背包装满:输出结果if (n == m){zerone_vect.push_back(n);for (it = zerone_vect.begin(); it != zerone_vect.end(); ++it)printf("%d ", *it);printf("\n");zerone_vect.pop_back();}// 子问题1:f(n-1, m)--组合中不包含nsyZeroneKnapsack(n-1, m);// 子问题2:f(n-1, m-n)--组合中包含nzerone_vect.push_back(n);syZeroneKnapsack(n-1, m-n);zerone_vect.pop_back();}


参考文献:

[1] 0/1背包问题:http://love-oriented.com/pack/P01.html.

[2] 解题笔记31:http://blog.csdn.net/wuzhekai1985/article/details/6728657.

原创粉丝点击