【算法】動態規劃+“揹包九講”原理超詳細講解+常見dp問題(9種)總結

一.動態規劃(DP)

動態規劃(DP)通俗講解
1、什麼是動態規劃?
這裏參考百度百科,動態規劃是求解決策過程最優化的數學方法。把多階段過程轉化爲一系列單階段問題,利用各階段之間的關係,逐個求解,創立了解決這類過程優化問題的新方法——動態規劃。

2、什麼時候要用動態規劃?
如果要求一個問題的最優解(通常是最大值或者最小值),而且該問題能夠分解成若干個子問題,並且小問題之間也存在重疊的子問題,則考慮採用動態規劃。

3、怎麼使用動態規劃?
我把下面稱爲動態規劃五部曲:

  1. 判題題意是否爲找出一個問題的最優解
  2. 從上往下分析問題,大問題可以分解爲子問題,子問題中還有更小的子問題
  3. 從下往上分析問題 ,找出這些問題之間的關聯(狀態轉移方程)
  4. 討論底層的邊界問題
  5. 解決問題(通常使用數組進行迭代求出最優解)
    文字來源

下面這個圖是我們離散老師講的一點動態規劃,第一問是最基礎的貪心,相信大家都會,第二問的DP如果是初學者建議把圖片裏的過程看一遍理解一下,(類似01揹包)對理解下面的揹包有好處。

在這裏插入圖片描述

二.揹包九講

(1)完全揹包

考慮有 n 種物品,第 i 種物品的每個重量爲 wi,價值爲 vi,有無限多個。 我們手頭有一個大小爲 m的揹包,需要算出能裝下的最大總價值。

我們設 f(i) 表示大小爲 i 的揹包最多能裝的價值,那麼可以得到一個轉移方程:f(i)=maxf(iw 1 )+v1,f(iw 2 )+v2,...,f(iw n )+v n  f(i) = max{f(i − w~1~) + v1,f(i − w~2~) + v2,...,f(i − w~n~) + v~n~} 寫成簡潔一些的形式:
f(i)=maxf(iw j )+v j  f(i) = max {f(i − w~j~) + v~j~} 初始條件爲:
f(0)=0 f(0) = 0
嘗試用代碼來實現:

for (int i = 0; i <= m; ++i) {
    for (int j = 1; j <= n; ++j) {
        if (i - w[j] >= 0)
            f[i] = max(f[i], f[i - w[j]] + v[j]);
    } 
}

交換兩重循環的順序也是可以的:

for (int i = 1; i <= n; ++i) {
    for (int j = w[i]; j <= m; ++j) {
        f[j] = max(f[j], f[j - w[i]] + v[i]); 
    }
}

完全揹包是正序,全程貪心的思想用max取最大值
我們總共有 m 個狀態,每個狀態需要進行 n 次轉移,故最終的複雜度爲 O(nm)

P1616 瘋狂的採藥(完全揹包)

P1616 瘋狂的採藥
LiYuxiang是個天資聰穎的孩子,他的夢想是成爲世界上最偉大的醫師。爲此,他想拜附近最有威望的醫師爲師。醫師爲了判斷他的資質,給他出了一個難題。醫師把他帶到一個到處都是草藥的山洞裏對他說:“孩子,這個山洞裏有一些不同種類的草藥,採每一種都需要一些時間,每一種也有它自身的價值。我會給你一段時間,在這段時間裏,你可以採到一些草藥。如果你是一個聰明的孩子,你應該可以讓採到的草藥的總價值最大。”
如果你是LiYuxiang,你能完成這個任務嗎?
此題和原題的不同點:
1.每種草藥可以無限制地瘋狂採摘。
2.藥的種類眼花繚亂,採藥時間好長好長啊!師傅等得菊花都謝了!
輸入格式
輸入第一行有兩個整數T(1 <= T <= 100000)和M(1 <= M <= 10000),用一個空格隔開,T代表總共能夠用來採藥的時間,M代表山洞裏的草藥的數目。接下來的M行每行包括兩個在1到10000之間(包括1和10000)的整數,分別表示採摘某種草藥的時間和這種草藥的價值。
輸出格式
輸出一行,這一行只包含一個整數,表示在規定的時間內,可以採到的草藥的最大總價值。
輸入輸出樣例
輸入

70 3
71 100
69 1
1 2

輸出

