动态规划理论篇

算法总纲:

在这里插入图片描述

动态规划理论篇

1、哪些问题可以使用动态规划?

  1. 最优子结构
  2. 子问题重叠
  3. 无后效性

例子:

在这里插入图片描述

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背包。

  1. 确定状态。 C [ i ] [ j ] C[i][j] C[i][j] 表示前 i i i 种物品放入容量为 j j j 的背包中获得的最大价值。
  2. 划分阶段。第 i i i 阶段处理第 i i i 种物品,第 i i i-1 阶段处理第 i i i 种物品。当处理第 i i i 种物品时,前 i − 1 i-1 i1 种物品已处理完毕,只需考虑第 i − 1 i-1 i1 阶段向第 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[i1][j]max{c[i1][j],c[i1][jw[i]]+v[i]},,j<w[i]jw[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]倒推出放入背包的物品

  1. 初始时 i = n , j = W i=n, j=W i=n,j=W
  2. c [ i ] [ j ] > c [ i − 1 ] [ j ] c[i][j]>c[i-1][j] c[i][j]>c[i1][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[i1][j],则第 i i i 种物品没被放入背包,令 x [ i ] = 0 x[i]=0 x[i]=0
  3. i − − i-- i,转向第2步,直到 i = 0 i=0 i=0处理完毕
算法优化

求解第 i i i行的时候,只需要第 i − 1 i-1 i1行的结果,前面的结果已经没有用了。求解 c [ i ] [ j ] c[i][j] c[i][j] 时,只需要上一行 j j j 列或上一行 j − w [ i ] j-w[i] jw[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[jw[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(Wi=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+...2pc[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(Wi=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) 的最优解(最少租金)问题,考察是否有最优子结构性质。

在这里插入图片描述

  1. 确定状态。 d p [ i ] [ j ] dp[i][j] dp[i][j] 表 示第 i i i 个站点到第 j j j 个站点的最少租金。
  2. 划分阶段。区间长度。
  3. 决策选择。原问题与子问题之间的关系。
    1. 在这里插入图片描述

    2. 状态转移方程:$dp[i][j]= min{dp[i][k]+ dp[k][j],dp[i][j]} $

  4. 边界条件。 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]
  5. 求解目标。 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、 动态规划优化

可以从三个方面进行动态规划优化:状态总数、每个状态的决策数和每次状态转移所需的时间。

  1. 减少状态总数。包括修改状态表示(状态压缩、倍增优化)、选择适当的DP方向(双向DP)等。
  2. 减少状态决策数。利用最优决策单调性进行单调队列优化、斜率优化、四边不等式优化、剪枝优化等。
  3. 减少状态转移所需的时间。可采用预处理、合适的计算方法和数据结构优化等。

在这里插入图片描述

单调队列是一种特殊的队列,可以在队列两端进行操作,并始终维护队列的单调性。单调队列有两种单调性:元素的值严格单调(递减或递增),元素的下标严格单调(递减或递增)。单调队列只可以从队尾入队,但可以从队尾或队首出队。

当状态转移为以下两种情况时,考虑优化。

  • 状态转移方程形如 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 0j<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 iajib。在这种情况下, 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 ik+1ji
  • 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 ik+1ji

如果对每个 i i i,枚举 [ i − k + 1 , i ] [i-k+1, i] [ik+1,i] k k k 个数的最小值或最大值,总时间复杂度为 O ( n − k + 1 ) k O(n-k+1)k O(nk+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]的步骤如下。

  1. 单调递增的队列,队头元素总是最小的。
  2. 若待入队的元素小于或等于队尾元素,则队尾元素出队,直到待入队的元素大于队尾元素,或队列为空,然后待入队的元素下标从队尾入队。
  3. 若队头元素下标小于 i − k + 1 i-k+1 ik+1,则说明队头元素已过时(不在窗口内),队头元素下标出队。

求解 M a x [ i ] Max[i] Max[i] 的步骤如下。

  1. 单调递减的队列,队头元素总是最大的。
  2. 若待入队的元素大于或等于队尾元素,则队尾元素出队,直到待入队的元素小于队尾元素,或队列为空,然后待入队的元素下标从队尾入队。
  3. 若队头元素下标小于 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值