从这几篇博客学习的:
DP优化小技巧(单调队列/单调栈)
(单调队列优化DP) 代码源每日一题 Div1 选元素(数据加强版)
算法学习笔记(67): 单调栈
牛客多校第九场I (单调栈优化dp/单调栈的常用套路)


主要思想:
假设我们需要维护长度为kkk的区间最大值,遍历过程中,对于一个数字ai+1a_{i+1}ai+1,如果ai+1>=aia_{i+1} >= a_iai+1>=ai,那么我们完全可以把aia_iai的影响忽略掉。因为后面的数字比你并且生命周期还比你大,所以最大值永远不可能取到aia_iai。具体在队列中的做法就是不断访问队尾元素,小于等于当前值就出队,结束后将当前元素入队。这样的话就可以保证队首元素一定是长度为kkk的区间的最大值。(当然你需要判断队首元素与当前位置距离与kkk的大小关系确定队首元素是否需要出队)。这个过程可以用deque很容易实现,数组模拟也很容易。
AC代码:
deque:
#include using namespace std;int n, k;
int ar[1000050];
deque q;int main()
{scanf("%d%d", &n, &k);for(int i = 1; i <= n; ++i) scanf("%d", &ar[i]);for(int i = 1; i <= n; ++i){while(!q.empty() && q.front() + k <= i) q.pop_front();while(!q.empty() && ar[q.back()] >= ar[i]) q.pop_back();q.push_back(i);if(i >= k) printf("%d ", ar[q.front()]);}putchar('\n');q.clear();for(int i = 1; i <= n; ++i){while(!q.empty() && q.front() + k <= i) q.pop_front();while(!q.empty() && ar[q.back()] <= ar[i]) q.pop_back();q.push_back(i);if(i >= k) printf("%d ", ar[q.front()]);}return 0;
}
数组模拟:
#include using namespace std;int n, k;
int ar[1000050];
int p[1000050];
int l, r;//左闭右开int main()
{scanf("%d%d", &n, &k);for(int i = 1; i <= n; ++i) scanf("%d", &ar[i]);l = 0;r = 1;p[0] = 1;if(k == 1) printf("%d ", ar[1]);for(int i = 2; i <= n; ++i){if(i - p[l] >= k && (l < r)) l++;while(r > l && ar[p[r - 1]] >= ar[i]) r--;p[r++] = i;if(i >= k) printf("%d ", ar[p[l]]);}putchar('\n');l = 0;r = 1;p[0] = 1;if(k == 1) printf("%d ", ar[1]);for(int i = 2; i <= n; ++i){if(i - p[l] >= k && (l < r)) l++;while(r > l && ar[p[r - 1]] <= ar[i]) r--;p[r++] = i;if(i >= k) printf("%d ", ar[p[l]]);}putchar('\n');return 0;
}
当我们为了实现给动态规划的复杂度降维的时候,通常就需要单调栈/队列,通常用来维护前面状态下可以取到的最大值或者最小值,然后直接进行转移。(ygg)
首先,我们可以看数据没加强版