140
#include<bits/stdc++.h>
using namespace std;
int dp[100005],tim[100005],vis[100005],t,m,n;
int main()
{
    cin>>t>>m;
    for(int i=0;i<m;i++)
        cin>>tim[i]>>vis[i];
    for(int i=0;i<m;i++)
        for(int j=tim[i];j<=t;j++)
        dp[j]=max(dp[j],dp[j-tim[i]]+vis[i]);//選拿和不拿中最大的那一個(拿的話就要把之前放進去的丟掉空出當前物品的重量)
    cout<<dp[t]<<endl;
    return 0;
}

(2)01揹包

將上面的問題稍微進行改動:

考慮有 n 種物品,第 i 種物品的每個重量爲 wi,價值爲 vi。 每種物品只有 1 件。 有一個大小爲 m的揹包,需要算出能裝下的最大總價值。

我們不能再像剛纔那樣設計狀態了。因爲每個物品取過之後就不能再取,所以狀態還需要記錄物品的相關信息。
考慮如何將問題轉化成規模更小的子問題。對於某個物品,在最終的方案中要麼不取、要麼取,於是我們對這兩種情況來分類討論,轉化爲子問題:

  • 如果沒有取第 n 個物品,那麼相當於只有前 n − 1 個物品、揹包大小相同的子問題;
  • 如果取了第 n 個物品,那麼相當於只有前 n − 1 個物品(n-1個物品已經經過了了選擇)、揹包大小減去 wn 的子問題,在它的答案上再加上 vn的價值。
    在兩種情況中取價值更高的,得到答案。
    狀態和轉移:
    設 f(i, j) 表示使用編號爲 1 ∼ i 的物品,揹包容量爲 j 時的最大價值, 有轉移方程

f(i,j)=max(f(i1,j),f(i1,jwi)+vi) f(i,j) = max({f(i − 1,j),f(i − 1,j − wi) + vi) }
初始條件爲:
f(0,)=0 f(0, ∗) = 0

for (int i = 1; i <= n; ++i) {
    for (int j = 0; j <= m; ++j) {
        if (j < w[i])
            f[i][j] = f[i - 1][j];
        else
            f[i][j] = max(f[i - 1][j], f[i - 1][j - w[i]] + v[i]);//f[i-1]是指已經選過i-1件物品了
} }

時間複雜度爲 O(nm)。注意到這段代碼的空間複雜度也達到了 O(nm), 在一些問題中無法接受。我們將嘗試對它進行優化。

滾動數組

觀察剛纔的代碼:每個 f[i][j] 在轉移時只用到了 f[i-1][*]。也 就是說,比 i-1 更小的再也不會被用到。如果把 f 看成一張二維的表 格,那麼只有兩行的格子是 “活躍” 的。基於這一思想,我們可以只保存這兩行。
具體的實現可以參考如下:

int p = 1, q = 0;
for (int i = 1; i <= n; ++i) {
    for (int j = 0; j <= m; ++j) { 
    if (j < w[i])
        f[p][j] = f[q][j];
    else
        f[p][j] = max(f[q][j], f[q][j - w[i]] + v[i]); 
        swap(p, q);//用第一行求出第二行以後,第二行又作爲第一行求下一行,每次只用到兩行的空間
} }

一維數組

繼續剛纔的思路:把 f 看成一張二維的表格,那麼每個格子在轉移時 只會用到上一行中在它左側的格子。如果我們調整一下轉移的順序,每 一行從右往左進行更新(j 從大到小),那麼 “活躍” 的格子就正好只有上一行的左半部分以及這一行的右半部分。
在這裏插入圖片描述
那麼實際上我們只需要保存這些活躍格子的狀態就行了。
一維數組
代碼實現:

for (int i = 1; i <= n; ++i) {
    for (int j = m; j >= w[i]; --j) {// 倒序!!!
        f[j] = max(f[j], f[j - w[i]] + v[i]); }
}

注意到當 j<w[i] 時,相當於直接用 f[i-1][j] 來更新 f[i][j],在一維數組中就什麼都不用做了。

P1048 採藥(01揹包)

P1048 採藥
題目描述
辰辰是個天資聰穎的孩子,他的夢想是成爲世界上最偉大的醫師。爲此,他想拜附近最有威望的醫師爲師。醫師爲了判斷他的資質,給他出了一個難題。醫師把他帶到一個到處都是草藥的山洞裏對他說:“孩子,這個山洞裏有一些不同的草藥,採每一株都需要一些時間,每一株也有它自身的價值。我會給你一段時間,在這段時間裏,你可以採到一些草藥。如果你是一個聰明的孩子,你應該可以讓採到的草藥的總價值最大。”
如果你是辰辰,你能完成這個任務嗎?
輸入格式
第一行有22個整數M(1≤M≤100),用一個空格隔開,TT代表總共能夠用來採藥的時間,M代表山洞裏的草藥的數目。
接下來的M行每行包括兩個在1到100之間(包括1和100)的整數,分別表示採摘某株草藥的時間和這株草藥的價值。
輸出格式
1個整數,表示在規定的時間內可以採到的草藥的最大總價值。
輸入

