ACM-动态规划(lintcode专题)总结

最近在lintcode上面刷dp算法,因为本菜鸟的dp实在是很渣,所以下定决心从简单的题目刷起。

lintcodeDp算法链接

我会挑选几个我觉得做起来不是那么得心应手的题目,把ac以及效率高的代码放上来。

中等题:

667.最长的回文序列

int longestPalindromeSubseq(string &s) {
        // write your code here
        int len = s.length(),dp[len+1][len+1];
        memset(dp,0,sizeof(dp));
        for(int i = len-1;i>=0;i--){
            dp[i][i] = 1;
            for(int j = i+1;j<len;j++)
                if(s[i] == s[j]) dp[i][j] = dp[i+1][j-1]+2;
                else dp[i][j] = max(dp[i+1][j],dp[i][j-1]);
        }
        return dp[0][len-1];
    }

分析: 线性dp的共性,找串与串之间的联系

---假设s[i] != s[j]

那么在sub(i,j)的最大回文串中,s[i]与s[j]不会同时出现,那么sub(i,j)的最大回文串要么出现在sub(i+1,j),要么出现在sub(i,j-1),因此我们的状态转移方程就得到了

---假设s[i]==s[j]

那么直接认为这俩个匹配,会同时出现在结果中,然后加上sub(i+1,j-1)的最大回文串即可

简单的dp方程 ,但是要注意的是i和j的遍历方向,因为对于串sub(i,j),如果需要用到sub(i+1,j-1)的值,那么dp(i+1,j-1)应该在之前已经求出来,所以我们需要顺着i减小的方向遍历,j增大的方向遍历

延伸到其他问题:

1. 给定一串字符,判断这个字符的非空子串(连续的)有多少个是回文串。

    暴力解(非dp,但是不包括像线段树那样的数据结构的其他解法):先N^2暴力每个区间,然后n判断回文,总的复杂度n^3

    优化解(dp,减少重复子问题的重复遍历):dp一次,O(n^2)的复杂度,然后再n^2遍历所有区间,判断即可。

2.给定一串字符,判断这个字符串的最大回文子串的长度(注意和母题区分,母题是求子序列即非连续)。(此处是连续的)

   算法:马拉车算法。时间复杂度O(n)

119.编辑距离

 int minDistance(string &word1, string &word2) {
        // write your code here
        int lenx = word1.length(),leny = word2.length(),dp[lenx+1][leny+1];
        for(int i = 0;i<=lenx;i++) dp[i][0] = i;
        for(int i = 0;i<=leny;i++) dp[0][i] = i;
        for(int i = 1;i <= lenx;i++){
            for(int j = 1;j <= leny;j++){
                if(word1[i-1]==word2[j-1]) dp[i][j] = dp[i-1][j-1];
                else dp[i][j] = min( dp[i-1][j] ,min( dp[i][j-1], dp[i-1][j-1] ) )+1;
            }
        }
        return dp[lenx][leny];
    }

分析:

1.类比一下最长公共子序列,发现应该是二维的状态方程类型。

2.接着就是找状态转移方程。

dp的初始状态就不详细说明了,现在准备求dp(i,j),

---假设word1[i] == word2[j]

那么我们就认为这俩个字符是匹配的,所以可能的一种结果就会是dp[i-1][j-1] ,状态转移方程dp(i,j) = dp(i-1,j-1)

---假设word1[i] != word2[j]

那么我们可以认为因为这俩个字符不相同,导致一定会在前一个状态下新增加一个操作,那么前一个状态是什么呢?

不难发现,前一个状态肯定是有三个的,假如对这俩个字符的有无的状态进行编码,有四种状态,而当前的dp(i,j)是一种,

另外的三种情况就是dp(i-1,j-1),dp(i-1,j),dp(i,j-1),结果一定是从这三种中得到的,因为我们的if的条件判断只涉及到ij俩个字符的讨论,这也是线性dp的一个特点。所以另外一个状态转移方程就是

dp(i,j) = min(dp(i-1,j-1),dp(i-1,j),dp(i,j-1)) +1

77.最长公共子序列

int longestCommonSubsequence(string &A, string &B) {
        // write your code here
        int lenx = A.length(),leny = B.length();
        int dp[lenx+1][leny+1];
        memset(dp,0,sizeof(dp));
        for(int i = 1;i<=lenx;i++)
            for(int j = 1;j<=leny;j++)
                if(A[i-1] == B[j-1]) dp[i][j] = dp[i-1][j-1] +1;
                else dp[i][j] = max(dp[i-1][j],dp[i][j-1]) ;
        return dp[lenx][leny];
    }

