目錄ios
狀態:要算什麼算法
轉移方程:如何去算數組
無後效性:把狀態看作一個點,轉移過程看做一條邊,動態規劃的全部概念組成了一個有向無環圖(\(DAG\)),因此在作題時必定要考慮是否是知足無後效性優化
一旦出現亂序的狀況,應該對這個圖先進行一個拓撲排序spa
有\(N\)個物品,有一個\(M\)溶劑的包,每一個物品有一個體積和價值,要求最大化價值之和code
第\(i\)個物品的價值爲\(V_i\),佔\(W_i\)的空間排序
\(dp[i][j]\)表示已經放好了前\(i\)個物品,如今放進去的物品的體積之和爲\(j\)。string
考慮轉移方程:it
第\(i+1\)個物品只有兩種狀況:放入揹包與不放入揹包io
若是放入第\(i+1\)個物品,體積不變,價值也不變
若是不放入第\(i+1\)個物品,應該轉移爲\(dp[i+1][j+V_{i+1}]\)
如今對於每個物品就只有放和不放兩種狀況(這是用本身更新別人的方法)
若是用別人更新本身呢?
對於\(dp[i][j]\),若是第\(i-1\)個物品沒有放,是由\(dp[i-1][j]\)轉移而來的
不然就是由\(dp[i-1][i-V_i]\)更新而來的(加上\(W_i\))
代碼以下:
#include<cstdio> #include<iostream> #include<algorithm> using namespace std; int dp[10010][10010]; int w[10010],v[10010]; int n,m; int ans=0; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) scanf("%d%d",&v[i],&w[i]); for(int i=1;i<=n;++i) for(int j=0;j<=m;++j) { dp[i][j]=dp[i-1][j]; if(j>=v[i]) dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]); } for(int i=0;i<=m;++i) ans=max(ans,dp[n][i]); printf("%d",ans); return 0; }
如今每一個物品能夠用無限次,這個時候要怎麼辦呢?
直接枚舉每一個物品用多少次就好了
可是複雜度過高\(\Omega \omega \Omega\)
咱們只須要將原來的\(dp[i-1][j-v_i]+w_i\)變爲\(dp[i][j-v_i]+w_i\)就行了
#include<cstdio> #include<iostream> #include<algorithm> using namespace std; int dp[10010][10010]; int w[10010],v[10010]; int n,m; int ans=0; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) scanf("%d%d",&v[i],w[i]); /*for(int i=1;i<=n;++i) for(int j=0;j<=m;++j) for(int k=0;k*v[i]<=j;++k) { dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]); }*/ for(int i=1;i<=n;++i) for(int j=0;j<=m;++j) { dp[i][j]=dp[i-1][j]; if(j>=v[i]) dp[i][j]=max(dp[i][j],dp[i][j-v[i]]+w[i]); } for(int i=0;i<=m;++i) ans=max(ans,dp[n][i]); printf("%d",ans); return 0; }
那若是每一個物品只能用有限次,那怎麼辦呢
仍是直接枚舉每一個物品用多少次就好了
這個東西的複雜度是\(O(n^3)\)的
那麼如何優化呢?(有點難)
有限揹包最慢的地方是枚舉每一個物品用了多少次
思想就是將一個揹包變成多個捆綁包(進行二進制分解,若是不夠了,就只能委屈一下最後一個捆綁包了),而後就變成了一個\(01\)揹包
以\(13\)爲例,能夠拆成:\(1\)、\(2\)、\(4\)、\(6\)四個捆綁包
代碼以下(主要是拆捆綁包的部分)
#include<cstdio> #include<iostream> #include<algorithm> using namespace std; int dp[10010][10010]; int w[10010],v[10010]; int n,m; int ans=0; int main() { scanf("%d%d",&n,&m); int cnt=0; for(int i=1;i<=n;++i) { int v_,w_,z; scanf("%d%d%d",&v_,&w_,&z); int x=1; while(x<=z) { cnt++; v[cnt]=v_*x; w[cnt]=w_*x; z-=x; x*=2; } if(z>0) { cnt++; v[cnt]=v_*z; w[cnt]=w_*z; } } for(int i=1;i<=cnt;++i) for(int j=0;j<=m;++j) { dp[i][j]=dp[i-1][j]; if(j>=v[i]) dp[i][j]=max(dp[i][j],dp[i][j-v[i]]+w[i]); } for(int i=0;i<=m;++i) ans=max(ans,dp[n][i]); printf("%d",ans); return 0; }
狀態:\(dp[i][j]\)表示走到第\(i\)行第\(j\)列時所走的路徑的最大值
狀態轉移方程:\(dp[i][j]=max(dp[i-1][j-1],f[i-1][j])+a[i][j]\)
誒誒,怎麼仍是水三角形???
如今求的是答案對\(100\)取模以後的最大值
怎麼作呢?
鍾皓曦:維度不夠加一維,維度不夠再加一維,你總有一天會過的
狀態:布爾狀態,\(dp[i][j][k]\)表示走到第\(i\)行第\(j\)列的數對\(100\)取模等於\(k\)是否是可行的
狀態轉移:考慮用本身更新別人,由\(dp[i][j][k]\)能夠轉移到\(dp[i+1][j][(a[i+1][j]+k)\mod 100]\)和\(dp[i+1][j+1][(a[i+1][j+1]+k)\mod 100]\)
代碼以下:
#include<cstdio> #include<iostream> #include<algorithm> using namespace std; bool dp[233][233][233]; int n; int a[233][233]; int ans; int main() { scanf("%d",&n); for(int i=1;i<=n;++i) for(int j=1;j<=i;++j) scanf("%d",&a[i][j]); dp[1][1][a[1][1]%100]=true; for(int i=1;i<=n;++i) for(int j=1;j<=i;++j) for(int k=0;k<100;++k) if(dp[i][j][k]) { dp[i+1][j][(k+a[i][j])%100]=true; dp[i+1][j+1][(k+a[i+1][j+1])%100]=true; } for(int i=1;i<=n;++i) for(int j=0;j<100;++j) if(dp[n][i][j]) ans=max(ans,j); printf("%d",ans); return 0; }
狀態:\(dp[i]\)表示以\(i\)結尾的最長上升子序列的長度
狀態轉移:\(f[i]=max(f[j])+1\)知足\(1\leq j<i\)而且\(a[j]<a[i]\)
這個算法複雜度是\(O(n^2)\)的
若是數據再大一點,就能夠用線段樹
把兩對相鄰的石子合併爲一堆石子,每次合併的代價就是兩堆石子的個數之和,如今問把\(n\)堆石子合併爲\(1\)堆石子的最小代價是多少
咱們能夠發現每次合併是將一段區間的石子合併
這就是一個區間\(DP\)
區間\(DP\)的狀態通常爲\(dp[l][r]\)
狀態:$dp[l][r] \(表示\)[l,r]\(區間合併的最小代價(\)dp[l][l]=0$)
每次均可以在區間中找到一個分界線,最後合併分界線兩邊的區間
那麼就能夠枚舉分界線\(p\)
狀態轉移:\(dp[l][r]=min(dp[l][r],dp[l][p]+dp[p+1][r]+sum[r]-sum[l-1]\)
把一排矩陣排排坐,保證矩陣是能夠相乘獲得一個結果的,如今在這幾個矩陣中加上括號,使得運算次數最小
狀態:\(dp[l][r]\)表示將第\(l\)個矩陣和第\(r\)個矩陣的最小運算次數
狀態轉移:\(dp[l][r]=min(dp[l][p]+f[p+1][r]+a[l]\times a[p+1]\times a[r+1])\),\(p\)是中點。
在平面上由\(n\)個點,給出每一個點的座標,如今由\(1\)號點出發,把全部點都走一遍而後回到\(1\)號點,求最短距離
首先,通常來講咱們沒有必要把一個點走兩次
當前在哪一個點、走過了那個點是兩個變化的量
狀態:\(dp[s][i]\)中\(i\)表示如今走到了第\(i\)個點,\(s\)表示走過了那些點
理論上來講,\(s\)是要用一個數組來維護的,可是咱們如今要用一個數來表示
那麼咱們就能夠用一個二進制數來表示哪些點沒走,哪些點走了(\(1\)表示走過,\(0\)表示沒走過)
狀壓\(DP\)能解決的範圍在\(n\leq 20-22\),由於複雜度爲\(O(2^n\times n^2)\)
可愛的代碼:
#include<cstdio> #include<iostream> #include<cmath> #include<algorithm> #include<cstring> using namespace std; double dp[233][233]; double x[233],y[233]; double ans=0x3f; int n; double dis(int xx,int yy) { return sqrt((x[xx]-x[yy])*(x[xx]-y[yy])+(y[xx]-y[yy])*(y[xx]-y[yy])); } int main() { scanf("%d",&n); for(int i=0;i<n;++i) scanf("%lf%lf",&x[i],&y[i]); memset(dp,0x3f,sizeof(dp)); dp[1][0]=0; for(int s=0;s<(1<<n);s++) for(int i=0;i<n;++i) if(dp[s][i]<0x3f)//這是一個可行的方案 { for(int j=0;j<n;++j) if((s>>j)&1==0)//將s二進制的第j爲取了出來 { int news=s|(1<<j);//把s的第j爲變成了1 dp[news][j]=min(dp[news][j],dp[s][i]+dis(i,j)); } } for(int i=0;i<n;++i) ans=min(ans,dp[(1<<n)-1][i]+dis(i,0)); printf("%d",ans); return 0; }
\(JOHN\)要在一片牧場上種草,每塊草坪之間沒有相鄰的邊
狀態:\(dp[i][s]\)表示前\(i\)行的草都種完了,\(s\)表示第\(i\)行的草種成了什麼樣,這種狀況下的方案數爲\(dp[i][s]\)
考慮第\(i+1\)行如何種草:
首先第\(i+1\)行種的草沒有兩個連續的
其次第\(i\)行的草和第\(i+1\)行的草沒有相鄰的草,就是\(s\)&\(s\)'=\(0\)
在\(N\times N\)的棋盤中,放\(k\)個國王,使這些國王不能相互攻擊到對方(國王的攻擊範圍就是其周圍的\(8\)個格子)
參照上一道題(種國王)
狀態:\(dp[i][s]\)表示前\(i\)行的國王都種完了,\(s\)表示第\(i\)行的國王種成了什麼樣,這種狀況下的方案數爲\(dp[i][s]\)
可是這個題要多放一個國王,因此咱們要多加一個維度
新狀態:\(dp[i][s][j]\)表示前\(i\)行的國王都種完了,\(s\)表示第\(i\)行的國王種成了什麼樣,如今放了\(j\)個國王,這種狀況下的方案數爲\(dp[i][s]\)
是在\(DP\)的過程當中按照數的位數進行轉移的
給出兩個數\(l、r\),求這之間有多少個數
首先,\([l,r]\)之間有多少數,就是求\([0,l]\)和\([0,r]\)之間分別有多少數,而後相減
數位\(DP\),就是將一個\(n\)位數,抽象爲\(n\)個格子,若是要求有多少個數小於這個數,能夠用數字填滿這\(n\)個格子
注意:必定要從高位向低位一位一位的填
狀態:\(dp[i][j]\)中,已經填好了前\(i\)位,\(j=0\)表示當前這個數必定小於\(x\),\(j=1\)表示當前這個數不肯定是否小於\(x\)
狀態轉移:數位\(DP\)的轉移都是去枚舉下一位填什麼數
代碼:
#include<cstdio> #include<iostream> #include<cstring> using namespace std; int l,r; int dp[10010][10010]; int z[10010]; int solve(int x) { int l=0; while(x>0) { l++; z[l]=x%10; x/=10; } memset(dp,0,sizeof(dp)); dp[l+1][1]=1;//在第l+1位以後都只能填0 /*轉移考慮用本身去更新別人*/ for(int i=l+1;i>=2;i--) for(int j=0;j<=1;++j) for(int k=0;k<=9;++k)//枚舉應該填哪個數 { if(j==1&&k>z[i-1]) continue; int j_; if(j==0) j_=0; else if(k==z[i-1]) j_=1; else j_=0; dp[i-1][j_]+=dp[i][j]; } return dp[1][0]+dp[1][1]; } int main() { scanf("%d%d",&l,&r); printf("%d",solve(r)-solve(l-1)); return 0; }
求\([l,r]\)中全部數的數位之和
一樣,咱們能夠轉化爲\([0,r]\)和\([0,l-1]\)之間有的數位之和
狀態:\(dp[i][j]\)表示第\(i\)位已經填完了,\(j=0\)表示當前這個數必定小於\(x\),\(j=1\)表示當前這個數不肯定是否小於\(x\),這時的方案數
求\([l,r]\)中有多少個相鄰兩位數字之差大於等於\(2\)的數
狀態:$ dp[i][j][k]\(表示當前已經填了\)i\(個數,\)j=0\(表示當前這個數必定小於\)x\(,\)j=1\(表示當前這個數不肯定是否小於\)x\(,第\)i\(位填的數是\)k$
求\([l,r]\)中知足各位數字之積位\(k\)的數有多少個
狀態:\(dp[i][j][r]\)表示當前已經填了\(i\)個數,\(j=0\)表示當前這個數必定小於\(x\),\(j=1\)表示當前這個數不肯定是否小於\(x\),當前乘積爲\(r\)
有些\(r\)是永遠不會用到的,就是那些大於\(10\)的質數的倍數
新狀態:\(dp[i][j][a][b][c][d]\)表示當前已經填了\(i\)個數,\(j=0\)表示當前這個數必定小於\(x\),\(j=1\)表示當前這個數不肯定是否小於\(x\),如今的乘積爲\(2^a+3^b+5^c+7^d\)
如今給你一棵\(n\)個點的樹,問這個樹有多少點???
樹形\(DP\)通常是\(DP\)以這個點爲根的子樹的狀態
狀態:\(f[i]\)表示以\(i\)爲根的樹有多少個點
狀態轉移:\(f[i]=\sum_{j\in}\)
給出一棵樹,求出樹的直徑
將兩個點的路徑看作由\(LCA\)向下的兩條路徑
狀態:\(f[i][0]\)表示第\(i\)個點向下的最大值,\(f[i][1]\)表示最小值
狀態轉移:\(f[i][0]=max(f[p_j][0])+1\),\(f[i][1]\)的值應該是剩下的全部兒子的最長路中的最長路
#include<cstdio> #include<iostream> using namespace std; void dfs(int i) { for(p is i's son)//教你們如何寫僞代碼,你只須要boomboomboom,而後boomboomboom,就能boomboom了 dfs(p); for(p is i's son) { int v=f[p][0]; if(v>f[i][0]) { f[i][1]=f[i][0]; f[i][0]=v; } else if(v>f[i][1]) f[i][1]=v; } } int main() { scanf("%d",&n); du ru jian shu; dfs(1);//強行令1號點爲根 return 0; }
求全部點之間的路徑之和爲多少
狀態:\(dp[i]\)表示以\(i\)爲根的子樹有多少個點
狀態:\(dp[i][0/1]\)表示到了第\(i\)個點,\(1\)表示選了改點,\(0\)表示沒有選
若是有一個點選了,那麼這個點的全部兒子都不能選
即:\(dp[i][1]=\sum_{j\in son_i}dp[j][0]+a[i]\)
\(dp[i][0]=\sum_{j\in son_i}max(dp[j][0],dp[j][1])\)
每一個士兵能夠守護全部與該結點直接相鄰的邊,請問在全部邊都被守護的條件下,最少要安排多少士兵?
狀態:\(dp[i][0/1]\)表示以\(i\)爲根的子樹的全部點都被守護,\(1\)表示有士兵,\(0\)表示沒有,此時的最少士兵
\(f[i][0]=\sum_{j\in son_i}dp[i][1]\)
\(dp[i][1]=\sum_{j\in son_i}max()\)