70 3
71 100
69 1
1 2

輸出

3
#include<bits/stdc++.h>
using namespace std;
const int maxn=1005;
int dp[maxn],w[maxn],vis[maxn],n,m,t;
int main()
{
    cin>>t>>m;
    for(int i=1;i<=m;i++)
        cin>>w[i]>>vis[i];
    for(int i=1;i<=m;i++)
        for(int j=t;j>=0;--j)
            if(j>=w[i])//01倒序完全正序
                dp[j]=max(dp[j],dp[j-w[i]]+vis[i]);
    cout<<dp[t]<<endl;
    return 0;
}

(3)多重揹包

在之前的問題基礎上,再進行一些改動:

考慮有 n 種物品,第 i 種物品的每個重量爲 wi,價值爲 vi。 第 i 種物品最多能取 ci 件。 有一個大小爲 m的揹包,需要算出能裝下的最大總價值。

轉化爲 01 揹包
嘗試將這個問題轉化爲我們已知的問題…
對於每種 ci 個的物品,將它拆成 ci個 “01 物品”,然後使用 01 揹包的算法進行解決。
問題解決了,但代價是什麼?但這樣的複雜度高達 O(m ∑ ci),難以接受。

更合理的分組
我們將物品拆分的目的是:使拆分後的物品的選取方法可以表示出拆分前的選取方法。
那麼顯然剛纔的拆分方法產生了大量冗餘信息,拆出來的物品都是相同 的,而我們卻重複計算了許多相同的選法。這就很不符合 dp 的核心思想。考慮換一種更加合理的、產生更少新物品的分組。

整數拆分(二進制拆分)

給出正整數 n,將 n 分爲儘可能少的正整數之和,使得從中選取若干個求和,可以得到 1 ∼ n 的所有整數。
如7=1+2+4,13=1+2+4+6。那麼1~13中任意一個數都可以用拆出來的1,2,4,6中的幾個數組合相加得到,放到題目中就可以實現所有物品試一遍來比較最大值的優化方案。
首先估計出答案的一個下界:假設分成了 k 個數,那麼根據乘法原理, 最多可以表示 2k 個不同的數。於是 2k ≥ n,即k ≥ log2 n
然後可以簡單地構造出一個解:

  1. 將1,2,4,…,2i 不斷地加入集合,直到再加入下一個會導致加起來
    超過 n;
  2. 然後將剩下的 n −∑加入集合。
    容易驗證,可以組合出所有整數。
    代碼實現:
void partition(int n)
{
for (int i = 1; i <= n; i *= 2) {
    /* 將 i 加入答案; */
    n -= i; 
}
if (n > 0) 
{/* 將 n 加入答案; */} 
}

複雜度O(nm log m)

P1776 寶物篩選(多重揹包)

P1776 寶物篩選
題目描述
終於,破解了千年的難題。小F 找到了王室的寶物室,裏面堆滿了無數價值連城的寶物。
這下小 F可發財了,嘎嘎。但是這裏的寶物實在是太多了,小 FF 的採集車似乎裝不下那麼多寶物。看來小 FF 只能含淚捨棄其中的一部分寶物了。

小 F 對洞穴裏的寶物進行了整理,他發現每樣寶物都有一件或者多件。他粗略估算了下每樣寶物的價值,之後開始了寶物篩選工作:小 FF 有一個最大載重爲 W 的採集車,洞穴裏總共有 n 種寶物,每種寶物的價值爲 vi
,重量爲 wi,每種寶物有 mi件。小 F 希望在採集車不超載的前提下,選擇一些寶物裝進採集車,使得它們的價值和最大。
輸入格式
第一行爲一個整數 n 和 W,分別表示寶物種數和採集車的最大載重。

