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\)时注意三个策略:
- 弹队尾时的策略:这里的弹队尾策略是先按照f[i]的最小值 再按照树的高度
- 状态合法的策略:因为状态合法时才可以转移 所以转移之前需要判断\(head\le tail\)
- 边界条件的策略:因为这次的\(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;
}