1. 程式人生 > >動態規劃之單調佇列優化專題【附題目練習清單】

動態規劃之單調佇列優化專題【附題目練習清單】

什麼是單調(雙端)佇列

單調佇列,顧名思義,就是一個元素單調的佇列,那麼就能保證隊首的元素是最小(最大)的,從而滿足動態規劃的最優性問題的需求。
單調佇列,又名雙端佇列。雙端佇列,就是說它不同於一般的佇列只能在隊首刪除、隊尾插入,它能夠在隊首、隊尾同時進行刪除。

【單調佇列的性質】

一般,在動態規劃的過程中,單調佇列中每個元素一般儲存的是兩個值:
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;
}