Dynamic Programming整理

1. 动归问题的核心

状态和状态转移方程。状态可以根据子问题定义,状态方程就是描述状态是怎么转移的,它的的本质不在于是递推或是递归,也不需要纠结是不是内存换时间。

2. 相关概念

2.1  递推

以斐波纳切数列为例,每一个斐波纳切都是一个 状态,每一个新数字只需要之前的两个状态。这种计算很直接,只需要固定的模式从旧状态计算出新状态(a[i] = a[i-1] + a[a-2]),不需要考虑是不是需要更多的状态,也不需要选择哪些旧状态来计算新状态。对于这样的解法,就是递推。

2.2 贪心

斐波纳切都是一个状态,其实更准确的说,是每个 阶段都只有一个状态。所谓阶段是指随着问题的解决,在同一个时刻可能会得到的不同状态的集合。这里换一个例子,棋盘寻路,从头开始走了几步就是第几个阶段,走了n步可能处于的位置称为一个状态,走了这n步所有可能到达的位置的集合就是这个阶段下所有可能的状态。
此时,再计算新状态就会用到不同算法,假如问题有n个阶段,每个阶段都有多个状态,不同阶段的状态数不必相同,一个阶段的一个状态可以得到下个阶段的所有状态中的几个。那么最终阶段的状态数就要经历之前每个阶段的某些状态。
(1)下一步最优是从当前最优得到的,即想要得到最终的最优值,只需存储每一步最优即可,这就是贪心算法。
(2)如果只看最优状态之间的计算过程(回想斐波纳切计算过程),就是递推算法。

后效性:当前状态会影响后面的状态,就是后效性;
最优子结构:每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到;
无后效性:每个阶段的最优状态与之前的状态无关;

3 算法选择

在知乎上看到一个 结论:一个问题是该用递推、贪心、搜索还是动态规划,完全是由这个问题本身阶段间状态的转移方式决定的!
每个阶段只有一个状态 -> 递推;
每个阶段的最优状态都是由 上一个阶段的最优状态得到的 -> 贪心;
每个阶段的最优状态是由 之前所有阶段的状态的组合得到的 -> 搜索;
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而 不管之前这个状态是如何得到的 -> 动态规划。



4. 一维DP

(1)最大和子数组O(n):

假设A(0, i)区间存在k,使得[k, i]区间是以i结尾区间的最大值, 定义为dp[i], 在这里,当求取dp[i+1]时,有两种情况:

dp[i+1] = dp[i] + A[i+1],   if (dp[i] + A[i+1] >0)

               = 0,                       if(dp[i]+A[i+1] <0),如果和小于零,A[i+1]必为负数,舍弃重新开始

然后从左往右扫描,求取dp数字的最大值即为所求。

int maxSubArr(vector<int> nums) {
    if (nums.empty()) return 0;
    int sum = nums[0];
//  vector<int> dp(nums.size());  //O(n)
    int dp = nums[0]; //O(1)
    for (int i = 1; i < nums.size(); i++) {
        dp = max((dp + nums[i]), nums[i]);
        sum = max(sum, dp);
     }
      return sum;
}
如果时间复杂度低一些O(n)可以采用分治的方法,采用二分。即最大和子数组区间要么在左半边,要么在右半边,要么横跨两部分。前两种情况可以递归的进行求解。第三种情况则需以以mid为中心,向两侧扫描求最大值。

//O(n^2)解法
int maxArr(vector<int> nums, int begin, int end, int &maxV) {
        if (begin > end) return INT_MIN;
        int mid = begin + (end - begin) /2;
        int lmax = maxArr(nums, begin, mid-1, maxV);
        int rmax = maxArr(nums, mid+1, end, maxV);
        maxV = max(max(maxV, lmax), rmax);
        int sum = 0, mlmax = 0;
        for (int i = mid-1; i>= begin; i--) {
                sum += nums[i];
                mlmax = max(sum, mlmax); 
         }
         sum = 0;
         int mrmax = 0;
         for (int i = mid+1; i <= end; i++) {
                  sum += nums[i];
                  mrmax = max(sum, mrmax);
         } 
         maxV = max(maxV, mlmax + mrmax + nums[mid]);
}
int maxSubArr(vector<int> nums) {
    if (nums.empty()) return 0;
    int maxV = INT_MIN;
    return maxArr(nums, 0, nums.size()-1, maxV); 
}

