单调队列多重背包时间复杂度O(vn)

2023-12-15 15:58

本文主要是介绍单调队列多重背包时间复杂度O(vn),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

版权声明:本文为博主原创文章,未经博主允许不得转载。

 

多重背包问题:

有N种物品和容量为V的背包,若第i种物品,容量为v[i],价值为w[i],共有n[i]件。怎样装才能使背包内的物品总价值最大?

 

网上关于“多重背包”的资料倒是不少,但是关于怎么实现O(N*V)算法的资料,真得好少呀,关于“单调队列”那部分算法,又没说明得很清楚,看了几遍没看懂原理,只好自己动脑去想怎么实现O(N*V)算法。

 

若用F[i][j]表示对容量为j的背包,处理完前i种物品后,背包内物品可达到的最大总价值,并记m[i] = min(n[i], j / v[i])。放入背包的第i种物品的数目可以是:0、1、2……,可得:

F[i][j] = max { F[i - 1] [j – k * v[i] ] + k * w[i] }  (0 <= k <= m[i])       

   

如何在O(1)时间内求出F[i][j]呢?

先看一个例子:取m[i] = 2, v[i] = v, w[i] = w, V > 9 * v,

并假设 f(j) = F[i - 1][j],观察公式右边要求最大值的几项:

j = 6*v:   f(6*v)、f(5*v)+w、f(4*v)+2*w 这三个中的最大值

j = 5*v:   f(5*v)、f(4*v)+w、f(3*v)+2*w 这三个中的最大值

j = 4*v:   f(4*v)、f(3*v)+w、f(2*v)+2*w 这三个中的最大值

显然,公式㈠右边求最大值的几项随j值改变而改变,但如果将j = 6*v时,每项减去6*w,j=5*v时,每项减去5*w,j=4*v时,每项减去4*w,就得到:

j = 6*v:   f(6*v)-6*w、f(5*v)-5*w、f(4*v)-4*w 这三个中的最大值

j = 5*v:   f(5*v)-5*w、f(4*v)-4*w、f(3*v)-3*w 这三个中的最大值

j = 4*v:   f(4*v)-4*w、f(3*v)-3*w、f(2*v)-2*w 这三个中的最大值

很明显,要求最大值的那些项,有很多重复。

 

根据这个思路,可以对原来的公式进行如下调整:

假设d = v[i],a = j / d,b = j % d,即 j = a * d + b,代入公式㈠,并用k替换a - k得:

F[i][j] = max { F[i - 1] [b + k * d] - k * w[i] } + a * w[i]   (a – m[i] <= k <= a)   

 

对F[i - 1][y] (y= b  b+d  b+2d  b+3d  b+4d  b+5d  b+6d  …  j)

F[i][j]就是求j的前面m[i] + 1个数对应的F[i - 1] [b + k * d] - k * w[i]的最大值,加上a * w[i],如果将F[i][j]前面所有的F[i - 1][b + k * d] – k * w放入到一个队列,那么,F[i][j]就是求这个队列最大长度为m[i] + 1时,队列中元素的最大值,加上a * w[i]。因而原问题可以转化为:O(1)时间内求一个队列的最大值

 

该问题可以这样解决:

① 用另一个队列B记录指定队列的最大值(或者记录最大值的地址),并通过下面两个操作保证队列B的第一个元素(或其所指向的元素)一定是指定队列的当前最大值。

② 当指定队列有元素M进入时,删除队列B中的比M小的(或队列B中所指向的元素小等于M的)所有元素,并将元素M(或其地址)存入队列B。

③ 当指定队列有元素M离开时,队列B中的第一个元素若与M相等(或队列B第一个元素的地址与M相等),则队列B的第一个元素也离队。

 

经过上述处理,可以保证队列B中的第一个元素(或其指向的元素)一定是所指定队列所有元素的最大值。显然队列B的元素(或其所指向的元素)是单调递减的,这应该就是《背包九讲》中的提到的“单调队列”吧,初看的时候被这个概念弄得稀里糊涂,网上的资料提到“维护队列的最大值”,刚开始还以为是维护这个单调队列的最大值,对其采用的算法,越看越糊涂。其实,只要明白用一个“辅助队列”,求另一个队列的最值,那么具体的算法,和该“辅助队列”的性质(单调变化),都很容易推导出来。

 

