ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

开通博客园

2022-01-23 20:35:34  阅读:218  来源: 互联网

标签:背包 int max 博客园 物品 开通 优化 DP


注意
1.0 学习

#include <iostream>
int main()
{
  return 0;
}

测试

  • 1
    1. 和的

动态规划

包括线性DP(数字三角形模型、最长上升子序列模型、编辑距离)、背包模型(01背包、完全背包、多重背包、分组背包、混合背包...)、状态机、状态压缩DP、区间DP(石子合并..)、树形DP、数位DP、单调队列优化DP、斜率优化DP等内容

动态规划

传统dp:
关键词:状态表示、状态转移、最优子结构、重叠子问题、后效性
DP问题一般是求若干有限集合中的最值问题,并且此类问题存在子问题重叠(Overlap-subproblems),如果通过暴力枚举每种可能解决问题,那么将会有大量重复的计算;如果从集合角度来分析DP问题,每次枚举一个集合并且利用子问题重叠特性,将子问题的解存储下来,可以大大提高效率;
DP问题的核心是状态集合 f(i) 的定义,在状态及转移计算中,划分子集的依据:寻找最后一个不同点,即处理最后一个子集的选择。

闫氏DP分析法
从集合角度考虑DP问题,又称闫氏分析法,包含以下两步:

  • 状态表示——集合以及属性(Max/Min/数量/存在性);
  • 状态计算——集合的划分。一个重要的划分依据:“最后一步”;集合划分原则:可重但不漏。

数字三角形模型

此类问题中:

  • 状态表示
    • 集合:从(1,1)移动到(i,j)的所有路线
    • 属性:最大/大小
  • 状态计算
    • 状态计算:集合划分->最后一步移动方向

  • 摘花生
    • 求最大值,初始化为0即可。
  • 最低通行费
    • 求最小值,初始化为0x3f3f3f3f
    • 注意对于首行首列的处理
  • 方格取数
    • 走两次
    • 与前两题相比,本题“共走两次”,且每一个数最多只能选一次。可根据前述速录,分两次分开来求,再判断两次是否到达相同格子。减去重复格子即可。

    • 维度压缩
      • 四维表示:f[i1,j1,i2,j2]:表示从(1,1)分别走到(i1,j1),(i2,j2)的路径上能取到的数的最大值。
        根据“最后一步”可划分为四种情况。每次均可向右向下组合。
      • 三维表示:关键在于 如果在(i,j)重合,必定有两者所走步数相等,此时i1+j1==i2+j2==k(是重合的必要条件); 设两次路径一起走,所走步数为k,则有第一次走到了(i1,k-i1),第二次走到(i2,k-i2);
        此时设f[k,i1,i2]为从(1,1)分别走到(i1,k-i1),(i2,k-i1)代路径上能取到的最大值。
        此时最后一步也是四种情况即,下下f[k-1,i1-1,i2-1],下右f[k-1,i1-1,i2],右下f[k-1,i1,i2-1],右右f[k-1,i1,i2];
    • 拓展:k取方格数。方法是费用流中的最大流。动态规划题变成了图论?其实动态规划包含于图论,90%DP都能转化为最短路问题。

最长上升子序列模型

