DP 單調佇列優化

2021-10-21 04:35:50 字數 3997 閱讀 5240

使用單調佇列優化的題目具有這樣的特點,他需要我們維持一段區間內的某個最優值,這個區間是隨著遍歷的順序變化的,但是其變化一定具有這樣的特性,也即維持的區間左右端點一定是單調遞增的,而不能出現回流的現象,否則我們在維持佇列單調性過程中剪枝的資料可能是新的區間中的最大值。

維持區間最優值的方法有很多,例如靜態演算法st演算法,動態更新區間最值的線段樹等等,對於區間左端點或者右端點不單調遞增的區間最值問題,我們一定只能使用線段樹優化,而不能使用單調佇列,我們也可以根據這個特性快速地判斷到底使用什麼方法優化dp問題。

當我們觀察到需要維持的區間的左右端點的變化趨勢後,如果發現左右端點隨著階段的遞增,也是不斷遞增的,那麼我們一定可以選擇使用單調佇列優化該dp問題。

對於乙個dp問題,當我們不知道該如何優化時,我們應該先寫出其樸素條件下的狀態轉移方程,再根據樸素狀態下的狀態轉移方程進行變化,找到乙個符合單調佇列優化式子即可知曉我們在單調佇列中需要維持什麼。

當然即使不用看題我們也知道,單調佇列中一定維持的是狀態中某一維的下標,但是我們按照什麼順序來維持這些下標,就是我們變化的目標了。通常來說使用單調佇列優化的問題,其狀態轉移方程中一定存在這樣乙個狀態轉移方程:

這裡l和r都是隨著i的不斷增加而單調增加的(可以維持原值不變,但不能遞減),而f(dp[j])則表示乙個之前階段的函式,這樣我們只需要實時地維持這段區間內的最值即可,當r遞增時,我們需要往佇列中插入元素,而當l遞增時我們需要刪除隊尾不合法的元素,這樣隊尾始終維持著區間[l,r]中的最大的f(dp[j])的j值。

圍欄

//每塊木板至多被粉刷一次

#include

using

namespace std;

int dp[

110]

[16010];

vector

int>> arr;

intcalc

(int i,

int k)

intmain()

sort

(arr.

begin()

,arr.

end(),

(vector<

int>

&a,vector<

int>

&b))

;//寫出樸素的dp方程

memset

(dp,0,

sizeof

(dp));

for(

int i=

1;i<=m;i++

)for

(int j=

1;j1;j++)}

} cout<

[n]<

return0;

}

裁剪序列

#include

using

namespace std;

const

int n=

100050

;long

long dp[n]

,vise[n]

;//用於懶惰標記

int deq[n]

,p[n]

,a[n]

;long

long n,m;

//使用小根堆的技巧,插入相反數

priority_queue

long

long

,int

>> que;

intmain()

}long

long sum=0;

int head=0;

int tail=-1

;int pre=1;

//單調佇列獲得一段區間中的最值,不需要使用線段樹,st時間複雜度線性。

for(

int i=

1;i<=n;i++

) head=0;

tail=-1

; sum=0;

pre=1;

for(

int i=

1;i<=n;i++))

; vise[deq[tail-1]

]=-dp[deq[tail-1]

]-arr[deq[tail]];

}//根據惰性刪除,刪除優先順序佇列中已經失效的值。

for(

;!que.

empty()

&&(vise[que.

top(

).second]

!=que.

top(

).first||vise[que.

top(

).second]==0

);que.

pop())

;if(!que.

empty()

)dp[i]

=min

(dp[i]

,-que.

top(

).first);}

cout<

<

return0;

}

分析相當棒的一道例題,可以讓我們更為細緻地理解如何使用單調佇列,為什麼使用單調佇列,當區間動態變化時,我們可以使用單調佇列+優先順序佇列的方式優化時間複雜度,當然本題實際上也可以利用線段樹求解,考慮到每乙個元素至多進入和離開單調佇列一次,這樣我們就可以在元素離開單調佇列時,更新線段樹中的值為無窮大,更新時使用線段樹查詢即可。

這道題的特點是並不存在直觀地可以用優先順序佇列進行優化的方法,這需要我們借助其他結構解決該問題,我們也要學會合理地利用各種資料結構,得到想要的結果。

多重揹包問題

#include

using

namespace std;

const

int n=

20010

;int dp[n]

,pre[n]

,q[n]

;int n,m;

intmain()

//剪枝隊頭不合法元素

while

(head<=tail&&pre[q[tail]]-

(q[tail]

-j)/v*w<=pre[k]

-(k-j)

/v*w)

--tail;

//入隊

//狀態轉移

if(head<=tail)

//新增元素

q[++tail]

=k;}}}

cout<

<

return0;

}

分析本題是一道非常好的單調佇列優化的題目,這裡用到很多實用的程式設計技巧,首先為了降低時間複雜度,我們拋棄實用雙端佇列,而是用乙個陣列模擬單調佇列,用陣列模擬單調佇列,我們需要保證插入元素在隊尾,而刪除元素在隊頭,否則會發生陣列越界的問題。

除此之外就是分析問題了,也即確定我們用什麼大小關係來維持乙個單調佇列,值的注意的是,單調佇列中一定維持的是體積這一維度的下標,並且維持的元素大小只與這一下標有關,而不與其他下標產生關聯關係。

本題中,當我們按照體積取餘後,發現不同的揹包大小之間,只有相差體積大小為第i個物品大小時,才會發生轉移,其他相互不影響,所以我們可以將整個揹包大小,按照第i個物品的體積大小分割成不同的餘數,餘數相同的一組揹包體積之間可以相互轉化,其他不能轉化。

這時我們就可以發現:

從狀態轉移方程中我們可以看到,實際上我們需要維持長度不超過s的一段連續同餘元素中的乙個關於公式:

上面這個式子中實際上只有k是變數,i-1是階段,在這一層迴圈中始終不變,而j則是與所求體積有關的下標,在求解上式是是乙個定值,也即在求解體積為j的揹包的最優值時,上式的變數只有k,而k指的是一段區間中使得上式取的最大值的k,區間的左右端點都是單調遞增的,所以我們可以用單調佇列來維持這一段區間中上式的最大值的下標,這樣我們就可以將時間複雜度優化到o(mn)。

單調佇列 優化DP

佇列元素保持單調遞增 減 而保持的方式就是通過插隊,把隊尾破壞了單調性的數全部擠掉,從而使佇列元素保持單調。單調佇列的作用 優化dp。許多單調佇列優化的dp可以使複雜度直接降維,下面就以最簡單的一道題為例 在某兩座城市之間有 n 個烽火台,每個烽火台發出訊號都有一定代價。為了使情報準確地傳遞,在連續...

單調佇列優化dp

形如f i max wi的問題都可以用單調佇列優化。例題 板題 注意乙個地方 求完所有的f後 ans不是f n 而是後面的一段字尾的f 注意字尾的左端點。很顯然是rmq問題 計算字首和sum i 對於固定的右端點 i,我們想讓答案最大等價於max,可以用個單調佇列維護。但是隨便乙個資料結構直接on ...

單調佇列優化DP

這段時間在重溫dp,發現dp中有很大很重要的一塊區域是關於dp優化的,於是來和大家分享一下各種dp優化方法,下面講一下單調佇列優化dp 首先來重溫下什麼是單調佇列,在單調佇列中,每個元素的決策時間單調增而它的決策單調更劣,這就顯然可以明白,最優決策永遠在隊首。如果對上面基礎單調佇列不了解,可以先做 ...