题意:
就是对与每个长度为为kkk的子区间,至少要有一个数被选择,一共可以选择xxx个数字,目标是让选择的xxx个数字最大。
分析:
考虑dp,定义dp[i][j]表示选了ar[i]的情况下,前iii个数字一共有jjj个被选的情况下的最大值。
状态转移方程:dp[i][j]=max(dp[i][j],dp[i−k,i−1][j−1]+ar[i])dp[i][j] =max(dp[i][j],dp[i-k,i-1][j-1]+ar[i])dp[i][j]=max(dp[i][j],dp[i−k,i−1][j−1]+ar[i])。复杂度O(n3)O(n^3)O(n3)。
AC代码:
#include using namespace std;
typedef long long ll;
#define int llint n, k, x;
int ar[205];
int dp[205][205];
int ans;signed main()
{scanf("%lld%lld%lld", &n, &k, &x);for(int i = 1; i <= n; ++i) scanf("%lld", &ar[i]);memset(dp, 128, sizeof(dp));//cout << dp[0][0] << '\n';dp[0][0] = 0;for(int i = 1; i <= n; ++i){for(int j = 1; j <= x; ++j){for(int p = max(0ll, i - k); p < i; ++p){dp[i][j] = max(dp[i][j], dp[p][j - 1] + ar[i]);}//cout << i << ' ' << j << ' ' << dp[i][j] << '\n';}}ans = -1;for(int i = n - k + 1; i <= n; ++i) ans = max(ans, dp[i][x]);printf("%lld\n", ans);return 0;
}
对于数据加强版,n,k,xn,k,xn,k,x的取值达到2500,O(n3)O(n^3)O(n3)一定超时。我们考虑优化,对于原来的状态转移式。dp[i][j]=max(dp[i][j],dp[i−k,i−1][j−1]+ar[i])dp[i][j] =max(dp[i][j],dp[i-k,i-1][j-1]+ar[i])dp[i][j]=max(dp[i][j],dp[i−k,i−1][j−1]+ar[i]),考虑以极快的速度求出max(dp[i−k,i−1][j−1])max(dp[i-k,i-1][j-1])max(dp[i−k,i−1][j−1])。本质上一个窗口大小为kkk的滑动窗口,故利用单调队列优化即可。复杂度O(n2)O(n^2)O(n2)。
AC代码:
#include using namespace std;
typedef long long ll;int n, k, x, top;
ll ar[2550];
ll dp[2550][2550];
ll ans;int main()
{scanf("%d%d%d", &n, &k, &x);for(int i = 1; i <= n; ++i) scanf("%lld", &ar[i]);memset(dp, 128, sizeof(dp));//cout << dp[0][0] << '\n';dp[0][0] = 0;for(int j = 1; j <= x; ++j){deque q;q.push_back(0);for(int i = 1; i <= n; ++i){while(!q.empty() && q.front() < i - k) q.pop_front();dp[i][j] = dp[q.front()][j - 1] + ar[i];while(!q.empty() && dp[q.back()][j - 1] <= dp[i][j - 1]) q.pop_back();q.push_back(i);}}ans = -1;for(int i = n - k + 1; i <= n; ++i) ans = max(ans, dp[i][x]);printf("%lld\n", ans);return 0;
}


题意:
输入n,q,pn,q,pn,q,p,之后一行nnn个数字,之后qqq行,qqq次询问。
你现在在玩一个游戏,初始在0点,你只可以向右走,游戏有很多关,对于第xxx关,你只能走到iii的倍数的点上,并且每步跨越的最大距离是ppp,每个点上有数字,当你到达这个点是,你的数值就会加上该点的权值,初始时你的数值为0,通关条件是到达点n+1n+1n+1及其之后的点,并且n+1n+1n+1及其之后的点的数值都为0,走到n+1n+1n+1之后的点不需要考虑他们是否是xxx的倍数,只需要考虑最大跨越距离的限制。你可以在不违规的前提下走任意多步,qqq次询问,问你对于第xxx关,通关时的最大数值。
分析:
考虑dp,定义dp[i]表示,对于第xxx关,走到第iii个能到达的点时所能获得的最大价值。
转移方程:dp[i]=max(dp[i],dp[i−p/x,i−1]+ar[i])dp[i] = max(dp[i], dp[i-p/x,i-1]+ar[i])dp[i]=max(dp[i],dp[i−p/x,i−1]+ar[i]) (不考虑Noob的情况)。
如果暴力跑,复杂度为O(TLE)O(TLE)O(TLE),依旧考虑单调队列优化取maxmaxmax的部分。复杂度O(nlogn)O(nlogn)O(nlogn)。
AC代码:
#include using namespace std;
typedef long long ll;
#define io ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)const ll inf = 0x3f3f3f3f3f3f3f3f;
int n, p, q, x;
ll ans[1000050];
int ar[1000050];
ll dp[1000050];
vector vt[1000005];
int qq[1000050];
int l, r;int main()
{io;cin >> n >> q >> p;for(int i = 1; i <= n; ++i) cin >> ar[i];for(int i = 1; i <= n; ++i){ans[i] = -inf;for(int j = 0; j <= n; j += i) vt[i].push_back(j);vt[i].push_back(n + 1);}while(q--){cin >> x;if(ans[x] != -inf) cout << ans[x] << '\n';else if(x > p) cout << "Noob\n";else{dp[0] = 0;l = r = 1;qq[r] = 0;for(int i = 1; i < vt[x].size(); ++i){while(l <= r && vt[x][i] - qq[l] > p) ++l;dp[vt[x][i]] = dp[qq[l]] + ar[vt[x][i]];while(l <= r && dp[qq[r]] <= dp[vt[x][i]]) --r;qq[++r] = vt[x][i];}ans[x] = dp[n + 1];cout << ans[x] << '\n';}}return 0;
}

