数据结构优化dp

单调队列优化\(DP\)

例题:滑动窗口

思路

在求\(\max\)时考虑到当一个数大于前面的数时,前面的数将无论如何也不会被使用了。当时间超过时,前面的数也应当不被记录。能支持从前和从后操作的数据结构:双端队列。因为队列中前面的数总是大于后面的数,所以被称为单调队列。

双端队列内存放下标\(i\)是最优的操作,通过下标即可判断是否从前出队,再通过\(x[i]\)判断是否需要从后出队。

\(Code\)
#include<bits/stdc++.h>
using namespace std;

deque<int> minn, maxn;
int n, m;
int x[1000005];

template<typename T>
inline void read(T&x){
    x = 0; char q; bool f = 1;
    while(!isdigit(q = getchar()))  if(q == '-')    f = 0;
    while(isdigit(q)){
        x = (x<<1) + (x<<3) + (q^48);
        q = getchar();
    }
    x = f?x:-x;
}

template<typename T>
inline void write(T x){
    if(x < 0){
        putchar('-');
        x = -x;
    }
    if(x > 9)	write(x/10);
    putchar(x%10+'0');
}

int main(){
	read(n), read(m);
	for(register int i = 1; i <= n; ++i)	read(x[i]);
	for(register int i = 1; i <= n; ++i){
		while(!minn.empty() && minn.front()+m <= i)	minn.pop_front();
		while(!minn.empty() && x[minn.back()] >= x[i])	minn.pop_back();
		minn.push_back(i);
		if(i >= m){
			write(x[minn.front()]);
			putchar(' ');
		}
	}
	putchar('\n');
	for(register int i = 1; i <= n; ++i){
		while(!maxn.empty() && maxn.front()+m <= i)	maxn.pop_front();
		while(!maxn.empty() && x[maxn.back()] <= x[i])	maxn.pop_back();
		maxn.push_back(i);
		if(i >= m){
			write(x[maxn.front()]);
			putchar(' ');
		}
	}
    return 0;
}

例题:琪露诺

思路

维护到当前点的最大值,那么\(dp[i]=\max(dp[j]+a[i]),j\in[i-r,i-l]\)​​,只需要维护一个\(i+l\)\(i+r\)的最大值即可,很容易想到类似于滑动窗口。

\(1\)\(l-1\)的都是无法到达的,从第\(l\)开始入队,有后入队且比前面大的前面肯定就无法更新内容了可以弹出,前面的下标加\(l\)也无法到达\(i\)​了也可以弹出。

\(ans\)应该赋初值为\(-0x3f3f3f3f\),因为值可能为负,调了一天

\(Code\)
#include<bits/stdc++.h> 
using namespace std;

deque<int> maxn;
int n, l, r;
int a[2000005];
int dp[2000005];
int ans = -0x3f3f3f3f;

template<typename T>
inline void read(T&x){
	x = 0; char q; bool f = 1;
	while(!isdigit(q = getchar()))	if(q == '-')	f = 0;
	while(isdigit(q)){
		x = (x<<1) + (x<<3) + (q^48);
		q = getchar();
	} 
	x = f?x:-x;
}

template<typename T>
inline void write(T x){
	if(x < 0){
		putchar('-');
		x = -x;
	}
	if(x > 9)	write(x/10);
	putchar(x%10^48);
}

int main(){
	read(n), read(l), read(r);
	for(register int i = 0; i <= n; ++i)	read(a[i]);
	dp[0] = 0;
	for(register int i = 1; i < l; ++i)	dp[i] = -0x3f3f3f3f;
	for(int i = l; i <= n; ++i){
		while(!maxn.empty() && dp[maxn.back()] <= dp[i-l])	maxn.pop_back();
		maxn.push_back(i-l);
		while(maxn.front()+r < i)	maxn.pop_front();
		dp[i] = dp[maxn.front()]+a[i];
	}
	for(register int i = n-r+1; i <= n; ++i)	ans = max(ans, dp[i]);
	write(ans);
	return 0;
}

例题:[NOIP2017 普及组] 跳房子

思路

如果不考虑他可以使用金币改进的话这就是一个单调队列优化\(dp\)的版题,用\(O(n)\)的时间复杂度求出能得到的最多金币数。然而为了满足要求,还需要改进能跳的距离,也就是改变\(l\)\(r\),用最少的金币满足要求,毫无疑问就是二分答案了。

\(Code\)
#include<bits/stdc++.h>
using namespace std;

const long long INF = 0x8080808080808080;
long long dp[500005];
int n, d, k, l, r, ans;
long long sum;
int pos[500005], num[500005];
deque<int> q;

template<typename T>
inline void read(T&x){
	x = 0; char q; bool f = 1;
	while(!isdigit(q = getchar()))	if(q == '-')	f = 0;
	while(isdigit(q)){
		x = (x<<1) + (x<<3) + (q^48);
		q = getchar();
	} 
	x = f?x:-x;
}

template<typename T>
inline void write(T x){
	if(x < 0){
		putchar('-');
		x = -x;
	}
	if(x > 9)	write(x/10);
	putchar(x%10^48);
}