(LIS)Longest Increasing Subsequence
此类问题中:

  • 状态表示
    • 集合:所有以a[i]为结尾的严格单调上升子序列
    • 属性:最大
  • 状态计算
    • 状态计算:集合划分->最后一个不同的点

  • 最长上升子序列LIS

    • 动态规划:O(n2)
      • 状态表示:f[i]表示从第一个数字开始算,以w[i]结尾的最大的上升序列。(以w[i]结尾的所有上升序列中属性为最大值的那一个)
      • 状态计算(集合划分):j∈(0,1,2,..,i-1), 在w[i] > w[j]时,f[i] = max(f[i], f[j] + 1)。
      • 有一个边界,若前面没有比i小的,f[i]为1(自己为结尾)。最后在找f[i]的最大值。
      cin >> n;
      for (int i = 0; i < n; i++) cin >> w[i];
      
      int mx = 1;    // 找出所计算的f[i]之中的最大值,边算边找
      for (int i = 0; i < n; i++) {
          f[i] = 1;    // 设f[i]默认为1,找不到前面数字小于自己的时候就为1
          for (int j = 0; j < i; j++) {
              if (w[i] > w[j]) f[i] = max(f[i], f[j] + 1);    // 前一个小于自己的数结尾的最大上升子序列加上自己,即+1
          }
          mx = max(mx, f[i]);
      }
      
    • (动态规划 + 二分) O(nlogn)
      • 状态表示:f[i]表示长度为i的最长上升子序列,末尾最小的数字。(长度为i的最长上升子序列所有结尾中,结尾最小min的) 即长度为i的子序列末尾最小元素是什么。
      • 状态计算:对于每一个w[i], 如果大于f[cnt-1] (下标从0开始,cnt长度的最长上升子序列,末尾最小的数字),那就cnt+1,使得最长上升序列长度+1,当前末尾最小元素为w[i]。 若w[i]小于等于f[cnt-1],说明不会更新当前的长度,但之前末尾的最小元素要发生变化,找到第一个 大于或等于 (这里不能是大于) w[i],更新以那时候末尾的最小元素。
      • f[i]一定以一个单调递增的数组,所以可以用二分法来找第一个大于或等于w[i]的数字。
      cin >> n;
      for (int i = 0 ; i < n; i++) cin >> w[i];
      
      f[cnt++] = w[0];        // cnt 比已有的大1!!!
      for (int i = 1; i < n; i++) {
          if (w[i] > f[cnt-1]) f[cnt++] = w[i];
          else {
              int l = 0, r = cnt - 1;
              while (l < r) {
                  int mid = (l + r) >> 1;
                  if (f[mid] >= w[i]) r = mid;
                  else l = mid + 1;
              }
              f[r] = w[i];    // 用w[i]替换第一个大于或等于w[i]的数字
          }
      }
      // 
      
      stl版本
          cin >> n;
          for (int i = 0 ; i < n; i++) cin >> w[i];
          vector<int> f;
          f.push_back(w[0]);
          for (int i = 1; i < n; i++) {
              if(w[i] > f.back()) f.push_back(w[i]);
              else 
                  *lower_bound(f.begin(), f.end(), w[i]) = w[i];
          }
          cout << f.size() << endl;
      
  • 怪盗基德的滑翔翼

    • 双向LIS 取最大
  • 登山

    • 双向LIS 取和 -1即可
  • 友好城市

    • 排序后LIS妙啊
  • 最大上升子序列和

  • 最长公共上升子序列LCS

  • 拦截导弹(贪心)

    • 1.贪心
      • 如何证明两个数相等?A>=B A<=B
        A表示贪心算法得到的序列个数,B;表示最优解;
        B<=A, ok A<=B, 使用调整法
    • 2.贪心流程
      • 从前往后扫描每个数,对于每个数:
        情况1:如果现有的子序列的结尾都小于当前数,则创建新子序列。
        情况2:将当前数放到结尾大于等于它的最小的子序列后面。
    • 3.???反链实现???Dilworth原理
  • 导弹防御系统(dfs)

背包模型

Article:dd大牛的背包九讲-背包问题汇总

背包模型一般求解

  1. 最大值、最小值
  2. 方案数
  3. 具体方案: 最短路问题,即记录转移过程,不能再用状态压缩了?而且如果输出要求字典序最小的方案,则需要对i的遍历顺序变化!!??

初始化汇总
背包问题中 体积至多是 j ,恰好是 j ,至少是 j 的初始化问题的研究