接下來 nn 行每行三個整數vi,wi,mi
輸出格式
輸出僅一個整數,表示在採集車不超載的情況下收集的寶物的最大價值。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1000005;
int dp[maxn],v[maxn],w[maxn],n,wc,a,b,c,cnt;
int main()
{
    cin>>n>>wc;
    for(int i=0;i<n;i++)
    {
        cin>>a>>b>>c;
        for(int j=1;j<=c;j<<=1)//二進制拆分防止爆炸
        {/*拆成好多個01揹包(比如原來是三種拆成15種用cnt計數)*/
            v[++cnt]=a*j;
            w[cnt]=b*j;
            c-=j;
        }
        if(c)v[++cnt]=a*c,w[cnt]=b*c;
    }
    for(int i=1;i<=cnt;i++)
        for(int j=wc;j>=w[i];j--)
            dp[j]=max(dp[j],dp[j-w[i]]+v[i]);//普通的01揹包
    printf("%d\n",dp[wc]);
    return 0;
}

(4)二維費用揹包

在之前問題的基礎上,每個物品有兩個維度的費用(例如重量和體積) wi 和 zi,均不能超過揹包對應的容量。
以 01 揹包爲例,設 f(i, j, k) 表示使用編號爲 1 ∼ i 的物品,揹包兩個維 度的容量分別爲 j 和 k 時的最大價值,有轉移方程
f(i,j,k)=maxf(i1,j,k),f(i1,jwi,kzi)+vi f(i,j,k) = max{f(i − 1,j,k),f(i − 1,j − wi,k − zi) + vi}
空間優化
滾動數組顯然是可以的。進一步的優化,想象一個三維的表格,我們發 現 “活躍” 的格子總數不會超過一層,於是可以倒序循環,去掉第一維。

P1507 NASA的食物計劃(二維費用揹包)

P1507 NASA的食物計劃
題目背景
NASA(美國航空航天局)因爲航天飛機的隔熱瓦等其他安全技術問題一直大傷腦筋,因此在各方壓力下終止了航天飛機的歷史,但是此類事情會不會在以後發生,誰也無法保證,在遇到這類航天問題時,解決方法也許只能讓航天員出倉維修,但是多次的維修會消耗航天員大量的能量,因此NASA便想設計一種食品方案,讓體積和承重有限的條件下多裝載一些高卡路里的食物.
題目描述
航天飛機的體積有限,當然如果載過重的物品,燃料會浪費很多錢,每件食品都有各自的體積、質量以及所含卡路里,在告訴你體積和質量的最大值的情況下,請輸出能達到的食品方案所含卡路里的最大值,當然每個食品只能使用一次.
輸入格式
第一行 兩個數 體積最大值(<400)和質量最大值(<400)
第二行 一個數 食品總數N(<50).
第三行-第3+N行
每行三個數 體積(<400) 質量(<400) 所含卡路里(<500)
輸出格式
一個數 所能達到的最大卡路里(int範圍內)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e3+7;
const ll mod=1e9+7;
ll mv,mw,n,v[N],w[N],c[N],dp[N][N];
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>mv>>mw;
    cin>>n;
    for(int i=1;i<=n;++i)
        cin>>v[i]>>w[i]>>c[i];
    for(int i=1;i<=n;++i)
        for(int j=mv;j>=v[i];--j)
            for(int k=mw;k>=w[i];--k)
            dp[j][k]=max(dp[j][k],dp[j-v[i]][k-w[i]]+c[i]);
    cout<<dp[mv][mw]<<endl;
    return 0;
}

(5)混合揹包

將剛纔的 01 揹包、多重揹包、完全揹包進行混合,有的物品只能取有限個,有的可以任意取,怎麼解決這個問題呢?
一個簡單粗暴的方法是,將所有的物品都當成多重物品(考慮取的個數上限),然後按照多重揹包來做。複雜度 O(nm log m)
如果只有 01 物品和無限物品,那麼可以使用一維數組,遇到 01 物品時 倒序循環,遇到無限物品時正序循環。複雜度 O(nm)

P1833櫻花(混合揹包)

P1833櫻花
題目描述
愛與愁大神後院裏種了n棵櫻花樹,每棵都有美學值Ci。愛與愁大神在每天上學前都會來賞花。愛與愁大神可是生物學霸,他懂得如何欣賞櫻花:一種櫻花樹看一遍過,一種櫻花樹最多看Ai遍,一種櫻花樹可以看無數遍。但是看每棵櫻花樹都有一定的時間Ti 。愛與愁大神離去上學的時間只剩下一小會兒了。求解看哪幾棵櫻花樹能使美學值最高且愛與愁大神能準時(或提早)去上學。
輸入格式
共n+1行:
第1行:三個數:現在時間Ts(幾點:幾分),去上學的時間Te(幾點:幾分),愛與愁大神院子裏有幾棵櫻花樹n。

