洛谷网校:动态规划(二)单调队列优化 p1725,p3572

2020.2.3

动态规划(二)单调队列优化

p1725 琪露诺

题目描述

小河可以看作一列格子依次编号为0到N,琪露诺只能从编号小的格子移动到编号大的格子。而且琪露诺按照一种特殊的方式进行移动,当她在格子i时,她只移动到区间[i+l,i+r]中的任意一格。

每一个格子都有一个冰冻指数A[i],编号为0的格子冰冻指数为0。当琪露诺停留在那一格时就可以得到那一格的冰冻指数A[i]。琪露诺希望能够在到达对岸时,获取最大的冰冻指数。

开始时,琪露诺在编号0的格子上,只要她下一步的位置编号大于N就算到达对岸。

输入格式

第1行:3个正整数N, L, R

第2行:N+1个整数,第i个数表示编号为i-1的格子的冰冻指数A[i-1]

输出格式

一个整数,表示最大冰冻指数。保证不超过231-1

题解(批注加粗)

fi表示从起点走到第i个格子的最大收益。

fi=max(fj+ai) (i−r≤j≤i−l)。由j的状态转移到i的状态(递推)

在当前决定的节点之前(用窗口更新当前而不是用当前更新窗口)维护一个滑动窗口,每次求窗口中fi的最大值。

用一个单调队列维护窗口里所有可能用到的决策点。

对于两个点x,y,如果x<y且fx≤ fy,那么y进入窗口后,决策点一定不是x。由于是从前往后递推,若x<y(y比x靠后),那么当前y可以更新格子的一定比x多,又因为由y更新得到的答案不小于x,所以x在此时此刻就没用了

如果一个节点y入队,则无需条件,但是入队前要弹出队列右端所有符合上文条件的x

在一番弹出后,就能保证队列中的fi从队首到队尾递减。

决策时,首先弹掉队首超过范围的点(<i-R)

这时队首点就是决策点。最左边头一个点,因为递减,所以最左边的值最大

依然是老师代码(简化实测AC)批注

#include <iostream>
#include <cstdio>
#include <algorithm>
#define inf 0x3f3f3f3f
#define N 200005
using namespace std;
const int INF=0x7f7f7f7f;
int n,L,R,a[N]/*题目输入的四个变量*/,f[N]/*dp数组*/,q[N]/*队列*/,l,r/*滑动窗口范围*/;
int main(){
	cin>>n>>L>>R;//第一行
	for(int i=0;i<=n;i++) {
        scanf("%d",&a[i]);//冰冻指数
    }
	f[0]=a[0];//边界:一开始就在第0个格子,无需移动
	l=0;
    r=-1;
	for(int i=1;i<=n;i++) {
		while(l<=r&&q[l]<i-R) {//q[l]存的格子已经超出i之前R的范围,且队列不为空,可以弹出(因为已经无法对i造成影响)
            l++;//弹出q[l]
        }
		if(i-L>=0) {//保证当前操作的格子编号为非负数
			while(l<=r&&f[i-L]>=f[q[r]]) {//保证队列不空且加入新格子[i-L]后,r不可以更新答案,i-L可以完全替代或者比r更优,弹出所有这样的r
                r--;
            }
			q[++r]=i-L;//加入i-L
		}
		if(l>r) {//队空
            f[i]=-INF;//没有到i的方案
        }
		else {
            f[i]=f[q[l]]+a[i];//队列有元素,用队列最左边的元素更新f[i](不用担心队中其他元素,是金子总会发光,如果某元素有用,一定会留下来,成为下一个l,如果l一直能更新新的格子,那么l会一直待到l<i-R被迫老死(弹出)的那一刻)
        }
	}//到这里,递推循环结束
	int ans=f[n];//开始筛选最大值为答案
	for(int i=n-R+1;i<=n;i++) {
        ans=max(ans,f[i]);//只要“大于”n就算到达对岸,所以最远可能从n-R+1直接跳R格到n+1;最近是从n跳任意k步都能到n+k,又因为n以后的位置没有收益,所以答案应该在n-R+1~n中找
    }
	cout<<ans;
	return 0;
}

p3572 PTA-Little Bird

题目描述

n棵树排成一行,第i棵树高度hi。一只鸟想从1跳到n。当它在第i棵树上时,它可以一步跳到 [i+1,i+K]中的任意一棵树。 它从较高的树向较矮的树跳时不消耗体力,否则消耗 1 的体力。 询问 Q 次,每次给定 K,求最小消耗体力。

从1开始,跳到比当前矮的不消耗体力,否则消耗一点体力,每次询问有一个步伐限制,求每次最少耗费多少体力

输入格式(简化)(自己翻译成汉语又翻译回英语)

There is a single integer n (2≤n≤1 000 000)

The second line holds n int d1dn~ is the height of the trees.

The third line Q (1≤q≤25)

The following Q lines: ki(1≤ki≤n−1)

输出格式(简化)

Print q lines,The ith line(the ith bird energy)

题解(批注加粗)

f(i)表示跳到第i棵树消耗的最少体力。dp数组

状态转移方程:f(i)=min(f(j)+(hj<hi)) (i−K≤j≤i−1)。

如果hi(目标树)大那体力就加一,hj(起点树)大体力就不变

设x<y,若f(x)>f(y),(无论i的高矮都是f(y)小,和上题类似,y明显比较优)或 f(x)=f(y) 且 hx ≤ hy(这时,如果转移到x,y都能得到一样的f,但是对以后的转移,如果从x到下一棵树消耗体力,那么y到同一棵树不一定消耗体力,y优于x),那么选y不会变差,x没用。这时弹出x

结论队尾为x,新点为 y,如果 f(x) > f(y),或f(x)=f(y)且hx≤hy, 把 x 弹掉。

所以,是靠队列状态转移到当前状态,和上一题一样

复杂度O(Qn)

老师的代码(简化实测AC)+批注

#include <iostream>
#include <cstdio>
#define N 1000010
using namespace std;
int n,Q/*题目给出的n,Q*/,K/*当前鸟的k值,避免浪费数组*/,a[N]/*树高*/,f[N]/*DP*/,q[N]/*队列*/,l,r/*队列左右界*/;
int main() {
    scanf("%d",&n);
    for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]);
    }
    scanf("%d",&Q);
    while(Q--){
        scanf("%d",&K);//本行以上都是读入
        q[0]=1;//第一棵树已经在队内
        l=0;
        r=0;//一开始的队列只有元素0
        for(int i=2;i<=n;i++) {
            while(i-q[l]>K) {//弹出队首的条件:老死(i,l距离已经超过k,鸟跳不过去)
                l++;
            }
            f[i]=f[q[l]]+(a[q[l]]<=a[i]);//同样是用队首更新当前,只要高度上升就使体力加一
            while(l<=r&&(f[i]<f[q[r]]||(f[i]==f[q[r]]&&a[i]>a[q[r]]))) {//条件说明:(队列不空&&(队尾体力大于当前体力||(当前体力和队尾体力相等&&当前高度大于队尾))):这里讨论是否弹出队尾,队列不空是前提,分两种情况符合弹出条件,一、队尾体力消耗已经比当前大了,不可能更新当前。二、队尾体力虽然和当前相等,但是因高度问题,只要到了当前树就会体力+1,也不能更新当前
                r--;//弹出
            }//一通弹出过后,可以保证队列单调递增
            q[++r]=i;//i入队,准备更新下一棵树
        }
        printf("%d\n",f[n]);//f[n]就是答案
    }
    return 0;
}
posted @ 2020-02-04 20:08  Wild_Donkey  阅读(255)  评论(0编辑  收藏  举报