有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?
number=4,capacity=8
物品编号(i) | W(体积) | V(价值) |
---|---|---|
1 | 2 | 3 |
2 | 3 | 4 |
3 | 4 | 5 |
4 | 5 | 6 |
1.动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,
使得问题能够以递推(或者说分治)的方式去解决。
2.动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),
按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。
在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,
丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
面对每个物品,我们只有选择拿取或者不拿两种选择,不能选择装入某物品的一部分,也不能装入同一物品多次。
解决办法:声明一个 大小为 m[n][c] 的二维数组,m[ i ][ j ] 表示 在面对第 i 件物品,且背包容量为 j 时所能获得的最大价值 ,那么我们可以很容易分析得出 m[i][j] 的计算方法,
(1). j < weight[i] 的情况,这时候背包容量不足以放下第 i 件物品,只能选择不拿
m[ i ][ j ] = m[ i-1 ][ j ]
(2). j>=w[i] 的情况,这时背包容量可以放下第 i 件物品,我们就要考虑拿这件物品是否能获取更大的价值。
如果拿取,m[ i ][ j ]=m[ i-1 ][ j-weight[ i ] ] + value[ i ]。 这里的m[ i-1 ][ j-weight[ i ] ]指的就是考虑了i-1件物品,背包容量为j-w[i]时的最大价值,也是相当于为第i件物品腾出了w[i]的空间。
如果不拿,m[ i ][ j ] = m[ i-1 ][ j ] , 同(1)
究竟是拿还是不拿,自然是比较这两种情况那种价值最大。
if(j>=w[i])
m[i][j]=max(m[i-1][j],m[i-1][j-weight[i]]+value[i]);
else
m[i][j]=m[i-1][j];
1)如,i=1,j=1,w(1)=2,v(1)=3,有j
2) 又如i=1,j=2,w(1)=2,v(1)=3,有j=w(1),故V(1,2)=max{ V(1-1,2),V(1-1,2-w(1))+v(1) }=max{0,0+3}=3;
3) 如此下去,填到最后一个,i=4,j=8,w(4)=5,v(4)=6,有j>w(4),故V(4,8)=max{ V(4-1,8),V(4-1,8-w(4))+v(4) }=max{9,4+6}=10;所以填完表如下图:
#define N 100
#include
#include
#include
using namespace std;
void Knap(int value[], int weight[], int c, int n, int m[][N])
{
for (int i = 1; i <= n; i++)//物品i
{
for (int j = 1; j <= c; j++)//重量j
{
if (j >= weight[i])
{
m[i][j] = max(m[i - 1][j], m[i - 1][j - weight[i]] + value[i]);
}
else m[i][j] = m[i - 1][j];
}
}
//for (int i = 0; i <= n; i++)//物品i
//{
// for (int j = 0; j <= c; j++)//重量j
// {
// cout << m[i][j] << ' ';
// }
// cout << endl;
//}
}
void Trace(int m[][N], int weight[], int c, int n, int x[])
{
int h = n, g = c;
while (h >= 1)
{
if (m[h][g] == m[h - 1][g])
x[h] = 0;
else
{
x[h] = 1;
g = g - weight[h];
}
h--;
}
}
int main()
{
int value[100];
int weight[100];
int m[N][N] = { 0 };
int x[N];
int c;
int n;
value[0] = 0;
weight[0] = 0;
cout << "请输入背包可以承受的重量:";
cin >> c;
cout << "请输入物体数量:";
cin >> n;
for (int i = 1; i <= n; i++)
{
printf("请输入第%d个物体的质量与价值:", i);
cin >> weight[i] >> value[i];
}
Knap(value, weight, c, n, m);
Trace(m, weight, c, n, x);
for (int i = 1; i <= n; i++)
cout << x[i] << " ";
cout << endl;
return 0;
}
1.贪心算法的基本思想是找出整体当中每个小的局部的最优解,
并且将所有的这些局部最优解合起来形成整体上的一个最优解。
2.使用贪心算法的问题必须满足下面的两个性质:
1)整体的最优解可以通过局部的最优解来求出;
2)一个整体能够被分为多个局部,并且这些局部都能够求出最优解。
使用贪心算法当中的两个典型问题是活动安排问题和背包问题。
3.贪心算法一般按如下步骤进行:
1)建立数学模型来描述问题。
2)把求解的问题分成若干个子问题。
3)对每个子问题求解,得到子问题的局部最优解。
4)把子问题的解局部最优解合成原来解问题的一个解。
首先我们按照物品的单位重量价值来进行排序,
然后按照单位重量价值从高到低依次进行选择,
若其能装入背包则将其装入,
不能则继续判断下一个直至所有物品都判断完,
就得到了问题的一个解。
但是对于0-1背包问题,用贪心算法并不能保证最终可以将背包装满,部分剩余的空间使得单位重量背包空间的价值降低了,这也是用贪心算法一般无法求得0-1背包最优解的原因。
#include
#include
using namespace std;
struct node
{
double v;//价值
double w;//重量
} wu[100];
bool cmp1(node a, node b)//重量
{
if (a.w == b.w)
return a.v > b.v;
return a.w < b.w;
}
bool cmp2(node a, node b)//价值
{
if (a.v == b.v)
return a.w < b.w;
return a.v > b.v;
}
bool cmp3(node a, node b)// 单位价值
{
if ((a.v / a.w) == (b.v / b.w))
return a.w < b.w;
return (a.v / a.w) > (b.v / b.w);
}
int fun1(int n, int c)
{
sort(wu, wu + n, cmp1);
int value = 0;
for (int i = 0; i < n; i++)
{
if (c >= wu[i].w)
{
c -= wu[i].w;
value += wu[i].v;
}
}
return value;
}
int fun2(int n, int c)
{
sort(wu, wu + n, cmp2);
int value = 0;
for (int i = 0; i < n; i++)
{
if (c >= wu[i].w)
{
c -= wu[i].w;
value += wu[i].v;
}
}
return value;
}
int fun3(int n, int c)
{
sort(wu, wu + n, cmp3);
int value = 0;
for (int i = 0; i < n; i++)
{
if (c >= wu[i].w)
{
c -= wu[i].w;
value += wu[i].v;
}
}
return value;
}
int random(int n, int c)
{
int ans = -1, m = 1000;
int flag, b[110];
while (m--)
{
int flag2 = 0, value = 0;
for (int i = 0; i < n; i++)
{
b[i] = 1;
}
while (1)
{
flag = rand() % n;
if (b[flag] != 0)
{
if (flag2 + wu[flag].w <= c)
{
flag2 += wu[flag].w;
b[flag] = 0;
value += wu[flag].v;
}
}
else
{
break;
}
}
if (value > ans)
{
ans = value;
}
}
return ans;
}
int main()
{
int n, c;//n个物品,c的容量
cin >> n >> c;
for (int i = 0; i < n; i++)
cin >> wu[i].w;
for (int j = 0; j < n; j++)
cin >> wu[j].v;
cout << "优先放重量最小的答案:";
cout << fun1(n, c) << endl;
cout << "优先放价值最大的答案:";
cout << fun2(n, c) << endl;
cout << "先放性价比最大的答案:";
cout << fun3(n, c) << endl;
cout << "随机1000次最大的答案:";
cout << random(n, c) << endl;
return 0;
}
1.回溯法概念:
回溯算法有“通用的解题法”之称。用它可以系统地搜索一个问题的所在解或任一解。
回溯法是一个即带有系统性又带有跳跃性的所搜算法。
2.回溯法思想:
1)在包含问题的所有解的解空间树中,按照深度优先搜索的策略,
从根结点出发深度探索解空间树。当探索到某一结点时,
要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,
如果该结点不包含问题的解,则逐层向其祖先结点回溯。
2)若用回溯法求问题的所有解时,要回溯到根,
且根结点的所有可行的子树都要已被搜索遍才结束;
而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
3)回溯法解题步骤:
1>针对所给问题,定义问题的解空间
2>确定易于搜索的解空间结构
3>以深度优先方式所搜解空间,并在搜索过程中用剪枝函数避免无效搜索。
01背包属于找最优解问题,用回溯法需要构造解的子集树。对于每一个物品i,对于该物品只有选与不选2个决策,总共有n个物品,可以顺序依次考虑每个物品,
这样就形成了一棵解空间树: 基本思想就是遍历这棵树,以枚举所有情况,最后进行判断,如果重量不超过背包容量,且价值最大的话,该方案就是最后的答案。
在搜索状态空间树时,只要左子节点是可一个可行结点,搜索就进入其左子树。对于右子树时,先计算上界函数,以判断是否将其减去(剪枝)。
上界函数bound():当前价值cw+剩余容量可容纳的最大价值<=当前最优价值bestp。
为了更好地计算和运用上界函数剪枝,选择先将物品按照其单位重量价值从大到小排序,此后就按照顺序考虑各个物品。
利用回溯法试设计一个算法求出0-1背包问题的解,也就是求出一个解向量xi (即对n个物品放或不放的一种的方案)
其中, (xi = 0 或1,xi = 0表示物体i不放入背包,xi =1表示把物体i放入背包)。
在递归函数Backtrack中,
当i>n时,算法搜索至叶子结点,得到一个新的物品装包方案。此时算法适时更新当前的最优价值
当i
因为物品只有选与不选2个决策,而总共有n个物品,所以时间复杂度为。
因为递归栈最多达到n层,而且存储所有物品的信息也只需要常数个一维数组,所以最终的空间复杂度为O(n)。
#include
#include
//#include
using namespace std;
int n;//物品数量
double c;//背包容量
double v[100];//各个物品的价值
double w[100];//各个物品的重量
double cw = 0.0;//当前背包重量
double cp = 0.0;//当前背包中物品总价值
double bestp = 0.0;//当前最优价值
double perp[100];//单位物品价值(排序后)
int order[100];//物品编号
//按单位价值排序
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] < perp[j])//冒泡排序perp[],order[],sortv[],sortw[]
{
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;
}
}
}
//计算上界函数,功能为剪枝
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;//返回计算出的上界
}
//回溯函数
void backtrack(int i)
{ //i用来指示到达的层数(第几步,从0开始),同时也指示当前选择玩了几个物品
if (i > n) //递归结束的判定条件
{
bestp = cp;
return;
}
//如若左子节点可行,则直接搜索左子树;
//对于右子树,先计算上界函数,以判断是否将其减去
if (cw + w[i] <= c)//将物品i放入背包,搜索左子树
{
cw += w[i];//同步更新当前背包的重量
cp += v[i];//同步更新当前背包的总价值
backtrack(i + 1);//深度搜索进入下一层
cw -= w[i];//回溯复原
cp -= v[i];//回溯复原
}
if (bound(i + 1) > bestp)//如若符合条件则搜索右子树
{
backtrack(i + 1); //后续节点的价值上界大于当前最优价值,则可以进入右界面 否则最优的都小于或等于当前的 就没必要再进入右节点
//进入右节点 因为不加入到背包 故 当前的价值 重量 都不发生改变
}
}
int main()
{
int i;
printf("请输入物品的数量和背包的容量:");
scanf_s("%d %lf", &n, &c);
printf("请依次输入%d个物品的重量:\n", n);
for (i = 1; i <= n; i++)
{
scanf_s("%lf", &w[i]);
order[i] = i;
}
printf("请依次输入%d个物品的价值:\n", n);
for (i = 1; i <= n; i++)
{
scanf_s("%lf", &v[i]);
}
knapsack();
backtrack(1);
cout << endl;
printf("最优价值为:%.lf\n", bestp);
return 0;
}
分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。
在分支限界法中,每一个活结点只有一次机会成为扩展结点。
活结点一旦成为扩展结点,就一次性产生其所有儿子结点。
在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,
其余儿子结点被加入活结点表中。
此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。
这个过程一直持续到找到所需的解或活结点表为空时为止。
函数MaxKnapSack使用分支限界法
用子集树表示,左子节点表示当前物品放入,右子节点表示当前物品不放入。
优先级:按照优先队列中节点元素N的优先级由上界函数bound计算出的uprofit给出。(就是当前背包的重量+剩余可以放的重量)(剩余的物品按照单位重量价值非升序排序,将单位重量价值大的先放 入,放不下整个物品的话,就放入一部分。)如果到达了叶节点,由于使用的是优先队列,所有活结点价值上届不超过该叶节点价值,是最优值
空间:最坏情况下需搜索2^(n +1) –2个节点,需O(2^n ) 个空间存储节点,则算法空间复杂性为O(2^n )
时间:最坏情况有 2^(n +1) - 2 个节点,限界函数时间复杂度O(n),若 对每个节点用限界函数判断,则其时间复杂度为O(n2^n).
#include
#include
#include
using namespace std;
const int N = 110;
struct stone {
int v, w;
stone() {
v = w = 0;
}
bool operator < (stone b) const {
return v / w > b.v / b.w;
}
}item[N];
struct node {
int level, cv, cw;
float bound;
bool operator< (const node& b) const {
return bound > b.bound;
}
};
int best, W, n;
inline void Initialize(priority_queue<node>& Q) {
while (Q.size()) Q.pop();
}
float bound(node& p) {
int left = W - p.cw;
float val = p.cv;
int i;
for (i = p.level; i <= n && item[i].w <= left; i++) {
left -= item[i].w;
val += item[i].v;
}
if (i <= n) val += item[i].v * 1.0 / item[i].w * left;
return val;
}
priority_queue<node> PQ;
void knapsack() {
node u, v;
Initialize(PQ);
v = { 0,0,0,0 };
best = 0;
v.bound = bound(v);
PQ.push(v);
while (PQ.size()) {
v = PQ.top();
PQ.pop();
if (v.level == n) continue;
if (v.bound > best) {
u.level = v.level + 1;
u.cw = v.cw + item[u.level].w;
u.cv = v.cv + item[u.level].v;
//printf("%d %d\n", u.cw, u.cv);
if (u.cw <= W && u.cv > best)
best = u.cv;
u.bound = bound(u);
if (u.bound > best) PQ.push(u);
u.cw = v.cw; u.cv = v.cv;
u.level = v.level + 1;
u.bound = bound(u);
if (u.bound > best) PQ.push(u);
}
}
}
int main() {
printf("请输入物品的数量和背包的容量:");
scanf_s("%d %d", &n, &W);
printf("请依次输入%d个物品的重量:\n", n);
for (int i = 1; i <= n; i++)
{
scanf_s("%d", &item[i].w);
}
printf("请依次输入%d个物品的价值:\n", n);
for (int i = 1; i <= n; i++)
{
scanf_s("%d", &item[i].v);
}
sort(item + 1, item + 1 + n);
knapsack();
printf("%d\n", best);
return 0;
}
/*
4 7
3 5 2 1
9 10 7 4
20
*/
#include
#include
#include
using namespace std;
//分支限界发求解01背包问题 基于的是贪心思想 在第1个物品放入 和没放入背包时 其实 都有一个理论上可能达到的最大价值
//我们选理论价值最大的那个可能性 如果该物品放入背包 可能的价值大 我们就走这条路 如果这个不放入 价值大 我们就走另外一条路
//总容量
int totalvolume;
//物品数量
int amount;
//递增系数
int id_add = 0;
//定义一个结构
struct element
{
//编号
int Id;
//重量
int Weight;
// 价值
int Worth;
element()
{
Id = id_add++;
Weight = 0;
Worth = 0;
}
};
//保存某个方案
struct PLAN
{
//每个方案都会保存一个数组 (每个物品 是否放入)
bool isIn[11] = { 0 };
// 已经存入的物品的价值
double alreadyWorth;
//可能最大利益
double mostWorth;
//剩余容量
int leftVolume;
//第Id个物品是否放入 这表示前Id-1个物品 是否放入都已经确定了
int Id;
PLAN()
{
}
//初始化 所有物品都没放入的初始状态
void Init(element* Array)
{
alreadyWorth = 0;
mostWorth = 0;
leftVolume = totalvolume;
Id = 0;
calculate(Array);
};
//后序所有的初始化都采用这个 都是在前面基础上 判断第n个物品是否放入 放入是1 不放入是0
void Init(PLAN& a, int is_in, element* Array)
{
std::copy(a.isIn, a.isIn + amount, isIn);
//不放入
Id = a.Id;
leftVolume = a.leftVolume;
alreadyWorth = a.alreadyWorth;
//放入
if (is_in == 1 && leftVolume - Array[Id].Weight >= 0)
{
leftVolume -= Array[Id].Weight;
alreadyWorth += Array[Id].Worth;
isIn[Id] = 1;
}
Id++;
calculate(Array);
};
//计算某个方案的 mostWorth
void calculate(element* Array)
{
int id_used = Id;
int leftVolume1 = leftVolume;
mostWorth = alreadyWorth;
while (id_used <= amount - 1 && leftVolume1 - Array[id_used].Weight >= 0)
{
leftVolume1 -= Array[id_used].Weight;
mostWorth += Array[id_used].Worth;
id_used++;
}
if (leftVolume1 > 0 && id_used <= amount - 1)
{
mostWorth += leftVolume1 * ((double)Array[id_used].Worth / (double)Array[id_used].Weight);
}
}
};
bool operator<(const PLAN& a, const PLAN& b)
{
return a.mostWorth < b.mostWorth;
}
bool operator>(const PLAN& a, const PLAN& b)
{
return a.mostWorth > b.mostWorth;
}
bool operator>=(const PLAN& a, const PLAN& b)
{
return a.mostWorth >= b.mostWorth;
}
ostream& operator<<(ostream& os, const PLAN& a)
{
for (int i = 0; i <= 10; i++)
{
if (a.isIn[i] == 1)
{
os << i + 1 << " ";
}
}
os << "最优方案价值为" << a.alreadyWorth << endl;
return os;
}
bool cmp_element(element& a, element& b)
{
return ((double)a.Worth / a.Weight) > ((double)b.Worth / b.Weight);
}
int main()
{
srand((unsigned)time(NULL));
//大根堆
priority_queue<PLAN>mHeap;
cout << "请输入总的容量" << endl;
cin >> totalvolume;
cout << "输入物品数量" << endl;
cin >> amount;
element* Item = new element[amount];
for (int i = 0; i < amount; i++)
{
cout << "输入第" << i + 1 << "组物品的重量和价值。以空格隔开" << endl;
cin >> Item[i].Weight >> Item[i].Worth;
}
//按性价比排序
std::sort(Item, Item + amount, cmp_element);
cout << " 排序之后:" << endl;
for (int i = 0; i < amount; i++)
{
cout << i + 1 << " " << Item[i].Weight << " " << Item[i].Worth << endl;
}
//临时方案
PLAN tempPlan;
tempPlan.Init(Item);
//临时方案
PLAN tempPlan1;
while (tempPlan.Id != amount)
{
//第n个物品放入
tempPlan1.Init(tempPlan, 0, Item);
mHeap.push(tempPlan1);
//第n个物品不放入
tempPlan1.Init(tempPlan, 1, Item);
mHeap.push(tempPlan1);
tempPlan = mHeap.top();
mHeap.pop();
}
//循环结束的方案 即为最优方案 因为到达叶子节点
cout << tempPlan;
system("pause");
delete[]Item;
return 0;
}