早期 複習資料 動態規劃的優化

2022-03-30 07:42:54 字數 3420 閱讀 9191

好久以前寫的部落格了,想了想還是搬過來吧。

這只是一篇很水的文章而已……

關於動態規劃轉移方面的優化,可能要用到以下資料結構:

單調佇列

樹狀陣列

線段樹st表

堆平衡樹

......

這道題相當於區間覆蓋問題的加強版,區間覆蓋問題中每個可選區間的權值都是1,而這道題的權值則可能不一。

我們用 \(l[i]\) 和 \(r[i]\) 表示第 \(i\) 個區間的左右端點, \(v[i]\) 表示權值。

先考慮狀態,我們可以定義 \(dp[i]\) 表示第 \(i\) 個區間必須選,且第 \(i\) 個區間的右端點之前的點都被覆蓋到了,則如果第 \(j\) 個區間可以轉移到當前的第 \(i\) 個區間,則第 \(j\) 個區間需滿足 \(l[i]-1\le r[j]\) (即第 \(j\) 個區間可以無縫接上第 \(i\) 個區間),於是得到轉移方程:

\[dp[i]=min(dp[j]+v[i])(l[i]-1\le r[j])

\]由此便可得到應該按 \(r[j]\) 遞增排序(為什麼?),於是便可以寫程式了。

但是這樣做時間複雜度是 \(o(n^2)\) (狀態( \(o(n)\) ),轉移( \(o(n)\) )),過不了,考慮一下優化轉移。

在求解 \(dp[i]\) 時,我們是考慮所有的滿足 \(l[i]-1\le r[i]\) 的 \(dp[j]\) 的最小值,區間查詢最小值,於是便想到線段樹,求解 \(dp[i]\) 時,查詢 \(l[i]-1\) 之後的最小值,求出乙個 \(dp[i]\) 後,便把它丟到線段樹中去,這樣時間複雜度就是 \(o(\log_2n)\) ,需要注意的就是要把每個區間的 \(r[i]\) 離散化一下,當然,由於每次只是修改最後乙個,也可以用st表來維護,這樣就可以過這道題了。

思考題:

q1: 請問能否按左端點排序?

當然可以,只需乙個 reverse() 就可以了。

實際上這樣做是不會有任何問題的,兩個區間用兩種方式排序後,如果先後順序不同,則兩個區間必定是包含關係(為什麼?),所以最終的答案是不可能包括兩個區間都選的(為什麼?),所以說,這兩個區間的先後關係對最終的答案並沒有影響,同樣,轉移方程還是原來的那個轉移方程,只是不能用st表了(為什麼?)。

q2: 請問 q1 對我們解決問題有沒有什麼幫助?

都找出解法還考慮那麼多幹嘛???

其實是有的,這樣,我們每次在轉移的時候就可以保證滿足 \(l[i]-1\le r[j]\) 的 \(l[i]-1\) 會始終保持遞增,過時了的 \(j(r[j]就回永遠保持過時狀態。

這時可能就會有人想到單調佇列,那麼這道題可不可以用單調佇列呢???

應該是不行的,因為 \(r\) 不是單調的,當前最優的 \(dp[j]\) 可能會「早退」,導致之前某個被放棄的方案 \(dp[k]\) ( \(kr[j]\land dp[k]>dp[j]\) )成為最優的(為什麼?)

所以 q1 還是沒用嘍。

不能用單調佇列,我們為何不試試堆,單調佇列會放棄一些暫時不是最優的方案,但堆還是會保留它們,我們只需要看看堆頂是否滿足當前要求,如果不滿足,彈掉就是了。

放上蒟蒻的**(堆優化):

#include#include#include#include#include#define ll_max 5000000005

using namespace std;

templatevoid read(t &x)

templatevoid write(t x)

struct sb

};priority_queue,cmp>q;

int main()

sort(p+1,p+n+1);

p[0].r=l-1;dp[0]=0;

q.push(0);

long long ans=ll_max;

for(int i=1;i<=n;++i)

q.push(i);

} if(ans!=ll_max)write(ans);

