zoukankan      html  css  js  c++  java
  • 夏令营讲课内容整理 Day 5.

    DP专场。。
    动态规划是运筹学的一个分支, 求解决策过程最优化的数学方法。
    我们一般把动态规划简称为DP(Dynamic Programming)
     
    1.动态规划的背包问题
    有一个容量为m的背包,有n个物品,每一个物品i的重量为w[i],价值为v[i]。
    要求选择一些物品放入背包中,每种物品只能最多使用一次,使得在不超重的情况下让背包中所有物品价值总和最大。
    正常向解法:设状态数组f[i][j]为把前i个物品放入一个容量为j的背包中所能获得的最大价值(以下同设),则状态转移方程为:
    f[i][j] = max(f[i-1][j],f[i-1][j-w[i]]+v[i])
    可优化至一维数组,令j从大到小枚举。
    f[j] = max(f[j],f[j-w[i]]+v[i])
    1.1完全背包
    仍然是容量为m的背包,n个物品,每一个物品i的重量为w[i],价值为v[i]。
    要求选择一些物品放入背包中,并且每一件物品可以选无限多次,使得在不超重的情况下让背包中所有物品价值总和最大。
    f[i][j] = max(f[i-1][j],f[i-1][j-k*w[i]]+k*v[i])
    第一维也可以优化。
    f[j] = max(f[j],f[j-k*w[i]]+k*v[i])
    1.2多重背包
    依旧是容量为m的背包,n个物品,每一个物品i的重量为w[i],价值为v[i]。
    要求选择一些物品放入背包中,并且每一件物品的数量为s[i],使得在不超重的情况下让背包中所有物品价值总和最大。
    它相对于完全背包的不同之处是每种物品可选的数量有一个上限。
    f[i][j] = max(f[i-1][j],f[i-1][j-k*w[i]]+k*v[i]),k*v[i] ≤ j, k ≤ s[i]
    它的时间复杂度是O(m*Sigma(s[i])),而且看起来并不能像之前那样优化了。。
    实际上它是可以优化的。
    用「二进制拆分」即可。
    对每种物品,我们将它转化成若干个物品,其中每个物品有一个系数,这个物品的费用和价值均是原费用和原价值乘这个系数。令这些系数分别为1,2,4,...,2^k-1,s[i]-(2^k)+1,且k是满足s[i]-(2^k)+1>0的最大整数,例如,如果s[i] = 11,就将这种物品分成系数分别为1,2,4,4这四个物品。
    这些物品的系数和能拼出[0,s[i]]中的任意一个整数。这样一来我们就把一个n个物品的多重背包问题转化成了一个Sigma(log s[i])个物品的01背包问题了。
    时间复杂度为O(m*Sigma(log s[i]))
    实际上,这还能再优化。。。
    用单调队列。
    单调队列形似一个普通队列,但其中的点按顺序存在某种单调性(我记得我Day1的整理上提到过)。
    不同于优先队列,他俩内部结构不太一样。
    注意转移式:f[i][j] = max(f[i-1][j],f[i-1][j-k*w[i]]+k*v[i]),这时候k的范围已经变成了k≤min(s[i],ceil(j/w[i])).
    对于某一个i而言,因为j = k*w[i]+s的点只能转移到j = (k+1)*w[i]+s , (k+2)*w[i]+s...,所以我们可以按对w[i]取模的值对j进行分类。
    把模w[i]相等的点放在一起进行转移,转移后的点会放入单调队列中。我们发现由于队列中的点j是从小到大的,所以会存在转移某一点k,队首点head已经不能作为转移点,即(k-head)/w[i]>s[i]。
    显然,该点在以后的转移中也不会成为转移点,所以直接弹出队首即可,直到队首可以作为转移点。转移时选择队首的点作为转移点, 因为这个单调队列是保证转移式是单调递减的。
    将当前点k压入队列时,需要判断队尾点tail与k的优劣。如果tail作为转移点不比k更优,显然tail在以后的转移也不会作为转移点了,弹出tail,直至tail比k更优。
    对于每个i来说,单次转移是O(m)的,所以时间复杂度是O(nm).
     
    2.动态规划的线性(序列)类(一维)问题
    经典例题:一个长度为n的数列,第i个数为a[i],要求选择连续且非空的一段,使这一段中所有数的和加起来最大,输出这个最大和。
    正在看这篇随笔的读者朋友,您应该会显然想到O(n^2)的做法。
    我们考虑DP。令f[i]表示以i为结尾的最大和子段,那么转移只有:和前面的最大子段和连起来,或者自己单独成为一段。
    f[i] = a[i] +max(f[i-1],0)
    答案是max(f[i]),i∈[1,n]。
    时间复杂度O(n)
     
    2.1 LIS
    有一个长度为n的序列,第i个数为a[i],求最长上升子序列的长度。
    最长上升子序列的英文名叫Longest Increasing Subsequence,简称LIS。
    正在看这篇随笔的读者朋友,您应该会显然想到O(n^2)的做法。
    我们重点讨论O(nlogn)的做法。
    我们设一个数组g[i],它表示长度为i的LIS中,作为结尾最小的数。
    如果原数组a[i] = {6,7,1,5,4,3,4,2,8},那么g数组的变化应该是:
    1:g [1] = 6
    2:g [1] = 6; g [2] = 7
    3:g [1] = 1; g [2] = 7
    4:g [1] = 1; g [2] = 5
    5:g [1] = 1; g [2] = 4
    6:g [1] = 1; g [2] = 3
    7:g [1] = 1; g [2] = 3; g [3] = 4
    8:g [1] = 1; g [2] = 2; g [3] = 4
    9:g [1] = 1; g [2] = 2; g [3] = 3; g [4] = 8
    容易看出,每次g数组只会修改一个值或者加入一个值。而且无论如何,g数组总是保持单调递增。
    显然,当LIS的长度相等时,结尾的数肯定是越小越好,这样才有更大的机会与后面的数相接,才能生成更长的LIS。这样一来,g数组一定是单调递增的,因为g数组不会存一个较长的LIS,使得它的结尾数比较短的LIS还小。每次考虑以i结尾的上升子序列时,我们只要在g数组中找到最大的的小于a[i]的位置j,令g[j+1] = len即可。
    由于g单调,所以可以用二分查找来找到位置j,单次转移复杂度为O(logn)。
    最终复杂度为O(nlogn).
     
    例:有两个序列a,b,长度分别为n,m,求它们的LCS。
    LCS指最长公共子序列。表示从两个序列中各自选出长度相等的子序列,这两个子序列的数对应相等。
    设f[i][j]表示两个序列分别dp到第i,j个数,公共子序列的最大长度。
    则有
    f[i][j] = max(f[i-1][j],f[i][j-1]) (a[i]!=b[j])
    f[i][j] = f[i-1][j-1] + 1(a[i] == b[j])
    时间复杂度O(n^2)。
     
    例2:在上例条件不变的情况下,求LCIS
    LCIS指最长公共上升子序列。表示从两个序列中各自选出长度相等的上升子序列,这两个子序列的数对应相等。
    设f[i][j]表示两序列分别以i,j结尾的LCIS。
    一个简单的想法是
    f[i][j] = max(f[k][l]+1) (k < i , l < j, a[k] < a[i], b[l] < b[j], a[i]==b[j])
    我们先枚举i,再枚举j,当a[i] != b[j] 时f[i][j] = 0。但当a[i] >b[j] 时,在同一个i下,所有f[k][j] (k<i)是可以作为转移点转移后面的状态f[i][j'],因为存在方案的f[k][j]一定保证a[k]<a[i],b[j]<b[j']。
    我们可以开一个数组来记录第二维为j且第一维小于i的f的最大值,不过第一维是可以被优化掉的。
    设计状态f[i]表示b序列以b[i]结尾的LCIS,具体实现:
    1 for (int i=1;i<=n;i++){
    2     int k= 0;
    3     for (int j=1;j<=m;j++)
    4         if (a[i] == b[j])
    5             f[j] = max(f[j],k+1);
    6         else
    7         if (a[i] > b[j])
    8             k = max(k,f[j]);
    9 }
    空间复杂度降为O(m)。
     
    3.树形(树上)DP
    顾名思义,是在树上所做的动态规划,其基础是树具有严格的层数关系而不会重复的特性。
    一般来说树形DP是处理子树的信息以及其相互关系来进行转移。其状态一般会表示成以i为根的子树的DP值。
     
    3.1邻接表存树
    在之前的知识点整理上提前说了,这里只提供代码。
     1 struct Edge_tree{
     2     int u,v,w;
     3     int next;
     4 
     5 };
     6 Edge_tree edge[maxn];
     7 int cnt = 0;
     8 int first[maxn];
     9 void add_edge(int from,int to,int dis){
    10     edge[++cnt].u = from;
    11     edge[cnt].v = to;
    12     edge[cnt].w = dis;
    13     edge[cnt].next = fisrt[from];
    14     first[from] =cnt;
    15 
    16     edge[++cnt].v = from;
    17     edge[cnt].u = to;
    18     edge[cnt].w = dis;
    19     edge[cnt].next = first[to];
    20     first[to] = cnt;
    21 
    22 }
    23 
    24 
    25 void dfs_tree(int x,int fa){
    26     //cout << x << " ";
    27     for (int i = first[x];i!=0;i = edge[i].next)
    28         if (edge[i].v != fa)
    29             dfs_tree(edge[i].v,x);
    30 }
     
    3.2最大连通子树
    有一个n个节点的树,每个点有点权a[i],求一棵连通子树使点权之和最大。
    (n ≤ 10^5,|a[i]| ≤ 10^9)
    f[u] = a[u] + Sigma(max(f[v],0))
    最终答案是max(f[i]), i∈[1,n]
     
    3.3树的直径
    有一个n个节点的树,每条边有边权w[i],求一条路径使得它的所有边权和最大。
    这条路径就叫做这棵树的直径。
    (n ≤ 10^5,|a[i]| ≤ 10^9)
    如果边权保证非负,我们可以用两遍bfs求得直径。具体做法是第一次随便选一个点,bfs求得与它距离最远的点x,再从x出发bfs求得与x距离最远的点y,x与y之间的距离就是树的直径。
    若边权存在负数,则不能使用这个方法。
    设f[u]表示在u为根的子树中,存在点u的最大路径边权和(u可以为端点也可以为中间的点)
    设g[u]表示在u为根的子树中,存在点u的最大路径边权和(u只能为端点,即由u发出的一条链)
    f[u] = max( firstmax{g[v] + w(u,v)}, 0) + max(secondmax{g[v] + w(u,v)},0)
    g[u] = max{g[v] + w (u,v)}
     
    4.区间DP以及其他DP
     
    4.1区间DP
    顾名思义,是在一个区间上进行的一系列动态规划,一般考虑对于每段区间,它们的最优值都是由两段或者更多段的小区间点的最优值得到,是分治思想的一种应用。
    一般定义状态f[i][j]表示从区间i到j的DP最优值,转移时枚举中间点k,从f[i][k],f[k+1][j]来进行合并
    例题:合并石子
    n堆石子排成一列,每堆石子有一个重量w[i],每次可以合并相邻的两堆石子,一次合并的代价为二者重量之和。问怎样安排合并顺序使得代价最小。
    n,w ≤100
    解:用s[i]表示石子的前缀和,有
    f[i][j] = min(f[i][k]+f[k+1][j]+s[j]-s[i-1])
    时间复杂度O(n^3)。
     
    变式:n堆石子排成一个圆,其他条件不变。
    解:在后面加上一条排列相同的石子堆,扩展到2n-1个石子。依然是用s[i]表示w[i]的前缀和。有
    f[i][j] = min(f[i][k]+f[k+1][j]+s[j]-s[i-1])
    时间复杂度O(n^3),最终答案是min(f[i][i+n-1]),其中1 ≤ i ≤ n。
     
    变式:使最终代价最大, 其他条件不变
    解:贪心的来想,我们肯定是让每个石子重量都尽可能多的被计算,也就是说每次只合并一个石子进来应该是一个最优的策略。
    f[i][j] = max(f[i+1][j],f[i][j-1])+s[j]-s[i-1]
    时间复杂度O(n^2)。
     
    4.2棋盘DP
    非常好想的一类DP,在一个二维网格(地图)上做DP。
    一般设f[i][j]表示走到(i,j)位置上的最优值。
     
    4.3DAG上的DP
    给定一个DAG(有向无环图),要求统计一些信息。
    DAG是一个比较规则的结构,我们可以对这些点进行拓扑排序后再DP。
     
    4.4状压DP
    (GTMDNOIP2016D2T3
    状态压缩DP,通过二进制位上的0/1表示状态,一般用于要记录一段较小规模的状态且该状态包含信息较多的问题。
    就目前来说,在NOIP史上只有去年考到了。
    谁知道今年还会考什么奇怪的东西呢……
     
    4.5概率期望DP
    (GTMDNOIP2016D1T3
    这种题目会丧心病狂的让你求某一事件的期望或者概率。。
    并不想写这类丧心病狂的东西。
     
    4.6数位DP
    (这已经超纲了吧。
    这种题目一般会让你统计某一区间内的与数位或与数相关的信息,但由于区间较大而没法暴力求解,所以要在数位上进行DP。
    就目前来说,没考过,谁知道今年考不考。。
     
    5.相关优化
    5.1前缀和优化
    一般用于转移点的取值是一段连续的区间,做一下前缀和可以把转移从O(n)降至O(1)。
     
    5.2滚动数组优化
    这个还挺常见的。一般用于二维及以上的DP。如果某一维i的dp值只与i-1的dp值有关那么我们不用存这一维全部的情况,用0/1状态来存储当前状态和转移点状态就可以了,这样会降低空间复杂度。
     
    5.3单调性优化
    利用对某一属性的单调性来加速转移,体现在减少转移点数量和快速查询转移点的情况,常见的是单调队列, 二分查找等。
     
    5.4数据结构优化
    即利用一些数据结构来进行优化。NOIP阶段常用堆。
     
    5.5其他优化
    减少冗余状态
    利用数据结构的特殊性质
    女装
     
     
     
    一切无法杀死我的,都将使我变得更加强大。
  • 相关阅读:
    硬件——STM32 , SN74HC573锁存器
    【模拟】【杂题】jzoj 6345. 【NOIP2019模拟2019.9.8】ZYB建围墙
    归并排序求逆序对
    归并排序求逆序对
    hdu 4135
    hdu 4135
    牛客小白月赛5 A-无关(relationship)
    牛客小白月赛5 A-无关(relationship)
    HDU4027:Can you answer these queries?
    HDU4027:Can you answer these queries?
  • 原文地址:https://www.cnblogs.com/OIerShawnZhou/p/7401401.html
Copyright © 2011-2022 走看看