動態規劃之單調佇列優化專題【附題目練習清單】
什麼是單調(雙端)佇列
單調佇列,顧名思義,就是一個元素單調的佇列,那麼就能保證隊首的元素是最小(最大)的,從而滿足動態規劃的最優性問題的需求。
單調佇列,又名雙端佇列。雙端佇列,就是說它不同於一般的佇列只能在隊首刪除、隊尾插入,它能夠在隊首、隊尾同時進行刪除。
【單調佇列的性質】
一般,在動態規劃的過程中,單調佇列中每個元素一般儲存的是兩個值:
1.在原數列中的位置(下標)
2.他在動態規劃中的狀態值
而單調佇列則保證這兩個值同時單調。
【單調佇列有什麼用】
我們來看這樣一個問題:一個含有n項的數列(n<=2000000),求出每一項前面的第m個數到它這個區間內的最小值。
這道題目,我們很容易想到線段樹、或者st演算法之類的RMQ問題的解法。但龐大的資料範圍讓這些對數級的演算法沒有生存的空間。我們先嚐試用動態規劃的方法。用代表第個數對應的答案,表示第個數,很容易寫出狀態轉移方程:
這個方程,直接求解的複雜度是O(nm)的,甚至比線段樹還差。這時候,單調佇列就發揮了他的作用:
我們維護這樣一個佇列:佇列中的每個元素有兩個域{position,value},分別代表他在原佇列中的位置和,我們隨時保持這個佇列中的元素兩個域都單調遞增。
那計算的時候,只要在隊首不斷刪除,直到隊首的position大於等於,那此時隊首的value必定是的不二人選,因為佇列是單調的!
我們看看怎樣將 插入到佇列中供別人決策:首先,要保證position單調遞增,由於我們動態規劃的過程總是由小到大(反之亦然),所以肯定在隊尾插入。又因為要保證佇列的value單調遞增,所以將隊尾元素不斷刪除,直到隊尾元素小於。
【時間效率分析】
很明顯的一點,由於每個元素最多出隊一次、進隊一次,所以時間複雜度是O(n)。用單調佇列完美的解決了這一題。
【為什麼要這麼做】
我們來分析為什麼要這樣在隊尾插入:為什麼前面那些比大的數就這樣無情的被槍斃了?我們來反問自己:他們活著有什麼意義?!由於是隨著單調遞增的,所以對於存在j<i,a[j]>a[i],在計算任意一個狀態的時候,都不會比優,所以j被槍斃是“罪有應得”。
我們再來分析為什麼能夠在隊首不斷刪除,一句話:是隨著單調遞增的!
【一些總結】
對於這樣一類動態規劃問題,我們可以運用單調佇列來解決:
其中bound[x]隨著x單調不降,而const[i]則是可以根據i在常數時間內確定的唯一的常數。這類問題,一般用單調佇列在很優美的時間內解決。
【Jzoj1771】烽火傳遞
【問題描述】
烽火臺又稱烽燧,是重要的軍事防禦設施,一般建在險要或交通要道上。一旦有敵情發生,白天燃燒柴草,定代價。 為了使情報準確地傳遞,在連續m個烽火臺中至少要有一個發出訊號。請計算總共最少花費多少代價,才能使敵軍來襲之時,情報能在這兩座城市之間準確傳遞。
【輸入格式】
第一行:兩個整數N,M。其中N表示烽火臺的個數,M表示在連續m個烽火臺中至少要有一個發出訊號。 接下來N行,每行一個數Wi,表示第i個烽火臺發出訊號所需代價。
【輸出格式】
一行,表示答案。
【輸入樣例1】
5 3 1 2 5 6 2
【輸出樣例1】
4
【解題思路】
設f[i]表示i必須選時最小代價。 初值: f[0]=0 f[1..n]=∞ 方程: f[i]=min(f[j])+w[i] 並且max(0,i-m)≤j<i 為什麼j有這樣的範圍?如果j能更小,那麼j~i這段區間中將有不符合條件的子區間,就會錯。應保證不能有縫隙。 最後在f[n-m+1..n]中取最小值即答案 , 時間複雜度O(nm)
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m;
int w[100001];
int que[100001],head=0,tail=0;
int f[100001];
int main()
{
scanf("%d%d",&n,&m);
int i,j;
for (i=1;i<=n;++i)
scanf("%d",&w[i]);
memset(f,127,sizeof f);
f[0]=0;
que[0]=0;
for (i=1;i<=n;++i)
{
if (que[head]<i-m)
++head;//將超出範圍的隊頭刪掉
f[i]=f[que[head]]+w[i];//轉移(用隊頭)
while (head<=tail && f[que[tail]]>f[i])
--tail;//將不比它優的全部刪掉
que[++tail]=i;//將它加進隊尾
}
int ans=0x7f7f7f7f;
for (i=n-m+1;i<=n;++i)
ans=min(ans,f[i]);
printf("%d\n",ans);
}
【Tyvj1305】最大最大子序和
【問題描述】
輸入一個長度為n的整數序列,從中找出一段不超過M的連續子序列,使得整個序列的和最大。 例如 1,-3,5,1,-2,3 當m=4時,S=5+1-2+3=7; 當m=2或m=3時,S=5+1=6。
【輸入格式】
第一行兩個數n,m; 第二行有n個數,要求在n個數找到最大子序和。
【輸出格式】
一個數,數出他們的最大子序和。
【輸入樣例】
6 4 1 -3 5 1 -2 3
【輸出樣例】
7
【資料範圍】
n,m≤300000;數列元素的絕對值≤1000。
【解題思路】
這是一個典型的動態規劃題目,不難得出一個1D/1D方程: f(i) = sum[i]-min{sum[k]|i-M≤k≤i} 由於方程是1D/1D的,所以我們不想只得出簡單的Θ(n^2)演算法。不難發現,此優化的難點是計算min{sum[i-M]..sum[i-1]}。在上面的連結中,我們成功的用Θ(nlgn)的演算法解決了這個問題。但如果資料範圍進一步擴大,運用st表解決就力不從心了。所以我們需要一種更高效的方法,即可以在Θ(n)的攤還時間內解決問題的單調佇列。 單調佇列(Monotone queue)是一種特殊的優先佇列,提供了兩個操作:插入,查詢最小值(最大值)。它的特殊之處在於它插入的不是值,而是一個指標(key)(wiki原文:imposes the restriction that a key (item) may only be inserted if its priority is greater than that of the last key extracted from the queue)。所謂單調,指當一組資料的指標1..n(優先順序為A1..An)插入單調佇列Q時,佇列中的指標是單調遞增的,佇列中指標的優先順序也是單調的。因為這裡要維護優先順序的最小值,那麼佇列是單調減的,也說佇列是單調減的。 查詢最小值 由於優先順序是單調減的,所以最小值一定是隊尾元素。直接取隊尾即可。 插入操作: 當一個數據指標i(優先順序為Ai)插入單調佇列Q時,方法如下: 1.如果佇列已空或隊頭的優先順序比Ai大,刪除隊頭元素。 2.否則將i插入隊頭 比如說,一個優先佇列已經有優先順序分別為 {5,3,-2} 的三個元素,插入一個新元素,優先順序為2,操作如下: 1.因為2 < 5,刪除隊頭,{3,-2} 2.因為2 < 3,刪除隊頭,{-2} 3.因為2 > -2,插入隊頭,{2,-2} 證明性質可以得到維護 證明指標的單調減 :由於插入指標i一定比已經在佇列中所有元素大,所以指標是單調減的。 證明優先順序的單調減:由於每次將優先順序比Ai大的刪除,只要原佇列優先順序是單調的,新佇列一定是單調的。用迴圈不變式易證正確性。 為什麼刪除隊頭:直觀的,指標比i小(靠左)而優先順序比Ai大的資料沒有希望成為任何一個需要的子序列中的最小值。這一點是我們使用優先佇列的根本原因。 維護區間大小 當一串資料A1..Ak插入時,得到的最小值是A1..Ak的最小值。反觀dp方程: f(i) = sum[i]-min{sum[k]|i-M≤k≤i} 1 在這裡,A = sum。對於f(i),我們需要的其實是Ai-M .. Ai的最小值,而不是所有已插入資料的最小值(A1..Ai-1)。所以必須維護區間大小,使佇列中的元素嚴格處於Ai-M..Ai-1這一區間,或者說刪去哪些A中過於靠前而違反題目條件的值。由於佇列中指標是單調的,也就是靠左的指標大於靠右的,或者說在優先佇列中靠左的值,在A中一定靠後;優先佇列中靠右的值,在A中一定靠前。我們想要刪除過於靠前的,只需要在優先佇列中從右一直刪除,直到最右邊(隊尾)的值符合條件。具體地:當隊頭指標p滿足i-m≤p時。 形象地說,就是忍痛割愛刪去哪些較好但是不符合題目限制的資料。
#include <iostream>
#include <list>
#include <cstdio>
using namespace std;
int n, m;
long long s[300005];
// 字首和
list<int> queue;
// 連結串列做單調佇列
int main() {
cin >> n >> m;
s[0] = 0;
for (int i=1; i<=n; i++) {
cin >> s[i];
s[i] += s[i-1];
}
long long maxx = 0;
for (int i=1; i<=n; i++) {
while (!queue.empty() and s[queue.front()] > s[i])
queue.pop_front();
// 保持單調性
queue.push_front(i);
// 插入當前資料
while (!queue.empty() and i-m > queue.back())
queue.pop_back();
// 維護區間大小,使i-m >= queue.back()
if (i > 1)
maxx = max(maxx, s[i] - s[queue.back()]);
else
maxx = max(maxx, s[i]);
// 更新最值
}
cout << maxx << endl;
return 0;
}
【Vijos1243】生產產品
時間限制:1S / 空間限制:256MB
【問題描述】
產品的生產需要M個步驟,每一個步驟都可以在N臺機器中的任何一臺完成,但生產的步驟必須嚴格按順序執行。由於這N臺機器的效能不同,它們完成每一個步驟的所需時間也不同。機器i完成第j個步驟的時間為T[i,j]。把半成品從一臺機器上搬到另一臺機器上也需要一定的時間K。同時,為了保證安全和產品的質量,每臺機器最多隻能連續完成產品的L個步驟。也就是說,如果有一臺機器連續完成了產品的L個步驟,下一個步驟就必須換一臺機器來完成。 請計算最短需要多長時間。
【輸入格式】
第一行有四個整數M, N, K, L; 接下來N行,每行有M個整數。第I+1行的第J個整數為T[J,I]。
【輸出格式】
輸出只有一行,表示需要的最短時間。
【輸入樣例1】
3 2 0 2 2 2 3 1 3 1
【輸出樣例1】
4
【資料範圍】
對於50%的資料,N≤5,L≤4,M≤10000 對於100%的資料,N≤5, L≤50000,M≤100000
【解題思路】
轉移方程為: f[i][j]=min( f[t][p]+sum[j][i]-sum[p][j]) 化簡後可以得到 f[i][j]=min( f[t][p]-sum[p][j])+sum[j][i] 對於每一個j考慮開一個單調佇列優化 ,維護 t和f[t][p]-sum[p][j]單調遞增。這樣每次從隊首取出符合要求的一個即可更新. q[a][x][0]表示佇列中這個位置的的t q[a][x][1]表示這個位置的p 每次先更新所有的f[i][j]然後再更新所有的佇列q[k]
#include <bits/stdc++.h>
using namespace std;
const int N=100010,INF=2099999999;
int q[10][N][2],l[10],r[10];
int m,n,cost,L,sum[10][N],f[N][10],ans=INF;
int main(){
scanf("%d%d%d%d",&m,&n,&cost,&L);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) scanf("%d",&sum[i][j]),sum[i][j]+=sum[i][j-1];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) f[j][i]=INF;
for(int i=1;i<=n;i++) q[i][ r[i]++ ][0]=0;
for(int i=1;i<=m;i++){
for(int k=1;k<=n;k++){
while(l[k]<r[k] && i-q[k][ l[k] ][0]>L) l[k]++;
int t=q[k][ l[k] ][0],p=q[k][ l[k] ][1];
f[i][k]=min(f[i][k],f[t][p]+sum[k][i]-sum[k][t]+cost);
}
for(int k=1;k<=n;k++)
for(int j=1;j<=n;j++)//用f[i][k] 的值來更新 q[j];
if(j!=k) {
while(l[j]<r[j] && f[i][k]-sum[j][i]<= f[ q[j][r[j]-1][0] ][q[j][r[j]-1][1]] - sum[j][q[j][r[j]-1][0]]) r[j]--;
q[j][r[j]][0]=i;q[j][r[j]++][1]=k;
}
}
for(int i=1;i<=n;i++) ans=min(ans,f[m][i]);
printf("%d",ans-cost);
return 0;
}
【Hdu3530】Subsequence
【問題描述】
給定一個包含n個整數序列,求滿足條件的最長區間的長度:該區間內的最大數和最小數的差不小於m,且不大於k。
【輸入格式】
輸入包含多組測試資料:對於每組測試資料: 第一行,包含三個整數n,m和k; 第二行,包含n個整數的序列。
【輸出格式】
對於每組測試資料,輸出滿足條件的最長區間的長度。
【輸入樣例】
5 0 0 1 1 1 1 1 5 0 3 1 2 3 4 5
【輸出樣例】
5 4
【資料範圍】
1≤n≤100000; 0≤m,k≤100000; 0≤ai≤100000
【解題思路】
用兩個單調佇列分別維護a[i]前元素中的最大值與最小值的下標,top為最值。 然後當最值之差過大時,a[i]的滿足題意的最長字串為最最後操作last與i的距離 其中last取離i最遠的一個。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
inline int max(int a ,int b){return a>b?a:b;}
const int N = 100010;
int s1[N],s2[N];
int a[N];
int main()
{
int n,m,k,top1,top2,last1,last2,tail1,tail2,ans;
while(scanf("%d%d%d",&n,&m,&k)!=EOF)
{
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
memset(s1,0,sizeof(s1));
memset(s2,0,sizeof(s2));
top1=0;top2=0;tail1=0;tail2=0;ans=0;last1=0;last2=0;
for(int i=1;i<=n;i++)
{
//max
while(top1<tail1&&a[s1[tail1-1]]<=a[i])tail1--; //top1最大元素
s1[tail1++]=i;
//min
while(top2<tail2&&a[s2[tail2-1]]>=a[i])tail2--; //top2最小元素
s2[tail2++]=i;
while(a[s1[top1]]-a[s2[top2]]>k)
{
if(s1[top1]<s2[top2])
last1=s1[top1++];
else last2=s2[top2++];
}
if(a[s1[top1]]-a[s2[top2]]>=m)
{
ans=max(ans,i-max(last1,last2));
}
}
cout<<ans<<endl;
}
return 0;
}
【Hdu3401】Subsequence
【問題描述】
知道之後n天的股票買賣價格(APi,BPi),以及每天股票買賣數量上限(ASi,BSi),問他最多能賺多少錢。開始時有無限本金,要求任兩次交易需要間隔W天以上,即第i天交易,第i+W+1天才能再交易。同時他任意時刻最多隻能擁有MaxP的股票。
【輸入格式】
第一行,一個整數t,表示有t組測試資料,對於每組測試資料: 第一行,包含三個整數T,MaxP和W,(0 ≤ W < T ≤ 2000, 1 ≤ MaxP ≤ 2000) 。 接下來T行,每行四個整數,APi,BPi,ASi,BSi( 1≤BPi≤APi≤1000,1≤ASi,BSi≤MaxP)。
【輸出格式】
對於每組測試資料,輸出一個整數,表示賺的最多的錢。
【輸入樣例】
1 5 2 0 2 1 1 1 2 1 1 1 3 2 1 1 4 3 1 1 5 4 1 1
【輸出樣例】
3
【解題思路】
易寫出DP方程 dp[i][j]=max{dp[i-1][j],max{dp[r][k]-APi[i]*(j-k)}(0
#include <bits/stdc++.h>
using namespace std;
#define MAX 2005
#define inf 0xfffff
#define max(a,b) ((a)>(b)?(a):(b))
int T,MaxP,W;
int APi[MAX],BPi[MAX],ASi[MAX],BSi[MAX];
int dp[MAX][MAX];//dp[i][j]第i天持有j股的最大值
//dp[i][j]=max{dp[i-1][j],max{dp[r][k]-APi[i]*(j-k)}(0<r<i-w,k<j),max{dp[r][k]+BPi[i]*(k-j)}(0<r<i-w,k>j)}
struct node
{
int x;//存dp[i-w-1][k]+APi[i]*k或dp[i-w-1][k]+BPi[i]*k
int p;//當前持股數
} q[2005],temp;
int front,back;
int main()
{
int cas;
scanf("%d",&cas);
for(; cas--;)
{
scanf("%d%d%d",&T,&MaxP,&W);
for(int i=1; i<=T; ++i)
scanf("%d%d%d%d",APi+i,BPi+i,ASi+i,BSi+i);
for(int i=0; i<=T; ++i)
for(int j=0; j<=MaxP; ++j)
dp[i][j]=-inf;
for(int i=1; i<=W+1; ++i)
for(int j=0; j<=ASi[i]; ++j)
dp[i][j]=(-APi[i]*j);
for(int i=2; i<=T; ++i)
{
for(int j=0; j<=MaxP; ++j)
dp[i][j]=max(dp[i][j],dp[i-1][j]);
if(i<=W+1) continue;
//買入
front=back=1;
for(int j=0; j<=MaxP; ++j)
{
temp.p=j;
temp.x=dp[i-W-1][j]+APi[i]*j;
for(;front<back&&q[back-1].x<temp.x;--back);
q[back++]=temp;
for(;front<back&&q[front].p+ASi[i]<j;++front);
dp[i][j]=max(dp[i][j],q[front].x-APi[i]*j);
}
//賣出
front=back=1;
for(int j=MaxP; j>=0; --j)
{
temp.p=j;
temp.x=dp[i-W-1][j]+BPi[i]*j;
for(;front<back&&q[back-1].x<temp.x;--back);
q[back++]=temp;
for(;front<back&&q[front].p-BSi[i]>j;++front);
dp[i][j]=max(dp[i][j],q[front].x-BPi[i]*j);
}
}
int ans=0;
for(int i=0;i<=MaxP;++i)
ans=max(ans,dp[T][i]);
printf("%d\n",ans);
}
return 0;
}
【Poj1742】Coins
【問題描述】
有n種不同面值的硬幣,面值分別為A1,A2,A3...An,對應的數量分別是C1,C2,C3...Cn,求能搭配出多少種不超過m的金額。
【輸入格式】
輸入包含多組測試資料,對於每組測試資料: 第一行,兩個整數,n和m;(1≤n≤100;m≤100000) 第二行,2*n個整數,一次表示A1,A2,A3...An,C1,C2,C3...Cn。(1≤Ai≤100000,1≤Ci≤1000) 輸入的最後用0 0表示結束。
【輸出格式】
對於每組測試資料,依次輸出一個整數。
【輸入樣例】
3 10 1 2 4 2 1 1 2 5 1 4 2 1 0 0
【輸出樣例】
8 4
【解題思路】
dp[i][j]= 用前i種硬幣能否湊成j 遞推關係式: dp[i][j] = (存在k使得dp[i – 1][j – k * A[i]]為真,0 < k < m 且下標合法
#include<bits/stdc++.h>
using namespace std;
bool dp[100 + 16][100000 + 16]; // dp[i][j] := 用前i種硬幣能否湊成j
int A[100 + 16];
int C[100 + 16];
int main(int argc, char *argv[])
{
int n, m;
while(cin >> n >> m && n > 0)
{
memset(dp, 0, sizeof(dp));
for (int i = 0; i < n; ++i)
{
cin >> A[i];
}
for (int i = 0; i < n; ++i)
{
cin >> C[i];
}
dp[0][0] = true;
for (int i = 0; i < n; ++i)
{
for (int j = 0; j <= m; ++j)
{
for (int k = 0; k <= C[i] && k * A[i] <= j; ++k)
{
dp[i + 1][j] |= dp[i][j - k * A[i]];
}
}
}
int answer = count(dp[n] + 1, dp[n] + 1 + m , true); // 總額0不算在答案內
cout << answer << endl;
}
return 0;
}
【Hdu4374】 One hundred layer
【問題描述】
有個遊戲叫“是男人就下100層”,規則如下: 1.開始時,你在第一層; 2.每一層被分成M個區間,你只能往一個方向走(左或者右),你也可以跳到下一層的同一個區間,比如你現在第y個區間,你將跳到下一層的第y個區間。(1≤y≤M); 3.你最多朝一個方向移動T個區間; 4.每個區間都有一個分數。最後的得分是你經過的各個區間的分數的總和。 求你可以得到的最大得分。
【輸入格式】
輸入包含多組測試資料,對於每組測試資料: 第一行,4個整數N, M, X, T(1≤N≤100, 1≤M≤10000, 1≤X, T≤M),其中N表示層數,M表示每層的區間數,開始時你在第X個區間,每層最多朝一個方向移動T個區間。 接下來N行,每行M個整數,依次表示每個區間的分數。 (-500≤score≤500)
【輸出格式】
對於每組測試資料輸出一行一個整數,表示最大得分。
【輸入樣例1】
3 3 2 1 7 8 1 4 5 6 1 2 3
【輸出樣例1】
29
【樣例說明】
8+7+4+5+2+3=29
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <queue>
using namespace std;
int dp[100+5][10000+5];
int num[100+5][10000+5];
int n,m,x,t;
struct node
{
int id,val;
node (int id=0,int val=0):id(id),val(val){}
};
void solve()
{
for(int i=0;i<=10000+4;i++) dp[0][i]=-0x3f3f3f3f;
dp[0][x]=0;
for(int i=1;i<=n;i++)
{
deque<node> Q;
for(int j=1;j<=m;j++)
{
int tem=dp[i-1][j]-num[i][j-1]; //從j出開始計算和的話,是減掉其前一個的
while(!Q.empty()&&tem>Q.back().val) Q.pop_back();
Q.push_back(node(j,tem));
while(!Q.empty()&&j-Q.front().id>t) Q.pop_front();
dp[i][j]=Q.front().val+num[i][j];
}
while(!Q.empty()) Q.pop_back();
for(int j=m;j>=1;j--)
{
int tem=dp[i-1][j]+num[i][j]; //逆向維護和正向的次序相反
while(!Q.empty()&&tem>Q.back().val) Q.pop_back();
Q.push_back(node(j,tem));
while(!Q.empty()&&Q.front().id-j>t) Q.pop_front();
dp[i][j]=max(dp[i][j],Q.front().val-num[i][j-1]);
}
}
int ans=dp[n][1];
for(int i=2;i<=m;i++) ans=max(ans,dp[n][i]);
printf("%d\n",ans);
}
int main()
{
while(scanf("%d%d%d%d",&n,&m,&x,&t)==4)
{
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
scanf("%d",&num[i][j]);
num[i][j]+=num[i][j-1];
}
solve();
}
return 0;
}
【CodeForces372C】 Watching Fireworks is Fun
【問題描述】
一個城鎮有n個區域,從左到右1編號為n,每個區域之間距離1個單位距離。節日中有m個煙火要放,給定放的地點a[i] 、時間t[i] ,如果你當時在區域x,那麼你可以獲得b[i] - | a[i] - x |的開心值。你每個單位時間可以移動不超過d個單位距離。你的初始位置是任意的(初始時刻為1),求你通過移動能獲取到的最大的開心值。
【輸入格式】
第一行包含3個整數n, m, d (1≤n≤150000; 1≤m≤300; 1≤d≤n). 接下來m行,每行包含3個整數, a[i] , b[i] , t[i] (1≤a[i] ≤n; 1≤b[i] ≤10^9; 1≤t[i] ≤10^9) 輸入保證t[i]≤t[i+1] (1≤i<m)
【輸出格式】
一行,一個整數,表示最大開心值。
【輸入樣例1】
50 3 1 49 1 1 26 1 4 6 1 10
【輸出樣例1】
-31
【輸入樣例2】
10 2 1 1 1000 4 9 1000 4
【輸出樣例2】
1992
【解題思路】
首先設dp[i][j]為到放第i個煙花的時候站在j的位置可以獲得的最大開心值。那麼我們可以很容易寫出轉移方程: dp[ i ] [ j ] =max(dp[ i - 1] [ k ]) + b[ i ] - | a[ i ] - j | ,其中 max(1,j-t*d)≤min(n,j+t*d) 。 不過我們可以發現b[ i ]是固定的,那麼我們轉化為求所有| a[ i ] - x |的最小值,即dp[ i ] [ j ] 表示到第i個煙花的時候站在j的位置可以獲得的最小的累加值,轉移方程: dp[ i ] [ j ] =min(dp[ i - 1] [ k ])+ | a[ i ] - j | ,其中 max(1,j-t*d)≤k≤min(n,j+t*d)。 由於是求一段區間的最小值,我們可以想到用單調佇列維護,維護一個單調升的佇列。不過這題有一點不同的是對於當前考慮的位置i來說其右端的點也需要考慮是否進入佇列,假設當前考慮位置i,所需維護區間長度為l,如果i+l≤n,那麼看他是否能丟進佇列。 還有一點需要注意,因為n、m都很大,所以直接開二維肯定炸記憶體,所以要用滾動陣列優化下。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN=150000+100;
const int inf=0x3fffffff;
#define L(x) (x<<1)
#define R(x) (x<<1|1)
int n,m,d,head,tail;
int a[MAXN],b[MAXN],t[MAXN];
ll dp[2][MAXN];
struct node
{
int index;
ll val;
}que[MAXN];
int main()
{
scanf("%d%d%d",&n,&m,&d);
ll ans=0;
for(int i=1;i<=m;i++){
scanf("%d%d%d",&a[i],&b[i],&t[i]);
ans+=b[i];
}
for(int i=1;i<=n;i++)
dp[0][i]=abs(a[1]-i);
int now=0;
ll k;//可以移動的最大距離
for(int j=2;j<=m;j++){
k=t[j]-t[j-1]; k*=d;
if(k>n) k=n;
head=tail=0;
for(int i=1;i<=k;i++){
while(head<tail && dp[now][i]<que[tail-1].val) tail--;
que[tail].val=dp[now][i]; que[tail++].index=i;
}
for(int i=1;i<=n;i++){
int l,r;
l=i-k;r=i+k;
if(l<=0) l=1;
while(head<tail && que[head].index<l) head++;
if(r<=n){
while(head<tail && dp[now][r]<que[tail-1].val) tail--;
que[tail].val=dp[now][r]; que[tail++].index=r;
}
dp[now^1][i]=que[head].val+abs(a[j]-i);
}
now^=1;
}
ll Min=dp[now][1];
for(int i=2;i<=n;i++)
Min=min(Min,dp[now][i]);
cout<<ans-Min<<endl;
return 0;
}