期待中的鐘神終於來了(果不其然帶來了摸魚時間qwq)html
emmm,談些正經的話題ios
今天算是一個有關於DP(動態規劃)的全集c++
那麼我來介紹一下有關DP的知識數組
【具備無後效性(先無論是什麼)】數據結構
以斐波那契數列爲例 { 據鍾神說通項公式爲:fn=【(sqrt(5)+1))/2】n-【(sqrt(5)-1))/2】n }spa
(←斐波那契數列就是這麼個東西)code
邊界條件:f0=0,f1=1htm
轉移方程:fn=fn-1+fn-2blog
方法:ip
常見的DP:
按照數字的位數劃分轉移階段
轉移方式:枚舉下一位數字填什麼
限制條件:數位的上下界要求
例題:
給定兩個數l,r;求l—r之間有多少個數?
思路:
將這個數按位拆開,【0-x】拆成xn位
原式化爲【0-r】–【0-l-1】
則需求v,知足0≤v≤x,數位DP從最高位開始填
分如下兩種狀況:
那麼須要枚舉第i-1位填什麼,便可求解。
#include <bits/stdc++.h> using namespace std; int l, r, z[233]; int f[23333][2]; int solve(int x) { int n = 0; while (x) //求出x的每一位 { z[n] = x % 10; x /= 10; n++; } n--; memset(f, 0, sizeof(f)); f[n + 1][1] = 1; for (int i = n; i >= 0; i--) { for (int j = 0; j <= 1; j++) { //先分類討論 if (j == 0) { for (int k = 0; k <= 9; k++) { f[i][0] += f[i + 1][j]; } } else { for (int k = 0; k <= z[i]; k++) { //繼續分類討論 if (k == z[i]) { f[i][1] += f[i + 1][j]; } else { f[i][0] += f[i + 1][j]; } } } } } return f[0][0] += f[0][1]; } int main() { cin >> l >> r; cout << solve(r) - solve(l - 1) << endl; return 0; }
例題;
l-r的數的數位的和?
例題:
求l-r中知足相鄰兩個數字之差至少爲2的個數有多少個?
直接看代碼吧qwq
#include <bits/stdc++.h> using namespace std; int l, r, z[233]; int f[23333][2], g[23333][2]; int solve(int x) { int n = 0; while (x) //求出x的每一位 { z[n] = x % 10; x /= 10; n++; } n--; memset(f, 0, sizeof(f)); memset(g, 0, sizeof(g)); f[n + 1][1] = 1; g[n + 1][1] = 0; for (int i = n; i >= 0; i--) { for (int j = 0; j <= 1; j++) { //先分類討論 if (j == 0) { for (int k = 0; k <= 9; k++) { f[i][0] += f[i + 1][j]; g[i][0] += g[i + 1][j] + f[i + 1][j] * k; } } else { for (int k = 0; k <= z[i]; k++) { //繼續分類討論 if (k == z[i]) { f[i][1] += f[i + 1][j]; g[i][0] += g[i + 1][j] + f[i + 1][j] * k; } else { f[i][0] += f[i + 1][j]; g[i][0] += g[i + 1][j] + f[i + 1][j] * k; } } } } } return g[0][0] += g[0][1]; } int main() { cin >> l >> r; cout << solve(r) - solve(l - 1) << endl; return 0; }
好像是Windy數(不過我不知道有什麼區別qwq)
思路:
題目中有多少條件,就用多少個維度去解決問題。
因此加一個維度k;
f【i】【j】【k】表示這種狀況的方案數;i表示已經填好了i位;j=0表示x前i位大於v前i位;j=1表示x前i 位等於v前i位;k表示第i位填了k
這個直接去作吧(畢竟題解比我講的好qwq)
按照樹從根往下或者葉子往上劃分階段
刪除方式:集合葉子或者父親的信息
限制條件:不詳(不是我騙大家,是真的不詳qwq)
若f【i】表示以i爲根的子樹有多少個點
則易得f【leaf】=1
能夠推得:f【p】=f【son 1】+ f【son 2】+······=+f【son k】+1(設p有k個兒子)
//計算根爲i的子樹的結點個數(僞代碼) #include <bits/stdc++.h> using namespace std; int n, f[233]; void dfs(int p) { for (x) //x表示p的各個兒子 { dfs(x); f[p] += f[x]; } f[p]++; } int main() { cin >> n; read_tree(); //讀入樹先暫時不寫 dfs(1); //以一爲根樹的大小 cout << f[1] << end; return 0; }
例子:
求任一個樹的直徑(最遠兩個點的距離)
咱們能夠清楚的看到:
這個最遠的距離必定是這樣的:↗↘(使得路徑儘可能的長)
即求下一個點向下走的最長和次長路徑的和。
狀態定義:
f【i】【0】表示i向下最長;f【i】【1】表示i向下次長。
轉移方程:
因此說f【p】【0】=max(f【p 1】【0】+ f【p 2】【0】+······+f【p k】【0】)+1(設p有k個不一樣的到達葉子路徑)
f【p】【1】=max(f【p 1】【0】+ f【p 2】【0】+······+f【p k】【0】)+1(設p有k個不一樣的到達葉子路徑)【在 f【p 1】【0】+ f【p 2】【0】+······+f【p k】【0】中把以前求的最大值去掉】
最後答案還應該將 f【p】【0】+f【p】【1】-1
練習題:
洛谷 P4408 [NOI2003]逃學的小孩 洛谷 P3304 [SDOI2013]直徑
思想:必定是枚舉一個斷點從而進行合併
例子:
【洛谷【NOI1995】石子合併】的非環情況
n堆石頭,每次能夠合併相鄰兩堆石子,合併兩堆石頭代價爲兩堆石頭之和,求合併爲一堆石頭時所花的代價最小。
不難發現,必定能夠找到一條分界線p,使得左邊【l-p】和右邊【p+1-r】各合併成一堆石子
狀態定義:
f【l】【r】表示從第l堆石子到第r堆石子合併爲一堆石子的最小代價
邊界條件:f【i】【i】=0
轉移方程:
f【l】【r】=min(f【l】【r】,f【l】【p】+f【p+1】【r】+sum【l】【r】)
但在枚舉的過程當中須要知足一個階段性:
for(int l=1;l<=n;l++) { for(int r=l+1;r<=n;r++) { for(int p=1;p<r;p++) { f[l][r]=min(f[l][r],f[l][p]+f[p+1][r]+sum[l][r]); } } }
這個代碼顯然不行,這顯然不行,由於在算f【l】【r】時,f【p+1】【r】並沒求出。
正解:
//石子合併(不是環的狀況) #include <bits/stdc++.h> using namespace std; const int INF = 0x3f3f3f3f; //兩個0x7f加起來爆int,推薦用0x3f int n, int main() { cin >> n; for (int i = 1; i <= n; i++) { cin >> z[a]; } memset(f, 0x3f, sizeof(f)); for (int i = 1; i <= n; i++) { f[i][i] = 0; } for (int len = 2; len <= n; len++) { for (int l = 1, r = len; r <= n; l++, r++) { for (int p = 1; p < r; p++) { f[l][r] = min(f[l][r], f[l][p] + f[p + 1][r] + sum[l][r]); } } } cout << f[1][n] << endl; return 0; }
就是複雜度有點高qwq,爲O(n^3)
若爲環,則需開兩個n的空間,取min(f【1】【n】,f【2】【n+1】,f【3】【n+2】······)就能夠了
由於這枚舉了全部斷點的狀況
複雜度不變——O(n^3),序列長度變成兩倍
練習題:
洛谷 P1880 [NOI1995]石子合併 洛谷 P1063 能量項鍊
按照選取集合的狀態劃分轉移階段
轉移方式:枚舉下一個要選取的物品
限制條件:不詳(qwq)
先總結一下根據數據能夠推出來的方法:
N<=22多半用狀壓
N<=12多半用爆搜
N<=32 多半放棄吧
N<=50多半放棄吧
N<=100多半O(n^3)
N<=1000多半O(n^2)
N<=10000 多半數據結構題O(nlogn)
N<=100000 多半線性的
N>100000 多半O(1)
這個方法是用作TSP問題(旅行商問題)
【屬於NP—hard問題(複雜度至少爲2^n)】
假設有一個旅行商人要拜訪n個城市,他必須選擇所要走的路徑,路徑的限制是每一個城市只能拜訪一次,並且最後要回到原來出發的城市。路徑的選擇目標是要求得的路徑路程爲全部路徑之中的最小值。
考慮狀態壓縮:
先設一個集合
若該點在這個集合裏,則用二進制數1表示;反之爲0
將這個二進制的數返回爲一個十進制的數。
f【s】【i】:s表示這是一個s位的二進制數,i表示停留在i,數組表示這種狀況最短距離
初始化:
f【1】【1】=0.
這裏的轉移方程大概是這麼寫的。(具體思路)
#include<iostream> #include<cstdio> #include<vector> #include<algorithm> #include<queue> #include<cstring> using namespace std; int read() { int f=1,x=0; char ss=getchar(); while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();} while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();} return f*x; } int n; int dp[1100010][25]; int map[25][25]; int main() { n=read(); for(int i = 1;i <= n;i ++) for(int j = 1;j <= n;j ++) map[i][j]=read();//讀入個村莊間距離 memset(dp,63,sizeof(dp)); dp[1][1] = 0;//狀態1表示此時只有1號點訪問過 for(int i =0;i<=(1 << n) -1;i++) for(int j=1;j<=n;j++) if( !( (1 << j-1) & i) ) for(int k=1;k<=n;k++) if( ( (1 << k-1) & i) ) dp[((1 << j-1) | i)][j] = min(dp[((1 << j-1) | i)][j],dp[i][k] + map[k][j]);//核心代碼,解釋如上所述 int ans = 2147483640; for(int i=2;i<=n;i++)//最後從狀態(1<<n)-1(二進制全爲1)中尋找到1最短的點 ans=min(ans,dp[(1<<n)-1][i] + map[i][1]); cout<<ans; return 0; }
If(j∉這個集合s),則f【s∪{j}】【i】=f【s】【i】+dis【i】【j】
複雜度(n^2 * 2^n),內存O(2^n *n)
洛谷1216
改編:使得最後%m後值最大
當一個題作不了的時候就往上加維度
考慮數據範圍
狀態定義:
bool f【i】【j】【k】表示走到第i行第j列%m=k是否是可能的
可能爲true;不可能爲false
因此,f【i】【j】【k】= f【i-1】【j-1】【k-aij】| f【i-1】【j】【k-aij】
邊界條件:f【1】【1】【a11】=true
//數字三角形 #include <iostream> #include <cstdio> #include <cmath> using namespace std; int f[1001][1001], a[1001][1001], n; int main() { cin >> n; for (int i = 1; i <= n; i++) { for (int j = 1; j <= i; j++) cin >> a[i][j]; } f[1][1] = a[1][1]; for (int i = 2; i <= n; i++) { for (int j = 1; j <= i; j++) f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j]; } int ans = 0; for (int i = 1; i <= n; i++) { ans = max(ans, f[n][i]); } cout << ans; return 0; }
練習:數字三角形2
最長上升子序列:
運用線段樹,區間詢問最大值和單點修改
揹包:揹包九講
01揹包
徹底揹包
練習:
洛谷 1048
#include<iostream> #include<cstdio> #include<cstring> #include<cmath> #include<iomanip> #include<string> #include<algorithm> #include<cstdlib> using namespace std; int dp[10000000],v[1000],p[1000]; int main() { int n,m; cin>>n>>m; for(int a=1;a<=m;a++) { cin>>v[a]>>p[a]; } for(int j=1;j<=m;j++) { for(int i=n;i>=v[j];i--) { dp[i]=max(dp[i],dp[i-v[j]]+p[j]); } } cout<<dp[n]; return 0; }