开通博客园
作者:互联网
注意
1.0 学习
#include <iostream>
int main()
{
return 0;
}
测试
- 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]为结尾的严格单调上升子序列
- 属性:最大
- 状态计算
- 状态计算:集合划分->最后一个不同的点
-
- 动态规划: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]的数字。
stl版本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]的数字 } } //
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;
- 动态规划:O(n2)
-
- 双向LIS 取最大
-
- 双向LIS 取和 -1即可
-
- 排序后LIS妙啊
-
最大上升子序列和
-
最长公共上升子序列LCS
-
- 1.贪心
- 如何证明两个数相等?A>=B A<=B
A表示贪心算法得到的序列个数,B;表示最优解;
B<=A, ok A<=B, 使用调整法
- 如何证明两个数相等?A>=B A<=B
- 2.贪心流程
- 从前往后扫描每个数,对于每个数:
情况1:如果现有的子序列的结尾都小于当前数,则创建新子序列。
情况2:将当前数放到结尾大于等于它的最小的子序列后面。
- 从前往后扫描每个数,对于每个数:
- 3.???反链实现???Dilworth原理
- 1.贪心
-
导弹防御系统(dfs)
背包模型
背包模型一般求解
- 最大值、最小值
- 方案数
- 具体方案: 最短路问题,即记录转移过程,不能再用状态压缩了?而且如果输出要求字典序最小的方案,则需要对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. 多重背包问题
方法:
- 暴力 转化成01背包
- 分组倍增优化 转化成01背包
- 最大长度的队列优化,即滑动窗口最大值,模型为单调队列(难)
分组倍增优化(二进制优化)
对于每种物品,均执行:
对于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