long long search(int l, int r) {
    memset(dp, 0x80, sizeof(dp));
    dp[0] = 0;
    q.clear();
    int j = 0;
    for(register int i = 1; i <= n; ++i){
        while(pos[i]-pos[j] >= l && j < i){
            if(dp[j] != INF){
                while(!q.empty() && dp[q.back()] <= dp[j])    q.pop_back();
                q.push_back(j);
            }
            j++;
        }
        while(!q.empty() && pos[i]-pos[q.front()] > r)    q.pop_front();
        if(!q.empty())
            dp[i] = dp[q.front()]+num[i];
    }
    long long num = INF;
    for(register int i = 1; i <= n; ++i)
        if(dp[i] > num)
            num = dp[i];
    return num;
}

int main(){
	read(n), read(d), read(k);
    for(register int i = 1; i <= n; ++i){
        read(pos[i]);
        read(num[i]);
        if(num[i] > 0)    sum += num[i];
    }
    if(sum < k){
		write(-1);
		return 0;
	}
    r = max(pos[n], d);
    while(l <= r){
        int mid = (l+r) >> 1;
        int dl = max(1, d-mid), dr = d+mid;
        long long num = search(dl, dr);
        if(num < k)    l = mid+1;
        else{
            ans = mid;
            r = mid-1;
        }
    }
    write(ans); 
    return 0;
}

例题:CF321E Ciel and Gondolas

单调队列优化dp

题目思路

很容易就可以看出来这道题是 \(dp\),如果设 \(dp[i][j]\) 表示 \(i\) 个人使用 \(j\) 条船的最优解,\(sum[i][j]\) 表示二维前缀和,很容易就可以得出式子:

\[dp[i][j]=min(dp[k-1][j-1]+sum[k][i]/2),k\in[j-1,i-1] \]

\(k\) 的取值范围是考虑优化不能让前面的船空了,也不能让新来的船没人。

决策是具有单调性的,因此想到用单调队列优化dp。

如果 \(dp[i][k]>dp[j][k]\)\(i<j\) 则决策 \(j\)\(i\) 优,可以确定决策 \(i\)不会被用到了就直接出队,外层枚举船,内层枚举人,可以容易证明决策是无后效性的。对于每个人二分查找最优解,时间复杂度 \(O(nk\log n)\),可以通过。

\(Code\)
#include<bits/stdc++.h>
using namespace std;

int n, dp[4005][4005], k, a[4005][4005], sum[4005][4005], head, tail;		//dp[i][j]表示前i个人用j条船的最优解 
struct node{
	int num, l, r;
}t;
deque<node> q;

template<typename T>
inline void read(T&x){
	x = 0; char q; bool f = 1;
	while(!isdigit(q = getchar()))	if(q == '-')	f = 0;
	while(isdigit(q)){
		x = (x<<1) + (x<<3) + (q^48);
		q = getchar();
	}
	x = f?x:-x;
}

template<typename T>
inline void write(T x){
	if(x < 0){
		putchar('-');
		x = -x;
	}
	if(x > 9)	write(x/10);
	putchar(x%10^48);
}

int val(int k, int u, int v){
	return dp[u][k-1]+(sum[u][u]+sum[v][v]-sum[u][v]-sum[v][u])/2;
}

int main(){
	read(n), read(k);
	for(register int i = 1; i <= n; ++i)
		for(register int j = 1; j <= n; ++j)
			read(a[i][j]);
	for(register int i = 1; i <= n; ++i)
		for(register int j = 1; j <= n; ++j)
			sum[i][j] = sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j];		//预处理前缀和 
	memset(dp, 0x3f, sizeof(dp));
	for(register int i = 1; i <= n; ++i)	dp[i][1] = sum[i][i]/2;			//dp[i][1]表示前i个全在1条船上的情况 
	for(register int j = 2; j <= k; ++j){									//枚举每一条船 
		q.clear();															//单调队列初始化 
		t.num = 0, t.l = 1, t.r = n;										//l,r记录人在船的区间,num记录最优人数 
		q.push_back(t);
		for(register int i = 1; i <= n; ++i){								//枚举人数 
			while(q.size() > 1 && q.front().r < i)	q.pop_front();			//当还存在不止一个解且最优人数区间小于需求人数i时直接出队 
			q.front().l = i;												//最小人数为i 
			dp[i][j] = val(j, q.front().num, i);							//k条船i个人最优解等于k-1条船预处理最优人数+剩余人数在一条船上的值 
			while(q.size() > 1 && val(j, i, q.back().l) <= val(j, q.back().num, q.back().l))	q.pop_back();
			int l = q.back().l, r = q.back().r;
			while(l < r){													//二分查找最优长度 
				int mid = (l+r) >> 1;
				if(val(j, q.back().num, mid) >= val(j, i, mid))	r = mid;	//队尾的值长度与二分长度判断 
				else l = mid+1;
			}
			if(val(j, q.back().num, q.back().r) <= val(j, i, q.back().r))	l++;
			if(l != q.back().l)	q.back().r = l-1;							//若边界不在队中,则修改队尾 
			else	q.pop_back();											//否则将队尾弹出 
			if(l <= n){														//将第p条船的信息加入队中 
				t.num = i, t.l = l, t.r = n;
				q.push_back(t);
			}
		}
	}
	write(dp[n][k]);
	return 0;
}
posted @ 2024-10-09 18:36  Zzzzzzzm  阅读(15)  评论(0)    收藏  举报