题意:
很类似上一个题,只不过跳的方式有所不同,是给定一个l,rl,rl,r,对于当前点iii,可以跳到[i+l,i+r][i+l,i+r][i+l,i+r]
AC代码:
#include using namespace std;const int inf = 0x3f3f3f3f;
int n, l, r;
int ar[600050];
int dp[600050];
deque q;int main()
{scanf("%d%d%d", &n, &l, &r);for(int i = 0; i <= n; ++i){scanf("%d", &ar[i]);dp[i] = -inf;}dp[0] = ar[0];q.push_back(0);dp[l] = ar[l];//3*n是我乱写的,反正不会超时,后面那一坨的dp值都是相同的。for(int i = l + 1; i <= 3 * n; ++i){while(!q.empty() && q.front() + r < i) q.pop_front();while(!q.empty() && dp[q.back()] <= dp[i - l]) q.pop_back();q.push_back(i - l);dp[i] = dp[q.front()] + ar[i];//cout << i << ' ' << dp[i] << '\n';}printf("%d\n", dp[3 * n]);return 0;
}
两大用处:
1.NGE问题(Next Greater Element),也就是,对序列中每个元素,找到下一个比它大的元素。
2.两元素间所有元素均(不)大/小于这两者


(其实也可以用单调队列,不需要队首元素出队的单调队列)
AC代码:
#include using namespace std;int n;
int ar[3000050];
stack st;
int ans[3000050];int main()
{scanf("%d", &n);for(int i = 1; i <= n; ++i) scanf("%d", &ar[i]);ans[n] = 0;st.push(n);for(int i = n - 1; i > 0; --i){while(!st.empty() && ar[st.top()] <= ar[i]) st.pop();if(!st.empty()) ans[i] = st.top();else ans[i] = 0;st.push(i);}for(int i = 1; i <= n; ++i) printf("%d ", ans[i]);putchar('\n');return 0;
}


(区间1~i内元素大小关系和单调栈内元素情况)