else write(-1);putchar('\n');

return 0;

}

題意就是詢問在 \(n\) 個數中嚴格遞增的長度大於等於 \(m\) 的子串行有多少個。

考慮最暴力的 \(dp\) ,設狀態 \(dp[i][j]\) 表示前 \(i\) 個數以 \(a[i]\) 結尾的長度為 \(j\) 的子串行個數,則有轉移:

\[dp[i][k]=\sum dp[j][k-1](k>0\land j

初始化 \(dp[i][0]=1\) ,時間複雜度 \(o(n^2m)\) 。

怎麼優化?看到 \(a[j]會想到什麼?值域線段樹(或樹狀陣列),我們可以開 \(m\) 個線段樹(樹狀陣列),就可以在 \(o(\log_2n)\) 的時間內求出 \(\sum dp[j][k-1]\) ,時間複雜度降為 \(o(nm\log_2n)\) ,應該是可以過的,需要注意的就是要離散化。

有沒有其他方式呢?

我們是從後往前列舉的,這樣滿足了 \(j,然後用線段樹來滿足第二個條件 \(a[j],那我們為何不反過來試試,先滿足 \(a[j],再利用資料結構滿足 \(j呢???

於是我們便找到了第二種優化方案,先對原序列按值進行一次排序,再從前往後掃一遍,這樣做的好處就是可以省去離散化(但是多了個排序,其實時間複雜度差不多)。

放上蒟蒻的**(第二種優化方式):

#include#include#include#includeusing namespace std;

templatevoid read(t &x)

templatevoid write(t x)

struct chibi

int lowbit(int t)

int n;

void add(int n,int k,long long v)return;

}long long sum(int n,int k)return ans;

}void output(int num)

sort(a+1,a+n+1);

long long ans=0;

memset(dp,0,sizeof dp);

memset(tree,0,sizeof tree);

for(int i=1;i<=n;++i){

dp[i][0]=1;add(a[i].i,0,dp[i][0]);

for(int k=1;k可以看到,列舉是先列舉的 \(n\) ,再列舉的 \(k\) ,我們也可以先列舉 \(k\) ,再列舉 \(n\) ,這樣就可以只開乙個樹狀陣列,同樣 \(dp\) 的一維也可以滾掉。

first

second

third

first

first

總之,動態規劃轉移方面的優化,就是要縮小當前的轉移集合或很快從當前的轉移集合中找出最優轉移,所以就是要考慮之前所求的轉移集合對當前的轉移集合有沒有影響,能否用一種資料結構很快地找出最優轉移。

遞迴的優化(動態規劃)

使用遞迴方法來計算組合數 從m個不同元素中,任取n n m 個元素並成一組,叫做從m個不同元素中取出n個元素的乙個組合 從m個不同元素中取出n n m 個元素的所有組合的個數,叫做從m個不同元素中取出n個元素的組合數。公 式 c m,n n m n n n m 性 質 c m,n c m,m n 可...

動態規劃 斜率優化

一 引用 一般dp 方程可以轉化成 dp i f j x i 的形式,其中 f j 中儲存了只與 j相關的量。這樣的 dp方程可以用單調佇列進行優化,從而使得 o n 2 的複雜度降到 o n 可是並不是所有的方程都可以轉化成上面的形式,舉個例子 dp i dp j x i x j x i x j ...

動態規劃優化方法

之前我們學習過動態規劃方法,但是並沒有對dp進行系統細緻的優化。今天來看一下dp的優化方法。做變換 等式就可以看作一條直線,每個j都確定出乙個點 x,y 對於當前要求解的i,其斜率已確定 且隨i單增 要使dp i 最大即要使這條直線過某乙個j形成的點並使得的截距最小。假設之前已處理完前9個點,將他們...