算法总纲:
动态规划理论篇
1、哪些问题可以使用动态规划?
- 最优子结构
- 子问题重叠
- 无后效性
例子:
1.1 动态规划概念
动态规划把原问题划分为若干子问题,每个子问题的求解过程构成一个阶段,求解完前一阶段后再求解后一阶段。根据无后效性,动态规划的求解过程构成一个有向无环图,求解的顺序就是该有向无环图的一个拓扑序。在有向无环图中,节点对应问题的状态,有向边对应状态之间的转移,转移时作出的选择就是决策。
动态规划的三个要素:
- 状态
- 阶段
- 决策
1.2 动态规划 模板分类
2、背包问题
背包问题是动态规划的经典问题之一。背包问题指在一个有容积或重量限制的背包中放入物品,物品有体积、重量、价值等属性,要求在满足背包限制的情况下放置物品,使背包中物品的价值之和最大。根据物品限制条件的不同,背包问题可分为01背包、完全背包、多重背包、分组背包和混合背包等。
- 01背包:每种物品只有 1 1 1 个
- 完全背包:每种物品有无穷多个
- 分组背包:第 i i i 组有 C i C_i Ci 个物品
- 多重背包:每种物品有 C i C_i Ci 个
2.1 01背包
给定 n n n 种物品,每种物品都有重量 w i w_i wi ,和价值 v i v_i vi ,每种物品只有一个,背包容量为 W W W 。求解在不超过背包容量的情况下,将哪些物品放入背包,使背包中的物品价值之和最大。每种物品只有一个,要么不放入(0),要么放入(1),因此称之为01背包。
- 确定状态。 C [ i ] [ j ] C[i][j] C[i][j] 表示前 i i i 种物品放入容量为 j j j 的背包中获得的最大价值。
- 划分阶段。第 i i i 阶段处理第 i i i 种物品,第 i i i-1 阶段处理第 i i i 种物品。当处理第 i i i 种物品时,前 i − 1 i-1 i−1 种物品已处理完毕,只需考虑第 i − 1 i-1 i−1 阶段向第 i i i 阶段的转移。
二维数组格式的01背包
c [ i ] [ j ] = { c [ i − 1 ] [ j ] , j < w [ i ] m a x { c [ i − 1 ] [ j ] , c [ i − 1 ] [ j − w [ i ] ] + v [ i ] } , j ⩾ w [ i ] c[i][j]=\left\{ \begin{aligned} c[i-1][j] & , &j<w[i] \\ max \{c[i-1][j],c[i-1][j-w[i]]+v[i]\} & ,& j \geqslant w[i] \end{aligned} \right. c[i][j]={c[i−1][j]max{c[i−1][j],c[i−1][j−w[i]]+v[i]},,j<w[i]j⩾w[i]
for(int i=1;i<=n;i++){
for(int j=1;j<=W;j++){
if(j<w[i]){
c[i][j]=c[i-1][j];
}else{
c[i][j]=max{c[i-1][j],c[i-1][j-w[i]]+v[i]};
}
}
}
时间复杂度 O ( n W ) O(nW) O(nW),空间复杂度 O ( n W ) O(nW) O(nW)
根据最后的 c [ i ] [ j ] c[i][j] c[i][j]倒推出放入背包的物品
- 初始时 i = n , j = W i=n, j=W i=n,j=W;
- 若 c [ i ] [ j ] > c [ i − 1 ] [ j ] c[i][j]>c[i-1][j] c[i][j]>c[i−1][j],则第 i i i 种物品被放入背包,令 x [ i ] = 1 x[i]=1 x[i]=1, j − = w [ i ] j-=w[i] j−=w[i];若 c [ i ] [ j ] ≤ c [ i − 1 ] [ j ] c[i][j] \leq c[i-1][j] c[i][j]≤c[i−1][j],则第 i i i 种物品没被放入背包,令 x [ i ] = 0 x[i]=0 x[i]=0
- i − − i-- i−−,转向第2步,直到 i = 0 i=0 i=0处理完毕
算法优化
求解第 i i i行的时候,只需要第 i − 1 i-1 i−1行的结果,前面的结果已经没有用了。求解 c [ i ] [ j ] c[i][j] c[i][j] 时,只需要上一行 j j j 列或上一行 j − w [ i ] j-w[i] j−w[i] 列的结果,可以进行空间优化,将二维数组转为一维数组。
- 状态表示: d p [ i ] dp[i] dp[i] 表示将物品放入容量为 j j j 的背包种可以获得的最大价值
- 状态转移方程: d p [ j ] = m a x { d p [ j ] , d p [ j − w [ i ] ] + v [ i ] } dp[j]=max\{ dp[j],dp[j-w[i]]+v[i] \} dp[j]=max{dp[j],dp[j−w[i]]+v[i]}
但是注意一点,一维数组要倒推,避免更新过的数据再次更新。
for(int i=1;i<=n;i++){
for(int j=W;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+v[i])
}
}
2.2 完全背包
有 N N N 种物品和一个容量为 T T T 的背包,每种物品都就可以选择任意多个,第i种物品的价值为 P [ i ] P[i] P[i] ,体积为 V [ i ] V[i] V[i] ,求解:选哪些物品放入背包,可卡因使得这些物品的价值最大,并且体积总和不超过背包容量。跟01背包一样,完全背包也是一个很经典的动态规划问题,不同的地方在于01背包问题中,每件物品最多选择一件,而在完全背包问题中,只要背包装得下,每件物品可以选择任意多件。从每件物品的角度来说,与之相关的策略已经不再是选或者不选了,而是有取0件、取1件、取2件…直到取⌊T/Vi⌋(向下取整)件。
最优化原理和无后效性
- 那这个问题可以不可以像01背包问题一样使用动态规划来求解呢?来证明一下即可。首先,先用反证法证明最优化原理:假设完全背包的解为 F ( n 1 , n 2 , . . . , n N ) F(n_1,n_2,...,n_N) F(n1,n2,...,nN)( n 1 n_1 n1, n 2 n_2 n2 分别代表第1、第2件物品的选取数量),完全背包的子问题为,将前i种物品放入容量为 t t t 的背包并取得最大价值,其对应的解为: F ( n 1 , n 2 , . . . , n i ) F(n_1,n_2,...,n_i) F(n1,n2,...,ni),假设该解不是子问题的最优解,即存在另一组解 F ( m 1 , m 2 , . . . , m i ) F(m_1,m_2,...,m_i) F(m1,m2,...,mi),使得 F ( m 1 , m 2 , . . . , m i ) F(m_1,m_2,...,m_i) F(m1,m2,...,mi) > F ( n 1 , n 2 , . . . , n i ) F(n_1,n_2,...,n_i) F(n1,n2,...,ni),那么 F ( m 1 , m 2 , . . . , m i , . . . , m N ) F(m_1,m_2,...,m_i,...,m_N) F(m1,m2,...,mi,...,mN) 必然大于 F ( n 1 , n 2 , . . . , n N ) F(n_1,n_2,...,n_N) F(n1,n2,...,nN),因此 F ( n 1 , n 2 , . . . , n N ) F(n_1,n_2,...,n_N) F(n1,n2,...,nN) 不是原问题的最优解,与原假设不符,所以 F ( n 1 , n 2 , . . . , n i ) F(n_1,n_2,...,n_i) F(n1,n2,...,ni)必然是子问题的最优解。
- 再来看看无后效性:对于子问题的任意解,都不会影响后续子问题的解,也就是说,前 i i i 种物品如何选择,只要最终的剩余背包空间不变,就不会影响后面物品的选择。即满足无后效性。
- 因此,完全背包问题也可以使用动态规划来解决。
for(int i=1;i<=n;i++){
for(int j=w[i];j<=W;j++){// 正向推
dp[j]=max(dp[j],dp[j-w[i]]+v[i])
}
}
2.3 多重背包
给定 n n n 种物品,每种物品都有重量 w i w_i wi 和价值 v i v_i vi ,第 i i i 种物品有 c i c_i ci 个。背包容量为 W W W ,求解在不超过背包容量的情况下如何放置物品,使背包中物品的价值之和最大。
可以通过暴力拆分或二进制拆分将多重背包问题转化为01背包问题,也可以通过数组优化解决可行性问题。
暴力拆分
暴力拆分是指将第 i i i 种物品看作 c i c_i ci 种独立的物品,每种物品只有一个,转化为01背包问题
void multi_knapsack1(int n, int W){// 容易TLE
for(int i=1;i<=n;i++){
for(int k=1;k<=c[i];k++){// 多一层循环
for(int j=W;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+v[i])
}
}
}
}
时间复杂度为 O ( W ∑ i = 1 n c i ) O(W\sum^n_{i=1}{c_i}) O(W∑i=1nci) ,空间复杂度为 O ( W ) O(W) O(W)
二进制拆分
二进制拆分,将 c [ i ] c[i] c[i] 个物品拆分成若干种新物种。存在一个最大的整数 p p p ,使 2 0 + 2 1 + . . . 2 p ≤ c [ i ] 2^0+2^1+...2^p\leq c[i] 20+21+...2p≤c[i] 。将剩余部分用 R i R_i Ri 表示, R i = c [ i ] − ( 2 0 + 2 1 + . . . 2 p ) R_i=c[i]-(2^0+2^1+...2^p) Ri=c[i]−(20+21+...2p),将 c [ i ] c[i] c[i] 拆分成 p + 2 p+2 p+2 个数: 2 0 , 2 1 , . . . 2 p , R i 2^0, 2^1, ...2^p, R_i 20,21,...2p,Ri 。
void multi_knapsack2(int n, int W){
for(int i=1;i<=n;i++){
if(c[i]*w[i]>=W){// 转为完全背包
for(int j=w[i];j<=W;j++){// 正向推
dp[j]=max(dp[j],dp[j-w[i]]+v[i])
}
}else{
for(int k=1;c[i]>0;k<<1){
int x=min(k,c[i]);
for(int j=W;j>=w[i]*x;j--){// 转化01背包
dp[j]=max(dp[j],dp[j-w[i]*x]+x*v[i]);
}
c[i]-=x;
}
}
}
}
void multi_knapsack2(int n, int W){
for(int i=1;i<=n;i++){
if(c[i]*w[i]>=W){// 转为完全背包
for(int j=w[i];j<=W;j++){// 正向推
dp[j]=max(dp[j],dp[j-w[i]]+v[i])
}
}else{
for(int k=1;k<=c[i];k<<1){
for(int j=W;j>=w[i]*x;j--){// 转化01背包
dp[j]=max(dp[j],dp[j-w[i]*x]+x*v[i]);
}
c[i]-=x;
}
if(c[i]>0){
for(int j=W;j>=w[i]*x;j--){// 转化01背包
dp[j]=max(dp[j],dp[j-w[i]*x]+x*v[i]);
}
}
}
}
}
时间复杂度为 O ( W ∑ i = 1 n l o g c i ) O(W\sum^n_{i=1}{logc_i}) O(W∑i=1nlogci) ,空间复杂度为 O ( W ) O(W) O(W)
2.4 区间DP
区间DP属于线性DP的一种,以区间长度作为DP的阶段,以区间的左右端点作为状态的维度。一个状态通常由被它包含且比它更小的区间状态转移而来。阶段(长度)、状态(左右端点)、决策三者按照由外到内的顺序构成三层循环。
是否可以使用动态规划?
最优子结构
分析第 i i i 个站点到第 j j j 个站点 ( i , i + 1 , . . . , j ) (i,i+1,...,j) (i,i+1,...,j) 的最优解(最少租金)问题,考察是否有最优子结构性质。
- 确定状态。 d p [ i ] [ j ] dp[i][j] dp[i][j] 表 示第 i i i 个站点到第 j j j 个站点的最少租金。
- 划分阶段。区间长度。
- 决策选择。原问题与子问题之间的关系。
-
状态转移方程:$dp[i][j]= min{dp[i][k]+ dp[k][j],dp[i][j]} $
- 边界条件。 i < j , d p [ i ] [ j ] = r [ i ] [ j ] i<j, dp[i][j]=r[i][j] i<j,dp[i][j]=r[i][j]
- 求解目标。 d p [ i ] [ n ] dp[i][n] dp[i][n]
例题-长江游艇俱乐部
题目描述(P1359/T1624):长江游艇俱乐部在长江上设置了n个游艇出租站,游客可以在这些出租站租用游艇,在下游的任何一个游艇出租站归还游艇。游艇出租站i到游艇出租站j之间的租金为r(i, j)。现在要求出从游艇出租站1到游艇出租站n所需的最少的租金。
问题分析:
当要租用游艇从一个站到另外一个站时,中间可能经过很多站点,不同的停靠站策略就有不同的租金。如果穷举所有的停靠策略,例如一共有10个站点, 当求子问题4个站点的停靠策略时,子问题有:(1,2,3,4)、(2,3,4,5)、(3,4,5,6)、(4,5,6,7)、(5,6,7,8)、(6,7,8,9)、(7,8,9,10)。如果再继续求解子问题,会发现有大量的子问题重叠,其算法的时间复杂度为 2 n 2^n 2n ,暴力穷举的办法是不可取的。
算法实现:
- 阶段(区间长度)
- 状态(区间起点和终点)
- 决策(状态转移方程)
void rent(){
for(int d=3;d<=n;d++){// 区间长度d
for(int i=1;i<=n-d+1;i++){// 状态起点i,终点j
int j=i+d-1;
for(int k=i+1;k<j;k++){// 枚举决策点k
if(dp[i][j]>dp[i][k]+dp[k][j])
dp[i][j]=dp[i][k]+dp[k][j];
}
}
}
}
延伸思考:
上面得到最优值只是第1个站点到第n个站点之间的最少租金,并不知道停靠了哪些站点,如果还想知道停靠的站点,怎么办?
- 状态转移方程:$dp[i][j]= min{dp[i][k]+ dp[k][j],dp[i][j]} $;
- 用辅助数组 s [ i ] [ j ] s[i][j] s[i][j] 记录各个子问题 ( i . . . j ) (i...j) (i...j) 的最优决策 k k k (停靠站点)。
void rent(){
for(int d=3;d<=n;d++){// 区间长度d
for(int i=1;i<=n-d+1;i++){// 状态起点i,终点j
int j=i+d-1;
for(int k=i+1;k<j;k++){// 枚举决策点k
if(dp[i][j]>dp[i][k]+dp[k][j]){
dp[i][j]=dp[i][k]+dp[k][j];
s[i][j]=k;
}
}
}
}
}
2.5 树形DP
在树形结构上实现的动态规划称为树形DP。动态规划是多阶决策问题,而树形结构有明显的层次性,正好对应动态规划的多个阶段。
树形DP一般自底向上,将子树从小到大作为DP的“阶段”,将结点编号作为DP状态的第一维,代表以该节点为根的子树。树形DP一般采用深度优先遍历,递归求解每棵子树,回溯时从子节点向上进行状态转移。在当前结点的所有子树都求解完毕后,才可以求解当前结点。
例题-周年派对
题目描述(POJ2342/HDU1520):Ural 大学将举行80周年校庆晚会。大学职员的主管关系像一棵以校长为根的树。为了让每一个人都玩的嗨皮,校长不希望职员和他的直接上司都在场。人事处已经评估了每个职员的欢乐度,你的任务是列出一个邀请职员名单,使参会职员的欢乐度总和最大。
输入:职员的编号从1到N。输入的第一行包含 数字N (1≤N≤6000)。后面的N行中的每一行都包含相应职员的欢乐度。欢乐度是一个从-128到127整数。之后的N-1行是描述主管关系树。每一行都具有以下格式: L K,表示第K名职员是第L名职员的直接主管。输入以包含 0 0 的行结尾。
输出:输出参会职员欢乐度的最大和值。
输入样例:
7
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
0 0
输出样例:
5
- 确定状态:
d
p
[
u
]
[
0
]
dp[u][0]
dp[u][0] 表示不选择节点
u
u
u 时,在以节点
u
u
u 为根的子树中参加职员的欢乐度最大和;
d p [ u ] [ 1 ] dp[u][1] dp[u][1] 表示选择节点 u u u 时,在以节点 u u u 为根的子树中参加职员的欢乐度最大和。 - 划分阶段:子树从小到大的顺序
- 决策选择:
- 若不选择当前节点 u u u ,则它的所有子节点 v v v 都可选或不选,取最大值即可。 d p [ u ] [ 0 ] + = m a x ( d p [ v ] [ 0 ] , d p [ v ] [ 1 ] ) dp[u][0]+=max(dp[v][0], dp[v][1]) dp[u][0]+=max(dp[v][0],dp[v][1])
- 若选择当前节点 u u u,则它的所有子节点 v v v 均不可选。 d p [ u ] [ 1 ] + = d p [ v ] [ 0 ] dp[u][1 ]+=dp[v][0] dp[u][1]+=dp[v][0]
- 边界条件: d p [ u ] [ 0 ] = 0 , d p [ u ] [ 1 ] = v a l [ u ] dp[u][0]=0, dp[u][1 ]=val[u] dp[u][0]=0,dp[u][1]=val[u]
- 求解目标: m a x ( d p [ r o o t ] [ 0 ] , d p [ r o o t ] [ 1 ] ) max(dp[root][0], dp[root][1]) max(dp[root][0],dp[root][1]), r o o t root root 为树根。
void dfs(int u){
dp[u][0]=0;
dp[u][1]=val[u];
for(int i=0;i<E[u].size();i++){
int v=E[u][i];
dfs(v);
dp[u][0]+=max(dp[v][1],dp[v][0]);
dp[u][1]+=dp[v][0];
}
}
例题2-工人请愿书
题目描述(UVA12186):公司有一个严格的等级制度,除了大老板,每个员工都只有十个
老板(直接上司)。不是其他员工老板的员工被称为工人,其余的员工和老板都叫作老板。要求加薪时,工人应向其老板提出请愿书。若至少T%的直接下属提交请愿书,则该老板会有压力,向自己的老板提交请愿书。每个老板最多向自己的老板提交一份请愿书。 老板仅统计他的直接下属的请愿书数量来计算压力百分比。当一份请愿书被提交给公司大老板时,所有人的工资都会增加。请找出为使大老板收到请愿书而必须提交请愿书的最少工人数。
输入:输入包含几个测试用例。每个测试用例都包括两行,第1行包含两个整数 n n n 和 T T T (1≤n≤ 1 0 5 10^5 105,1≤T≤100), n n n 表示公司的员工人数(不包括公司大老板), T T T 是上面描述的参数。每个员工的编号都为1~n,大老板编号为0;第2行包含整数列表,列表中的位置 i i i (从1开始) 为整数 b b b (0≤b;≤i-l),表示员工i的直接上司的编号。在最后一个测试用例后面包含两个0。
输出:对每个测试用例,都单行输出为使大老板收到请愿书而必须提交请愿书的最少工人数。
输入样例:
3 100
0 0 0
3 50
0 0 0
14 60
0 0 1 1 2 2 2 5 7 5 7 5 7 5
0 0
输出样例:
3
2
5
本题求解递交请愿书的最少工人数。求解以节点 u u u 为根的子树中递交请愿书的最少工人数时, u u u 的子树中递交请愿书的工人数越少越好,所以可以将 u u u 的子节点按照递交请愿书的工人数非递减排序,选择前 c c c 个子节点将其递交请愿书的工人数加起来。
- 确定状态。 d p [ u ] dp[u] dp[u] 表示让 u u u 给上级发请愿书,最少需要多少个工人递交请愿书。
- 划分阶段。子树从小到大的顺序。
- 决策选择。对于 u u u 的子节点 v v v ,将 $ dp[v]$ 按照非递减排序,选择前 c c c 个累加,$c=\lceil k \times T% \rceil $ , d p [ u ] + = s u m ( d p [ v ] ) dp[u]+=sum(dp[v]) dp[u]+=sum(dp[v])。
- 边界条件。叶子结点, d p [ u ] = 1 dp[u]=1 dp[u]=1
- 求解目标。 d p [ 0 ] dp[0] dp[0]
int dfs(int u){
if(E[u].size()==0) return 1;
int k=E[u].size();
vector<int> d;
for(int i=0;i<k;i++){
d.push_back(dfs(E[u][i]));
}
sort(d.begin(),d.end());
int c=(k*T-1)/100+1,ans=0;//c=ceil(k*T/100.0)
for(int i=0;i<c;i++){
ans+=d[i];
}
return ans;
}
- 时间复杂度:深度优先遍历每个节点,执行次数为 O ( n ) O(n) O(n),对每个节点的子节点最优值排序最多 O ( n l o g n ) O(nlogn) O(nlogn),总时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
- 空间复杂度:空间复杂度为递归树的深度 O ( l o g n ) O(logn) O(logn)。
3、 动态规划优化
可以从三个方面进行动态规划优化:状态总数、每个状态的决策数和每次状态转移所需的时间。
- 减少状态总数。包括修改状态表示(状态压缩、倍增优化)、选择适当的DP方向(双向DP)等。
- 减少状态决策数。利用最优决策单调性进行单调队列优化、斜率优化、四边不等式优化、剪枝优化等。
- 减少状态转移所需的时间。可采用预处理、合适的计算方法和数据结构优化等。
单调队列是一种特殊的队列,可以在队列两端进行操作,并始终维护队列的单调性。单调队列有两种单调性:元素的值严格单调(递减或递增),元素的下标严格单调(递减或递增)。单调队列只可以从队尾入队,但可以从队尾或队首出队。
当状态转移为以下两种情况时,考虑优化。
- 状态转移方程形如 d p [ i ] = m i n { d p [ j ] + f [ j ] } dp[i]=min\{dp[j]+f[j]\} dp[i]=min{dp[j]+f[j]}, 0 ≤ j < i 0≤j<i 0≤j<i。在这种情况下,下界不变, i i i 增加1时, j j j 的上界也增加1,决策的候选集合只扩大、不缩小,仅用一个变量维护最值。用一个变量 v a l val val 维护 ( 0 , i ) (0, i) (0,i) 区间中 d p [ j ] + f [ j ] dp[j]+f[j] dp[j]+f[j] 的最小值即可。
- 状态转移方程形如 d p [ i ] = m i n { d p [ j ] + f [ j ] } dp[i]=min\{dp[j]+f[j]\} dp[i]=min{dp[j]+f[j]}, i − a ≤ j ≤ i − b i-a≤j≤i-b i−a≤j≤i−b。在这种情况下, i i i 增加1时, j j j 的上界、下界同时增加1,在一个新的决策加入候选集时,需要把过时(前面一个超出区间的)的决策从候选集合中剔除。例如,当前 j j j 的范围为[2,4],当 i i i 增加 1 时, j j j 的范围变为 [3,5] ,此时 2已过时(不属于[3,5]区间)。当决策的取值范围的上、下界均单调变化时,每个决策都在候选集合中插入或删除最多一次,可以用一个单调队列维护 [i-a, i-b] 区间 d p [ j ] + f [ j ] dp[j]+f[j] dp[j]+f[j] 的最小值。
例题-滑动窗口
题目描述(POJ2823): 有n (n≤106) 个元素的数组,以及一个大小为k的滑动窗口,将滑动窗口从数组的最左边移动到最右边,只可以在该窗口中看到k个数字,滑动窗口每次都向右移动一个位置,请确定滑动窗口在每个位置的最大值和最小值。下面是一个例子, 数组是[1 3 -1 -3 5 3 6 7],k是3。
输入:第1行包含整数n和k,表示元素个数和滑动窗口的长度;第2行包含n个整数。
输出:第1行从左到右分别输出每个窗口中的最小值,第2行输出最大值。
- M i n [ i ] = m i n { a j } Min[i]=min\{a_j\} Min[i]=min{aj}, i − k + 1 ≤ j ≤ i i-k+1≤j≤i i−k+1≤j≤i。
- M a x [ i ] = m a x { a j } Max[i]=max\{a_j\} Max[i]=max{aj}, i − k + 1 ≤ j ≤ i i-k+1≤j≤i i−k+1≤j≤i。
如果对每个 i i i,枚举 [ i − k + 1 , i ] [i-k+1, i] [i−k+1,i]中 k k k 个数的最小值或最大值,总时间复杂度为 O ( n − k + 1 ) k O(n-k+1)k O(n−k+1)k,时间复杂度为 O ( n k ) O(nk) O(nk)。
单调队列优化:
当 i i i增加1时,j的上下界也增加1,省略 j j j,用单调队列维护即可。本题求解 M i n [ i ] Min[i] Min[i]时采用元素单调递增(队头最小),求解 M a x [ i ] Max[i] Max[i]时采用元素单调递减(队头最大)。注意:在队列中存储的是下标,队头最小(最大)是指队头下标对应的元素最小(最大)。
求解 M i n [ i ] Min[i] Min[i]的步骤如下。
- 单调递增的队列,队头元素总是最小的。
- 若待入队的元素小于或等于队尾元素,则队尾元素出队,直到待入队的元素大于队尾元素,或队列为空,然后待入队的元素下标从队尾入队。
- 若队头元素下标小于 i − k + 1 i-k+1 i−k+1,则说明队头元素已过时(不在窗口内),队头元素下标出队。
求解 M a x [ i ] Max[i] Max[i] 的步骤如下。
- 单调递减的队列,队头元素总是最大的。
- 若待入队的元素大于或等于队尾元素,则队尾元素出队,直到待入队的元素小于队尾元素,或队列为空,然后待入队的元素下标从队尾入队。
- 若队头元素下标小于 i-k+1,则说明队头元素已过时(不在窗口内),队头元素下标出队。
元素是否过时与其下标有关,所以在队列中存储的是下标,求最值时只需访问队头元素下标在序列中对应的元素即可得到答案。每个元素下标最多入队、出队一次,总时间复杂度为 O ( n ) O(n) O(n) ,均摊时间为 O ( 1 ) O(1) O(1)。
#include<cstdio>
#include<cstring>
#define MAXN 1000010
using namespace std;
int a[MAXN],Min[MAXN],Max[MAXN],Q[MAXN],n,k;
void get_min(){
int st=0,ed=0;
Q[ed++]=1;
Min[1]=a[1];
for(int i=2;i<=n;i++){
while(st<ed&&a[i]<a[Q[ed-1]])//删除队尾元素
ed--; //插入队尾元素
Q[ed++]=i;
while(st<ed&&Q[st]<i-k+1)//删除队首过时元素
st++;
Min[i]=a[Q[st]];
}
}
void get_max(){
int st=0,ed=0;
Q[ed++]=1;
Max[1]=a[1];
for(int i=2;i<=n;i++){
while(st<ed&&(a[i]>a[Q[ed-1]]))
ed--;
Q[ed++]=i;
while(st<ed&&Q[st]<i-k+1)
st++;
Max[i]=a[Q[st]];
}
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
get_min();
get_max();
for(int i=k;i<n;i++)
printf("%d ",Min[i]);
printf("%d\n",Min[n]);
for(int i=k;i<n;i++)
printf("%d ",Max[i]);
printf("%d\n",Max[n]);
return 0;
}