(2)最长升序子序列LIS

定义dp[i],表示前i个数中以A[i]结尾(前i个元素)的最长非降子序列的长度.那么dp[i+1]可以利用如下状态转移方程得到

d[i+1] = max{1, d[j]+1},其中j<i,A[j]<=A[i].即如果A[i+1]>A[j],那么第i+1个元素可以接在dp[i]长的子序列的后面构成一个更长的子序列。与此同时,A[i+1]本身就可以构成一个长度为1的子序列。

//O(n^2)的解法
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if (nums.empty()) return 0;
        vector<int> dp(nums.size(), 1);
        for (int i = 0; i < nums.size(); i++) {//对于第i+1个元素,不考虑前i个元素的情况
            
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j] && (dp[j] + 1 > dp[i]))
                    dp[i] = dp[j] + 1;
            }
            
        }
        return *(max_element(dp.begin(), dp.end()));
    }
};
<pre name="code" class="cpp">/*O(nlogn)的解法
利用一个数组纪录递增子序列的尾元素,即数组下标为i对应的元素是长度为i的子序列的尾元素
*/
Class Solution() {
    public:
    int lengthOfLIS(vector<int> &nums) {
           vector<int> ret;
           for (int i = 0; i < nums.size(); i++) {
                auto it = upper_bound(ret.begin(), ret.end(), nums[i]);//返回的是大于某个数的第一个数的位置
                if(it == ret.end()) ret.push_back(nums[i]); 
                 else *it = nums[i];
     
           }
    return ret.size();
    }
};

 

5. 二维DP

1. Minimum Path Sum:给一个矩阵,从上至下从左至右的走,求路径最小和。

注意初始条件。设dp[i][j]表示走到A[i][j]时,路径中所包含的最小和。状态转移方程dp[i][j]=min(dp[i-1][j], dp[i][i-1])+A[i][j-1]

int minPathSum(vector<vector<int> > &mat) {
    if(mat.empty()) return 0;
    int rows = mat.size();
    int cols = mat[0].size();
    vector<vector <int>> dp(rows, vector<int>(cols, 0));
    dp[0][0] = mat[0][0];
    for (int i = 1; i < rows; i++) 
        dp[i][0] = dp[i-1][0] + mat[i][0];
    for (int j = 1; j < cols; j++)
        dp[0][j] = dp[0][j-1] + mat[0][j]; 
    for (int i = 1; i < rows; i++) {
        for (int j = 1; j < cols; j++) {
            dp[i][j] = (dp[i-1][j], dp[i][j-1]) + mat[i][j];
        }
    }
    return dp[rows-1][cols-1];
}

2. Interleaving String:给定三个字符串s1, s2, s3,判断s3是不是由前两个字符串交替组成的。

设dp[i][j]表示s1取i长度的字符,最后一个字符是s1[i-1],s2取j个长度的字符串,最后一个字符是s2[j-1],能否与s3(i+j)长度的字符串匹配上。

边界条件是,其中一个字符串长度为0,就用另一个字符串去匹配s3

bool table[s1.length()+1][s2.length()+1];
    
    for(int i=0; i<s1.length()+1; i++)
        for(int j=0; j< s2.length()+1; j++){
            if(i==0 && j==0)
                table[i][j] = true;
            else if(i == 0)
                table[i][j] = ( table[i][j-1] && s2[j-1] == s3[i+j-1]);
            else if(j == 0)
                table[i][j] = ( table[i-1][j] && s1[i-1] == s3[i+j-1]);
            else
                table[i][j] = (table[i-1][j] && s1[i-1] == s3[i+j-1] ) || (table[i][j-1] && s2[j-1] == s3[i+j-1] );
        }
        
    return table[s1.length()][s2.length()];
    }



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值