[分享]简单的背包问题,非递归解法(详细注释解释)

BaihowFF 2008-11-09 10:25:26
虽然这个问题是老问题了...但是最近看到好多人问...结贴了一个又来一个...我特意详细写了注释...用效率最低的办法...最好懂的非递归写法写出来了一个...希望对大家有用...呵呵...大牛不要笑哈...献丑了....

如果排版乱的话,可以到我Blog上看...有一篇一样的....
简单的背包问题,非递归解法(详细注释解释)

// ---------------------------------------------------
// 问题描述: 一个背包可以装M个重量,有N个物品,重量为
// W1,W2, ..... ,Wn 如何正好装满背包?
// 注1: 必须用非递归解决
// 注2: 一般要求一个解,我的程序是得到所有解
// 注3: 由于32位unsigned int限制,最多32个物品
//
// 这就是最简单的背包问题,没有加入价值因素
// 真正的0-1背包问题是有价值的....
// 好多人竟然不会做这题...我就好好写个注释详细的吧..
//
// 递归写法我相信很多人都会...但是非递归就傻眼了
// 其实也很简单...无非是模拟递归...用标记数组而已
// 认真看看那个模拟递归的for循环...
// 这种for循环应该算是状态转移了..有点像动态规划
//
// 另外,程序根本没有优化...这样是最好懂的...
// 优化的方法也很多...比如先排序...
// 然后用类似剪枝的方法修改状态转移的for循环
// 或者根本就用3层循环描述状态转移方程...那都是后话了
// 如果你真的很想了解背包问题
// 请Google一下 《背包问题九讲》
//
//
// PS:竟然打了这么多注释...汗一下...
//
// BaihowFF
// 08.11.9
// ---------------------------------------------------

#include <iostream>
using namespace std;
// 如果你使用的是万恶的VC6,可以加上下面这句,省的出错
#define for if(1) for

// 物品总数
const int N_ITEM=5;

// 背包能装的重量
const int BAG=15;

// 初始化每个物品的重量
int item[N_ITEM]={2,3,5,7,8};

// 标记数组
int flag[N_ITEM]={0,0,0,0,0};

// 结果计数器
int resultCount(0);

// 打印结果
void Print();

int main()
{
// 一开始打印一下已知条件
cout<<"BAG Weight:"<<BAG<<endl;
cout<<"Item Number:"<<N_ITEM<<endl;
for (int i=0;i!=N_ITEM;i++)
{
cout<<"Item."<<i+1<<" W="<<item[i]<<"\t";
}
cout<<endl;


// 所有可能性次数,即2的n次方
// 注意最大不能超过4G,即不能有32个以上物品
// 实际上有20个电脑就得算一下了...那有1M的可能了
// 一般写算法不能超过1M的时间复杂度...否则瞬间无结果
// 当然..我说的是台式电脑...如果你拿曙光5000就是另外的事情了
unsigned int count(0);
unsigned int all_count(1);
for (int i=0;i!=N_ITEM;i++)
{
all_count*=2;
}

while (1)
{
// 模拟递归...列举所有flag数组可能
// 其实就这个for循环是关键
for (int i=0;i!=N_ITEM;i++)
{
if ( 0 == flag[i] )
{
flag[i]=1;
continue;
}
// 下面换成if ( 1 == flag[i] )可能更好理解
else
{
flag[i]=0;
break;
}
}

// 如果你不理解上面的循环
// 可以打开下面注释掉的循环
// 把#if(0)改成#if(1)就可以了
// 看看flag数组变化
// 在纸上画一下...
// 应该很容易理解哦~~~
// ^_^

#if(0)
for (int i=0;i!=N_ITEM;i++)
{
cout<<flag[i];
}
cout<<endl;
#endif

// 判断时候装满背包
// 多加一个括号...使得临时变量每次重新创建而已
// 我认为...这样temp比较容易懂...呵呵...
{
// 本次重量,初始化0
int temp(0);

// 按标记计算所有选中物品重量和
for (int i=0;i!=N_ITEM;i++)
{
if ( 1 == flag[i] )
{
temp+=item[i];
}
}

// 满足背包重量就打印
if ( temp == BAG )
{
resultCount++;
Print();
}
}


// 如果遍历了所有情况就break掉while(1)循环
count++;
if (count==all_count)
{
break;
}
}


return 0;
}

