本篇博文將詳細總結貪心和動態規劃部分,貪心和動態規劃是非常難以理解和掌握的,但是在筆試面試中經常遇到,關鍵還是要理解和掌握其思想,然後就是多刷刷相關一些算法題就不難了。本篇將會大篇幅總結其算法思想。
貪心和動態規劃思想
馬爾科夫模型
對於
相應的,對於
高階馬爾科夫模型的推理,叫做“動態規劃”,而馬爾科夫模型的推理,對應“貪心法”。
無後效性
- 計算
A[i] 時只讀取A[0…i−1] ,不修改——歷史 - 計算
A[i] 時不需要A[i+1…n−1] 的值——未來
理解貪心,動態規劃:
動態規劃:
可以如下理解動態規劃:計算
貪心:
根據實際問題,選取一種度量標準。然後按照這種標準對
這一處理過程一直持續到
字符串迴文劃分問題
問題描述
給定一個字符串
單個字符構成的字符串,顯然是迴文串;所以,這個的劃分一定是存在的。
如:
方法一:深度優先搜索
思考:若當前計算得到了
剪枝:在每一步都可以判斷中間結果是否爲合法結果。
- 回溯+剪枝——如果某一次發現劃分不合法,立刻對該分支限界。
- 一個長度爲
n 的字符串,最多有n−1 個位置可以截斷,每個位置有兩種選擇,因此時間複雜度爲O(2n−1)=O(2n) 。
在利用
線性探索:
j 從i 到n−1 遍歷即可,從字符串str[i,i+1,...,j] 兩端開始比較,然後得出是否對稱迴文。事先緩存所有
str[i,i+1,..,j] 是迴文串的那些記錄,用二維布爾數組p[n][n] 的true/false 表示str[i,i+1,...,j] 是否是迴文串。它本身是個小的動態規劃:如果已知
str[i+1,...,j−1] 是迴文串,那麼判斷str[i,i+1,...,j] 是否是迴文串,只需要判斷str[i]==str[j] 就可以了。
// 判斷str[i,j]迴文與否
void CalcSubstringPalindrome(const char* str, int size, vector<vector<bool>>& p)
{
int i, j;
for (i = 0; i < size; i++)
p[i][i] = true;//單個字符肯定是迴文串
for (i = size - 2; i >= 0; i--)
{
p[i][i + 1] == (str[i] == str[i + 1]);//得出字符串內每兩個相鄰字符迴文與否,也就是得出初始狀態
for (j = i + 2; j < size; j++)//以i子串左端並且在內循環i固定,j爲子串右端,並且j不斷向外擴展,
//遞進的判斷str[i,j]迴文與否
{
if ((str[i] == str[j]) && p[i + 1][j - 1])
p[i][j] = true;
}
}
}
//以str[nStart]爲起點,不斷的判斷str[nSart,i]迴文與否,若是迴文加入solution
void FindSolution(const char* str, int size, int nStart, vector<vector<string>>& all, vector<string>& solution, const vector<vector<bool>>& p)
{
if (nStart >= size)//表示當前方向遞歸深入到頭
{
all.push_back(solution);//將當前方向的所有迴文壓入到all
return;
}
for (int i = nStart; i < size; i++)
{
if (p[nStart][i])
{
solution.push_back(string(str + nStart, str + i + 1));
FindSolution(str, size, i + 1, all, solution, p);//沿着這個方向深入遞歸
solution.pop_back();//回溯到當前初始狀態,選擇其他方向
}
}
}
void MinPalindrome3(const char* str, vector<vector<string>>& all)
{
int size = (int)strlen(str);
vector<vector<bool>> p(size, vector<bool>(size));
CalcSubstringPalindrome(str, size, p);
vector<string> solution;
FindSolution(str, size, 0, all, solution, p);
}
方法二:動態規劃
如果已知:
算法:
- 將集合
φ(i+1) 置空; - 遍歷
j(0≤j<i) ,若str[j,j+1…i] 是迴文串,則將str[j…i] 添加到φ(j−1) ,然後再把φ(j−1) 添加到φ(i+1) 中; i 從0 到n ,依次調用上面兩步,最終返回φ(n) 即爲所求。
//to 表示prefix[i],長度爲i的迴文集合;from表示prefix[j]長度爲j的迴文集合;sub表示要添加的迴文str[j,i]
void Add(vector<vector<string>>& to, const vector<vector<string>>& from, const string& sub)
{
if (from.empty())//from 爲空時,直接將sub壓入to
{
to.push_back(vector<string>(1, sub));//vector<string>(1, sub):初始化vector,長度爲1,一個字符串sub
return;
}
to.reserve(from.size());
for (vector<vector<string>>::const_iterator it = from.begin(); it != from.end(); it++)//遍歷from裏面每個迴文
{
to.push_back(vector<string>());
vector<string>& now = to.back();
now.reserve(it->size() + 1);
//將from某個個迴文裏面的每個字符依次壓入now,然後在末尾加上要添加的迴文子串sub
for (vector<string>::const_iterator s = it->begin(); s != it->end(); s++)
now.push_back(*s);
now.push_back(sub);
}
}
void MinPalindrome4(const char* str, vector<vector<string>>& all)
{
int size = (int)strlen(str);
vector<vector<bool>> p(size, vector<bool>(size));
CalcSubstringPalindrome(str, size, p);
vector<vector<string>>* prefix = new vector<vector<string>>[size];//注意這裏是vector<vector>* 相當於一個三維數組
prefix[0].clear();
int i, j;
for (i = 1; i <= size; i++)
{
for (j = 0; j < i; j++)
{
if (p[j][i - 1])//prefix[i]表示長度爲i的迴文集合,這裏是指長度,那麼索引應該到i-1
{
Add((i == size) ? all : prefix[i], prefix[j], string(str + j, str + i));
}
}
}
delete[] prefix;
}
DFS和DP的深刻認識
- 顯然
DFS 比DP 好理解,而代碼上DP 更加簡潔。 DFS 的過程,是計算完成了str[0…i] 的切分,然後遞歸調用,繼續計算str[i+1,i+2…n−1] 的過程;- 而
DP 中,假定得到了str[0…i−1] 的所有可能切分方案,如何擴展得到str[0…i] 的切分; - 上述兩種方法都可以從後向前計算得到對偶的分析。
從本質上說,二者是等價的:最終都搜索了一顆隱式樹。
DFS 顯然是深度優先搜索的過程,而DP 更像層序遍歷 的過程。- 如果只計算迴文劃分的最少數目,動態規劃更有優勢;如果計算所有迴文劃分,
DFS 的空間複雜度比DP 略優。
利用貪心思想的幾個重要算法
最小生成樹MST
最小生成樹要求從一個帶權無向連通圖中選擇
Prim算法
實例:
實現代碼:
#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#define MAXINT 6
using namespace std;
//聲明一個二維數組,C[i][j]存儲的是點i到點j的邊的權值,如果不可達,則用1000表示
//藉此二維數組來表示一個連通帶權圖
int c[MAXINT][MAXINT] = { { 1000, 6, 1, 5, 1000, 1000 }, { 6, 1000, 5, 1000, 3, 1000 }, { 1, 5, 1000, 5, 6, 4 }, { 5, 1000, 5, 1000, 1000, 2 }, { 1000, 3, 6, 1000, 1000, 6 }, { 1000, 1000, 4, 2, 6, 1000 } };
void Prim(int n)
{
int lowcost[MAXINT];//lowcost[i]表示V-S中的點i到達S的最小權值
int closest[MAXINT];//closest[i]表示V-S中的點i到達S的最小權值S中對應的點
bool s[MAXINT];//bool型變量的S數組表示i是否已經包括在S中
int i, k;
s[0] = true;//從第一個結點開始尋找,擴展
for (i = 1; i <= n; i++)//簡單初始化,這個時候S爲{0},V-S爲{1,2,3,4,5}
{
lowcost[i] = c[0][i];//這個時候S中只有0
closest[i] = 0;//現在所有的點對應的已經在S中的最近的點是1
s[i] = false;
}
cout << "0->";
for (i = 0; i<n; i++)//執行n次,也即是向S中添加所有的n個結點
{
int min = 1000;//最小值,設大一點的值,後面用來記錄lowcost數組中的最小值
int j = 1;
for (k = 1; k <= n; k++)//尋找lowcost中的最小值,並且找出此時V-S中對應的點j
{
if ((lowcost[k]<min) && (!s[k]))
{
min = lowcost[k]; j = k;
}
}
cout << j << " " << "->";
s[j] = true;//添加點j到集合S中
for (k = 1; k <= n; k++)//因爲新加入了j點,需要更新V-S到S的最小權值,只需要與剛加進來的c[j][k]比較即可
{
if ((c[j][k]<lowcost[k]) && (!s[k])){ lowcost[k] = c[j][k]; closest[k] = j; }
}
}
}
int main()
{
Prim(MAXINT - 1);
return 0;
}
Kruskal算法
實例:
在實現
基於
實現代碼:
#include <iostream>
#include <vector>
#include <algorithm>
#include <fstream>
using namespace std;
struct Edge
{
int u; //邊連接的一個頂點編號
int v; //邊連接另一個頂點編號
int w; //邊的權值
friend bool operator<(const Edge& E1, const Edge& E2)
{
return E1.w < E2.w;
}
};
//創建並查集,uset[i]存放結點i的根結點,初始時結點i的根結點即爲自身
void MakeSet(vector<int>& uset, int n)
{
uset.assign(n, 0);
for (int i = 0; i < n; i++)
uset[i] = i;
}
//查找當前元素所在集合的代表元
int FindSet(vector<int>& uset, int u)
{
int i = u;
while (uset[i] != i) i = uset[i];
return i;
}
void Kruskal(const vector<Edge>& edges, int n)
{
vector<int> uset;
vector<Edge> SpanTree;
int Cost = 0, e1, e2;
MakeSet(uset, n);
for (int i = 0; i < edges.size(); i++) //按權值從小到大的順序取邊
{
e1 = FindSet(uset, edges[i].u);
e2 = FindSet(uset, edges[i].v);
if (e1 != e2) //若當前邊連接的兩個結點在不同集合中,選取該邊併合並這兩個集合,如果相等連接則成環
{
SpanTree.push_back(edges[i]);
Cost += edges[i].w;
uset[e1] = e2; //合併當前邊連接的兩個頂點所在集合
}
}
cout << "Result:\n";
cout << "Cost: " << Cost << endl;
cout << "Edges:\n";
for (int j = 0; j < SpanTree.size(); j++)
cout << SpanTree[j].u << " " << SpanTree[j].v << " " << SpanTree[j].w << endl;
cout << endl;
}
int main()
{
int n, m;
cin >> n >> m;
vector<Edge> edges;
edges.assign(m, Edge());
for (int i = 0; i < m; i++)
cin >> edges[i].u >> edges[i].v >> edges[i].w;
sort(edges.begin(), edges.end()); //排序之後,可以以邊權值從小到大的順序選取邊
Kruskal(edges, n);
system("pause");
return 0;
}
Dijkstra最短路徑算法
該算法爲單源點最短路徑算法,要求邊的權值爲正數。在圖
S 爲已經找到的從v0 出發的最短路徑的終點集合,它的初始狀態爲空集,那麼從v0 出發到圖中其餘各頂點(終點)vi(vi∈V−S) ,記arcs[i][j] 爲結點vi 直接到達 結點vj 的距離。記d[j] 爲源點v0 到達結點vj 的最短距離。初始時:d[j]=arcs[0][j] 選擇
vj ,使得d[j]=minj(d[i],vi∈V−S),vj 就是當前求得的一條從v0 出發的最短路徑的終點。令S=S∪j ;修改從
v0 出發到集合V−S 上任一頂點vk 可達的最短路徑長度。如果d[j]+arcs[j][k]<d[k] , 則修改d[k] 爲:d[k]=d[j]+arcs[j][k] ;以上
2,3 步驟重複n−1 次。
在網上找了個
實現代碼:
#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<set>
#include<queue>
using namespace std;
const int MAXINT = 32767;
const int MAXNUM = 10;//結點總數
int d[MAXNUM];//單源點到其他結點的最短路徑
int prev[MAXNUM];//記錄前驅結點
int arcs[MAXNUM][MAXNUM];//鄰接矩陣,arcs[i][j]也即是兩結點(vi,vj)之間的直接距離
void Dijkstra(int v0, int* prev)//源點 v0
{
bool S[MAXNUM];// 判斷是否已存入該點到S集合中
int n = MAXNUM;
for (int i = 1; i <= n; ++i)
{
d[i] = arcs[v0][i];//初始時最短距離爲直接距離
S[i] = false;// 初始都未用過該點
if (d[i] == MAXINT)
prev[i] = -1;
else
prev[i] = v0;
}
d[v0] = 0;
S[v0] = true;//S集合中加入v0
for (int i = 2; i <= n; i++)
{
int mindist = MAXINT;
int u = v0; // 找出當前未使用的點j的dist[j]最小值
for (int j = 1; j <= n; ++j)
if ((!S[j]) && d[j] < mindist)
{
u = j; // u爲在V-S中到源點v0的最短距離對應的結點
mindist = d[j];
}
S[u] = true;//將u加入到S中
//更新其他結點到單源點的最短距離,查看其他結點經過u到單源點的距離會不會比之前單元點的直接距離要短
for (int j = 1; j <= n; j++)
if ((!S[j]) && arcs[u][j]<MAXINT)
{
if (d[u] + arcs[u][j] < d[j])//在通過新加入的u點路徑找到離v0點更短的路徑
{
d[j] = d[u] + arcs[u][j]; //更新dist
prev[j] = u; //記錄前驅頂點
}
}
}
}
總結
Prim算法與貪心
Kruskal算法與貪心
對邊的權值進行從小到大的排序,依次加入小的權值邊,且不能形成環,我們把邊的集合
Dijkstra算法與貪心
只需要對
以上幾種算法的狀態轉移示意圖如下,是一個馬爾科夫過程:
可以看到,在從
最長遞增子序列LIS
在字符串部分我們詳解過這個問題,利用的是最長公共子序列解的,現在我們嘗試利用動態規劃解。
以序列
前綴分析
以
顯然以
LIS記號
長度爲
記
假定已經計算得到了
已知
求解LIS
根據定義,
從而:
- 計算
b[i] :遍歷在i 之前的所有位置j ,找出滿足條件aj≤ai 的最大的b[j]+1 ; - 計算得到
b[0…n−1] 後,遍歷所有的b[i] ,找出最大值即爲最大遞增子序列的長度。
時間複雜度爲
實現代碼
#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<set>
#include<queue>
#include<algorithm>
using namespace std;
int LIS(const int *p, int length, int *pre, int& nIndex)
{
int* longest = new int[length];//longest[i]表示以p[i]結尾的遞增序列長度
int i, j;
for (i = 0; i < length; i++)
{
longest[i] = 1;//初始時以每個字符結尾的遞增序列長度都爲1
pre[i] = -1;
}
int nLst = 1;//最長的遞增子序列長度
nIndex = 0;
for (i = 1; i < length; i++)
{
for (j = 0; j < i; j++)
{
if (p[j] <= p[i])
{
if (longest[i] < longest[j] + 1)
{
longest[i] = longest[j] + 1;
pre[i] = j;//記錄前驅
}
}
}
if (nLst < longest[i])//記錄所有的遞增子序列裏面最長的長度
{
nLst = longest[i];
nIndex = i;//nIndex記錄最長遞增子序列最後一個結點
}
}
delete[] longest;
return nLst;
}
void GetLIS(const int* array, const int* pre, int nIndex, vector<int>& lis)
{
while (nIndex>=0)//nIndex爲最長遞增子序列最後一個結點
{
lis.push_back(array[nIndex]);
nIndex = pre[nIndex];
}
reverse(lis.begin(), lis.end());//逆向輸出
}
void Print(int *p, int size)
{
for (int i = 0; i < size; i++)
cout << p[i]<<" ";
cout << endl;
}
int main()
{
int array[] = { 1, 4, 5, 6, 2, 3, 8, 9, 10, 11, 12, 12, 1 };
int size = sizeof(array) / sizeof(int);
int* pre = new int[size];
int nIndex;
int max = LIS(array, size, pre, nIndex);
vector<int> lis;
GetLIS(array, pre, nIndex, lis);
delete[] pre;
cout << max << endl;
Print(&lis.front(), (int)lis.size());
return 0;
}
矩陣乘積
問題描述
根據矩陣相乘的定義來計算
三個矩陣A、B、C的階分別是
問題分析
可以利用矩陣乘法的結合律 來降低乘法的計算量。
給定
將矩陣連乘積記爲
A[i:j] ,這裏i≤j ,顯然,若i==j ,則A[i:j] 即A[i] 本身。考察計算
A[i:j] 的最優計算次序。設這個計算次序在矩陣Ak 和Ak+1 之間將矩陣鏈斷開,i≤k<j ,則其相應的完全加括號方式爲:(Ai,Ai+1...Ak)(Ak+1,Ak+2,...,Aj) 計算量:
A[i:k] 的計算量加上A[k+1:j] 的計算量,再加上A[i:k] 和A[k+1:j] 相乘的計算量。
最優子結構
特徵:計算
矩陣連乘計算次序問題的最優解包含着其子問題的最優解。這種性質稱爲最優子結構性質。
最優子結構性質是可以使用動態規劃算法求解的顯著特徵。
狀態轉移方程
設計算
A[i:j](1≤i≤j≤n) 所需要的最少數乘次數爲m[i,j] ,則原問題的最優值爲m[1,n] ;記
Ai 的維度爲pi−1∗pi 當
i==j 時,A[i:j] 即Ai 本身,因此,m[i,i]=0;(i=1,2,…,n) 當
i<j 時有:
k 遍歷(i,j) ,找到一個使得計算量最小的k,也即是:
實現代碼
#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<set>
#include<queue>
#include<algorithm>
using namespace std;
//p[0,..,n]存儲n+1個數,其中(p[i-1],p[i])是矩陣i的階
//s[i][j]記錄了矩陣i連乘到矩陣j應該在哪斷開;m[i][j]記錄了矩陣i連乘到矩陣j最小計算量
void MatrixMultiply(int* p, int n, int** m, int** s)
{
int r, i, j, k, t;
for (i = 1; i <= n; i++)
m[i][i] = 0;
//r個連續矩陣相乘,r不斷擴展,不斷計算任意兩點之間最優斷開點,最小計算量
for (r = 2; r <= n; r++)
{
for (i = 1; i <= n - r + 1; i++)
{
j = i + r - 1;
m[i][j] = m[i][i]+m[i + 1][j] + p[i - 1] * p[i] * p[j];//初始值,第一項m[i][i]=0
s[i][j] = i;//初始在i處斷開
for (k = i + 1; k < j; k++)
{
t = m[i][k] + m[k + 1][j] + p[i - 1] * p[k]*p[j];
if (t < m[i][j])
{
m[i][j] = t;//(i,j)最小計算量
s[i][j] = k;//記錄(i,j)中最優斷開點
}
}
}
}
}
找零錢問題
問題描述
給定某不超過
問題分析
此問題涉及兩個類別:面額和總額。
- 如果面額都是
1 元的,則無論總額多少,可行的組合數顯然都爲1 。 - 如果面額多一種,則組合數有什麼變化呢?
定義
dp[100][500]=dp[50][500]+dp[100][400]
dp[50][500] :50 以下的面額的組成500 是一種組合方式,這裏面就包括了dp[20][500],dp[10][500]等
dp[100][400] :表示首先拿出100 的面額,剩餘的400 用小於等於100 的面額組合。
上述兩種組合方式沒有包含關係,兩種組合合在一起組成所有的組合方式。dp[i][j]=dp[ismall][j]+dp[i][j−i]
如果把i 看成數組下標,則有:dp[i][j]=dp[i−1][j]+dp[i][j−dom[i]]
遞推公式
使用
dom[]=1,2,5,10,20,50,100 表示基本面額,i 的意義從面額變成面額下標,則:dp[i][j]=dp[i−1][j]+dp[i][j−dom[i]] 從而有:
初始條件( 注意都爲1,而不是0):
按照上面的狀態轉移方差我們可以從初始狀態一直推導到終止狀態
實現代碼
#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<set>
#include<queue>
#include<algorithm>
using namespace std;
int Charge(int value, const int* denomination, int size)
{
int i;//i是下標
int** dp = new int*[size];//dp[i][j]:用i面額以下的組合成j元
for (i = 0; i < size; i++)
dp[i] = new int[value + 1];
int j;
for (j = 0; j <= value; j++)//i=0表示用面額1元的
dp[0][j] = 1;//這個時候只有一種組合方式
for (i = 1; i < size; i++)//先用面額小的,再用面額大的
{
dp[i][0] = 1;//添加任何一種面額,都是一種組合
for (j = 1; j <= value; j++)//先組合小的,然後擴展,在小的基礎上一直組合到dp[size-1][value]
{
if (j >= denomination[i])
dp[i][j] = dp[i-1][j]+dp[i][j-denomination[i]];
else
dp[i][j] = dp[i - 1][j];
}
}
int time = dp[size - 1][value];
//清理內存
for (i = 0; i < size; i++)
delete[] dp[i];
return time;
}
int main()
{
int denomination[] = { 1, 2, 5, 10, 20, 50, 100 };
int size = sizeof(denomination) / sizeof(int);
int value = 200;
int c = Charge(value, denomination, size);
cout << c << endl;
return 0;
}
滾動數組
將狀態轉移方程去掉第一維,很容易使用滾動數組,降低空間使用量。
原狀態轉移方程:
滾動數組版本的狀態轉移方程:
這個
#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<algorithm>
using namespace std;
int Charge2(int value, const int* denomination, int size)
{
int i;//i是下標
int* dp = new int[value+1];//dp[i][j]:用i面額以下的組合成j元
int* last = new int[value + 1];
int j;
for (j = 0; j <= value; j++)//只用面額1元的
{
dp[j] = 1;
last[j] = 1;
}
for (i = 1; i < size; i++)
{
for (j = 1; j <= value; j++)
{
if (j >= denomination[i])
dp[j] = last[j] + dp[j - denomination[i]];
}
memcpy(last, dp, sizeof(int)*(value + 1));//相當於dp[i-1][j] 賦值給last
}
int times = dp[value];
delete[] last;
delete[] dp;
return times;
}
int main()
{
int denomination[] = { 1, 2, 5, 10, 20, 50, 100 };
int size = sizeof(denomination) / sizeof(int);
int value = 200;
int c = Charge2(value, denomination, size);
cout << c << endl;
return 0;
}
在動態規劃的問題中,如果不求具體解的內容,而只是求解的數目,往往可以使用滾動數組的方式降低空間使用量(甚至空間複雜度),由於滾動數組減少了維度,甚至代碼會更簡單,但是代碼會更加難以理解。
走棋盤/格子取數
問題描述
給定
狀態轉移方程
走的方向決定了同一個格子不會經過兩次。
- 若當前位於
(x,y) 處,它來自於哪些格子呢? dp[x,y] 表示從起點走到座標爲(x,y) 的方格的最小權值。dp[0,0]=a[0,0] , 第一行(列)累積dp[x,y]=min(dp[x−1,y]+a[x,y],dp[x,y−1]+a[x,y]) - 即:
dp[x,y]=min(dp[x−1,y],dp[x,y−1])+a[x,y]
狀態轉移方程:
在上邊界時只能向右走,在左邊界時只能向下走。
滾動數組去除第一維:
實現代碼
#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int MinPath(vector<vector<int>> &chess, int M, int N)
{
vector<int> pathLength(N);
int i, j;
//初始化
pathLength[0] = chess[0][0];
for (j = 1; j < N; j++)
pathLength[j] = pathLength[j - 1] + chess[0][j];
//依次計算每行
for (i = 1; i < M; i++)
{
pathLength[0] += chess[i][0];
for (j = 1; j < N; j++)
{
if (pathLength[j - 1] < pathLength[j])
pathLength[j] = pathLength[j - 1] + chess[i][j];
else
pathLength[j] += chess[i][j];
}
}
return pathLength[N - 1];
}
int main()
{
const int M = 10;
const int N = 8;
vector<vector<int>> chess(M, vector<int>(N));
//初始化棋盤(隨機給定)
int i, j;
for (i = 0; i < M; i++)
{
for (j = 0; j < N; j++)
chess[i][j] = rand() % 100;
}
cout << MinPath(chess, M, N) << endl;
return 0;
}
帶陷阱的走棋盤問題
問題分析
在
狀態轉移方程
dp[i][j] 表示從起點到(i,j) 的路徑條數。- 只能從左邊或者上邊進入一個格子。
- 如果
(i,j) 被佔用,dp[i][j]=0 - 如果
(i,j) 不被佔用,dp[i][j]=dp[i−1][j]+dp[i][j–1]
故狀態轉移方程:
一共要走
問題解決
我們把
實現代碼
#include<stdio.h>
#include <stdlib.h>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int MinPath(vector<vector<int>> dp,int M, int N)
{
int i, j;
//在左邊界和下邊界上的點路徑都只有1
for (i = 0; i < M+1; i++)
dp[i][0] = 1;
for (j = 0; j < N+1; j++)
dp[0][j] = 1;
for (i = 1; i < M+1; i++)
{
for (j = 1; j < N+1; j++)
{
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[M][N];
}
int main()
{
const int M = 6;
const int N = 8;
vector<vector<int>> dp(M, vector<int>(N));
int x = 3;
int y = 5;
int allPath = MinPath(dp,M-1, N-1);//從起點到終點的所有路徑
int path1 = MinPath(dp,x, y);//起點到佔用點所有路徑
int path2 = MinPath(dp, M-x-1, N-y-1);//從佔用點到終點的所有路徑
int path = allPath - path1*path2;//在所有路徑中除去經過佔用點路徑數
cout << path << endl;
return 0;
}
動態規劃總結
動態規劃是方法論,是解決一大類問題的通用思路。事實上,很多內容都可以歸結爲動態規劃的思想。
何時可以考慮使用動態規劃:
初始規模下能夠方便的得出結論
空串、長度爲0的數組、自身等’能夠得到問題規模增大導致的變化
遞推式——狀態轉移方程
在實踐中往往忽略無後效性
哪些題目不適合用動態規劃?
- 狀態轉移方程 的推導,往往陷入局部而忽略全局。在重視動態規劃的同時,別忘了從總體把握問題。