学背包问题的过程
1、一开始学背包问题时遇到的大多数的状态表示是:从前i个物品中选,且总体积不超过j的问题。
2、慢慢地在提高课中,就有出现状态表示是:从前i个物品中选,且总体积恰好是j的问题。例如 AcWing 1023. 买书 ,求的是恰好是j的总方案数问题。
3、同时还出现了状态表示是:从前i个物品中选,且总体积至少是j的问题。例如 AcWing 1020. 潜水员 ,求的是总体积至少是j的最小价值
可以观察到,他们的分析方法以及状态转移方程都是一样的,唯独是初始化有很大的不同

  • 方案数初始化总结
    • 二维情况
      1、体积至多j,f[0][i] = 1, 0 <= i <= m,其余是0
      2、体积恰好j,f[0][0] = 1, 其余是0
      3、体积至少j,f[0][0] = 1,其余是0

    • 一维情况
      1、体积至多j,f[i] = 1, 0 <= i <= m,
      2、体积恰好j,f[0] = 1, 其余是0
      3、体积至少j,f[0] = 1,其余是0

最大值最小值初始化总结
- 二维情况
1、体积至多j,f[i,k] = 0,0 <= i <= n, 0 <= k <= m(只会求价值的最大值)
2、体积恰好j,
当求价值的最小值:f[0][0] = 0, 其余是INF
当求价值的最大值:f[0][0] = 0, 其余是-INF
3、体积至少j,f[0][0] = 0,其余是INF(只会求价值的最小值)

- 一维情况
1、体积至多j,f[i] = 0, 0 <= i <= m(只会求价值的最大值)
2、体积恰好j,
当求价值的最小值:f[0] = 0, 其余是INF
当求价值的最大值:f[0] = 0, 其余是-INF
3、体积至少j,f[0] = 0,其余是INF(只会求价值的最小值)
  • 01背包问题(滚动数组)
    每个物品最多只能放一次。
  • 完全背包问题(优化成二维,以及滚动数组)
    每种物品可以放无限多次。(空间优化为1维后,只有完全背包是正序遍历)
  • 多重背包问题(倍增优化NVlogS成01背包、单调队列优化)
    每种物品有一个固定的次数上限。
  • 混合三种背包问题
    将前面三种简单的问题叠加成较复杂的问题。
  • 二维费用的背包问题
    一个简单的常见扩展。
  • 分组的背包问题
    每组里面只能选一种。后两节的基础。
  • 有依赖的背包问题
    另一种给物品的选取加上限制的方法。
  • 泛化物品
    我自己关于背包问题的思考成果,有一点抽象。
  • 背包问题问法的变化
    试图触类旁通、举一反三。
for 物品 i  
    for 体积 j  
        for 决策 k或s   

1.01背包问题

有N件物品和一个容量为V的背包。第i件物品的费用是v[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
f[i][j]=max{f[i-1][j],f[i-1][j-v[i]]+w[i]}

“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为f[i-1][j];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为j-v[i]的背包中”,此时能获得的最大价值就是f[i-1][j-v[i]]再加上通过放入第i件物品获得的价值w[i]。

优化
以上时间和空间复杂度均为O(N*V)
可把空间优化为O(V)

for i=1..N
    for j=V..0     // !!!
        f[j]=max{f[j],f[j-v[i]]+w[i]};

说明:
其中的f[v]=max{f[v],f[v-c[i]]}一句恰就相当于我们的转移方程f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]},因为现在的f[v-c[i]]就相当于原来的f[i-1][v-c[i]]。如果将v的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v]由f[i][v-c[i]]推知,与本题意不符,但它却是另一个重要的背包问题P02最简捷的解决方案,故学习只用一维数组解01背包问题是十分必要的。

关于初始化

  • “恰好装满背包”时的最优解
    • 要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞
  • 小于容量的最优解
    • 初始化时应该将f[0..V]全部设为0

2. 完全背包问题

有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

通过


其中s为体积限制下该物品的最大数量。
即优化过程为:

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w ,  f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v]= max(            f[i-1,j-v]   ,  f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)
由上两式,可得出如下递推关系: 
    f[i][j]=max(f[i,j-v]+w , f[i-1][j]) 

f[i][j]=max{f[i-1][j-k*v[i]]+k*w[i] | 0<=k*c[i]<=v}

for(int i = 1 ; i<=n ;i++)
    for(int j = 0 ; j<=m ;j++)
    {
        for(int k = 0 ; k*v[i]<=j ; k++)
            f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
    }