// 额..竟然打了这么多注释..这个函数我就不解释了...
void Print()
{
cout<<"Result "<<resultCount<<endl;
for (int i=0;i!=N_ITEM;i++)
{
if ( 1 == flag[i] )
{
cout<<"Item."<<i+1<<" Weight:"<<item[i]<<"\t";
}
}
cout<<endl;
}
...全文
964 1 打赏 收藏 转发到动态 举报
写回复
用AI写文章
1 条回复
切换为时间正序
请发表友善的回复…
发表回复
yyfhz 2010-10-28
  • 打赏
  • 举报
回复
本来打算在CSDN上找一下有没有更好的算法,结果看到了LZ的大作... ^-^

8过针对正数序列(只是正数序列哦,如果序列有元素<0就不行了),这个算法还有可以改进的地方,正好我为别的事情也写了一个代码,也是用类似的循环来处理的。这个循环,其实就是将数组从低位往高位依次遍历所形成的。
现在来看以下两种情况:

Case1:
重量数组内容为 [1,2,3,4,5]
循环数组内容为 [1,1,1,1,0]
背包容纳度为9
现在把这个情况扔到循环体里面去,则
下一个循环数组变成[0,0,0,0,1]
显然这个时候的结果是背包没有吃饱。
那么照此循环执行下去,要什么时候背包才可能吃饱呢?
至少要在[1,1,0,0,1]这个状态之后才行。
也就是说,当中的状态[1,0,0,0,1]和[0,1,0,0,1]其实完全可以跳过不做判断的。
这个算法,摆开来可以是:

先将状态从[1,1,1,1,0]转换为[1,1,1,1,1](这是个临时转换),然后计算这个临时状态下的总重量(算出来是15)。
如果这个时候背包还没有吃饱或刚刚吃饱,那么就表示从状态[1,1,1,1,0]到[1,1,1,1,1]之间的所有状态背包都不可能吃饱,因此直接用[1,1,1,1,1]作为下一个状态就可以了。
如果这个时候背包已经撑爆了,那么就从当前的转换位的前一位开始,从后向前依次将状态位清0,也就是
[1,1,1,1,1] =>[1,1,1,0,1]=>[1,1,0,0,1]=>[1,0,0,0,1]=>[0,0,0,0,1]
一直减到前面所有的状态位都为0或者背包没有被撑破为止。

当然喽,如果重量数组之间的分量拉得比较开,这个算法并不能节省多少时间,因为每一次高状态位进一后低状态位几乎要全部清0才可以,但是即便如此至少也不会花更多的时间在上面。


Case2:
假设
重量数组内容为 [1,2,3,4,5,6,7,8,9,10]
循环数组内容为 [0,0,0,0,1,0,1,0,0,0 ]
背包容纳度为9
在此状态下,背包已经被撑破了(5+7>9),那么自然的,在向上面增加任何其它东西肯定也会把背包撑破。
特别的,在状态位第1个“1”[X,X,X,X,1,...]之前的所有状态位,它们中的任意组合必然导致背包撑破。
因此,从状态 [0,0,0,0,1,0,1,0,0,0 ] 到状态[1,1,1,1,1,0,1,0,0,0]这16个状态均不可能是解状态。
因此,直接将它在[1,1,1,1,1,0,1,0,0,0]的基础上去算下一个状态就可以了
即,下一个状态可以跳到[0,0,0,0,0,1,1,0,0,0]。
另外一个不太重要的改进是,对于这个从
[0,0,0,0,1,0,1,0,0,0]通过"最低非0位进位"而得到的
[0,0,0,0,0,1,1,0,0,0]如果背包仍然被撑破,则可以直接进行下一轮的"最低非0位进位"
得到
[0,0,0,0,0,0,0,1,0,0],这里可以省下下一次找“最低非0位”的时间。

这里节约的时间就大了,应该是呈指数的缩减时间才是。
===============================================================
另外,还可以事先将分量数组从小到大排列(这个排列的时间对于背包问题来讲应该可以忽略),合并相同重量的元素并另开一个数组来表示每种重量的物体个数,对于有大量相同重量的序列来说,也可以降低运算量。

我用Delphi写的那段程序采用了这种改进后的方法,测试结果如下:

测试结果1:背包容量为100,重量数组为1到50的连续不重复自然数,计算出来所有的复合条件的排列总数量>41w,计算花费时间0.3秒

测试结果2:背包容量为100,重量数组为[1到50的连续自然数]+[1到40的连续自然数]+[1到30的连续自然数]+[1到20的连续自然数]+[1到100的连续自然数],计算出来所有的复合条件的排列总数量>5700w,计算花费时间35秒


68亿


33,007

社区成员

发帖
与我相关
我的任务
社区描述
数据结构与算法相关内容讨论专区
社区管理员
  • 数据结构与算法社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