YbtOJ 「动态规划」 第6章 单调队列

单调队列

A. 【例题1】滑动窗口

版子题 注意先插入数值再弹出的写法 有效区间是\([i-k+1,i]\) 所以所有\(i\le k\)的点都需要被弹出

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long 
const int N = 2e6 + 10;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , k;

struct node { int val , pos; } q[N] , a[N];

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , k = read();
	for ( int i = 1 ; i <= n ; i ++ ) a[i] = { read() , i };
	int head = 1 , tail = 0;
	for ( int i = 1 ; i <= n ; i ++ )
	{
		while ( head <= tail && q[tail].val >= a[i].val ) tail --;
		q[++tail] = a[i];
		while ( head <= tail && q[head].pos <= i - k ) head ++;
		if ( i >= k ) cout << q[head].val << ' ';
		//当前的正经区间应该是[[i-k+1,i]
	}
	cout << endl;
	
	head = 1 , tail = 0;
	for ( int i = 1 ; i <= n ; i ++ )
	{
		while ( head <= tail && q[tail].val <= a[i].val ) tail --;
		q[++tail] = a[i];
		while ( head <= tail && q[head].pos <= i - k ) head ++;
		if ( i >= k ) cout << q[head].val << ' ';
	}
	return 0;
}

B. 【例题2】粉刷木板

我们可以写出朴素 \(dp\) : 设置 \(f[i][j]\) 表示前\(i\)个人粉刷前 \(j\) 个木板所获得的最大价值(有木板可以不粉刷 若不考虑则 \(90pts\) )

先将工人数据读入并按照\(s_i\)排序

此时 \(f[i][j]=max(max(f[i-1][j],f[i][j-1]),f[i-1][k]+(j-k)*p_i)(j-l_i\le k< s_i)\)

我们如果想转移 粉刷这一段木板的起始位置一定在 \([j-l_i+1,s_i]\) 之间 那么上一层转移的起始点一定在 \((j-l_i\le k< s_i)\) 之间

对于最后一个柿子 可以转化成 \(p_i*j+\max_{j-l_i\le k< s_i}(f[i-1][k]+p_i*k)\) 这样就可以保证柿子后半部分只和\(k\)有关

在循环枚举的时候 我们要枚举粉刷木板的数量 从 \(s_i\) 开始转移 每次找到满足 \((j-l_i\le k< s_i)\) 的最小值 并转移即可

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long 
const int N = 1e5 + 10;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , f[105][N];

struct node { int l , p , s; } a[N];

struct kk { int val , pos; } q[N];

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read();
	for ( int i = 1 ; i <= m ; i ++ ) a[i].l = read() , a[i].p = read() , a[i].s = read();
	sort ( a + 1 , a + m + 1 , [](const node &a , const node &b) { return a.s < b.s; } );
	for ( int i = 1 ; i <= m ; i ++ )
	{
		int head = 1 , tail = 0;
		for ( int j = max ( a[i].s - a[i].l , (int)0 ) ; j < a[i].s ; j ++ )//只有这些状态才可能转移
		{
			while ( head <= tail && q[tail].val <= f[i-1][j] - j * a[i].p ) tail --;
			q[++tail] = { f[i-1][j] - j * a[i].p , j }; 
		}
		for ( int j = 1 ; j <= n ; j ++ )
		{
			f[i][j] = max ( f[i][j-1] , f[i-1][j] );
			if ( j >= a[i].s )
			{
				while ( head <= tail && q[head].pos < j - a[i].l ) head ++;
				if ( head <= tail ) f[i][j] = max ( f[i][j] , q[head].val + a[i].p * j );
			}
		}
	}
	cout << f[m][n] << endl;
	return 0;
}

C. 【例题3】耗费体力

先写出朴素\(dp\):设置\(f[i]\)表示跳到\(i\)树所需要的最小体力 那么\(f[i]=min(f[pos]+(a[pos]\le a[i]))(i-k\le pos<i)\)其中\(k\)表示该次游戏的步数

\(dp\)时注意三个策略:

  1. 弹队尾时的策略:这里的弹队尾策略是先按照f[i]的最小值 再按照树的高度
  2. 状态合法的策略:因为状态合法时才可以转移 所以转移之前需要判断\(head\le tail\)
  3. 边界条件的策略:因为这次的\(i\)取不到 所以需要先使用单调队列的头值 再将这一次的新\(f[i]\)加入单调队列中
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 1e6 + 10;
const int inf = 0x3f3f3f3f;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , k , a[N] , f[N];

int head = 1 , tail = 0;
struct node { int pos , val; } q[N];

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read();
	for ( int i = 1 ; i <= n ; i ++ ) a[i] = read();
	m = read();
	for ( int T = 1 ; T <= m ; T ++ )
	{
		k = read();
		head = 1 , tail = 0;
		memset ( f , inf , sizeof f );
		f[1] = 0 , q[++tail] = { 1 , f[1] };
		for ( int i = 2 ; i <= n ; i ++ )
		{
			while ( head <= tail && q[head].pos < i - k ) ++ head;
			f[i] = q[head].val + ( a[q[head].pos] <= a[i] );
			while ( head <= tail && ( q[tail].val > f[i] || q[tail].val == f[i] && a[q[tail].pos] <= a[i] ) ) -- tail;
			q[++tail] = { i , f[i] };
		}
		cout << f[n] << endl;
	}
	return 0;
}

