关于0-1背包的动态规划,回溯和分支限界法的一些分析和代码
2011-12-17 23:00
615 查看
首先,说一下什么是0-1背包问题。
有N件物品和一个容量为V的背包。(每种物品均只有一件)第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
特点是:每种物品仅有一件,可以选择放或不放。
然后,我分别用了动态规划法,分支限界法和回溯法来解决这个问题。
//=======================================
以下是测时间的模板:
随机数据的测量结果:
当数据量为20的物品数和200的背包容量的时候。动态规划快于排序优化的回溯法快于回溯法快于分支限界法。
(可以根据输出的随机数据判断时候得到正确的算法)
当数据量加大到40的物品数和400的背包容量的时候。此时分支限界法和动态规划法的效率依旧不错,时间增长的并不是太多。而回溯法和排序加速的回溯法则时间上升很快。
(为截图方便,此次将随机生成的数据不输出)
以下进行算法分析
//========================================
首先说下动态规划法:
用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
把这个过程理解下:在前i件物品放进容量v的背包时,
它有两种情况:
第一种是第i件不放进去,这时所得价值为:f[i-1][v]
第二种是第i件放进去,这时所得价值为:f[i-1][v-c[i]]+w[i]
(第二种是什么意思?就是如果第i件放进去,那么在容量v-c[i]里就要放进前i-1件物品)
最后比较第一种与第二种所得价值的大小,哪种相对大,f[i][v]的值就是哪种。
(这是基础,要理解!)
当然,这只是比较容易理解的一种,时间上似乎没有什么办法继续优化了,但是空间上却可以进一步优化。可以用一维数组实现。
关于一维数组的实现和讲解在这篇blog里有讲,讲的非常详细易懂。
本文为了输出所选择的物品的编号,还是选择了二维数组。实现起来非常方便(详见代码及注释)。
//========================================
接着说回溯法:
首先说一下解空间树(也称状态空间树):
树的根节点位于第1层,表示搜索的初始状态。
第2层的节点表示对解向量的第一个分量做出选择后达到的状态,第1层到第2层的边上标出对第一个分量选择的结果。
……
所有从根到叶子节点的路径就构成了解空间的一个可能解。
例子:
三个物品重量:20, 15, 10。
三个物品价值:20, 30, 20。
参照下图理解: 0表示不选第i个物品,1表示选择(i表示层数)。
图和文字选自这里。
这不重点,只是为了引出回溯法,若还有些不理解的,可以自己去Google下(个人更喜欢用这个),后面的理解要建立在这个基础上。
在回溯法之前,还有一点准备活动——蛮力法。
所谓蛮力法,是对整个解空间树中的所有可能的解进行穷举搜索的一种方法(即每个叶子都代表一种解)。
但是,我们可以对其进行剪枝。
剪枝,就是在搜索至树中的任意一个结点时,先判断该结点对应的部分是否满足约束条件,或者是否超出目标函数的值,也就是判断该结点是否包含问题的(最优)解,如果肯定不包含,则跳过对以该结点为根的子树的搜索。
以下对照图和上图对比理解。
其实,把整个解空间树当成一棵真正的树(倒过来看),剪枝这个词的由来和含义就比较好理解了。
好了,这就是回溯法,也就是深度优先搜索代(码中递归实现和输出选择的物品数)。
其实想想,回溯法也就是对一颗搜索树进行剪枝而来的。
回溯法关于0-1背包的再次优化
当然,最朴素的回溯法已经包含了一定的剪枝,那有没有什么办法,让回溯法在不进行大幅度改动的情况下进一步提高效率呢——答案是肯定的,是的,排序。
在理解上面回溯法的基础上,其实可以比较容易的想到,对体积进行从小到大的排序,注意,是体积,后面的分支限界法用的是单位价值。
对体积排序之后,当小体积的放入已经超出背包容量的时候,自然在其后的稍微大一点或者相等体积的都不用再次尝试了。因此,可以进一步的减少运算量,而实际生成的随机数据此种优化方法速度快了一半以上(由于回溯法时间复杂度的原因,未能进行大一些的随机检查,但是,很明显的趋势是,数据越多,体积排序后的时间优化效果越明显(从代码中也能看出,几乎和普通的代码量和操作难度一样)。
//========================================
最后出场的是分支限界法:
虽然回溯法求0-1背包问题,运用剪枝已经极大的减少了搜索空间,但是整个空间都是按深度优先搜索策略机械地进行,这种搜索太过盲目,能不能让电脑像人一样的思考呢?
这里,我推荐一个讲A*算法的blog。
懂分支限界法思想的朋友可以继续看这个例子,不懂的可以点这里。
其实分支限界法理解了也很好实现,举一个《算法设计与分析》上的例子。
例:0/1背包问题。假设有4个物品,其重量分别为(4, 7, 5, 3),价值分别为(40, 42, 25, 12),背包容量W=10。首先,将给定物品按单位重量价值从大到小排序,结果如下:
我们使用的启发式函数为
通过这个启发式函数得到的一个解空间树如下图:
可以对照一下步骤,具体的搜索过程如下:(红色表示我的代码实现)
(1)在根结点1,没有将任何物品装入背包,因此,背包的重量和获得的价值均为0,根据限界函数计算结点1的目标函数值为10×10=100;
(计算完之后推入队列,作为起始点)。
(2)在结点2,将物品1装入背包,因此,背包的重量为4,获得的价值为40,目标函数值为40 + (10-4)×6=76,将结点2加入待处理结点表PT中;在结点3,没有将物品1装入背包,因此,背包的重量和获得的价值仍为0,目标函数值为10×6=60,将结点3加入表PT中;
(推出结点1,对选择和不选择物品1分别计算ub值,并推入队列)
(3)在表PT中选取目标函数值取得极大的结点2优先进行搜索;
(出队ub值大的点)
(4)在结点4,将物品2装入背包,因此,背包的重量为11,不满足约束条件,将结点4丢弃;在结点5,没有将物品2装入背包,因此,背包的重量和获得的价值与结点2相同,目标函数值为40 + (10-4)×5=70,将结点5加入表PT中;
(重复结点1的操作并入队新结点)
(5)在表PT中选取目标函数值取得极大的结点5优先进行搜索;
(6)在结点6,将物品3装入背包,因此,背包的重量为9,获得的价值为65,目标函数值为65 + (10-9)×4=69,将结点6加入表PT中;在结点7,没有将物品3装入背包,因此,背包的重量和获得的价值与结点5相同,目标函数值为40 + (10-4)×4=64,将结点6加入表PT中;
(7)在表PT中选取目标函数值取得极大的结点6优先进行搜索;
(8)在结点8,将物品4装入背包,因此,背包的重量为12,不满足约束条件,将结点8丢弃;在结点9,没有将物品4装入背包,因此,背包的重量和获得的价值与结点6相同,目标函数值为65;
(9)由于结点9是叶子结点,同时结点9的目标函数值是表PT中的极大值,所以,结点9对应的解即是问题的最优解,搜索结束。
(判断结束并跳出)
我实现的方法是先按单位密度排序优先队列,每次踢出ub值最大的,对排在这个点之后的点选择或者不选择分别进行一次计算,得出相应的ub,放入优先队列中。
跳出的条件设定了两个:
1、当踢出的最大ub在叶子结点上时,结束。
2、当踢出的最大ub和v值相等时,结束。
第一点显而易见,现在说说第二点。
当ub和v相等的时候,可以这么理解,此时背包已经被完全装满了,因此完全不用再继续试下去了,对于剩下的物品,直接全都不选即可,不用再进行计算。
有N件物品和一个容量为V的背包。(每种物品均只有一件)第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
特点是:每种物品仅有一件,可以选择放或不放。
然后,我分别用了动态规划法,分支限界法和回溯法来解决这个问题。
//=======================================
以下是测时间的模板:
LARGE_INTEGER BegainTime ; LARGE_INTEGER EndTime ; LARGE_INTEGER Frequency ; QueryPerformanceFrequency(&Frequency); QueryPerformanceCounter(&BegainTime) ; //要测试的代码放在这里 DP(); QueryPerformanceCounter(&EndTime); //输出运行时间(单位:s) cout<<" 运行时间(单位:s):"<<(double)( EndTime.QuadPart - BegainTime.QuadPart )/ Frequency.QuadPart<<endl<<endl;
随机数据的测量结果:
当数据量为20的物品数和200的背包容量的时候。动态规划快于排序优化的回溯法快于回溯法快于分支限界法。
(可以根据输出的随机数据判断时候得到正确的算法)
当数据量加大到40的物品数和400的背包容量的时候。此时分支限界法和动态规划法的效率依旧不错,时间增长的并不是太多。而回溯法和排序加速的回溯法则时间上升很快。
(为截图方便,此次将随机生成的数据不输出)
以下进行算法分析
//========================================
首先说下动态规划法:
用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
把这个过程理解下:在前i件物品放进容量v的背包时,
它有两种情况:
第一种是第i件不放进去,这时所得价值为:f[i-1][v]
第二种是第i件放进去,这时所得价值为:f[i-1][v-c[i]]+w[i]
(第二种是什么意思?就是如果第i件放进去,那么在容量v-c[i]里就要放进前i-1件物品)
最后比较第一种与第二种所得价值的大小,哪种相对大,f[i][v]的值就是哪种。
(这是基础,要理解!)
当然,这只是比较容易理解的一种,时间上似乎没有什么办法继续优化了,但是空间上却可以进一步优化。可以用一维数组实现。
关于一维数组的实现和讲解在这篇blog里有讲,讲的非常详细易懂。
本文为了输出所选择的物品的编号,还是选择了二维数组。实现起来非常方便(详见代码及注释)。
//========================================
接着说回溯法:
首先说一下解空间树(也称状态空间树):
树的根节点位于第1层,表示搜索的初始状态。
第2层的节点表示对解向量的第一个分量做出选择后达到的状态,第1层到第2层的边上标出对第一个分量选择的结果。
……
所有从根到叶子节点的路径就构成了解空间的一个可能解。
例子:
三个物品重量:20, 15, 10。
三个物品价值:20, 30, 20。
参照下图理解: 0表示不选第i个物品,1表示选择(i表示层数)。
图和文字选自这里。
这不重点,只是为了引出回溯法,若还有些不理解的,可以自己去Google下(个人更喜欢用这个),后面的理解要建立在这个基础上。
在回溯法之前,还有一点准备活动——蛮力法。
所谓蛮力法,是对整个解空间树中的所有可能的解进行穷举搜索的一种方法(即每个叶子都代表一种解)。
但是,我们可以对其进行剪枝。
剪枝,就是在搜索至树中的任意一个结点时,先判断该结点对应的部分是否满足约束条件,或者是否超出目标函数的值,也就是判断该结点是否包含问题的(最优)解,如果肯定不包含,则跳过对以该结点为根的子树的搜索。
以下对照图和上图对比理解。
其实,把整个解空间树当成一棵真正的树(倒过来看),剪枝这个词的由来和含义就比较好理解了。
好了,这就是回溯法,也就是深度优先搜索代(码中递归实现和输出选择的物品数)。
其实想想,回溯法也就是对一颗搜索树进行剪枝而来的。
回溯法关于0-1背包的再次优化
当然,最朴素的回溯法已经包含了一定的剪枝,那有没有什么办法,让回溯法在不进行大幅度改动的情况下进一步提高效率呢——答案是肯定的,是的,排序。
在理解上面回溯法的基础上,其实可以比较容易的想到,对体积进行从小到大的排序,注意,是体积,后面的分支限界法用的是单位价值。
对体积排序之后,当小体积的放入已经超出背包容量的时候,自然在其后的稍微大一点或者相等体积的都不用再次尝试了。因此,可以进一步的减少运算量,而实际生成的随机数据此种优化方法速度快了一半以上(由于回溯法时间复杂度的原因,未能进行大一些的随机检查,但是,很明显的趋势是,数据越多,体积排序后的时间优化效果越明显(从代码中也能看出,几乎和普通的代码量和操作难度一样)。
//========================================
最后出场的是分支限界法:
虽然回溯法求0-1背包问题,运用剪枝已经极大的减少了搜索空间,但是整个空间都是按深度优先搜索策略机械地进行,这种搜索太过盲目,能不能让电脑像人一样的思考呢?
这里,我推荐一个讲A*算法的blog。
懂分支限界法思想的朋友可以继续看这个例子,不懂的可以点这里。
其实分支限界法理解了也很好实现,举一个《算法设计与分析》上的例子。
例:0/1背包问题。假设有4个物品,其重量分别为(4, 7, 5, 3),价值分别为(40, 42, 25, 12),背包容量W=10。首先,将给定物品按单位重量价值从大到小排序,结果如下:
我们使用的启发式函数为
通过这个启发式函数得到的一个解空间树如下图:
可以对照一下步骤,具体的搜索过程如下:(红色表示我的代码实现)
(1)在根结点1,没有将任何物品装入背包,因此,背包的重量和获得的价值均为0,根据限界函数计算结点1的目标函数值为10×10=100;
(计算完之后推入队列,作为起始点)。
(2)在结点2,将物品1装入背包,因此,背包的重量为4,获得的价值为40,目标函数值为40 + (10-4)×6=76,将结点2加入待处理结点表PT中;在结点3,没有将物品1装入背包,因此,背包的重量和获得的价值仍为0,目标函数值为10×6=60,将结点3加入表PT中;
(推出结点1,对选择和不选择物品1分别计算ub值,并推入队列)
(3)在表PT中选取目标函数值取得极大的结点2优先进行搜索;
(出队ub值大的点)
(4)在结点4,将物品2装入背包,因此,背包的重量为11,不满足约束条件,将结点4丢弃;在结点5,没有将物品2装入背包,因此,背包的重量和获得的价值与结点2相同,目标函数值为40 + (10-4)×5=70,将结点5加入表PT中;
(重复结点1的操作并入队新结点)
(5)在表PT中选取目标函数值取得极大的结点5优先进行搜索;
(6)在结点6,将物品3装入背包,因此,背包的重量为9,获得的价值为65,目标函数值为65 + (10-9)×4=69,将结点6加入表PT中;在结点7,没有将物品3装入背包,因此,背包的重量和获得的价值与结点5相同,目标函数值为40 + (10-4)×4=64,将结点6加入表PT中;
(7)在表PT中选取目标函数值取得极大的结点6优先进行搜索;
(8)在结点8,将物品4装入背包,因此,背包的重量为12,不满足约束条件,将结点8丢弃;在结点9,没有将物品4装入背包,因此,背包的重量和获得的价值与结点6相同,目标函数值为65;
(9)由于结点9是叶子结点,同时结点9的目标函数值是表PT中的极大值,所以,结点9对应的解即是问题的最优解,搜索结束。
(判断结束并跳出)
我实现的方法是先按单位密度排序优先队列,每次踢出ub值最大的,对排在这个点之后的点选择或者不选择分别进行一次计算,得出相应的ub,放入优先队列中。
跳出的条件设定了两个:
1、当踢出的最大ub在叶子结点上时,结束。
2、当踢出的最大ub和v值相等时,结束。
第一点显而易见,现在说说第二点。
当ub和v相等的时候,可以这么理解,此时背包已经被完全装满了,因此完全不用再继续试下去了,对于剩下的物品,直接全都不选即可,不用再进行计算。
//============================================== //author: FreeAquar //data:2011-12 //============================================== #include <iostream> #include <cstdio> #include <algorithm> #include <cstring> #include <ctime> #include <windows.h> #include <queue> #include <time.h> #include <cmath> #define eps 1e-7 #define nMax 1000 using namespace std; int n; //总共的物品数量 int W; //背包容量 int cw; //背包中物品总容量 int cp; //背包中物品总价值 double bestP;//0-1背包中最大价值 int record[nMax][nMax]; //0-1背包中当前最大值 bool x[nMax]; bool y[nMax]; struct Node { int cw; //背包体积 int cv; //背包总价值 int cnt; //背包中试过多少种物品,当cnt==n时为叶子结点 bool x[nMax]; //选中的物品 Node() { memset(x, 0, sizeof(x)); cnt=1; cw=0; cv=0; } double ub; //启发函数的结果 friend bool operator< (Node n1, Node n2) //按启发函数建立大顶堆 { return n1.ub < n2.ub; } }node[nMax], maxn; struct Item { int w; //物品的重量 int v; //物品的价值 int i; //物品的编号 double p; //物品的单位价值 }item[nMax]; priority_queue<Node> Q; //优先队列 double Max_ub; double Min_ub; int Rand(int x); //随机数生成函数 void input(); //读入数据 bool cmp1(Item a, Item b); //按密度排序 bool cmp2(Item a, Item b); //按体积排序 //动态规划法 void DP(); //动态规划 void get_DP(); //找路径 //分支限界法 double calculate_ub(int v, int w, double p); //启发函数 void Insert(bool flag, int i, int Cv, int Cw, double ub, Node temp);//判断和入队 void bfs(); //优先队列搜索 //回溯法 void BackTrack1(int i); //体积排序回溯 void BackTrack2(int i); //回溯 //输出函数 void output_way(bool x[]); int main() { input(); LARGE_INTEGER BegainTime ; LARGE_INTEGER EndTime ; LARGE_INTEGER Frequency ; QueryPerformanceFrequency(&Frequency); QueryPerformanceCounter(&BegainTime) ; //要测试的代码放在这里 DP(); QueryPerformanceCounter(&EndTime); printf("动态规划: %d\n", record [W]); printf(" DP背包选择的物品编号:\n "); get_DP(); output_way(x); //输出运行时间(单位:s) cout<<" 运行时间(单位:s):"<<(double)( EndTime.QuadPart - BegainTime.QuadPart )/ Frequency.QuadPart<<endl<<endl; maxn.cv=0; QueryPerformanceFrequency(&Frequency); QueryPerformanceCounter(&BegainTime) ; //要测试的代码放在这里 sort(item+1, item+n+1, cmp1); bfs(); QueryPerformanceCounter(&EndTime); printf("分支限界法:%d\n", maxn.cv); printf(" 分支限界法选择的物品编号:\n "); output_way(maxn.x); //输出运行时间(单位:s) cout<<" 运行时间(单位:s):"<<(double)( EndTime.QuadPart - BegainTime.QuadPart )/ Frequency.QuadPart<<endl<<endl; bestP=0; cp=cw=0; memset(x, 0, sizeof(x)); memset(y, 0, sizeof(y)); QueryPerformanceFrequency(&Frequency); QueryPerformanceCounter(&BegainTime) ; //要测试的代码放在这里 sort(item+1, item+n+1, cmp2); BackTrack1(1); QueryPerformanceCounter(&EndTime); printf("按体积排序的回溯法:%.0lf\n", bestP); printf(" 回溯法选择的物品编号:\n "); output_way(x); //输出运行时间(单位:s) cout<<" 运行时间(单位:s):"<<(double)( EndTime.QuadPart - BegainTime.QuadPart )/ Frequency.QuadPart<<endl<<endl; bestP=0; cp=cw=0; memset(x, 0, sizeof(x)); memset(y, 0, sizeof(y)); QueryPerformanceFrequency(&Frequency); QueryPerformanceCounter(&BegainTime) ; //要测试的代码放在这里 BackTrack2(1); QueryPerformanceCounter(&EndTime); printf("回溯法:%.0lf\n", bestP); printf(" 回溯法选择的物品编号:\n "); output_way(x); //输出运行时间(单位:s) cout<<" 运行时间(单位:s):"<<(double)( EndTime.QuadPart - BegainTime.QuadPart )/ Frequency.QuadPart<<endl<<endl; return 0; } //===================================== //随机数,比较函数和读入数据 //===================================== int Rand(int x) { int t=x,sum=0; while (t>0) { sum+=rand()%t; t-=32767; } return sum; } bool cmp1(Item a, Item b) { return a.p>b.p; } bool cmp2(Item a, Item b) { return a.w<b.w; } void input() { n=40; W=400; //scanf("%d%d", &n, &W); cout<<"物品数目为:"<<n<<endl; cout<<"背包容量为:"<<W<<endl; cout<<"------------------------------------------"<<endl; double Min=1e20; double Max=-1; for(int i=1; i<=n; i++) { //每个物品的状态 //scanf("%d%d", &item[i].w, &item[i].v); item[i].w=Rand(100); item[i].v=Rand(1000); item[i].p=1.0*item[i].v/item[i].w; item[i].i=i; Min=min(Min, item[i].p); Max=max(Max, item[i].p); //启发值的边界 } //for(int i=1; i<=n; i++) //cout<<item[i].w<<" "<<item[i].v<<endl; Max_ub=W*Max; Min_ub=W*Min; } //===================================== //读入结束 //动态规划法开始 //===================================== void DP() { for(int i=0; i<=n; i++) record[i][0]=0; for(int j=0; j<=W; j++) record[0][j]=0; for(int i=1; i<=n; i++) for(int j=1; j<=W; j++) { if(j<item[i].w) record[i][j]=record[i-1][j]; else record[i][j]=max(record[i-1][j], record[i-1][j-item[i].w]+item[i].v); } } void get_DP() { int j=W; for(int i=n; i>0; i--) { if(record[i][j]>record[i-1][j]) { x[i]=1; j=j-item[i].w; } else x[i]=0; } } //===================================== //动态规划结束 //分支限界法开始 //===================================== double calculate_ub(int v, int w, double p) //计算启发函数 { return v+(W-w)*p; } void Insert(bool flag, int i, int Cv, int Cw, double ub, Node temp) { if((ub<Max_ub+eps && ub>Min_ub-eps) && (Cw<=W)) //结点符合要求 { //cout<<ub<<endl; Node cm; cm.cw=Cw; cm.cv=Cv; cm.ub=ub; for(int j=1; j<=n; j++) { cm.x[j]=temp.x[j]; } if(flag) cm.x[item[i].i]=1; cm.cnt=i+1; Q.push(cm); } } void bfs() //广搜 { Node node; node.cw=0; node.cv=0; node.ub=calculate_ub(0, 0, item[1].p); Q.push(node); while(!Q.empty()) { Node temp=Q.top(); Q.pop(); //当前值与启发式函数结果相同或者叶子结点启发式结果最大 if(abs(temp.ub-temp.cv)<eps || temp.cnt>n) { if(maxn.cv<temp.cv+eps) maxn=temp; break; } int Cw=temp.cw; int Cv=temp.cv; for(int flag=0; flag<2; flag++) //flag=0时,计算不放第temp.cnt+1件物品 //flag=1时,计算放入第temp.cnt+1件物品 { int i=temp.cnt; if(flag) { Cw+=item[i].w; Cv+=item[i].v; } double ub=calculate_ub(Cv, Cw, item[i+1].p); Insert(flag, i, Cv, Cw, ub, temp); } } return ; } //===================================== //分支限界法结束 //体积排序回溯开始 //===================================== void BackTrack1(int i) { if(i>n || (W<cw+item[i].w)) { if(bestP<cp)//更新 { bestP=cp; for(int i=1; i<=n; i++) x[i]=y[i]; } return ; } //cout<<cw<<" "<<item[i].c<<" "<<item[i].w<<" "<<cp<<endl; cw=cw+item[i].w; //搜索 cp=cp+item[i].v; y[item[i].i]=1; BackTrack1(i+1); //左子树 y[item[i].i]=0; cw=cw-item[i].w; //回溯 cp=cp-item[i].v; BackTrack1(i+1); //右子树 } //===================================== //体积排序回溯结束 //回溯法开始 //===================================== void BackTrack2(int i) { if(i>n) { if(bestP<cp) //更新 { bestP=cp; for(int i=1; i<=n; i++) x[i]=y[i]; } return ; } //cout<<cw<<" "<<item[i].c<<" "<<item[i].w<<" "<<cp<<endl; if(W>=cw+item[i].w) //判断背包是否能放下 { cw=cw+item[i].w; //搜索 cp=cp+item[i].v; y[item[i].i]=1; BackTrack2(i+1); //左子树 y[item[i].i]=0; cw=cw-item[i].w; //回溯 cp=cp-item[i].v; } BackTrack2(i+1); //右子树 } //===================================== //所有方法结束 //输出路径 //===================================== void output_way(bool x[]) { for(int i=1; i<=n; i++) { if(x[i]) printf("%d ", i); } cout<<endl; }
相关文章推荐
- 【01背包问题】:动态规划、回溯法和分支限界法 三种算法的对比与分析(时间复杂度方面)
- 算法-0-1背包的动态规划,回溯,分支限界三种解法
- 动态规划分析—以01背包为例
- 动态规划之背包问题之逻辑(无代码)
- 关于Contiki中Rime代码的一些分析记录
- 动态规划与回溯法解决0-1背包问题
- 动态规划-背包问题求解过程【代码 from eason】
- 关于Nehe‘s OpenGL tutorial on Win32 Multisampling Application Creation的一些代码分析。
- 关于动态规划的一些经验与总结
- 关于一些简单典型的动态规划
- 关于动态规划0-1背包的算法
- 关于iOS内购的一些代码整理分析
- 修改设置->关于手机->法律信息 下有一些Item,如开放源代码许可、Google法律信息等,这里分析的是Android 4.4的代码
- 0-1背包问题动态规划代码实现(C++实现)
- 0-1背包问题与动态规划的C/C++代码
- 动态规划分析和代码实现(Java版)
- [dp]关于动态规划的一些理解
- 0-1背包 动态规划原理与c代码
- 动态规划的典型分析与代码实现----主java
- 动态规划 || Divisibility(背包变种)