cout<<f[n][m]<<endl;

可优化为
f[i][j]=max{f[i-1][j], f[i-1][j-v[i]]+w[i]}
具体代码为:

for(int i = 1 ; i <=n ;i++)
    for(int j = 0 ; j <=m ;j++)
    {
        f[i][j] = f[i-1][j];
        if(j-v[i]>=0)
        // 注意第二部分 :  f[i][j-v[i]]
            f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);   
    }
cout<<f[n][m]<<endl;

滚动数组优化后代码为
f[j] = max(f[j], f[j-v[i]] + w[i]);
具体代码为:

 for(int i = 1 ; i<=n ;i++)
    for(int j = v[i] ; j<=m ;j++)//注意了,这里的j是从小到大枚举,和01背包不一样
    {
            f[j] = max(f[j],f[j-v[i]]+w[i]);
    }

对比01背包:
f[i][j]=max{f[i-1][j], f[i-1][j-v[i]]+w[i]}
具体代码为:

for(int i = 1 ; i <= n ; i++)
    for(int j = 0 ; j <= m ; j ++)
    {
        f[i][j] = f[i-1][j];
        if(j-v[i]>=0)
        // 注意第二部分 :  f[i-1][j-v[i]]
            f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);  
    }

优化后为

综上:
滚动优化前 两者相差一点:
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包
f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题
滚动优化后 两者代码相同:
f[j] = max(f[j],f[j-v[i]]+w[i]);//01背包 倒序啊!

f[j] = max(f[j],f[j-v[i]]+w[i]);//完全背包问题

分组倍增优化
完全背包也能进行分组倍增优化,每种物品的最大数量为j/v[i],但优化后复杂度不如单调队列优化,所有暂不用该优化。此优化可用于多重背包问题中。见下文。

3. 多重背包问题

方法:

  1. 暴力 转化成01背包
  2. 分组倍增优化 转化成01背包
  3. 最大长度的队列优化,即滑动窗口最大值,模型为单调队列(难)

分组倍增优化(二进制优化)
对于每种物品,均执行:
对于i种物品,最多用s个。此时把s拆分成logs组,1,2,4,...2^logs,每组选或不选,最终能凑成选择0~s个的任意值。将原始物品删除,用新的物品代替其体积和价值。转化为01背包。

将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为1,2,4,...,2(k-1),n[i]-2k+1,且k是满足n[i]-2^k+1>0的最大整数。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。
分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。另外这种方法也能保证对于0..n[i]间的每一个整数,均可以用若干个系数的和表示,这个证明可以分0..2k-1和2k..n[i]两段来分别讨论得出.
这样就将第i种物品分成了O(log n[i])种物品,将原问题转化为了复杂度为O(V*Σlog n[i])的01背包问题.

未优化代码:

for (int i = 1; i <= n; i ++ )
    for (int j = 0; j <= m; j ++ )
        for (int k = 0; k <= s[i] && k * v[i] <= j; k ++ )
            f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);

优化过程:

在完全背包中,通过两个状态转移方程:

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2v]+2w , f[i-1,j-3v]+3w, .....)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2v] + w, f[i-1,j-2v]+2w , .....)

通过上述比较,可以得到 f[ i ][ j ] = max(f[ i - 1 ][ j ],f[ i ][ j - v ] + w)。

再来看下多重背包,

f[i , j ] = max( f[i-1,j] ,f[i-1,j-v]+w ,f[i-1,j-2v]+2w ,..... f[i-1,j-Sv]+Sw, )
f[i , j-v]= max( f[i-1,j-v] ,f[i-1,j-2v]+w, ..... f[i-1,j-Sv]+(S-1)w, f[i-1,j-(S+1)v]+Sw )

二进制优化,它为什么正确,为什么合理,凭什么可以这样分??

