日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

动态规划解题思路与总结(三万字)

發(fā)布時間:2025/3/19 编程问答 12 豆豆
生活随笔 收集整理的這篇文章主要介紹了 动态规划解题思路与总结(三万字) 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

動態(tài)規(guī)劃

  • 前言
  • 手把手如何寫動態(tài)規(guī)劃
    • 最長路徑
    • 網(wǎng)格
  • 最長上升子序列問題
    • 求最長上升子序列
    • 最長非嚴(yán)格遞增子序列
    • 至少修改多少次能將序列變?yōu)樯仙蛄?/li>
  • 最長公共上升子序列
  • 分級問題
  • 移動服務(wù)
  • 傳紙條
  • 排序不等式
  • 背包問題
    • 前言
    • 01背包問題
      • 01背包進階
      • 數(shù)字組合
  • 完全背包
      • 自然數(shù)拆分
      • 陪審團
    • 多重背包
      • 二進制優(yōu)化版
      • 單調(diào)隊列優(yōu)化版
        • 分組背包
    • 區(qū)間DP
      • 環(huán)形石子和并

前言

本篇依據(jù)y總的方法,采用集合的框架力來力求解釋好dp的原因。
框架的整體是:

手把手如何寫動態(tài)規(guī)劃

  • 第一點肯定是經(jīng)驗了,寫了一定量的題目后你才能動態(tài)規(guī)劃知其所以然。
  • 難點是動態(tài)規(guī)劃的計算上面難點就是狀態(tài)的計算上面了。動態(tài)規(guī)劃是一種特殊的最短路問題(在拓補圖上的)。從圖論方面考慮狀態(tài)的計算,其實就是,不同狀態(tài)之間的轉(zhuǎn)換關(guān)系(有向邊的關(guān)系)
  • 轉(zhuǎn)換關(guān)系一般有兩大類:

  • 用其所依賴的狀態(tài)來更新當(dāng)前狀態(tài)。(80%)
  • 用當(dāng)前狀態(tài)更新依賴它的狀態(tài)。
    畫圖來說就是
    這兩種其實是等價的。
  • 下面以兩個題目來進行說明

    最長路徑

    題目轉(zhuǎn)送門

    首先我們可以寫出動態(tài)規(guī)劃的表達式(很大程度考經(jīng)驗),dp[i]表示 y以節(jié)點i結(jié)尾的最長路徑長度。那么我們?nèi)绾芜M行更新呢,首先這個點的前一個狀態(tài)是什么情況我呢吧不好知道,但是因為由這個點指向的下一個點的情況我們是可以很好的知道的,就是該點的路徑長度+1,(就是上圖第二類的情況),我們可以有當(dāng)前狀態(tài)更新依賴它的狀態(tài)。
    那么更新的順序是什么呢?結(jié)合題目的有向無環(huán)圖,那么我們就可以按照拓?fù)湫虻捻樞銇磉M行更新。

    代碼:

    #include <iostream> #include <algorithm> #include <cstring> #include <queue> using namespace std; const int N = 2e5 + 10; int n, m, cnt; int h[N], ne[N], e[N], in[N]; int f[N];void add(int u, int v) {e[++cnt] = v, ne[cnt] = h[u], h[u] = cnt; }void tosort() {queue<int>q;for (int i = 1; i <= n; ++i)if (!in[i])q.push(i);while (q.size()) {int u = q.front();q.pop();for (int i = h[u] ; ~i ; i = ne[i]) {int v = e[i];f[v] = max(f[v], f[u] + 1);if (--in[v] == 0)q.push(v);}} }int main() {cin >> n >> m;memset(h, -1, sizeof h);while (m--) {int a, b;cin >> a >> b;add(a, b);in[b]++;}tosort();int res = 0;for (int i = 1 ; i <= n ; ++i)res = max(res, f[i]);cout << res << endl;return 0; }

    網(wǎng)格

    題目轉(zhuǎn)送門
    上面一題是第二類情況,那么這題就講一下第一類的情況;

    首先我們定義的動態(tài)規(guī)劃的狀態(tài)是:dp[i][j] : 表示從(1,1)到(i,j)的路徑數(shù)量。
    由題意,對于每一個dp[i][j] ,他都是只能從上方或者左方過來的,因此對于每一個dp[i][j]加上它的這兩個方向的路徑數(shù)量即可。

    代碼:

    #include <iostream> #include <algorithm> #include <cstring> using namespace std; const int N = 1010, mod = 1e9 + 7 ; int n, m ; char g[N][N]; int f[N][N];int dx[2] = {-1, 0}, dy[2] = {0, -1};int main() {cin >> n >> m;for (int i = 0 ; i < n ; ++i)cin >> g[i];f[0][0] = 1;for (int i = 0 ; i < n ; ++i)for (int j = 0 ; j < m ; ++j) {if (g[i][j] == '#')continue;for (int k = 0 ; k < 2 ; ++k) {int xx = i + dx[k], yy = j + dy[k];if (xx < 0 || xx >= n || yy < 0 || yy >= m)continue;if (g[xx ][yy ] == '.')f[i][j] = (f[i][j] + f[xx][yy]) % mod;}}cout << f[n - 1][m - 1] << endl;return 0; }

    最長上升子序列問題

    求最長上升子序列

    首先我們依據(jù)框架來寫一下思路

    我們講一下狀態(tài)計算中為什么劃分成這樣。f[i]是以A[I]為結(jié)尾的所有情況中的最大值。那么我們還是引入last這個概念,對于所有以A[i]結(jié)尾的上升序列,由于最后一個都是A[i]那么它們的不同點就是去掉A[I]之后的第一個數(shù),即倒數(shù)第二個數(shù)。因此我們都去掉A[i]而去枚舉前面滿足,而有含義可以知道這些就是f[1~(i-1]的定義。因此我們就枚舉從1~(i-1)滿足上升的。取最大值。
    核心代碼

    for(int i = 2 ; i <= n ;++i){for(int j = 1 ; j < i ; ++j )if(a[i] > a[j]) f[i] = max(f[i] , f[j] + 1); }

    上面做法很明顯是O(n2)O(n^2)O(n2)的一個做法。很明顯不能接收。那么我們看如何進行優(yōu)化呢?重復(fù)計算主要是在第二層循環(huán)上邊,因為對于每一個i我們都要去前邊找一個最優(yōu)的前綴序列,不過呢這個題不能想下邊最長公共子序列那樣記錄一個最值,因為在這里后邊的值是可能比前邊小的。因此我們利用貪心的思想,對于同一長度的的LIS我們肯定是希望結(jié)尾越小越好。同時這樣維護的序列由于是具有單調(diào)性的,因此我們就可以利用二分來優(yōu)化最終使得算法時間復(fù)雜度為:O(nlongn)O(nlongn)O(nlongn)

  • f[i]的定義是:表示長度為i的LIS結(jié)尾元素的最小值。到時候我們的答案就是i的值。
  • 為什么是對的?對于一個LIS序列當(dāng)遍歷到后邊的時候如果如果我們更新了結(jié)尾的值,由貪心思想這是最優(yōu)的,同時也是可以的,對于如果我們用后邊一個值去修改一個在中間的值,表面上我們不可以這樣因為我們要按順序來取,但是這不會影響最終的結(jié)果,因為它不會增加LIS的長度。具體來說,我們想修改的值只是結(jié)尾的值,然其最小化,不過如果加上特判只修改結(jié)尾值會麻煩,而都修改不會影響最終結(jié)果,那何樂而不為之呢?因此我們用這個方法能求出正確的LIS的長度,但里邊存的值不一定是正確的LIS序列。即
    但是!!!B[ ] 中的序列并不一定是正確的最長上升子序列
  • 我們二分找的是比a[i]大于或等于的第一個位置,因此我們可以利用lower_bound( )函數(shù):來實現(xiàn)。//返回第一個大于等于val值的位置
  • 核心代碼:

    #include <stdio.h> #include <iostream> #include <algorithm> #include <cstring> using namespace std; const int N = 1010, INF = 0x3f3f3f3f; int n, cnt, a[N]; int f[N]; int find(int num) {// 由于加1之后,那么減完 f之后就相當(dāng)于f以1開始存的下標(biāo)return lower_bound(f + 1, f + 1 + cnt, num) - f; }int main() {cin >> n;for (int i = 1 ; i <= n ; ++i)cin >> a[i], f[i] = INF;f[1] = a[1], cnt = 1;//從第2個數(shù)開始for (int i = 2 ; i <= n ; ++i) {if (a[i] > f[cnt]) // 大于的話直接接在后邊f[++cnt] = a[i];elsef[find(a[i])] = a[i];}cout << cnt << endl;return 0; }

    上面做法就有點遍歷dp了,那么我們對dp式子進行分析一下:
    我們再來回顧O(n^2)DP的狀態(tài)轉(zhuǎn)移方程:F [ i ] = max { F [ j ] + 1 ,F [ i ] } (1 <= j < i,A[ j ] < A[ i ])
    我們在遞推F數(shù)組的時候,每次都要把F數(shù)組掃一遍求F[ j ]的最大值,時間開銷比較大。我們可以借助數(shù)據(jù)結(jié)構(gòu)來優(yōu)化這個過程。那么我們?nèi)绾蝺?yōu)化呢?我們耗時是在一個一個的去與前邊滿足條件的取最大值,那么我們是否可以更快的進行呢?我們就可以采用樹狀數(shù)組來進行維護,采用以數(shù)值范圍建樹的思想,將LIS長度的最大值記錄下來。那么取最大值這個過程優(yōu)化了,不過我們利用樹狀數(shù)組維護的時候是與該編號前邊的所有取一個最大值,因此對于該編號我們要保證它的值必須要是大的(上升),因此我們就可以用結(jié)構(gòu)體來存值和編號,對值進行排序,然后從前往后遍歷,那么我們在遍歷的時候就一定是滿足上升的性質(zhì)。
    代碼:

    #include <iostream> #include <cstdio> #include <algorithm> #include <cstdlib> #include <cstring> #include <cmath> using namespace std; const int maxn =103,INF=0x7f7f7f7f; struct Node{int val,num; }z[maxn]; int T[maxn]; int n; bool cmp(Node a,Node b) {return a.val==b.val?a.num<b.num:a.val<b.val; } void modify(int x,int y)//把val[x]替換為val[x]和y中較大的數(shù) {for(;x<=n;x+=x&(-x)) T[x]=max(T[x],y); } int query(int x)//返回val[1]~val[x]中的最大值 {int res=-INF;for(;x;x-=x&(-x)) res=max(res,T[x]);return res; } int main() {int ans=0;scanf("%d",&n);for(int i=1;i<=n;i++){scanf("%d",&z[i].val);//如果題目數(shù)據(jù)沒有說各不相同的話,需要去重。//如果不去重那就是求最長非嚴(yán)格單調(diào)上升子序列的長度z[i].num=i;//記住val[i]的編號,有點類似于離散化的處理,但沒有去重 }sort(z+1,z+n+1,cmp);//以權(quán)值為第一關(guān)鍵字從小到大排序 for(int i=1;i<=n;i++)//按權(quán)值從小到大枚舉 {int maxx=query(z[i].num);//查詢編號小于等于num[i]的LIS最大長度modify(z[i].num,++maxx);//把長度+1,再去更新前面的LIS長度ans=max(ans,maxx);//更新答案}printf("%d\n",ans);return 0; }

    最長非嚴(yán)格遞增子序列

    這個和上面是幾乎一樣的,由于是非嚴(yán)格因此可以等于,因此我們就有可能是等于的情況而已
    核心代碼:

    for(int i = 2 ; i <= n ;++i){for(int j = 1 ; j < i ; ++j )if(a[i] >= a[j]) f[i] = max(f[i] , f[j] + 1); }

    對于第二種方法:

    for (int i = 2 ; i <= n ; ++i) {if (a[i] >= f[cnt]) // 大于的話直接接在后邊f[++cnt] = a[i];elsef[find(a[i])] = a[i];}

    至少修改多少次能將序列變?yōu)樯仙蛄?/h2>
  • 對于非嚴(yán)格遞增序列,我們只需要用序列的總長度減去最長非嚴(yán)格遞增序列的長度即可。因為對于不在最長非嚴(yán)格遞增序列的數(shù)字我們可以將其改為與臨近的數(shù)字相等就可以實現(xiàn)了。
  • 對于用嚴(yán)格遞增序列,我們先構(gòu)造出一個數(shù)組b, b[i] = a[i] - 1 ,然后求b數(shù)組的最長非嚴(yán)格遞增子序列的長度,然后用序列總長度減去就得到正確答案了。原因是:對于嚴(yán)格遞增序列我們有a[i]>a[j](i>j)a[i] > a[j] (i > j )a[i]>a[j](i>j)等價變形為a[i]?i>=a[j]?ja[i] - i >= a[j] - ja[i]?i>=a[j]?j.對于這個形式我們發(fā)現(xiàn)出現(xiàn)了等于號,因此我們就按上述方法構(gòu)造出一個新的序列。然后用這個新的序列來求。
  • 最長公共上升子序列

    題目轉(zhuǎn)送門


    下面就來重點講一下,狀態(tài)計算的部分。

    首先 ,依據(jù)最長公共子序列的思想,因為b[j]一定在這個集合中了那么我們就看a[i]這個元素,由題目有兩個條件,一個是公共,這個是兩個序列之間的關(guān)系,一個是上升是序列內(nèi)部的關(guān)系。我們就由公共,分為a[i]不在公共上升子序列中和a[i]在公共上升子序列中兩部分。對于前一個部分,

    也就是f[i-1][j]的含義了,我們都是從含義出發(fā)來找的。對于后一部分,我們需要借助,lat這個概念,這個是什么意思呢,就最后一個不同的地方,由于后半部分a[i] == b[j]的,那么其不同點就是b[j]上的一個元素是什么了。而b[j]元素前邊有的元素個數(shù)可能情況是0~j-1 個。對于0的情況,因為b[j]前邊沒有元素,因此只要我滿足公共就一定是上升了也就是1.(因為就自己一個肯定是上升的)。對于 1 ~ j-1 。我們只要它們的值小于 b[j] 那么就可以進行去max的計算,而在取max的時候,我們的式子是f[i][j] = max(f[i][j] , f[i-1][k] + 1);首先解釋下 i - 1,和為什么我們以有無a[i]來區(qū)分,這與我們的遍歷有關(guān),我們外層循環(huán)是 a ,如果我們外層循環(huán)是 b,那么我們也可以按理將b[i]是否在來進行劃分。然后是f[i-1][k] + 1 。 因為在這個的前提是a[i] = a[j] ,那么就已經(jīng)滿足了公共的條件了,又有b[k] < b[j] ,那么又滿足了上升的條件那么,f[i-1][k]是這個的前一部分位置,1是a[i] == b[j]這個部分。
    我們有以下核心代碼:

    for(int i = 1 ; i <= n ; ++i){for(int j = 1 ; j <= n ; ++j){f[i][j] = f[i-1][j];if(a[i] == b[j]){int maxv = 1;for(int k = 1 ; k < j ; ++k)if(b[k] < b[j])maxv = max(maxv , f[i-1][k] + 1);f[i][j] = max(maxv , f[i][j]);}}}int res = 0 ;for(int i = 1 ; i <= n ; ++i)res = max(res , f[n][i]);

    這個算法是 O(n3)O(n^3)O(n3)的一個數(shù)量級,我們就想辦法對其進行優(yōu)化一下。
    由于a[i] == b[j],上式代碼就變?yōu)榱?#xff1a;if(b[k] < a[i])maxv = max(maxv , f[i-1][k] + 1);
    那么第三層循環(huán)求的就是:

    這里邊小于 a[i]的 , 因為這個是固定的,因為b里邊哪些值小于a[i]都是已經(jīng)成定居了的。所以這個循環(huán)就是求所有的b,b滿足小于a[i]對應(yīng)的方案數(shù)+1的一個最大值。

    for(int i = 1 ; i <= n ; ++i){int maxv = 1;for(int j = 1 ; j <= n ; ++j){f[i][j] = f[i-1][j];if(a[i] == b[j])f[i][j] = max(maxv , f[i][j]);if(b[j] < a[i]) maxv = max(maxv,f[i-1][j] + 1 );}}

    分級問題

    問題轉(zhuǎn)送們

    有了前面的鋪墊我們就直接來說如何進行轉(zhuǎn)態(tài)計算的。首先對于f[i][j]這個狀態(tài)它是定的了,就是我第i個位置放的一定是 b[j]這個數(shù)了,那么在這個情況下(就是指第i個位置放的一定是 b[j]這個數(shù)的所有的可能情況下) ,還是引用last這個概念,我們的第一個不同點應(yīng)該是上一個位置所放的值,由于要符合非嚴(yán)格單調(diào)遞增的性質(zhì),我們上一個位置能放的值的可能就是 1~j因此我們就對該問題進行了劃分。至于如何求呢我們看下邊

    我們發(fā)現(xiàn)如果我們用循環(huán)來計算,就會有很多的重復(fù)計算。我們發(fā)現(xiàn)對于后一個j來說除了f[i-1][j+1] 是與j的時候不同的其余都是一樣的。因此我們就可以用一個值來維護這個最小值。

  • 因為這題是有非嚴(yán)格遞增和遞減兩種情況,因此我們這兩種情況中取一個最小值即可。那么如果我們先求了遞增,那么只需要,將原數(shù)組翻轉(zhuǎn)一下即可。
  • #include<stdio.h> #include<iostream> #include<algorithm> #include<cmath>using namespace std;const int N = 2010 , INF = 0X3f3f3f3f3f; int n , a[N] , b[N]; int f[N][N];int dp(){for(int i = 1 ; i <= n ; ++i)b[i] = a[i];sort(b+1,b+1+n);for(int i = 1 ; i <= n ; ++i){int minv = INF;for(int j = 1 ; j <= n ; ++j){minv = min(minv , f[i-1][j]);f[i][j] = minv + abs(b[j] - a[i]);}}int res = INF;for(int i = 1 ; i <= n ;++i)res = min(res , f[n][i]);return res; }int main(){scanf("%d",&n);for(int i = 1 ; i <= n ; ++i)scanf("%d",&a[i]);int res = dp();reverse(a+1 , a+1+n );res = min(res , dp());printf("%d\n",res);return 0; }

    移動服務(wù)

    題目轉(zhuǎn)送們

    解題思路:

    首先我們要想如何表示狀態(tài)呢?我們可以用完成了第幾個請求作為一個階段,但是光有這個信息還是不夠的,我們就需要附加一些信息。在這個題目中,我們用三個服務(wù)員的位置來作為附加的信息。因此我們就有了,f[i][x][y][z]表示的含義是處理第i個請求后三個服務(wù)員所在的位置分別為x,y,z。不夠如果我們直接這樣計算時間上直接會TM的。那么我們就需要分析這些附加的信息是否有關(guān)聯(lián)性呢?由題目含義出發(fā),當(dāng)我們處理了第i個請求后肯定有一個服務(wù)員是在pos[i]這個位置上的,因此后邊的三個信息我們只需要兩個即可。到了這里我們先畫出初步的框架出來。

    那么下一個難點就是狀態(tài)的計算上面了。動態(tài)規(guī)劃是一種特殊的最短路問題(在拓補圖上的)。從圖論方面考慮狀態(tài)的計算,其實就是,不同狀態(tài)之間的轉(zhuǎn)換關(guān)系(有向邊的關(guān)系)

    轉(zhuǎn)換關(guān)系一般有兩大類:

  • 用其所依賴的狀態(tài)來更新當(dāng)前狀態(tài)。(80%)
  • 用當(dāng)前狀態(tài)更新依賴它的狀態(tài)。
    畫圖來說就是
    這兩種其實是等價的。
  • 那么我們會過頭來看一下這個問題,這個狀態(tài)的狀態(tài)中,對于當(dāng)前狀態(tài)f[i][x][y]它出去的狀態(tài)就只有三種,無非是x服務(wù)員去,要么是y去,要么是z去。而對于它的入邊就很復(fù)雜。因此這一題我們就選用第二種來進行計算。

    啟發(fā):

  • 求線性DP的問題,一般先確定“階段”。如果“階段”不足以表示一個狀態(tài),則需要把所需的附加信息也作為狀態(tài)的維度。
  • 在確定DP的狀態(tài)的時候,要選擇最小的能夠覆蓋整個狀態(tài)空間的:維度集合。好比這題我們當(dāng)然可以用四維的來表示,但是如果能用三維來表示不是更好。
  • 代碼:

    #include<stdio.h> #include<iostream> #include<cstring> #include<algorithm> using namespace std;const int N = 1010 , M = 210; int n , m , pos[N]; int w[M][M] , f[N][M][M];int main(){scanf("%d%d",&m,&n);for(int i = 1 ; i <= m ; ++i)for(int j = 1 ; j <= m ; ++j)scanf("%d",&w[i][j]);for(int i = 1 ; i <= n ;++i)scanf("%d",&pos[i]);memset(f , 0x3f , sizeof f);pos[0] = 3 , f[0][1][2] = 0;for(int i = 0 ; i < n ; ++i)for(int x = 1 ; x <= m ; ++x)for(int y = 1 ; y <= m ; ++y){int z = pos[i] , u = pos[i+1] , val = f[i][x][y];if(x == y || x == z || y == z)continue;f[i+1][x][y] = min(f[i+1][x][y] , val + w[z][u]);f[i+1][z][y] = min(f[i+1][z][y] , val + w[x][u]);f[i+1][x][z] = min(f[i+1][x][z] , val +w[y][u]);}int res = 0x3f3f3f3f;for(int x = 1 ; x <= m ; ++x)for(int y = 1 ; y <= m ; ++y){int z = pos[n]; //因為是順序處理請求,因此最后處理的應(yīng)該是這個請求。if(x == y || y == z || x == z)continue;res = min(res , f[n][x][y]);//}printf("%d\n",res);return 0; }

    傳紙條

    題目轉(zhuǎn)送門

    解題思路:

    這題首先我們回想一下如果是只能走一條路徑的時候我們是如何定于狀態(tài)的:f[i][j]:表示走到(xi,yj)(x_i,y_j)(xi?,yj?)這個路徑的和的最大值。那么我們可以類似的定義為:f[x1][y1][x2[y2]。不過由上面的啟發(fā)我們試著想想能不能縮小一下狀態(tài)空間呢?我們用了兩個坐標(biāo),是為了判斷是否位于同一個格子上即x1 == x2 && y1 == y2,不過這樣分太細(xì)了。如果我們記錄的是橫縱坐標(biāo)的和,那么如果和相同的情況下,只有x相同那么就可以判斷出來了。因此我們最終的狀態(tài)表示是:f[k]x1][x2];

    那么什么如何由劃分的子區(qū)間的含義如何得到表達式呢?

    類比,我們得到四個區(qū)間的表達式分別為:

    • 兩個人同時向右走,最大分值是 f[k - 1, i, j] + score(k, i, j);
    • 第一個人向右走,第二個人向下走,最大分值是 f[k - 1, i, j - 1] + score(k, i, j);
    • 第一個人向下走,第二個人向右走,最大分值是 f[k - 1, i - 1, j] + score(k, i, j);
    • 兩個人同時向下走,最大分值是 f[k - 1, i - 1, j - 1] + score(k, i, j);

    還有一個注意點是x的范圍,我們有1<=x<=n和1<=k?x<=m1 <= x <= n 和 1 <= k - x <= m1<=x<=n1<=k?x<=m 得到
    max(1,k?m)<=x<=min(n,k?1)max(1,k-m) <= x <= min(n,k-1)max(1,k?m)<=x<=min(n,k?1)

    對于本題還可以與方格取數(shù)有關(guān),相關(guān)的證明為這篇博客:博客

    代碼:

    #include<stdio.h> #include<iostream> #include<algorithm> using namespace std; const int N = 55; int n , m ; int w[N][N], f[N<<1][N][N];int main(){cin>>n>>m;for(int i = 1 ; i <= n ; ++i)for(int j = 1 ; j <= m ; ++j)cin>>w[i][j];for(int k = 2 ; k <= n + m ; ++k)for(int x1 = max(1 , k - m) ; x1 <= min(n , k -1) ; ++x1)for(int x2 = max(1,k-m); x2 <= min(n, k - 1) ; ++x2){int t = w[x1][k-x1] ;if(x1!=x2) t += w[x2][k-x2];for(int a = 0 ; a <= 1 ; ++a)for(int b= 0 ; b <= 1 ; ++b)f[k][x1][x2] = max(f[k][x1][x2] , f[k-1][x1-a][x2-b] + t);}cout<<f[n+m][n][n]<<endl;return 0; }

    排序不等式

    背包問題

    前言

    相信大家應(yīng)該都看過很多講解背包問題的博客或者視頻了。不過這里采用的還是利用上面的框架來進行分析。希望都大家理解上面和運用上面有一個更深的理解。同時在背包問題中我的順序上也是做了一些巧妙的安排。

    01背包問題

    題目轉(zhuǎn)送們

    如下圖:我們主要來講一下狀態(tài)計算這部分。對于f[i][j]我們對比一下這些在這個集合中的所有方案的上一個不同點,對于i來說有的可能是沒有去取第i個物品體積就是j了。因此先分為取和不取兩個劃分。對于取的這部分,那么它們最后一個都是取的i物品是一樣的,那么它們的last不同點就是沒取第i個物品的j的不同。
    我們一般是分為變和不變兩個部分,這些方案最后都是取了V[I]這個價值是一樣的,那么不一樣的就是前 i - 1 ,個物品時候的總體積。而我們?yōu)榱四苋〉侥敲纯傮w積就不能小于W[I[

    小技巧:

  • 對于一個小數(shù):可以用memset(f , 0xcf , sizeof f); 來初始化。
  • 代碼:

    #include<iostream> #include<cstring> using namespace std; const int N = 1010; int n , V; int v[N] , w[N]; int f[N][N]; int main(){memset(f , 0xcf , sizeof f);cin>>n>>V;for(int i = 1 ; i <= n ; ++i)cin>>w[i]>>v[i] ;f[0][0] = 0;for(int i = 1 ; i <= n ; ++i)for(int j = 0 ; j <= V ; ++j){f[i][j] = f[i-1][j];if(j >= w[i])f[i][j] = max(f[i][j] , f[i-1][j-w[i]] + v[i]);}int res = 0;for(int i = 1 ; i <= V ; i++) res = max(res , f[n][i]);cout<<res<<endl;return 0; }

    我們觀察發(fā)現(xiàn),每一次求i的時候只與i-1層有關(guān),因此我們可以不用記錄所有的狀態(tài),而采用滾動數(shù)組的方法來優(yōu)化。這里主要講的是另一種優(yōu)化,我們發(fā)現(xiàn),在每一個階段開始的時候,實際上執(zhí)行了f[i-1][] 到f[i][]的拷貝,那么我們是不是就可以將狀態(tài)空間優(yōu)化為一維?f[j]表示背包放入總體積為j的物品的最大價值。
    先寫出初步的等價變化

    不過在第二層循環(huán)我們注意,之前我們是f[i-1][j-w[i]] + v[i]表我們在求第i個階段的時候利用的是第i-1階段的信息。現(xiàn)在如果有 V = 2 * w[i] 的話,那么就有 f[V] = max(f[V] , f[w[i]] + v[i]) , 而 f[w[i]]是我們這個階段剛求出來的這很明顯就不符合了。而這個的解決辦法也很簡單,我們只需要,倒序遍歷即可。

  • 小技巧:如果我們令f數(shù)組都是0,而不是只有f[0] = 0 ,那么最后的結(jié)果就是f[V]
    代碼:
  • #include<stdio.h> #include<iostream> #include <iostream> #include <algorithm> using namespace std; const int N = 110, M = 1e5 + 10; typedef long long LL; int n, W; int w[N], v[N] ; LL f[M];int main() {cin >> n >> W;for (int i = 1; i <= n ; ++i)cin >> w[i] >> v[i];for (int i = 1; i <= n ; ++i)for (int j = W ; j >= w[i]; --j)f[j] = max(f[j], f[j - w[i]] + v[i]);LL res = 0 ;for (int i = 1 ; i <= W ; ++i)res = max(res, f[i]);cout << res << endl;return 0; }

    01背包進階

    上面點的01背包的時間復(fù)雜度是O(NM)其中N是物品個數(shù),M是背包的容積。因此如果遇到的題目是背包的體積很大,但每一個物品的價值相對小的時候就不能用了。
    我們在上面轉(zhuǎn)態(tài)的表示是dp[j]:表示體積為j的物品的最大價值。那么我們就可以修改其中的含義變成:dp[i]:表示價值為i的物品的最小體積。
    那么遇到這類情況我們也可以解決了。

    代碼:

    #include <iostream> #include <cstdio> #include <algorithm> #include <cstring> using namespace std; const int N = 1E5 + 10; typedef long long LL; int n, m ; LL f[N] ; int w[110], v[110];int main() {memset(f, 0x3f, sizeof f );int sum = 0, res = 0;cin >> n >> m;for (int i = 1 ; i <= n ; ++i) {cin >> w[i] >> v[i];sum += v[i];}f[0] = 0;for (int i = 1; i <= n ; ++i)for (int j = sum ; j >= v[i] ; --j) {f[j] = min(f[j], f[j - v[i]] + w[i]);if (f[j] <= m)res = max(j, res);}cout << res << endl;return 0; }

    數(shù)字組合

    題目轉(zhuǎn)送們

    之所以將這兩個放在一起,是因為它們是本質(zhì)相同的只不過屬性發(fā)生了變化的題目。
    我們先來寫一些動態(tài)規(guī)劃的框架

    對吧驚人的相識。不過這里計算的是方案的數(shù)量。因此我們還是講一下狀態(tài)計算的部分。首先是左半部分,那么我們就直接加上其含義的方案數(shù)量即可。不過因為對于每一個階段的f[i][j]值都是0,因此我們就可以寫為f[i][j] = f[i-1][j]; 對于右邊的部分,我們只需要把max操作改為加即可。 利用上面提到的小技巧我們直接輸出f[n][m]就是答案了。

    代碼:

    #include<stdio.h> #include<iostream> #include<algorithm> #include<cstring> using namespace std;const int N = 110 , M = 1e4 + 10; int n , m ; int a[N] , f[N][M];int main(){ cin>>n>>m;for(int i = 1 ; i <= n ;++i) cin>>a[i] ;f[0][0] = 1;for(int i = 1 ; i <= n ; ++i){for(int j = 0 ; j <= m ; ++j){ f[i][j] = f[i-1][j];if(j >= a[i])f[i][j] += f[i-1][j-a[i]];}}cout<<f[n][m]<<endl;return 0; }

    還是考慮優(yōu)化版:

    #include<stdio.h> #include<iostream> #include<algorithm> #include<cstring> using namespace std;const int N = 110 , M = 1e4 + 10; int n , m ; int a[N] , f[M];int main(){ cin>>n>>m;for(int i = 1 ; i <= n ;++i) cin>>a[i] ;f[0] = 1;for(int i = 1 ; i <= n ; ++i)for(int j = m ; j >= a[i] ; --j)f[j] += f[j-a[i]];cout<<f[m]<<endl;return 0; }

    完全背包

    題目轉(zhuǎn)送門

    完全背包與01背包的區(qū)別就是每件物品可以選無限次,只要沒有超過上限的體積。

    如果我們直接利用上面的01背包的倒序遍歷的話,我們可以就需要這樣寫。(第i個物品可以選多個) 不過時間復(fù)雜度是O(n2long(n))O(n^2 long(n))O(n2long(n))是不行的。

    講優(yōu)化01背包的時候,我們?yōu)?在計算第i個階段的時候只用i-1個階段的,因此我們從大到小來進行枚舉。而對于完全背包我們只需要將倒序變成正序就可以。

    #include<iostream> using namespace std; const int N = 1010; int n , m, w[N] , v[N]; int f[N];int main(){cin>>n>>m;for(int i = 1 ; i <= n ; ++i)cin>>w[i]>>v[i];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]);cout<<f[m]<<endl;return 0; }

    自然數(shù)拆分

    題目轉(zhuǎn)送們

    解題思路

    我們將自然數(shù)N看成是容量為N的一個背包,那么這題就是有 1 ~N 一共N個物品 , 每一物品的體積分別從 1 ~N , 問有這些數(shù)字組合為N的所有方案的數(shù)量,不過由于可以重復(fù)使用,因此該題就是一個完全背包模型了。

  • 同時這個題目是會爆int的,因此我們用unsigned :232?12^{32} - 1232?1.
  • 在c++中有一個性質(zhì)就是對模上一個負(fù)數(shù)得到的結(jié)果與模上負(fù)數(shù)的絕對值(正數(shù))其實是一樣的。
  • 陪審團

    題目轉(zhuǎn)送們

    解題思路:我們要找的是和之差最小,因為題目兩者之差在?400???400-400 ---400?400???400那么我們就有一個思路就是以差來劃分,在計算完之后,中差為0開始依次往兩邊找,找到的第一個(因為對稱所以一正一負(fù))就是差最小的,在這兩個之間比較找出最大值,我們通過框架梳理一下思路

    還是重點來講一下狀態(tài)計算這個部分。結(jié)合背包問題的思路,對于f[i][j][k]我們找出最后的不同點出來以此來劃分集合。首先,對于第i個人我們有選與不選兩種劃分。不選由含義出發(fā)就是f[i-1][j][k],如果選的話,

    我們將第二部分,的所有情況分為變與不變兩部分,對于不變的就是第i個人,那么前一個部分的含義就是:前i-1個人中,選了j-1個人,差值為 k - (p[i] - d[i]) 而這個就正好是f[i-1][j-1][k - (p[i]-d[i])]。

  • 實現(xiàn)上的難點:由于數(shù)組下標(biāo)不能為負(fù)數(shù),因此我們設(shè)置一個偏移量 , 對于0~399 表示負(fù)數(shù),401~800表示正數(shù)。
  • 可以剪枝的地方,因為我們知道差值只能在-400~400 之間,也就是偏移量之后的0~800.因此如果不在這個范圍內(nèi)的肯定不是答案。還有一個是我們在第二個方案之前j必須要大于1才可以。
  • 還有一個難點是本題還要輸出一個正確的方案順序:一般這種有兩個解決方法,一個是另開一個數(shù)組來存儲狀態(tài)的轉(zhuǎn)移過程,另一個是倒推得到方案,這里我們采用倒推的解決方案。對于這個方法,我們倒推的時候是看當(dāng)前這個值的一個最值是有dp公式中哪里得到的,另一個關(guān)鍵點是,理解倒推時候變量之間的變化。
  • 代碼:

    #include<stdio.h> #include<algorithm> #include<iostream> #include<cstring> using namespace std;const int N = 210,M = 810 , base = 400; int n , m ; int ans[N] , p[N] , d[N]; int f[N][21][M]; int main(){int T = 1;while(scanf("%d%d",&n,&m),n&&m){for(int i = 1 ; i <= n; ++i)scanf("%d%d",&p[i],&d[i]);memset(f , -0x3f , sizeof f);f[0][0][base] = 0 ;for(int i = 1 ; i <= n ; ++i)for(int j = 0 ; j <= m; ++j )for(int k = 0 ; k < M ; ++k){f[i][j][k] = f[i-1][j][k];int t = k - (p[i] - d[i]);if(t <0 || t >= M)continue;if(j < 1)continue;f[i][j][k] = max(f[i][j][k] , f[i-1][j-1][t] + p[i] + d[i]);}int v = 0 ;while(f[n][m][base - v] < 0 && f[n][m][base+v] < 0)v++;if(f[n][m][base-v] > f[n][m][base+v]) v = base - v;else v = base + v;int cnt = 0 , i = n , j = m , k = v;while (j){if (f[i][j][k] == f[i - 1][j][k]) i -- ;else{ans[cnt ++ ] = i;k -= (p[i] - d[i]);i --, j -- ;}}int sp = 0 , sd = 0;for(int i = 0 ; i < cnt ; ++i) sp += p[ans[i]] , sd += d[ans[i]];printf("Jury #%d\n",T++);printf("Best jury has value %d for prosecution and value %d for defence:\n",sp,sd);sort(ans, ans + cnt);for(int i = 0 ; i < cnt ; ++i)printf(" %d",ans[i]);puts("\n");}return 0; }

    我們對代碼中的倒推過程來進行解釋一下,首先我們明確我們是將選出的m個人的編號給輸出出來,因此我們的循環(huán)條件初始條件就是m個人,只有沒有到0也就是還沒有記錄完。 我們看dp中的公式,一個是有可能從f[i-1][j][k]這個狀態(tài)中轉(zhuǎn)移過來的,如果是的話,那么前一個狀態(tài)對應(yīng)的是 i - 1 , 因此要 i – 。 否則就是由f[i-1][j-1][k-(p[i] - d[i])]得出,因為我們需要先讓 k-(p[i] - d[i]),然后 i-- , j--;

    多重背包

    題目轉(zhuǎn)送們

    對于多重背包,我們先想一最樸素的做法應(yīng)該就是將其轉(zhuǎn)化為01背包問題來解決。在轉(zhuǎn)化為01背包的問題上我們有兩種想法:

    原本的01背包框架應(yīng)該是這個,一種想法是在選這一步進行改進。對于01背包我們只要選與不選兩種,而對于多重背包在選這個基礎(chǔ)上我們可能選1… c[i]個。那么就有

    因此就有:

    #include<iostream> using namespace std; const int N = 110; int f[N] , n , m;int main(){cin>>n>>m;for(int i = 1 ; i <= n ; ++i){int w , v , s;cin>>w>>v>>s;for(int j = m ; j >= w ; --j)// k * w <= j , 即我選了這些后的前一個狀態(tài)不可能是小于0的for(int k = 1 ; k <= s && k * w <= j ; k++)f[j] = max(f[j] , f[j- k * w] + k * v); }cout<<f[m]<<endl;return 0; }

    還有一種思路是我將這個k * w[i[,分為k次01背包,也就是說將其拆分成一個一個的,然后利用01背包的寫法求解。這兩種是等價的做法。

    代碼:

    #include<iostream> using namespace std; const int N = 110; int f[N] , n , m;int main(){cin>>n>>m;for(int i = 1 ; i <= n ; ++i){int w , v , s;cin>>w>>v>>s;for(int k = 1 ; k <= s ; ++k)for(int j = m ; j >= w ; --j)f[j] = max(f[j] , f[j- w] + v); }cout<<f[m]<<endl;return 0; }

    二進制優(yōu)化版

    首先我們回想一下上面直接拆分為01背包的問題,我們是將其拆為一個一個的物品,因此我們的復(fù)雜度很高。這里我們要明確說明一個問題?對于0~n 的數(shù),我們需要使用多少的數(shù)才可以將它們表示出來,當(dāng)然肯定可以,當(dāng)是最笨的一種方法就是n個1 ,如果一個都不選就表示0,選n個1就表示n。不過這顯然就想我們上面的拆分方式就不可取。這里我們有一個結(jié)論:至少需要log?2n\log_{2}{n}log2?n這么一個數(shù)量的數(shù)字。這什么實現(xiàn)呢?好比10: 1 2 4 8 。這顯然是不可以的,因為這就能表示0~15了大于10了。因此我們對于不超過2k2^k2k的話就直接用剩下就好,就是:1 , 2 ,4,3。這就可以了。

    代碼:

    #include<stdio.h> #include<iostream> #include<vector> using namespace std; const int N = 2010; int n , m ,f[N];struct GOOD{int v,w; };int main(){cin>>n>>m;vector<GOOD>goods;for(int i = 1 ; i <= n ; ++i){int v ,w, s;cin>>w>>v>>s;for(int k = 1 ; k <= s ; k *=2){s -= k;goods.push_back({k*v, k *w});}if(s > 0) goods.push_back({s*v,s*w});}for(auto good : goods){for(int j = m ; j >= good.w ; --j)f[j] = max(f[j] , f[j - good.w] + good.v);}cout<<f[m]<<endl;return 0; }

    單調(diào)隊列優(yōu)化版

    視頻講解轉(zhuǎn)送門

    代碼:

    #include<iostream> #include<algorithm> #include<cstring> using namespace std; const int N = 1010 , M = 20010; int n , m; int f[M] , g[M] ,q[M];int main(){cin>>n>>m;for(int i = 1 ; i <= n ; ++i){memcpy(g , f ,sizeof f);int v , w , s;cin>>v>>w>>s;for(int j = 0 ; j < v ; ++j){int h = 0 ,t = -1;for(int k = j ; k <= m ; k += v){if(h <= t && q[h] < k - s*v )h++;// 【k-s*v ,k-v】//g[q[h]] 當(dāng)前體積(q[h])下的最大價值,(k - q[h])/v*w 在k下,除了q[h]外還能放的價值if(h <= t)f[k] = max(g[k] , g[q[h]] + (k - q[h])/v*w);//在g中找的,while(h <= t && g[k] >= g[q[t]] + (k - q[t])/v*w)t--;q[++t] = k;}}}cout<<f[m]<<endl;return 0; }

    分組背包

    題目轉(zhuǎn)送們
    解題思路

    這題可以說是01背包模型的拓展吧,與01背包有一點不同的是,我們在選擇一堆物品的時候需要枚舉這一推物品中的所有物品后只選擇一個。我么01的是這一推物品是否選,如果選的話只能在這一推物品中選一個。也可以從另一種角度來想,多重背包是這類題的一個特殊情況,對于多重背包我們可以將一個物品與它的數(shù)量看成一堆物品。在這一堆物品中,分別為1個該物品,…,s個該物品。

    代碼:

    #include<iostream> #include<cstring> #include<algorithm> using namespace std; const int N = 110; int n , m ; int w[N] , v[N] ; int f[N];int main(){cin>>n>>m;for(int i = 1 ; i <=n ; ++i){ // 第幾組int s;cin>>s;for(int i = 1 ; i <= s ; ++i)cin>>w[i]>>v[i];for(int j = m ; j >= 1 ; j--) // 體積for(int k = 1 ; k <= s; k++) // 選哪個物品if(j >= w[k])f[j] = max(f[j] , f[j-w[k]] + v[k]);}cout<<f[m]<<endl;return 0; }

    區(qū)間DP

    在區(qū)間DP中,一個狀態(tài)由若干個比它小且包含與它的區(qū)間所代表的狀態(tài)轉(zhuǎn)移過來。區(qū)間DP一般是枚舉區(qū)間長度,左端點,和劃分區(qū)間的位置。(因為右端點可以由區(qū)間長度和左端點得到)。

    環(huán)形石子和并

    解題思路:這題關(guān)鍵點是,只能合并相鄰的石子,因此我們就能知道如果我們能合并在l和r位置的兩個石子堆,說明這中間的的石子堆已經(jīng)合并完全了。以下是分析過程:(我們通過分析最大花費來展示)

    我們還是來解釋一下狀態(tài)計算,對于這個問題我們不好直接計算,那么我們就劃分為子問題,分而治之就簡單了。對于所有和并L和R的所有方案,我們想一下它們的最后的不同點,不同點應(yīng)該是中間以哪一個位置k作為劃分點將[L,R]區(qū)間分為了[L,K],和[K+1,R]兩個部分,因此我們要做的就是枚舉k的位置。同時我們在合并一個區(qū)間[L,R]的時候都需要從L到R之間數(shù)的和的一個代價,這一步我們就可以利用前綴個來進行優(yōu)化。

  • 對于環(huán)狀我們一般是將其拆分成長度為2n的一個鏈?zhǔn)健V挥形覀兊淖笥叶它c就會發(fā)生改變,之后我們遍歷一邊[fi,n+i-1](意思是在第i處將環(huán)斷開)取出一個最大值。
  • 代碼:

    int n; int a[MAXN]; int g[MAXN][MAXN],f[MAXN][MAXN]; //最大和最小 int sum[MAXN];int main(){scanf("%d",&n);for(int i =1;i<=n;i++){scanf("%d",&a[i]);a[n+i] = a[i]; //構(gòu)造}for(int i=1;i<=2*n;i++){ //前綴和sum[i] =sum[i-1] + a[i];}memset(g,0x3f,sizeof(g));memset(f,-0x3f,sizeof(f));for(int lena=1;lena<=n;lena++){for(int l=1;l+lena-1 <=n*2;l++){int r = l + lena -1;if(l == r){g[l][r] = f[l][r] = 0;}else {for(int k = l;k < r;k++){g[l][r] = min(g[l][r],g[l][k]+g[k+1][r] + (sum[r] - sum[l-1]));f[l][r] = max(f[l][r],f[l][k]+f[k+1][r] + (sum[r] - sum[l-1]));}}}}int maxn = -INF,minx = INF;for(int i=1;i<=n;i++){ //看n種分割哪種最大和最小minx = min(minx,g[i][i+n-1]);maxn = max(maxn,f[i][i+n-1]);}printf("%d\n%d\n",minx,maxn);return 0; }

    總結(jié)

    以上是生活随笔為你收集整理的动态规划解题思路与总结(三万字)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。