第2行~第n+1行:每行三個數:看完第i棵樹的耗費時間Ti,第i棵樹的美學值Ci,看第i棵樹的次數Pi(Pi=0表示無數次,Pi是其他數字表示最多可看的次數Pi)。

輸出格式
只有一個整數,表示最大美學值。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1000005;
int w[maxn],vis[maxn],c,nx,ny,ex,ey,n,m,cnt,a,b,dp[maxn];
int main()
{
    scanf("%d:%d%d:%d%d",&nx,&ny,&ex,&ey,&n);//scanf()的妙用
    int tim=(ex*60+ey)-(nx*60+ny);
    for(int i=1;i<=n;i++)
    {
        cin>>a>>b>>c;
        if(!c)c=999999;
        for(int j=1;j<=c;j<<=1)
        {
            vis[++cnt]=b*j;
            w[cnt]=a*j;
            c-=j;
        }
        if(c)vis[++cnt]=c*b,w[cnt]=c*a;
    }
    for(int i=1;i<=cnt;i++)
        for(int j=tim;j>=w[i];j--)
        dp[j]=max(dp[j],dp[j-w[i]]+vis[i]);
    cout<<dp[tim]<<endl;
    return 0;
}

(6)分組揹包

在 01 揹包的基礎上,每個物品屬於一個組,每組中的物品是互斥的 (最多隻能取一件)。
考慮把每個組當成一個 “物品”,可以取其中的一個,或者不取。
設 f(i, j) 表示對於前 i 組物品,使用容量爲 j 的揹包,可以裝的最大價
值。那麼轉移方程爲
f(i,j)=max(f(i1,j),f(i1,jwk)+vk)kGi f(i,j) = max({f(i − 1,j),f(i − 1,j − wk) + vk}) k∈Gi
也可以優化成一維數組

P1757 通天之分組揹包(分組揹包)

P1757 通天之分組揹包
題目描述
自01揹包問世之後,小A對此深感興趣。一天,小A去遠遊,卻發現他的揹包不同於01揹包,他的物品大致可分爲k組,每組中的物品相互衝突,現在,他想知道最大的利用價值是多少。
輸入格式
兩個數m,n,表示一共有n件物品,總重量爲m

接下來n行,每行3個數ai,bi,ci,表示物品的重量,利用價值,所屬組數

輸出格式
一個數,最大的利用價值

輸入輸出樣例
輸入

45 3
10 10 1
10 5 1
50 400 2

輸出

10
#include<bits/stdc++.h>
using namespace std;
const int maxn=100005;
int dp[1005][005],w[maxn],vis[maxn],d[maxn],f[maxn],n,m,t,x,num=0;
int main()
{
    std::ios::sync_with_stdio(false);
    cin>>m>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>w[i]>>vis[i]>>x;
        num=max(num,x);
        ++d[x];
        dp[x][d[x]]=i;
    }
    for(int k=1;k<=num;k++)
        for(int j=m;j>=0;j--)
            for(int i=1;i<=d[k];i++)
            {
                int xx=dp[k][i];
                if(j>=w[xx])
                    f[j]=max(f[j],f[j-w[xx]]+vis[xx]);
            }

    cout<<f[m]<<endl;
    return 0;
}

(7)樹形揹包

在 01 揹包的基礎上,每個物品可能依賴於某個其他物品(需要選那個 物品,才能選這個物品),並且這個依賴關係構成一棵樹。
有時候,可能有多個物品沒有依賴,可以增加一個重量和價值爲 0 的虛 擬物品,將它作爲這些物品的依賴,從而構成一棵樹。

1.dfs 序

對於一棵有根樹,我們從樹根開始進行 dfs,按照遇到結點的順序給結 點重新編號,稱爲 dfs 序(先序),那麼有如下性質:
對於每棵子樹,樹根在 dfs 序列中最先出現 一棵子樹在 dfs 序中對應連續的一段區間
在這裏插入圖片描述

2.利用 dfs 序解決樹形揹包

