最近在lintcode上面刷dp算法,因为本菜鸟的dp实在是很渣,所以下定决心从简单的题目刷起。
我会挑选几个我觉得做起来不是那么得心应手的题目,把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];
}