最简单的模型

108 分割回文串

题目大意:将输入的字符串进行k次分割,使得分割后的所有串全是回文串,求最小的分割次数k

TLE代码:暴力求解

int minCut(string &s) {
        // write your code here
        int len = s.length(),dp1[len+1][len+1],dp[len+1][len+1];
        memset(dp1,0,sizeof(dp1));
        memset(dp,0,sizeof(dp));
        for(int i = 1;i<=len;i++){
            dp1[i][i] = 1;
            dp[i][i] = 0;
        }
        for(int i = len-1;i>=0;i--){
            for(int j = i+1;j<len;j++){
                if(s[i] == s[j]) dp1[i][j] = dp1[i+1][j-1] + 2;
                else dp1[i][j] = max(dp1[i+1][j],dp1[i][j-1]);
                if(j-i+1 == dp1[i][j]) dp[i][j] = 0;
                else{
                    int Min = 1e9;
                    for(int k = i;k<j;k++){
                        Min = min(Min,dp[i][k] + dp[k+1][j]+1);
                    }
                    dp[i][j] = Min;
                }
                
            }
        }
        return dp[0][len-1];
    }

稍作优化:将预处理后的结果(可以判断出任意子串是否是回文串) 再一维dp即可

int minCut(string &s) {
        // write your code here
        int len = s.length(),dp1[len+1][len+1],dp[len+1];
        memset(dp1,0,sizeof(dp1));
        memset(dp,0,sizeof(dp));
        for(int i = 0;i<=len;i++) dp1[i][i] = 1;
        for(int i = len-1;i>=0;i--){
            for(int j = i+1;j<len;j++){
                if(s[i] == s[j]) dp1[i][j] = dp1[i+1][j-1] + 2;
                else dp1[i][j] = max(dp1[i+1][j],dp1[i][j-1]);
            }
        }
        for(int i = len-1;i>=0;i--){
            int Min = 1e9;
            if(dp1[i][len-1] == len-i){
    			dp[i] = 0;
    			continue;
    		}
            for(int k = i;k<len-1;k++)
                if(dp1[i][k] == k-i+1) Min = min(Min,dp[k+1] + 1);
            dp[i] = Min==1e9?0:Min;
        }
        return dp[0];
    
    }

因为区间可以完全判断是否是回文串,所以一定是优先考虑包含回文串的区间的最值

这次结果居然时间超过其他人了。。

感觉练了上面那些问题后,进步不少了,看到题目能分辨出是区间dp还是线性dp,下面的题目也容易的分析出最优解的结构

上菜:

670.预测能否胜利

简单的区间dp,dp存下区间选择的最优解,然后每次需要往前推俩步才能到达下一个状态,因为是俩个人轮流选数,并且要考虑对手也是选择最优解(就是那个最小值的操作,对手一定是把较小的选择方案留给自己了)。

bool PredictTheWinner(vector<int> &nums) {
        // write your code here
        int n = nums.size(),sum = 0;
        int dp[n][n];
        memset(dp,0,sizeof(dp));
        for(int i = 0;i<n;i++) dp[i][i] = nums[i],sum+=nums[i];
        for(int i = n-1;i>=0;i--) 
            for(int j = i+1;j<n;j++){
                int len = j-i+1;
                if(len == 2) dp[i][j] = max(nums[i],nums[j]);
                else dp[i][j] = max(nums[i] + min(dp[i+2][j],dp[i+1][j-1]),nums[j] + min(dp[i][j-2],dp[i+1][j-1]));
            }
        return (dp[0][n-1]<<1)>=sum;
    }

603.最大整除子集

根据需要的解分析,俩俩数之间必须成倍数关系,那么排序之后,每个数都必须是前面所有数的倍数。

所以先对数组排序,然后dp[i]表示第i个数放在集合里面能找到的最大集合的元素个数。

那么对每个i ,遍历[0,i-1] 使得 nums[i]%nums[j] == 0 即 有点像最长上升子序列的dp方程。

然后我们最后找到最大的dp[i]就完事了。但是答案需要把每个元素输出,所以每次求dp[i]的时候 记录下能被nums[i]整除的最大dp[j]的位置就好了,然后通过pre来找到这个最大的路径。有点像最短路算法用pre记录路径的方法。