D. 【例题4】燃放烟火

恳请 YbtOJ 好好审核题面 输入数据中\(t_i\)\(b_i\)是反着的

朴素\(dp\):设置\(f[i][j]\)表示当前正在放第\(i\)个烟花 人的位置在\(j\)的最大幸福值

那么\(f[i][j]=max(f[i-1][k]+b_i-|a_i-j|)\ (j-(t_i-t_{i-1})*d\le k\le j+(t_i-t_{i-1})*d)\)

那么我们可以将\(b\)提出来 令\(f[i][j]=min(f[i-1][k]+|a_i-j|)\ (j-(t_i-t_{i-1})*d\le k\le j+(t_i-t_{i-1})*d))\)

所以最终柿子就是\(\sum_{i=1}^nb_i-min(f[m\%][i])(1\le i\le n)\)

因为人可以向左或向右走 所以前后都扫一遍取最小值即可

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'

#define int long long 

const int N = 1e7 + 10;
const int inf = 0x3f3f3f3f;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , d , f[2][N] , a[N] , t[2] , x , y , minn = inf , ans , u;

struct kk { int val , pos; } q[N];

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read() , d = read();
	for ( int i = 1 ; i <= m ; i ++ )
	{
		u = i % 2;
		x = read() , y = read() , t[u] = read();//u是当前这一个
		int len = ( t[u] - t[u^1] ) * d;
		memset ( f[u] , 0x3f , sizeof f[u] );
		ans += y;
		int head = 1 , tail = 0;
		for ( int j = 1 ; j <= n ; j ++ )
		{
			while ( head <= tail && q[head].val >= f[u^1][j] ) tail --;
			q[++tail] = { f[u^1][j] , j };
			while ( head <= tail && q[head].pos < j - len ) head ++;
			f[u][j] = min ( f[u][j] , q[head].val + abs ( x - j ) );
		}
		head = 1 , tail = 0;
		for ( int j = n ; j ; j -- )
		{
			while ( head <= tail && q[head].val >= f[u^1][j] ) tail --;
			q[++tail] = { f[u^1][j] , j };
			while ( head <= tail && q[head].pos > j + len ) head ++;
			f[u][j] = min ( f[u][j] , q[head].val + abs ( x - j ) );
		}
	}
	for ( int i = 1 ; i <= n ; i ++ ) minn = min ( minn , f[m%2][i] );
	cout << ans - minn << endl;
	return 0;
}

E. 【例题5】跳房子

我们可以看出答案\(g\)具有单调性 因为如果花\(g\)的钱就能达成目标 那么花更多的钱是一定不会使它达不成目标 所以考虑二分答案

设置\(f[i]\)表示到第\(i\)个格子的最大价格 那么\(f[i]=max(f[k])+s[i](i-maxlen\le k\le i-minlen)\)

那么使用单调队列优化即可

注意不能跳到的点需要赋值为\(-inf\) 而且每次要**推进去一批前置状态 **而不是传统的一个

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid ((l+r)>>1)
#define int long long 
const int N = 1e6 + 10;
const int inf = 0x3f3f3f3f3f3f3f3f;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , d , k , x[N] , s[N] , sum , f[N];

struct kk { int val , pos; } q[N];

int check ( int g )
{
	memset ( f , 0 , sizeof f );
	int minlen = max ( 1ll , d - g ) , maxlen = d + g;//跳跃距离
	int head = 1 , tail = 0 , j = 0;
	for ( int i = 1 ; i <= n ; i ++ )
	{
		for ( j ; x[j] <= x[i] - minlen ; j ++ )
		{
			if ( f[j] == -inf ) continue;
			while ( head <= tail && q[tail].val <= f[j] ) tail --;
			q[++tail] = { f[j] , x[j] };
		}
		while ( head <= tail && q[head].pos < x[i] - maxlen ) head ++;
		if ( head <= tail ) f[i] = q[head].val + s[i];
		else f[i] = -inf;
		if ( f[i] >= k ) return 1;
	}
	return 0;
}

signed main ()
{
//	ios::sync_with_stdio(false);
//	cin.tie(0) , cout.tie(0);
	n = read() , d = read() , k = read();
	for ( int i = 1 ; i <= n ; i ++ ) 
	{
		x[i] = read() , s[i] = read();
		if ( s[i] > 0 ) sum += s[i];
	}
	if ( sum < k ) { cout << -1 << endl; return 0; }
	int l = 0 , r = x[n];
	while ( l <= r )
	{
		if ( check ( mid ) ) r = mid - 1;
		else l = mid + 1;
	}
	cout << l << endl;	
	return 0;
}
posted @ 2023-07-02 16:58  Echo_Long  阅读(69)  评论(0编辑  收藏  举报