在多重背包问题中,所有要进入队列的元素个数的上限值是已知的,可以直接用一个大数组模拟队列。

 

“多重背包”通用模板:

 

[cpp]  view plain copy
  1. //“多重背包”通用模板  
  2. const int MAX_V = 100004;  
  3. //v、n、w:当前所处理的这类物品的体积、个数、价值  
  4. //V:背包体积, MAX_V:背包的体积上限值  
  5. //f[i]:体积为i的背包装前几种物品,能达到的价值上限。  
  6. inline void pack(int f[], int V, int v, int n, int w)  
  7. {  
  8.   if (n == 0 || v == 0) return;  
  9.   if (n == 1) {               //01背包  
  10.     for (int i = V; i >= v; --i)  
  11.       if (f[i] < f[i - v] + w) f[i] = f[i - v] + w;  
  12.     return;  
  13.   }  
  14.   if (n * v >= V - v + 1) {   //完全背包(n >= V / v)  
  15.     for (int i = v; i <= V; ++i)  
  16.       if (f[i] < f[i - v] + w) f[i] = f[i - v] + w;  
  17.     return;      
  18.   }  
  19.   
  20.   int va[MAX_V], vb[MAX_V];   //va/vb: 主/辅助队列  
  21.   for (int j = 0; j < v; ++j) {     //多重背包  
  22.     int *pb = va, *pe = va - 1;     //pb/pe分别指向队列首/末元素  
  23.     int *qb = vb, *qe = vb - 1;     //qb/qe分别指向辅助队列首/末元素    
  24.     for (int k = j, i = 0; k <= V; k += v, ++i) {  
  25.       if (pe  == pb + n) {       //若队列大小达到指定值,第一个元素X出队。  
  26.         if (*pb == *qb) ++qb;   //若辅助队列第一个元素等于X,该元素也出队。   
  27.         ++pb;  
  28.       }  
  29.       int tt = f[k] - i * w;  
  30.       *++pe = tt;                  //元素X进队  
  31.       //删除辅助队列所有小于X的元素,qb到qe单调递减,也可以用二分法  
  32.       while (qe >= qb && *qe < tt) --qe;  
  33.       *++qe = tt;              //元素X也存放入辅助队列          
  34.       f[k] = *qb + i * w;      //辅助队列首元素恒为指定队列所有元素的最大值  
  35.     }  
  36.   }  
  37. }  

 

 

 

多重背包特例:物品价值和体积相等(w = v)

由于w = v,上面的代码可进行如下修改:

入队的元素: tt = f[k] - (k / v) * w = f[k] - (k - j) = f[k] - k + j

    返回的最大值:*qb + (k / v) * w =  *qb + k - j

由于j是定值,可调整入队的元素为: f[k] - k,最大值为 *qb + k

 

但这种做法相当低效。实际上,这相当于一个“覆盖”问题:在放入前i个物品后,体积为j的背包,只存在两种状态:是否能刚好装满,也就是,是否能被覆盖。因而只要记录下该状态就可以了,前面的分析进行相应的调整:

对F[i - 1][y] (y= b  b+d  b+2d  b+3d  b+4d  b+5d  b+6d  …  j)

F[i][j]就是求j的前面m[i] + 1个数对应的F[i - 1] [b + k * d](其值为0或1)的最大值,即j前面的m[i] + 1个0、1数据中是否存在1,这又可以简化为判断它们的和是否不等于0。

 

 

 