优化后代码:

    int cnt = 0; //分组的组别
    for(int i = 1;i <= n;i ++)
    {
        int a,b,s;
        cin >> a >> b >> s;
        int k = 1; // 组别里面的个数
        while(k<=s)
        {
            cnt ++ ; //组别先增加
            v[cnt] = a * k ; //整体体积
            w[cnt] = b * k; // 整体价值
            s -= k; // s要减小
            k *= 2; // 组别里的个数增加
        }
        //剩余的一组
        if(s>0)
        {
            cnt ++ ;
            v[cnt] = a*s; 
            w[cnt] = b*s;
        }
    }

    n = cnt ; //枚举次数正式由个数变成组别数

    //01背包一维优化
    for(int i = 1;i <= n ;i ++)
        for(int j = m ;j >= v[i];j --)
            f[j] = max(f[j],f[j-v[i]] + w[i]);

解析:

  • 二进制优化,它为什么正确,为什么合理,凭什么可以这样分??
    二进制优化,它为什么正确,为什么合理,凭什么可以这样分??
    我们首先确认三点:

(1)我们知道转化成01背包的基本思路就是:判断每件物品我是取了你好呢还是不取你好。

(2)我们知道任意一个实数可以由二进制数来表示,也就是20~2k其中一项或几项的和。

(3)这里多重背包问的就是每件物品取多少件可以获得最大价值。

分析:

如果直接遍历转化为01背包问题,是每次都拿一个来问,取了好还是不取好。那么根据数据范围,这样的时间复杂度是O(n^3),也就是1e+9,这样是毫无疑问是会TLE的。

假如10个取7个好,那么在实际的遍历过程中在第7个以后经过状态转移方程其实已经是选择“不取”好了。现在,用二进制思想将其分堆,分成k+1个分别有2k个的堆,然后拿这一堆一堆去问,我是取了好呢,还是不取好呢,经过dp选择之后,结果和拿一个一个来问的结果是完全一样的,因为dp选择的是最优结果,而根据第二点任意一个实数都可以用二进制来表示,如果最终选出来10个取7个是最优的在分堆的选择过程中分成了20=1,21=2,22=4,10 - 7 = 3 这四堆,然后去问四次,也就是拿去走dp状态转移方程,走的结果是第一堆1个,取了比不取好,第二堆2个,取了比不取好,第三堆四个,取了比不取好,第四堆8个,取了还不如不取,最后依旧是取了1+2+4=7个。

Tip:参考博客

如果仍然不是很能理解的话,取这样一个例子:要求在一堆苹果选出n个苹果。我们传统的思维是一个一个地去选,选够n个苹果就停止。这样选择的次数就是n次

二进制优化思维就是:现在给出一堆苹果和10个箱子,选出n个苹果。将这一堆苹果分别按照1,2,4,8,16,.....512分到10个箱子里,那么由于任何一个数字x ∈[1,1024]
都可以从这10个箱子里的苹果数量表示出来,但是这样选择的次数就是 ≤10次 。

这样利用二进制优化,时间复杂度就从O(n3)降到O(n2logS),从4*109降到了2*107。

单调队列优化

多重背包III

关键词: 单调队列、斜率优化、

for (int i = 0; i < n; i ++ )
    {
        int v, w, s;
        cin >> v >> w >> s;
        memcpy(g, f, sizeof f);
        for (int j = 0; j < v; j ++ )
        {
            int hh = 0, tt = -1;
            for (int k = j; k <= m; k += v)
            {
                if (hh <= tt && q[hh] < k - s * v) hh ++ ;
                while (hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt -- ;
                q[ ++ tt] = k;
                f[k] = g[q[hh]] + (k - q[hh]) / v * w;
            }
        }
    }

4. 二维费用的背包问题

空间优化后同一维费用,每个费用均采取倒序方式。

5. 分组背包

有 N 组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。si为第i组的物品种类数量。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

for (int i = 1; i <= n; i ++ )
    for (int j = m; j >= 0; j -- )
        for (int k = 0; k < s[i]; k ++ )
             if (v[i][k] <= j)
                f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);

...

状态机

状态压缩

区间DP

树形DP

数位DP

单调队列优化DP

斜率优化DP

标签:背包,int,max,博客园,物品,开通,优化,DP
来源: https://www.cnblogs.com/afengcodes/p/15837189.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有