得到 dfs 序之後,我們就可以解決樹形揹包問題了。按照 dfs 序從後往 前,對於每件物品,考慮它選/不選的兩種情況:
如果不選,那麼它對應的整棵子樹也不能選,變成(dfs 序中它子樹 右側的下一個)的子問題;
如果選,就變成(dfs 序中它的下一個)的子問題。
設 f(i, j) 表示對於 dfs 序中 i ∼ n 的物品,容量爲 j 的揹包,最多能裝的
價值。設結點 i 在 dfs 序中子樹最後一個的結點爲 ri,則有轉移方程 f(i,j) = max{f(ri + 1,j),f(i + 1,j − wi) + vi}
時間複雜度爲 O(nm)。

P2014 選課(樹形揹包)

題目描述
在大學裏每個學生,爲了達到一定的學分,必須從很多課程裏選擇一些課程來學習,在課程裏有些課程必須在某些課程之前學習,如高等數學總是在其它課程之前學習。現在有 N 門功課,每門課有個學分,每門課有一門或沒有直接先修課(若課程a是課程b的先修課即只有學完了課程a,才能學習課程b)。一個學生要從這些課程裏選擇 M 門課程學習,問他能獲得的最大學分是多少?

輸入格式
第一行有兩個整數 N , M 用空格隔開。(1≤M≤300)

接下來的 N行,第 I+1 行包含兩個整數k i 和 s i ,k i 表示第I門課的直接先修課,s i
​表示第I門課的學分。若 k i=0 表示沒有直接先修課(1≤k i≤N , 1≤s i ≤20)。
輸出格式
只有一行,選 M 門課程的最大得分。
輸入:

7  4
2  2
0  1
0  4
2  1
7  1
7  6
2  2

輸出:

13

P1064 金明的預算方案(樹形揹包/分組揹包)

(8)方案數問題凡是求方案數的問題一定都需要初始化(dp[0]=1)

1.P1164 小 A 點菜

題目描述
不過uim由於買了一些輔(e)輔(ro)書,口袋裏只剩M元(M≤10000)。餐館雖低端,但是菜品種類不少,有N種(N≤100),第ii種賣ai 元(a i≤1000)。由於是很低端的餐館,所以每種菜只有一份。小A奉行“不把錢喫光不罷休”,所以他點單一定剛好吧uim身上所有錢花完。他想知道有多少種點菜方法。
由於小A肚子太餓,所以最多隻能等待1秒。
輸入格式
第一行是兩個數字,表示N和M。
第二行起N個正數a i
(可以有相同的數字,每個數字均在1000以內)。
輸出格式
一個正整數,表示點菜方案數,保證答案的範圍在int之內。

輸入

4 4
1 1 2 2

輸出

3

與 01 揹包很類似,只是變成了計數問題。
設f(i,j)表示使用編號爲1∼i的菜,花完j元的方法數; 得到轉移方程

f(i,j)=f(i1,j)+f(i1,jai) f(i,j) = f(i−1,j)+f(i−1,j−ai)
當前情況的方案總數應該是所有能到達當前位置途徑的方案數的總和(遞推)
注意初始條件是f(0,0)=1,f(0,)=0 f(0,0) = 1, f(0,∗) = 0
也可把二維轉換爲一維,運用滾動數組,每次都只用到了i-1所以只需一維即可(類似01揹包所以要倒序)初始條件也將改成dp[0]=1,即花0元的方案數爲1(什麼都不買)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4;
int a[maxn],dp[maxn],n,m;
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    dp[0]=1;
    for(int i=1;i<=n;i++)
        for(int j=m;j>=a[i];j--)
        dp[j]+=dp[j-a[i]];//
    cout<<dp[m]<<endl;
    return 0;
}

2.P1466 集合

1 ∼ n 的整數分爲兩個集合,使它們的總和相等。求本質不同的方案數。n ≤ 39 由於所有的數的總和一定,兩個集合的和相等就意味着它們都等於 n(n+1) /4;
於是問題變爲從 1 ∼ n 中選若干個數,使它們的和爲 n(n+1) /4;
dp[i]代表和爲i的方案數
注意最後答案要除以二,因爲重複不算

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e3+7;
const ll mod=1e9+7;
ll n,dp[N];
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    ll sum=n*(n+1)/2;
    if(sum&1){cout<<0<<endl;return 0;}
    dp[0]=1;
    for(int i=1;i<=n;++i)
    {
        for(int j=sum;j>=i;j--)
        {
            dp[j]+=dp[j-i];
        }
    }
    cout<<dp[sum/2]/2<<endl;
}

二. DP簡單應用

[nico和niconiconi]

題目描述