[cpp] view plain copy
  1. //pack-01  
  2. const int MAX_V = 100004;  
  3. //w = v 特例  
  4. inline void pack(bool f[], int V, int v, int n)  
  5. {  
  6.   if (n == 0 || v == 0) return;  
  7.   if (n == 1) {  //01背包  
  8.     for (int i = V; i - v >= 0; --i)  
  9.       if (! f[i] && f[i - v]) f[i] = true;  
  10.       //if (f[i - v]) f[i] = true;  
  11.     return;  
  12.   }  
  13.   if (n * v >= V - v + 1) {  //完全背包 n >= V / v  
  14.     for (int i = v; i <= V; ++i)  
  15.       if (! f[i] && f[i - v]) f[i] = true;  
  16.       //if (f[i - v]) f[i] = true;        
  17.     return;  
  18.   }  
  19.     
  20.    bool va[MAX_V];  
  21.     for (int j = 0; j < v; ++j) {     //多重背包  
  22.     bool *pb = va, *pe = va - 1;   
  23.     size_t sum = 0;  
  24.     for (int k = j; k <= V; k += v) {  
  25.       if (pe == pb + n) sum -= *pb++;  //队列已满,队首元素出队  
  26.       *++pe = f[k];  //进队  
  27.       sum += f[k];       
  28.       if (! f[k] && sum != 0) f[k] = true;   
  29.       //f[k] = (bool)sum;         
  30.     }  
  31.   }  
  32. }  

 

另外,可以倒着读数据,这样就不需要额外使用一个数组存放临时数据:

 

[cpp]  view plain copy
  1. //pack-02  
  2. //w = v 特例  
  3. inline void pack(bool f[], int V, int v, int n)  
  4. {  
  5.   if (n == 0 || v == 0) return;  
  6.   if (n == 1) {  //01背包  
  7.     for (int i = V; i - v >= 0; --i)  
  8.       if (! f[i] && f[i - v]) f[i] = true;  
  9.       //if (f[i - v]) f[i] = true;  
  10.     return;  
  11.   }  
  12.   if (n * v >= V - v + 1) {  //完全背包 n >= V / v  
  13.     for (int i = v; i <= V; ++i)  
  14.       if (! f[i] && f[i - v]) f[i] = true;  
  15.       //if (f[i - v]) f[i] = true;        
  16.     return;  
  17.   }  
  18.     
  19.   for (int j = 0; j < v; ++j) {    //多重背包  
  20.     int k = V - j, sum = 0;  
  21.     //前n + 1个元素入队,前面的判断可以保证: V - j - n * v > 0  
  22.     for (; k >= std::max(0, V - j - n * v); k -= v) sum += f[k];  
  23.     for (int i = V - j; i > 0; k -= v, i -= v) {  
  24.       if (f[i]) --sum;      //出队: sum -= f[i]   
  25.       else if (sum != 0) f[i] = true;  
  26.       //int tt = f[i]; f[i] = (bool)sum; sum -= tt;  
  27.       if (k >= 0) sum += f[k];        
  28.     }  
  29.   }   
  30. }  

 

 

前面的代码,都在循环中对队列的元素个数进行判断,这可以通过下面的方法避免,将循环拆分成两部分:一部分都有入队和出队操作、另一部分只有入队(或出队)操作。

 

对该特例,还有一种O(N * V)解法:用一个数组记录当前物品已经使用数,关键代码:

if (! f[i] && f[i - v] && count[i - v] < n)

f[i] = true, count[i] = count[i - v] + 1;

每计算一类物品,count数组都要初始化一次,比较费时,可以再用一个数组记录上一次处理的物品编号,通过判断上一次放入那一类的物品编号与当前这类物品编号是否一致(不一致时,相当于count[i]值为0的情况),从而避免对count数组的初始化操作。还可以将初始化count数组和后面的循环整合在一起。

 

 

[cpp]  view plain copy
  1. //pack-1    
  2. for (int i = 0; i <= V; ++i)  count[i] = 0;  
  3. for (int i = v; i <= V; ++i) {     //多重背包  
  4.   if (! f[i] && f[i - v] && count[i - v] < n) {  
  5.     count[i] = count[i - v] + 1;   
  6.     f[i] = true;  
  7.   }  
  8. }  

 

 

