DP的本質是巧妙定義狀態,並找到狀態的遞推關係
一、經典
3.三角形最小路徑和
狀態:對於第i行Row[i], 假設以該行j元素Row[i][j]結尾的路徑最小和爲sum[i][j].
則sum[i][j]=Row[i][j]+min{sum[i-1][j-1],sum[i-1][j]};(注意j的邊界)
最終求得最後一行裏sum的最小值。
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
if(triangle.size() == 0) return 0;
if(triangle.size() == 1){
return *min_element(triangle[0].begin(),triangle[0].end());
}
//第0行的各個元素的最小路徑和就是元素本身
auto lastRow = triangle[0];
for(int i = 1; i<triangle.size();++i){
//取得當前行元素
auto currRow = triangle[i];
//對當前行元素遍歷,計算以每個元素爲路徑結尾的最小和
for(int j = 0;j<triangle[i].size();++j){
if(j==0){
currRow[j] = triangle[i][j]+lastRow[0];
}else if(j==triangle[i].size()-1){
currRow[j] = triangle[i][j]+lastRow[j-1];
}else{
currRow[j] = std::min(triangle[i][j]+lastRow[j],triangle[i][j]+lastRow[j-1]);
}
}
lastRow = currRow;
}
return *min_element(lastRow.begin(),lastRow.end());
}
};
6.雞蛋掉落
二、股票系列
所有套路一致: 參照文章 團滅股票買賣問題
121 只能交易一次
122 交易任意次數
123 只能交易2次
188 只能交易K次
309 交易之間需要間隔一天
714 交易收取交易費
三、字符串匹配
四、區間DP
五、揹包DP
六、樹形DP
所謂樹形DP即當前問題依賴於左右子樹的最優值,類似於後序遍歷。
124 二叉樹中的最大路徑和
class Solution {
int result=0;
int _maxP(TreeNode*root){
int l=0,r=0;
if(root->left)l=_maxP(root->left);
if(root->right)r=_maxP(root->right);
int sub_max= max(root->val,max(l+root->val,r+root->val));
//子樹的最大值 不能同時包括左右子樹與根節點,但是最終要求的值可以包含左右子樹和根節點
//所以這裏單獨計算一下result的值
result=max(result,max(sub_max,l+r+root->val));
return sub_max;
}
public:
int maxPathSum(TreeNode* root) {
if(root==NULL) return 0;
result=root->val;
_maxP(root);
return result;
}
};
337 打家劫舍 III
將問題分爲偷當前root和不偷。並得到與子樹遞推關係
class Solution{
//返回位置0爲偷root 返回位置1爲不偷root
vector<int> _rob(TreeNode*root){
vector<int> ret={0,0};
if(root==NULL) return ret;
auto l=_rob(root->left);
auto r=_rob(root->right);
//ret[0]爲偷當前樹root,故子樹只能不偷root
ret[0]=l[1]+r[1]+root->val;
//ret[1]爲不偷當前root,故左右子樹均可偷或不偷root,選取較大值。
ret[1]=max(l[0],l[1])+max(r[0],r[1]);
return ret;
}
public:
int rob(TreeNode*root){
auto ret=_rob(root);
return max(ret[0],ret[1]);
}
};
七、計數DP
class Solution {
public:
int numTrees(int n) {
if(n <= 0 ) return 0;
vector<int>dp(n+1,0);
dp[0]=dp[1]=1; //dp[0]代表空樹
for(int i=2;i<=n;++i){
for(int j=1;j<=i;++j){ //依次假設1...i爲root節點,則左子樹 右子樹數目分別爲:dp[j-1] dp[i-j]
dp[i]+=dp[j-1]*dp[i-j];
}
}
return dp[n];
}
};
八、博弈DP
參考文章:動態規劃之博弈問題
預測勝利者
//採用博弈問題的狀態方程解題框架
class Solution{
public:
bool PredictTheWinner(vector<int>& nums) {
if(nums.size()<=1)return true;
pair<int,int>dp[nums.size()][nums.size()];
for(int j=0;j<nums.size();++j){
for(int i=j;i>=0;--i){
if(i==j){
dp[i][j].first=nums[i];
dp[i][j].second=0;
}else{
int l=dp[i+1][j].second+nums[i];
int r=dp[i][j-1].second+nums[j];
dp[i][j].first=max(r,l);
if(l>r){
dp[i][j].second=dp[i+1][j].first;
}else{
dp[i][j].second=dp[i][j-1].first;
}
}
}
}
return dp[0][nums.size()-1].first>= dp[0][nums.size()-1].second;
}
};
翻轉游戲
292 Nim 遊戲
877 石子游戲
1140 石子游戲 II
井字遊戲
348 判定井字棋勝負
九 生成特定數列
關鍵是生成算子
264 醜陋數
class Solution {
public:
int nthUglyNumber(int n) {
if(n<=0) return 0;
if(n == 1) return 1;
//對於任何一個 ugly 數,必然可以通過該數之前的 ugly數列中的某三個ugly元素 L1 L2 L3 乘以2,3,5得到。
//即可以設置三個生成數L1 L2 L3,這三個數分別通過*2 *3 *5生成新的ugly 數。而ugly數列中唯一的新元素是當前三個生成數生成的元素中最小的元素。
//對於被選中爲當前ugly 數列新元素所對應的 生成數Li,將Li更新爲ugly數列中下一個元素,原因是當前Li所生成的元素已經是最小的新元素,其不可能再生成新元素了。
std::vector<int>uglys;
uglys.push_back(1);
int L1=0,L2=0,L3=0;
while(uglys.size()<n){
//得到當前L1 L2 L3算子生成元素的最小值,即爲當前數列的新元素
int new_ugly = min(uglys[L1]*2,min(uglys[L2]*3,uglys[L3]*5));
uglys.push_back(new_ugly);
//當前元素是由L1算子生成
if(new_ugly == uglys[L1]*2) ++L1;
//當前元素是由L2算子生成
if(new_ugly == uglys[L2]*3) ++L2;
//當前元素是由L3算子生成
if(new_ugly == uglys[L3]*5) ++L3;
}
return uglys[uglys.size()-1];
}
};
class Solution {
public:
int nthSuperUglyNumber(int n, vector<int>& primes) {
vector<int>numbers={1};
vector<int>L(primes.size(),0);
while(numbers.size()<n){
int curr_min = numbers[L[0]]*primes[0];
for(int i=0; i<L.size();++i){
curr_min=min(curr_min,numbers[L[i]]*primes[i]);
}
numbers.push_back(curr_min);
for(int i=0;i<L.size();++i){
if(numbers[L[i]]*primes[i] == numbers.back()){
L[i]++;
}
}
}
return numbers.at(n-1);
}
};