“にっこにっこにー” ——nico
nico平時最喜歡說的口頭禪是niconiconi~。
有一天nico在逛著名彈幕網站"niconico"的時候驚異的發現,n站上居然有很多她的鬼畜視頻。其中有一個名爲《讓nico爲你洗腦》的視頻吸引了她的注意。
她點進去一看,就被洗腦了:“niconicoh0niconico*^vvniconicoG(vniconiconiconiconiconicoG(vniconico…”
彈幕中剛開始有很多“nico1 nico2”等計數菌,但到後面基本上都是“計數菌陣亡”的彈幕了。
nico也想當一回計數菌。她認爲:“nico” 計 a 分,“niconi” 計 b 分,“niconiconi” 計 c 分。
她拿到了一個長度爲 n 的字符串,請幫她算出最大計數分數。
注:已被計數過的字符不能重複計數!如"niconico"要麼當作"nico"+“nico"計a+b 分,要麼當作"niconi”+"co"計 c 分。
輸入描述:
第一行四個正整數 。
第二行是一個長度爲 n 的字符串。
輸出描述:
一個整數,代表最大的計數分數。

示例1
輸入

19 1 2 5
niconiconiconiconi~

輸出

7

說明
"niconi"co"niconiconi"~故爲2+5=7分
思路
比較簡單的dp
計 dp[i]dp[i] 代表前 ii 個字符的計數最大值。
那麼可得轉移方程:
if(substr(i4,4)==nico)thendp[i]=max(dp[i],dp[i4]+a) if(substr(i−4,4)==nico)then dp[i]=max(dp[i],dp[i-4]+a) if(substring(i6,6)==niconi)thendp[i]=max(dp[i],dp[i6]+b) if(substring(i−6,6)==niconi)then dp[i]=max(dp[i],dp[i-6]+b) if(substring(i10,10)==niconiconi)thendp[i]=max(dp[i],dp[i10]+c) if(substring(i−10,10)==niconiconi)then dp[i]=max(dp[i],dp[i-10]+c)
substr()函數詳解
最後輸出 dp[n] 即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=3e5+7;
const ll mod=1e9+7;
ll n,a,b,c,dp[N];
string s;
int main()
{
    cin>>n>>a>>b>>c;
    cin>>s;
    for(int i=0;i<=n;i++)
    {
        string s1="",s2="",s3="";
        if(i>=4)s1=s.substr(i-4,4);
        if(i>=6)s2=s.substr(i-6,6);
        if(i>=10)s3=s.substr(i-10,10);
        if(s1=="nico")dp[i]=max(dp[i],dp[i-4]+a);
        else dp[i]=max(dp[i],dp[i-1]);
        if(s2=="niconi")dp[i]=max(dp[i],dp[i-6]+b);
        else dp[i]=max(dp[i],dp[i-1]);
        if(s3=="niconiconi")dp[i]=max(dp[i],dp[i-10]+c);
        else dp[i]=max(dp[i],dp[i-1]);
    }
    cout<<dp[n]<<endl;
}

在這裏插入圖片描述
輸出:

2

思路

先將元素按能量值排序,下文默認已排序。

可以證明存在一個最優方案,滿足每個魔法一定消耗一段連續的元素。

注意是至少取 k段,那麼從 k+1開始DP

定義 dp[i]代表:前i項最優解dp[i]代表:前i項最優解 dp[i]代表:前 i 項最優解dp[i]代表:前i項最優解,那麼可以得到,對於任意位置:

是拓展前面,從長度 m變成 m+1;(數學歸納法思想,任意位置肯定都是合法的,m>=k成立)(數學歸納法思想,任意位置肯定都是合法的,m>=k成立) (數學歸納法思想,任意位置肯定都是合法的,m>=k成立)(數學歸納法思想,任意位置肯定都是合法的,m>=k成立)
斷開前面,從當前位置往前 k−1項,和當前第 i 項,組成長度爲 k 的序列。

時間複雜度O(N−K)

#include<bits/stdc++.h>
using namespace std;
#define Pi acos(-1.0)
typedef long long ll;
const ll N=3e5+7;
const ll mod=1e9+7;
ll n,k,dp[N],a[N];
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>k;
    for(int i=1;i<=n;++i)
        cin>>a[i],dp[i]=mod;
    sort(a+1,a+1+n);
    dp[k]=a[k]-a[1];
    for(int i=k+1;i<=n;++i)//至少要選k個那麼從k+1開始DP
        dp[i]=min(dp[i-1]+a[i]-a[i-1],dp[i-k]+a[i]-a[i-k+1]);
    cout<<dp[n]<<endl;//對於第i個魔法,要麼跟着上一組做,要麼另起爐竈新開一組,用min取最小值
    return 0;
}

缺席的神官

廣工的騰訊杯,是真的NB,一套題附贈一篇小說
題目描述
面前的巨漢,讓我想起了多年前的那次,但這個巨漢身上散佈着讓人畏懼害怕的黑霧。即使看不到臉,但是威嚴卻在這個從者身邊不斷圍繞。 「吾乃七騎之中的騎士(rider),你們就是御主所說的阻擾者吧」 「是」我從雪茄盒裏面掏出一根雪茄,想稍微冷靜一下。 「那便無需多言了」和我簽訂了暫時契約的理查一世倒是直接拔劍了,如此看來查理一世的職介就是劍士(saber)。 「我看你的御主倒是沒有這個想法吧」 他似乎看出了我的想法,雖然只是亡魂的影子,但也曾是人,能洞察人心。 「您是這樣的想法嗎」理查一世把劍收了起來。 「是啊,雖然參與聖盃戰爭的御主和從者目的是實現願望,但既然是殘缺的聖盃,我也會猜想是否從者對聖盃的渴望並沒有那麼高,是否有值得交涉的餘地」 「哈」巨漢笑了,「真是大膽的妄想啊,但你應該明白聖盃顯現的方法吧,所以這一切都是不可避免的。但我也不想使用武力,解答我的困惑吧,魔術師,如果你們能回答出來,我就會放棄」 「我明白了,洗耳恭聽」 「古時有一個懶惰的祭司,而祭司在連續m天內必須一直去神廟內工作,但祭司的怠惰在誘惑着祭司,於是祭司決定這段時間內只選出k個連續的時間段去神廟工作,但是高級祭司(祭司的上級)又會定期對神廟內的工作人員進行點名。祭司不想因此失去這份工作,所以提前知道了高級祭司會點名n次以及每次點名的日子。所以祭司把點名的日子納入工作的日子當中的同時又儘可能的偷懶。那麼,這個祭司到底工作了多少天呢」 「這個答案很簡單,荷魯斯」
輸入描述:
第一行輸入三個整數n,m,k (1 <= n <= 2000) (n <= m <= 109) (1<= k <= n),分別爲高級祭司的點名次數,原本需要工作的天數和懶惰的祭司的工作次數。第二行輸入n個數字ai (1 <= ai <= m),爲高級祭司檢查的日期。輸入保證對於任意的i,j (1<= i<j <= n),都有ai < aj。
輸出描述:
輸出懶惰的祭司進行工作的最少天數

#include<bits/stdc++.h>
#define debug cout<<"ok"<<endl
typedef long long ll;
const int maxn=1e5+10;
const int mod=1e9+7;
using namespace std;
int n,m,k;
ll dp[2019][2019],a[maxn];
int main()
{
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);//加速cin;
    cin>>n>>m>>k;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    for(int i=0;i<=n;i++)
        for(int j=0;j<=k;j++)//全部置INF;
            dp[i][j]=1e10;
    dp[0][0]=0;//初始化
    for(int i=1;i<=n;i++)//N件東西
        for(int j=1;j<=k;j++)//取K次
            dp[i][j]=min(dp[i-1][j-1],dp[i-1][j]+a[i]-a[i-1]);//取最小值;
    cout<<dp[n][k]+k<<endl;//要加k每次算a[i]-a[i-1]時都少了1,少K次;
    return 0;
}

篇幅所限,放一下我的博客鏈接,我自己寫的博客哦!

三.LCS最長公共子序列

LCS最長公共子序列

四.棋盤型高維(三維)動態規劃

棋盤型高維動態規劃

五.區間DP

我自己寫的博客哦!
區間DP入門

六.前綴DP

七.樹形DP

【樹形DP】樹的重心詳解+多組例題詳解

八.狀壓DP

我自己寫的博客哦!
【狀壓DP】狀態壓縮動態規劃入門超詳解

九.斜率優化DP

待更!待學

十.概率DP/期望DP

概率DP/期望DP

動態規劃:DP從入門到破門而出(入門必刷例題)

注:如果您通過本文,有(qi)用(guai)的知識增加了,請您點個贊再離開,如果不嫌棄的話,點個關注再走吧,日更博主每天在線答疑 ! 當然,也非常歡迎您能在討論區指出此文的不足處,作者會及時對文章加以修正 !如果有任何問題,歡迎評論,非常樂意爲您解答!( •̀ ω •́ )✧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章