动态规划
动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
与分治法不同的是,适合于用动态规划法求解的问题,经分解得到的子问题往往不是相互独立的。
该算法的有效性依赖于问题本身所具有的两个重要性质:最优子结构性质和子问题重叠性质。从一般意义上讲,问题所具有的这两个重要性质是该问题可用动态规划算法求解的基本要素。这对于在设计求解具体问题的算法时,是否选择动态规划算法具有指导意义 。
最优子结构
设计动态规划算法的第一步通常是要刻画最优解的结构。当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。问题的最优子结构性质提供了该问题可用动态规划算法求解的重要线索。利用问题的最优子结构性质,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解。
重叠子问题
可用动态规划算法求解的问题应具备的另一基本要素是子问题的重叠性质。在用递归算法自顶向下解此问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,当再次需要解此子问题时,只是简单地用常数时间查看一下结果。通常,不同的子问题个数随问题的大小呈多项式增长。因此,用动态规划算法通常只需要多项式时间,从而获得较高的解题效率。
备忘录方法
备忘录方法是动态规划算法的变形。与动态规划算法一样,备忘录方法用表格保存已解决的子问题的答案,在下次需要解此子问题时,只要简单地查看该子问题的解答,而不必重新计算。与动态规划算法不同的是,备忘录方法的递归方式是自顶向下,而动态规划算法则是自底向上递归的。因此备忘录方法的控制结构与直接递归方法的控制结构相同,区别在于备忘录方法为每个解过的子问题建立了备忘录以备需要时查看,避免了相同子问题的重复求解。
动态规划解 0-1 背包问题的基本思想:
对于每个物品我们可以有两个选择,放入背包,或者不放入,有 n 个物品,故而我们需要做出 n 个选择,于是我们设 F[i][v] 表示做出第 i 次选择后,所选物品放入一个容量为 v 的背包获得的最大价值。现在我们来找出递推公式,对于第 i 件物品,有两种选择,放或者不放。
① 如果放入第 i 件物品,则 F[i][v] = F[ i-1 ][ v-w[i] ] + p[i] ,表示,前 i-1 次选择后所选物品放入容量为 v-w[i] 的背包所获得最大价值为 F[ i-1 ][ v-w[i] ] ,加上当前所选的第 i 个物品的价值 p[i] 即为 F[i][v] 。
② 如果不放入第 i 件物品,则有 F[i][v] = F[ i-1 ][ v ],表示当不选第i件物品时,F[i][v] 就转化为前 i-1 次选择后所选物品占容量为 v 时的最大价值 F[i-1][v]。则:F[i][v] = max{ F[ i-1 ][v], F[ i-1 ][ v-w[i] ] + p[i] }。
#include
#include
#define N 100
#define MAX(a,b) a < b ? b : a
using namespace std;
struct goods{
int wight;//物品重量
int value;//物品价值
};
int n,bestValue,cv,cw,C;//物品数量,价值最大,当前价值,当前重量,背包容量
int X[N],cx[N];//最终存储状态,当前存储状态
struct goods goods[N];
int KnapSack(int n,struct goods a[],int C,int x[]){
int V[N][N+1];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= C; j++)
if(j < a[i-1].wight)
V[i][j] = V[i-1][j];
else
V[i][j] = MAX(V[i-1][j],V[i-1][j-a[i-1].wight] + a[i-1].value);
for(int i = n,j = C; i > 0; i--)
{
if(V[i][j] > V[i-1][j])
{
x[i-1] = 1;
j = j - a[i-1].wight;
}
else
x[i-1] = 0;
}
return V[n][C];
}
int main()
{
printf("物品种类n:");
scanf("%d",&n);
printf("背包容量C:");
scanf("%d",&C);
for(int i = 0; i < n; i++)
{
printf("物品%d的重量w[%d]及其价值v[%d]:",i+1,i+1,i+1);
scanf("%d%d",&goods[i].wight,&goods[i].value);
}
int sum = KnapSack(n,goods,C,X);
printf("动态规划法求解0/1背包问题:\nX=[");
for(int i = 0; i < n; i++)
cout< printf("] 装入总价值[%d]\n", sum);
return 0;
}
回溯法
回溯法有 "通用的解题法" 之称。用它可以系统地搜索一个问题的所有解或任一解。回溯法是一个既带有系统性又带有跳跃性的搜索算法。它在问题的解空间树中,按深度优先策略,从根节点出发搜索解空间树。算法搜索至解空间树的任一结点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先策略搜索。回溯法求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索才结束。回溯法求问题的一个解时,只要搜索到问题的一个解就可结束。这种以深度优先方式系统搜索问题解的算法称为回溯法,它适用于解组合数较大的问题。
回溯法的算法框架
(1)问题的解空间
用回溯法解问题时,应明确定义问题的解空间。问题的解空间至少应包含问题的一个(最优)解。例如,对于有n中可选物品的0-1背包问题,其解空间由长度为n的0-1向量组成。该解空间包含对变量的所有可能的 0-1 赋值。当 n=3 时,其解空间是{ (0,0,0),(0,1,0),(0,0,1),(1,0,0),(0,1,1),(1,0,1),(1,1,0),(1,1,1) }。定义了问题的解空间后,还应将解空间很好地组织起来,使得能用回溯法方便地搜索整个解空间。通常将解空间组织成树或图的形式。
(2)回溯法的基本思想
确定了解空间的组织结构后,回溯法从根结点出发,以深度优先方式搜索整个解空间。这个开始结点称为活结点,同时也称为当前的扩展结点。在当前的扩展结点处,搜索向纵向移至一个新结点。这个新结点就成为新的活结点,并成为当前扩展结点。如果在当前的扩展结点处不能再纵向移动,则这个结点就是死结点。此时,应该往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前扩展结点。回溯法以这种工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已无活结点为止。
回溯法解 0-1 背包问题的基本思想:
为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数 (bounding function) 来处死那些实际上不可能产生所需解的活结点,以减少问题的计算量。这种具有限界函数的深度优先生成法称为回溯法。对于有 n 种可选物品的 0/1 背包问题,其解空间由长度为 n 的 0-1 向量组成,可用子集数表示。在搜索解空间树时,只要其左儿子结点是一个可行结点,搜索就进入左子树。当右子树中有可能包含最优解时就进入右子树搜索。
#include
#include
//#include
using namespace std;
int n;//物品数量
double c;//背包容量
double v[100];//各个物品的价值 value
double w[100];//各个物品的重量 weight
double cw = 0.0;//当前背包重量 current weight
double cp = 0.0;//当前背包中物品总价值 current value
double bestp = 0.0;//当前最优价值best price
double perp[100];//单位物品价值(排序后) per price
int order[100];//物品编号
int put[100];//设置是否装入,为1的时候表示选择该组数据装入,为0的表示不选择该组数据
//按单位价值排序
void knapsack()
{
int i,j;
int temporder = 0;
double temp = 0.0;
for(i=1;i<=n;i++)
perp[i]=v[i]/w[i]; //计算单位价值(单位重量的物品价值)
for(i=1;i<=n-1;i++)
{
for(j=i+1;j<=n;j++)
if(perp[i] {
temp = perp[i]; //冒泡对perp[]排序
perp[i]=perp[j];
perp[j]=temp;
temporder=order[i];//冒泡对order[]排序
order[i]=order[j];
order[j]=temporder;
temp = v[i];//冒泡对v[]排序
v[i]=v[j];
v[j]=temp;
temp=w[i];//冒泡对w[]排序
w[i]=w[j];
w[j]=temp;
}
}
}
//回溯函数
void backtrack(int i)
{ //i用来指示到达的层数(第几步,从0开始),同时也指示当前选择玩了几个物品
double bound(int i);
if(i>n) //递归结束的判定条件
{
bestp = cp;
return;
}
//如若左子节点可行,则直接搜索左子树;
//对于右子树,先计算上界函数,以判断是否将其减去
if(cw+w[i]<=c)//将物品i放入背包,搜索左子树
{
cw+=w[i];//同步更新当前背包的重量
cp+=v[i];//同步更新当前背包的总价值
put[i]=1;
backtrack(i+1);//深度搜索进入下一层
cw-=w[i];//回溯复原
cp-=v[i];//回溯复原
}
if(bound(i+1)>bestp)//如若符合条件则搜索右子树
backtrack(i+1);
}
//计算上界函数,功能为剪枝
double bound(int i)
{ //判断当前背包的总价值cp+剩余容量可容纳的最大价值<=当前最优价值
double leftw= c-cw;//剩余背包容量
double b = cp;//记录当前背包的总价值cp,最后求上界
//以物品单位重量价值递减次序装入物品
while(i<=n && w[i]<=leftw)
{
leftw-=w[i];
b+=v[i];
i++;
}
//装满背包
if(i<=n)
b+=v[i]/w[i]*leftw;
return b;//返回计算出的上界
}
int main()
{
int i;
printf("请输入物品的数量和背包的容量:");
scanf("%d %lf",&n,&c);
/*printf("请输入物品的重量和价值:\n");
for(i=1;i<=n;i++)
{
printf("第%d个物品的重量:",i);
scanf("%lf",&w[i]);
printf("第%d个物品的价值是:",i);
scanf("%lf",&v[i]);
order[i]=i;
}*/
printf("请依次输入%d个物品的重量:\n",n);
for(i=1;i<=n;i++){
scanf("%lf",&w[i]);
order[i]=i;
}
printf("请依次输入%d个物品的价值:\n",n);
for(i=1;i<=n;i++){
scanf("%lf",&v[i]);
}
knapsack();
backtrack(1);
printf("最优价值为:%lf\n",bestp);
printf("需要装入的物品编号是:");
for(i=1;i<=n;i++)
{
if(put[i]==1)
printf("%d ",order[i]);
}
return 0;
}
分支限界法
分支限界法类似于回溯法,也是在问题的解空间上搜索问题解的算法。一般情况下,分支限界法与回溯法的求解目标不同。回溯法的求解目标是找出解空间中满足约束条件的所有解,而分支限界法的求解目标是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即某种意义的最优解。
由于求解目标不同,导致分支限界法与回溯法对解空间的搜索方式也不相同。回溯法以深度优先的方式搜索解空间,而分支限界法则以广度优先或最小消耗优先的方式搜索解空间。分支限界法的搜索策略是,在扩展结点处,先生成其所有的孩子结点(分支),然后再从当前的活结点表中选择下一个扩展结点。为了有效地选择下一扩展结点,加速搜索的进程,在每一个活结点处,计算一个函数值(限界),并根据函数值,从当前活结点表中选择一个最有利的结点作为扩展结点,使搜索朝着解空间上有最优解的分支推进,以便尽快地找出一个最优解。这种方法称为分支限界法。
分支限界法解 0-1 背包问题的基本思想:
首先,要对输入数据进行预处理,将各物品依其单位重量价值从大到小进行排列。在下面描述的优先队列分支限界法中,节点的优先级由已装袋的物品价值加上剩下的最大单位重量价值的物品装满剩余容量的价值和。算法首先检查当前扩展结点的左儿子结点的可行性。如果该左儿子结点是可行结点,则将它加入到子集树和活结点优先队列中。当前扩展结点的右儿子结点一定是可行结点,仅当右儿子结点满足上界约束时才将它加入子集树和活结点优先队列。当扩展到叶节点时为问题的最优值。
#include
#include
#include
using namespace std;
#define N 100 //最多可能物体数
struct goods //物品结构体
{
int sign; //物品序号
int w; //物品重量
int p; //物品价值
}a[N];
bool m(goods a,goods b)
{
return (a.p/a.w) > (b.p/b.w);
}
int max(int a,int b)
{
return a }
int n,C,bestP=0,cp=0,cw=0;
int X[N],cx[N];
struct KNAPNODE //状态结构体
{
bool s1[N]; //当前放入物体
int k; //搜索深度
int b; //价值上界
int w; //物体重量
int p; //物体价值
};
struct HEAP //堆元素结构体
{
KNAPNODE *p; //结点数据
int b; //所指结点的上界
};
//交换两个堆元素
void swap(HEAP &a, HEAP &b)
{
HEAP temp = a;
a = b;
b = temp;
}
//堆中元素上移
void mov_up(HEAP H[], int i)
{
bool done = false;
if(i != 1)
{
while(!done && i != 1)
{
if(H[i].b > H[i/2].b)
{
swap(H[i], H[i/2]);
}
else
{
done = true;
}
i = i/2;
}
}
}
//堆中元素下移
void mov_down(HEAP H[], int n, int i)
{
bool done = false;
if((2*i)<=n)
{
while(!done && ((i = 2*i) <= n))
{
if(i+1 <= n && H[i+1].b > H[i].b)
{
i++;
}
if(H[i/2].b < H[i].b)
{
swap(H[i/2], H[i]);
}
else
{
done = true;
}
}
}
}
//往堆中插入结点
void insert(HEAP H[], HEAP x, int &n)
{
n++;
H[n] = x;
mov_up(H,n);
}
//删除堆中结点
void del(HEAP H[], int &n, int i)
{
HEAP x, y;
x = H[i]; y = H[n];
n --;
if(i <= n)
{
H[i] = y;
if(y.b >= x.b)
{
mov_up(H,i);
}
else
{
mov_down(H, n, i);
}
}
}
//获得堆顶元素并删除
HEAP del_top(HEAP H[], int&n)
{
HEAP x = H[1];
del(H, n, 1);
return x;
}
//计算分支节点的上界
void bound( KNAPNODE* node,int M, goods a[], int n)
{
int i = node->k;
float w = node->w;
float p = node->p;
if(node->w > M)
{ //物体重量超过背包载重量
node->b = 0; //上界置为0
}
else
{
while((w+a[i].w <= M)&&(i < n))
{
w += a[i].w; //计算背包已装入载重
p += a[i++].p; //计算背包已装入价值
}
if(i {
node->b = p + (M - w)*a[i].p/a[i].w;
}
else
{
node -> b = p;
}
}
}
//用分支限界法实现0/1背包问题
int KnapSack(int n,goods a[],int C, int X[])
{
int i, k = 0; //堆中元素个数的计数器初始化为0
int v;
KNAPNODE *xnode, *ynode, *znode;
HEAP x, y, z, *heap;
heap = new HEAP[n*n]; //分配堆的存储空间
for(i = 0; i < n; i++)
{
a[i].sign=i; //记录物体的初始编号
}
sort(a,a+n,m); //对物体按照价值重量比排序
xnode = new KNAPNODE; //建立父亲结点
for(i = 0; i < n; i++)
{ //初始化结点
xnode->s1[i] = false;
}
xnode->k = xnode->w = xnode->p = 0;
while(xnode->k < n)
{
ynode = new KNAPNODE; //建立结点y
*ynode = *xnode; //结点x的数据复制到结点y
ynode->s1[ynode->k] = true; //装入第k个物体
ynode->w += a[ynode->k].w; //背包中物体重量累计
ynode->p += a[ynode->k].p; //背包中物体价值累计
ynode->k ++; //搜索深度++
bound(ynode, C, a, n); //计算结点y的上界
y.b = ynode->b;
y.p = ynode;
insert(heap, y, k); //结点y按上界的值插入堆中
znode = new KNAPNODE; //建立结点z
*znode = *xnode; //结点x的数据复制到结点z
znode->k ++; //搜索深度++
bound(znode, C, a, n); //计算节点z的上界
z.b = znode->b;
z.p = znode;
insert(heap, z, k); //结点z按上界的值插入堆中
delete xnode;
x = del_top(heap, k); //获得堆顶元素作为新的父亲结点
xnode = x.p;
}
v = xnode->p;
for(i = 0; i < n; i++)
{ //取装入背包中物体在排序前的序号
if(xnode->s1[i])
{
X[a[i].sign] =1 ;
}
else
{
X[a[i].sign] = 0;
}
}
delete xnode;
delete heap;
return v; //返回背包中物体的价值
}
/*测试以上算法的主函数*/
int main()
{
goods b[N];
printf("物品种数n: ");
scanf("%d",&n); //输入物品种数
printf("背包容量C: ");
scanf("%d",&C); //输入背包容量
for (int i=0;i {
printf("物品%d的重量w[%d]及其价值v[%d]: ",i+1,i+1,i+1);
scanf("%d%d",&a[i].w,&a[i].p);
b[i]=a[i];
}
int sum=KnapSack(n,a,C,X);//调用分支限界法求0/1背包问题
printf("分支限界法求解0/1背包问题:\nX=[ ");
for(int i=0;i cout< printf("] 装入总价值[%d]\n",sum);
return 0;
}