vector<int> largestDivisibleSubset(vector<int> &nums) {
        // write your code here
        int n = nums.size(),lens=0,pos=0;
    	int dp[n],pre[n];
    	sort(nums.begin(),nums.end());
    	memset(dp,1,sizeof(dp));
    	memset(pre,-1,sizeof(pre));
    	for(int i = 1; i<n; i++) {
    		int p,Max = 0;
    		for(int j = 0; j<i; j++) {
    			if(nums[i]%nums[j] == 0) {
    				Max = i==j?0:dp[j];
    				p = j;
    			}
    		}
    		if(Max){
    		    dp[i] = Max + 1;
    		    pre[i] = p;
    		}
    		if(dp[i]>lens) {
    			lens = dp[i];
    			pos = i;
    		}
    	}
    	vector<int> ans;ans.clear();
    	while(pos!=-1) {
    		ans.push_back(nums[pos]);
    		pos = pre[pos];
    	}
       return ans;
    }

但是 >---< ,时间复杂度让我不满意。

dp优化方案:

191.乘积最大序列

思路还是挺简单的,线性dp,但是要注意的是,需要维护每个阶段的最大值和最小值,因为当前数值可能是负数,那么最大值会从(上一个阶段的负数乘积当前负数)产生,所以俩个dp 一个维护最大值,一个维护最小值即可。

int maxProduct(vector<int> &nums) {
        // write your code here
        int n = nums.size(),ans = -2e9;
        int dp1[n],dp2[n];
        for(int i = 0;i<n;i++) dp1[i] = dp2[i] = nums[i];
        ans = nums[0];
        for(int i = 1;i<n;i++){
            int a = dp1[i-1]*nums[i],b = dp2[i-1]*nums[i];
            dp1[i] = min(a,min(b,nums[i]));
            dp2[i] = max(a,max(b,nums[i]));
            ans = max(ans,dp2[i]);
        }
        return ans;
    }

436.最大正方形

线性dp dp[i][j]表示以matrix[i][j]为顶点的最大正方形的边长,那么限制它的就是相邻的三个位置,

开心,时间效率最高的 n^2复杂度

int maxSquare(vector<vector<int>> &matrix) {
        // write your code here
        int ans = 0,n = matrix.size(),m = matrix[0].size();
        int dp[n][m];
        for(int i = 0;i<n;i++) dp[i][0] = matrix[i][0],ans = max(ans,dp[i][0]);
        for(int i = 0;i<m;i++) dp[0][i] = matrix[0][i],ans = max(ans,dp[0][i]);
        for(int i = 1;i<n;i++)
            for(int j = 1;j<m;j++)
                if(matrix[i][j]) dp[i][j] = min(dp[i-1][j],min(dp[i][j-1],dp[i-1][j-1])) + 1,ans = max(ans,dp[i][j]);
                else dp[i][j] = 0;
        return ans*ans;
    }

但是还有其他的方案:

例如:前缀和。先预处理求出前缀和,然后对于每个点,遍历其他的所有对角顶点,使得这个正方形区域的求和是等于覆盖的面积的。然而这个时间复杂度是n^3多了遍历的操作,代码就不上了。

116.跳跃游戏

1. 贪心 每次更新能达到的最远端,如果当前遍历的位置i大于最远端则退出(即不能跳跃),最后判断最右端是否到达数组最后

int max(int a,int b){
        return a>b?a:b;
    }
    bool canJump(vector<int> &A) {
        // write your code here
        int Max = 0,len = A.size();
        for(int i = 0;i<len;i++){
            if(i>Max) break;
            Max = max(Max,i + A[i]);
        }
        return Max>=len-1;
    }

2. dp[i] 判断位置i是否可达,每次从[0,i-1]寻找可以到达i的点,如果不存在就无法跳跃到dp[i]

bool canJump(vector<int> &A) {
        // write your code here
        int Max = 0,len = A.size();
        bool dp[len];
        for(int i = 0;i<len;i++) dp[i] = false;
        dp[0] = true;
        for(int i = 1;i<len;i++){
            for(int j = i-1;j>=0;j--)
                if(dp[j] == true && j + A[j] >= i){
                    dp[i] = true;break;
                }
        }
        return dp[len-1];
    }

猜你喜欢

转载自blog.csdn.net/qq_34465787/article/details/81157914
今日推荐