[cpp]  view plain copy
  1. //pack-2  cur为当前这类物品的编号  
  2. for (int i = v; i <= V; ++i) {     //多重背包  
  3.   if (! f[i] && f[i - v]) {  
  4.     if (last[i - v] != cur)  count[i] = 1, last[i] = cur, f[i] = true;  
  5.     else if (count[i - v] < n) {  
  6.       count[i] = count[i - v] + 1;   
  7.       last[i] = cur;  
  8.       f[i] = true;  
  9.     }  
  10.   }  
  11. }  

 

[cpp]  view plain copy
  1. //pack-4  
  2. for (int i = v; i <= V; ++i) {     //多重背包  
  3.   if (f[i]) count[i] = 0;  
  4.   else if (f[i - v]) {  
  5.     if (i < 2 * v)  count[i] = 1, f[i] = true;    
  6.     else if (count[i - v] < n) {  
  7.       count[i] = count[i - v] + 1;  
  8.       f[i] = true;  
  9.     }    
  10.   }  
  11. }  

 

 

 

POJ 1742:有若干不同面值的纸币,问能组合出1到m中的几种面值?

[cpp] view plain copy
  1. //poj1742   
  2. #include<algorithm>  
  3. #include<cstdio>  
  4. #define AB 0  
  5.   
  6. //MAX_N 物品种类数最大值 MAX_n每种物品数目的最大值,MAX_V背包体积最大值  
  7. const int MAX_N = 101, MAX_V = 100004;  
  8.   
  9. //w = v特例  
  10. inline void pack(bool f[], int V, int v, int n, int& total)  
  11. {  
  12.   //if (n == 0 || v == 0) return;  
  13.   if (n == 1) {  //01背包  
  14.     for (int i = V; i - v >= 0; --i)  
  15.       if (! f[i] && f[i - v]) f[i] = true, ++total;  
  16.     return;  
  17.   }  
  18.   if (n * v >= V - v + 1) {  //完全背包 n >= V / v  
  19.     for (int i = v; i <= V; ++i)  
  20.       if (! f[i] && f[i - v]) f[i] = true, ++total;  
  21.     return;  
  22.   }  
  23.   
  24.   for (int j = 0; j < v; ++j) {    //多重背包  
  25.     int k = V - j, sum = 0;  
  26.     //前n + 1个元素入队,前面的判断可以保证: V - j - n * v > 0  
  27.     for (; k >= V - j - n * v; k -= v) sum += f[k];  
  28.     for (int i = V - j; i > 0; k -= v, i -= v) {  
  29.       if (f[i]) --sum;      //出队: sum -= f[i]   
  30.       else if (sum != 0) f[i] = true, ++total;  
  31.       //int tt = f[i]; f[i] = (bool)sum; sum -= tt;  
  32.       if (k >= 0) sum += f[k];        
  33.     }  
  34.   }  
  35. }  
  36.   
  37. struct Node {  
  38.   int n, v, V;  
  39.   bool operator<(const Node& other) const { return V < other.V;}   
  40. };  
  41.   
  42. int main()  
  43. {  
  44. #if AB == 1  
  45.   freopen("src.txt","r",stdin);  
  46.   freopen("z-b.txt","w",stdout);  
  47. #endif    
  48.   Node node[MAX_N];  
  49.   int V, N;  
  50.   bool f[MAX_V];  
  51.   while (scanf("%d %d",&N,&V) != EOF) {  
  52.     if (N + V == 0) break;  
  53.     Node *np = node + N;  
  54.     for (Node *p = node; p < np; ++p) scanf("%d", &p->v);  
  55.     for (Node *p = node; p < np; ++p) {  
  56.       scanf("%d", &p->n);  
  57.       p->V = std::min(p->n * p->v, V);  
  58.     }  
  59.     std::sort(node, np);  
  60.     int total = 0;  
  61.     f[0] = true;  
  62.     for (int i = 1; i <= V; ++i) f[i] = false;  
  63.     int mv = 0;  
  64.     for (Node *p = node; p < np && total < V; ++p) {  
  65.       mv = std::min(mv + p->V, V);  
  66.       pack(f,mv,p->v,p->n, total);  
  67.     }  
  68.     printf("%d/n",total);  
  69.   }  
  70. }  

 