AC代码:
#include using namespace std;
typedef long long ll;
#define int llint n;
int ar[500050];
stack > st;
pair pi;
int ans, cnt;signed main()
{scanf("%lld", &n);for(int i = 1; i <= n; ++i) scanf("%lld", &ar[i]);st.push({ar[1], 1});for(int i = 2; i <= n; ++i){int cnt = 1;while(!st.empty() && st.top().first <= ar[i]){pi = st.top();st.pop();//cout << pi.first << ' ' << pi.second << '\n';ans += pi.second;if(pi.first == ar[i]) cnt = pi.second + 1;else cnt = 1;}if(!st.empty()) ++ans;st.push({ar[i], cnt});//cout << i << ' ' << ans << '\n';}printf("%lld\n", ans);return 0;
}


题意:
输入一个nnn,之后输入nnn个数字ar[i]ar[i]ar[i]。
让你选择一个点作为山峰。假设选择点pospospos作为山峰,其他的n−1n-1n−1点的高度将发生变化。假设修改后的高度记为h[i]h[i]h[i],对于1~pos-1中的一个点i,必须满足h[i]=min(h[i+1],ar[i])h[i]=min(h[i+1],ar[i])h[i]=min(h[i+1],ar[i]),对于pos+1~n中的一个点i,必须满足h[i]=min(h[i−1],ar[i])h[i] = min(h[i-1],ar[i])h[i]=min(h[i−1],ar[i])。求∑1nh[i]\sum_1^n h[i]∑1nh[i]的最大值。输出取最大值时的h[i]数组。
分析:
首先,我们可以看一下easy版本。区别在于n的大小。easy版本n最大1000。O(n2)O(n^2)O(n2)能过。我们考虑dp,定义dp1[i],dp2[i]分别表示选择iii作为山峰时iii极其左侧的最大值。答案就是max(dp1[i]+dp2[i]−ar[i])max(dp1[i]+dp2[i]-ar[i])max(dp1[i]+dp2[i]−ar[i])。
我们考虑快速求dp1[i]的方法,假设ar[j]是iii之前第一个满足ar[j]<=ar[i]的元素,那么dp1[i]=dp1[j]+(i−j)∗ar[i]dp1[i]=dp1[j]+(i-j)*ar[i]dp1[i]=dp1[j]+(i−j)∗ar[i]。而对于第一个小于等于的元素,显然可以利用单调栈快速找出。(dp2求法同理)复杂度O(n)O(n)O(n)。
(单调栈用法1,利用单调栈找到区间内第一个比他大/小的元素)
AC代码:
#include using namespace std;
typedef long long ll;int n, pos;
ll ar[500050];
ll dp1[500050];
ll dp2[500050];
ll ans[500050];
ll mx;
stack st;void work()
{int tmp = pos;ll mxx = ar[tmp];ans[tmp] = ar[tmp];while(tmp > 0){--tmp;ans[tmp] = min(mxx, ar[tmp]);mxx = ans[tmp];}tmp = pos;mxx = ar[tmp];while(tmp < n){++tmp;ans[tmp] = min(mxx, ar[tmp]);mxx = ans[tmp];}for(int i = 1; i <= n; ++i) printf("%lld ", ans[i]);putchar('\n');
}int main()
{scanf("%d", &n);for(int i = 1; i <= n; ++i) scanf("%lld", &ar[i]);st.push(0);for(int i = 1; i <= n; ++i){while(!st.empty() && ar[st.top()] >= ar[i]) st.pop();dp1[i] = dp1[st.top()] + (i - st.top()) * ar[i];st.push(i);}while(!st.empty()) st.pop();st.push(n + 1);for(int i = n; i > 0; --i){while(!st.empty() && ar[st.top()] >= ar[i]) st.pop();dp2[i] = dp2[st.top()] + (st.top() - i) * ar[i];st.push(i);}for(int i = 1; i <= n; ++i){if(dp1[i] + dp2[i] - ar[i] > mx){mx = dp1[i] + dp2[i] - ar[i];pos = i;}}work();return 0;
}

(295是正解,换成解绑之后的cin,cout用线段树暴力维护居然卡过了)。


题意:
输入一个n,之后n个数字ar[i]。
一开始你在位置1,你要到达位置n,从位置i到位置j必须满足一下条件之一。

问你到达n最少需要跳几步。
分析:
单调栈的用法2,找两元素间所有元素均(不)大/小于这两者

(区间1~i内元素大小关系和单调栈内元素情况)
借助这个图理解和P1823 [COI2007] Patrik 音乐会的等待这个题理解一下。
AC代码:
#include using namespace std;int n;
stack st1, st2;
int dp[300050];
int ar[300050];int main()
{scanf("%d", &n);for(int i = 1; i <= n; ++i) scanf("%d", &ar[i]);memset(dp, 0x3f, sizeof(dp));dp[1] = 0;for(int i = 1; i <= n; ++i){dp[i] = min(dp[i], dp[i - 1] + 1);while(!st1.empty() && ar[st1.top()] <= ar[i]){int pos = st1.top();st1.pop();if(!st1.empty() && ar[i] > ar[pos]) dp[i] = min(dp[st1.top()] + 1, dp[i]);}st1.push(i);while(!st2.empty() && ar[st2.top()] >= ar[i]){int pos = st2.top();st2.pop();if(!st2.empty() && ar[i] < ar[pos]) dp[i] = min(dp[st2.top()] + 1, dp[i]);}st2.push(i);}printf("%d\n", dp[n]);return 0;
}



题意:
输入一个n,之后输入数组ar[n]。你需要将区间[1,n]分成k个子区间,这k个区间两两不交,且并集是全集。每个子区间的值是这个区间中最大的那个数字,你需要最小化这k个区间的值的和。输出k取1~n时对应的答案。
分析:
官方题解:


说人话:

AC代码:
#include using namespace std;const int inf = 0x3f3f3f3f;
int n;
int ar[8005];
int dp[8005][8005];
struct node
{int val, mi, mi_dp;//ar[k],dp[k][j - 1],min(dp[k][j])
}st[8005];int main()
{scanf("%d", &n);for(int i = 1; i <= n; ++i) scanf("%d", &ar[i]);memset(dp, 0x3f, sizeof(dp));dp[0][0] = 0;for(int j = 1; j <= n; ++j){int top = 1;st[top] = {inf, inf, inf};for(int i = 1; i <= n; ++i){int mi = dp[i - 1][j - 1];while(top >= 1 && st[top].val <= ar[i]) mi = min(mi, st[top--].mi);st[top + 1] = {ar[i], mi, min(mi + ar[i], st[top].mi_dp)};++top;dp[i][j] = st[top].mi_dp;}printf("%d\n", dp[n][j]);}return 0;
}