用自己随机生成的数据测试了下,上面提到的几种方法,所用时间都是7秒多点,有排序的比没排序的稍微快点。但在POJ上提交的结果,不同代码的耗时相差挺大,快的在1秒左右,慢的接近1.5秒。

 

 

还有一种特例:

给定面值为1、2、5的纸币若干个,问其所不能支付的最低价格(假设为自然数)。

这可以用多重背包(或母函数)来解决,但实际上是有O(1)解法的。

 

这篇关于单调队列多重背包时间复杂度O(vn)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/497013

相关文章

Python的time模块一些常用功能(各种与时间相关的函数)

《Python的time模块一些常用功能(各种与时间相关的函数)》Python的time模块提供了各种与时间相关的函数,包括获取当前时间、处理时间间隔、执行时间测量等,:本文主要介绍Python的... 目录1. 获取当前时间2. 时间格式化3. 延时执行4. 时间戳运算5. 计算代码执行时间6. 转换为指

Java中字符串转时间与时间转字符串的操作详解

《Java中字符串转时间与时间转字符串的操作详解》Java的java.time包提供了强大的日期和时间处理功能,通过DateTimeFormatter可以轻松地在日期时间对象和字符串之间进行转换,下面... 目录一、字符串转时间(一)使用预定义格式(二)自定义格式二、时间转字符串(一)使用预定义格式(二)自

Java的栈与队列实现代码解析

《Java的栈与队列实现代码解析》栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶,栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等,本文给大家介绍J... 目录栈的概念(Stack)栈的实现代码队列(Queue)模拟实现队列(双链表实现)循环队列(循环数组

Redis消息队列实现异步秒杀功能

《Redis消息队列实现异步秒杀功能》在高并发场景下,为了提高秒杀业务的性能,可将部分工作交给Redis处理,并通过异步方式执行,Redis提供了多种数据结构来实现消息队列,总结三种,本文详细介绍Re... 目录1 Redis消息队列1.1 List 结构1.2 Pub/Sub 模式1.3 Stream 结

SpringKafka错误处理(重试机制与死信队列)

《SpringKafka错误处理(重试机制与死信队列)》SpringKafka提供了全面的错误处理机制,通过灵活的重试策略和死信队列处理,下面就来介绍一下,具有一定的参考价值,感兴趣的可以了解一下... 目录引言一、Spring Kafka错误处理基础二、配置重试机制三、死信队列实现四、特定异常的处理策略五

Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码

《Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码》:本文主要介绍Java中日期时间转换的多种方法,包括将Date转换为LocalD... 目录一、Date转LocalDateTime二、Date转LocalDate三、LocalDateTim

golang获取当前时间、时间戳和时间字符串及它们之间的相互转换方法

《golang获取当前时间、时间戳和时间字符串及它们之间的相互转换方法》:本文主要介绍golang获取当前时间、时间戳和时间字符串及它们之间的相互转换,本文通过实例代码给大家介绍的非常详细,感兴趣... 目录1、获取当前时间2、获取当前时间戳3、获取当前时间的字符串格式4、它们之间的相互转化上篇文章给大家介

Feign Client超时时间设置不生效的解决方法

《FeignClient超时时间设置不生效的解决方法》这篇文章主要为大家详细介绍了FeignClient超时时间设置不生效的原因与解决方法,具有一定的的参考价值,希望对大家有一定的帮助... 在使用Feign Client时,可以通过两种方式来设置超时时间:1.针对整个Feign Client设置超时时间

springboot+dubbo实现时间轮算法

《springboot+dubbo实现时间轮算法》时间轮是一种高效利用线程资源进行批量化调度的算法,本文主要介绍了springboot+dubbo实现时间轮算法,文中通过示例代码介绍的非常详细,对大家... 目录前言一、参数说明二、具体实现1、HashedwheelTimer2、createWheel3、n

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义