尾递归的构建方式 在参数当中完成运算。
本题最优解不依赖于子问题的解,因为有可能子问题中出现最大解但是对于父问题来说不符合加1的要求,例如最长Z路径出现在中间段,所以采用外部设置全局变量比较更新的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution {public : int maxlen=0 ; void tail_recusion (int len,int direct,TreeNode* root) { maxlen = max (maxlen,len); if (root) { if (direct==1 ) { if (root->left) tail_recusion (len+1 ,0 ,root->left); if (root->right) tail_recusion (1 ,1 ,root->right); } else { if (root->left) tail_recusion (1 ,0 ,root->left); if (root->right) tail_recusion (len+1 ,1 ,root->right); } } } int longestZigZag (TreeNode* root) { tail_recusion (0 ,0 ,root); tail_recusion (0 ,1 ,root); return maxlen; } };
重点:描述出状态 找到状态转移方程 本题特征: 利用函数的单调性来逼近答案(这是本题规划的部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class Solution {public : map<int ,int > memo; int dp (int k,int n) { if (memo.find (100 *n+k)==memo.end ()) { int ans = 0 ; if (k==1 ) ans=n; else if (n==0 ) ans=0 ; else { int low =1 ; int high=n; while (low+1 <high) { int x = low + (high-low)/2 ; int t1 = dp (k,n-x); int t2 = dp (k-1 ,x-1 ); if (t1>t2) low = x; else if (t1<t2) high=x; else low = high = x; } ans=1 +min (max (dp (k,n-low),dp (k-1 ,low-1 )),max (dp (k,n-high),dp (k-1 ,high-1 ))); } memo[100 *n+k]=ans; } return memo[100 *n+k]; } int superEggDrop (int K, int N) { return dp (K,N); } };
20220118 树状分析改为动态规划:二分法想到的是对的 最后本质采用了二分查找;压缩的本质是 m~n可以用n-m来表示
回溯法写对了 关键在于去除冗余计算
在判断回文串的时候有冗余 dp判断是否是回文串
如果直接采用课堂上二维矩阵的方式来记录的话,需要分配的空间过大
dp判断s[i]~s[j]是否是回文串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class Solution {public : vector<vector<string>> ans; vector<vector<bool >> jdg; void judge (string s) { for (int i=s.size ()-1 ;i>=0 ;--i) { for (int j=i+1 ;j<s.size ();++j) { jdg[i][j]=(s[i]==s[j])&&jdg[i+1 ][j-1 ]; } } } vector<vector<string>> partition (string s) { int len=s.size (); jdg.assign (len,vector<bool >(len,true )); judge (s); vector<string> set; backtrace (0 ,s,set); return ans; } void backtrace (int left,string s,vector<string>& set) { if (left>=s.size ()) ans.push_back (set); else { for (int i=left;i<s.size ();++i) { if (jdg[left][i]) { set.push_back (s.substr (left,i-left+1 )); backtrace (i+1 ,s,set); set.pop_back (); } } } } };
时间代价: 字符串长度为n,若每个字符都一样,则有2^(n-1) = O(2^n) 的分法,而每种分发都需要遍历一次字符串来获得对应的划分,加上动态规划的代价
O(n*2^n+n^2)=O(n*2^n)
空间代价 dp的数组 O(n^2)
变式 132. Palindrome Partitioning II
基于上一题的思路 就是在回溯具体切分的过程中做统计 每次到底得到答案的时候记录切分次数,在切分回溯的过程中利用已经有的答案做出剪枝
还有一个更快的思路是只统计次数 不在乎情况
不要贪心 思路是对的 先用dp求出回文的情况,计算最小分割数的时候的方程是互相依赖的且是一维的 在用上述标记做一次dp
取决于问题是从后往前分析还是从前往后;如果数组定义为从0到位置i需要的最短分割,那么是从前往后(注意!!)数组的定义会影响到问题规划的方式
方程:s[0][i]是回文的话 memo[i]=0 因为无需分割,否则对于j=[0,i], 找到一个最小的memo[j]+1 在此处使用了贪心的解法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution {public : int minCut (string s) { vector<int > memo; vector<vector<bool >> jdg; memo.resize (s.size (),s.size ()-1 ); jdg.assign (s.size (),vector<bool >(s.size (),true )); for (int i=s.size ()-1 ;i>=0 ;--i) { for (int j=i+1 ;j<s.size ();++j) jdg[i][j]=(s[i]==s[j])&&jdg[i+1 ][j-1 ]; } for (int i=0 ;i<s.size ();++i) { if (jdg[0 ][i]) memo[i]=0 ; else { for (int j=0 ;j<i;++j) if (jdg[j+1 ][i]) memo[i]=(memo[i]>memo[j]+1 )?(memo[j]+1 ):memo[i]; } } return memo[s.size ()-1 ]; } };
变式 1278. Palindrome Partitioning III
采用同样的预处理,changed[i][j]表示从s[i]到s[j]若修改为字符串需要修改的字符个数,转移方程只需要在判断的基础上略作修改即可。原本考虑用递归来表示待分割的次数k,但是这也转换为一个二维空间
为什么changed是二维而不是一维——因为同一层循环可能有不同的分割点
定义 minch[m][i] 还有m次分割机会(很重要!划重点!)下,从0号位置到i号位置最少改变的字符数
卡在的地方: 解决问题的思路(明确有二维的变量),m的取值(一开始搞成了k+1,k+1是用在不包括0的计数的情况,这里的确只有0~k),内部规划的时候 j 和 i 的关系,注意分割的时候要从下一个字符开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 class Solution {public : int min (int a,int b) { if (a<b) return a; else return b; } int palindromePartition (string s, int k) { int len = s.size (); vector<vector<int >> changed; changed.assign (len, vector<int >(len, 0 )); for (int i = len - 1 ; i >= 0 ; --i) { for (int j = i + 1 ; j < len; ++j) { if (s[i] == s[j]) changed[i][j] = changed[i + 1 ][j - 1 ]; else changed[i][j] = changed[i + 1 ][j - 1 ] + 1 ; } } vector<vector<int >> minch; minch.assign (k, vector<int >(len, INT_MAX - 2 )); for (int i = 0 ; i < k; ++i) minch[i][0 ] = 0 ; for (int m = 0 ; m < k; ++m) { if (m == 0 ) { for (int i = 0 ; i < len; ++i) { minch[0 ][i] = changed[0 ][i]; } } else { for (int i = 0 ; i < len; ++i) { for (int j = 0 ; j < i; ++j) { minch[m][i] = min (minch[m][i], minch[m - 1 ][j] + changed[j + 1 ][i]); } } } } return minch[k-1 ][len - 1 ]; } };
变式 5. Longest Palindromic Substring
在原本架构上增加全局变量找最大值 一开始脑子转不过来没有意识到本质是变式。可以接机复习回文串的动态规划架构
在规划顺序卡住,取决于如何分解问题:可以长度由小到大显式:
也可以采用如下:右侧限定了左侧,右侧从小到大,左侧从最左开始填写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution {public : string longestPalindrome (string s) { int len=s.size (); if (len<2 ) return s; int maxlen=0 ,start=0 ; vector<vector<bool >> judge (len,vector<bool >(len)); for (int right=0 ;right<len;++right) { for (int left=0 ;left<=right;++left) { if (s[left]!=s[right]) { judge[left][right]=false ; continue ; } if (right-left<=2 ) judge[left][right]=true ; else judge[left][right]=judge[left+1 ][right-1 ]; if (judge[left][right] && right+1 -left>maxlen) { maxlen=right+1 -left; start=left; } } } return s.substr (start,maxlen); } };
最初想到的:中心扩散法
可以优化的地方是:在剩余的长度小于maxlen/2的时候停止,因为此时不可能再有回文串比现行最大长度回文串要长;出现重复字符的时候移动指针直到下一个字符不重复,相当于根据重复字符也是回文串把重复部分压缩,然后再照常判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution {public : string longestPalindrome (string s) { if (s.size ()<2 ) return s; int len=s.size (); int maxlen=0 ,start=0 ; for (int i=0 ;i<len;) { if (len-i<=maxlen/2 ) break ; int left=i,right=i; while (right<len-1 && s[right]==s[right+1 ]) right++; i=right+1 ; while (right<len-1 && left>0 && s[left-1 ]==s[right+1 ]) { left--; right++; } if (maxlen<right+1 -left) { maxlen=right+1 -left; start=left; } } return s.substr (start,maxlen); } };
变式:647. Palindromic Substrings
评论区RoundOne的dp解法很简洁 学习
遇到可以匹配的词就切分并且以切分后的字串为开头继续切分 但该思路的本质是贪心策略,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Solution {public : bool wordBreak (string s, vector<string>& wordDict) { int len=s.size (); vector<bool > judge (len+1 ,false ) ; judge[0 ]=true ; string str; int cnt=0 ; for (int i=0 ;i<len;++i) { str.push_back (s[i]); cnt++; if (find (wordDict.begin (),wordDict.end (),str)!=wordDict.end ()) { if (judge[i+1 -cnt]) { cout<<"i " <<i<<" " ; cout<<"str " <<str<<" " ; cout<<"cnt " <<cnt<<" " ; judge[i+1 ]=true ; cnt=0 ; str.clear (); } } } return judge[len]; } };
动态规划 一维
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : bool wordBreak (string s, vector<string>& wordDict) { int len=s.size (); vector<bool >judge (len+1 ,false ); judge[0 ]=true ; for (int i=0 ;i<len;++i) { for (int j=0 ;j<=i;++j) { string str = s.substr (j,i+1 -j); if (!judge[i+1 ]&&find (wordDict.begin (),wordDict.end (),str)!=wordDict.end ()) { judge[i+1 ]=judge[j]; } } } for (auto x:judge) cout<<x<<" " ; return judge[len]; } };
关键在于意识到需要用到动态规划才能解决问题,是在测试用例
s=”aab” p=”ac*a*b”的时候意识到的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Solution {public : bool isMatch (string s, string p) { auto match=[&](int i,int j) { if (i==0 ) return false ; if (p[j-1 ]=='.' ) return true ; else return s[i-1 ]==p[j-1 ]; }; int m=s.size (),n=p.size (); vector<vector<bool >> judge (m+1 ,vector<bool >(n+1 )); judge[0 ][0 ]=true ; for (int i=0 ;i<=m;++i) { for (int j=1 ;j<=n;++j) { if (p[j-1 ]=='*' ) { judge[i][j]=judge[i][j-2 ]; if (match (i,j-1 )) { judge[i][j]=judge[i-1 ][j]||judge[i][j]; } } else { if (match (i,j)) { judge[i][j]=judge[i-1 ][j-1 ]||judge[i][j]; } } } } return judge[m][n]; } };
直接做法是哈希表硬记忆,后用回溯
这里看到了一个技巧做法:统计出所有可能性(本身是连乘),然后根据计数规律来构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution {public : vector<string> letterCombinations (string digits) { if (digits.size ()==0 ) return {}; vector<string> res; vector<string> dict={"" ,"" ,"abc" ,"def" ,"ghi" ,"jkl" ,"mno" ,"pqrs" ,"tuv" ,"wxyz" }; int len=1 ,size=digits.size (); for (int i=0 ;i<digits.size ();++i) len=len*dict[digits[i]-'0' ].size (); for (int i=0 ;i<len;++i) { int last=i; string str; for (int j=size-1 ;j>=0 ;--j) { int c = digits[j]-'0' ; int pos = last%dict[c].size (); last/=dict[c].size (); str.push_back (dict[c][pos]); } reverse (str.begin (),str.end ()); res.push_back (str); } return res; } };
类似题:
上述编号取余的方法并没有比回溯法更加高效 但是学习了unique的用法 返回一个迭代器 从begin到该迭代器 只有一个元素重复出现一次 即对于不重复出现的部分 返回的迭代器相当于end()
回溯法 注意是引用 要创建新的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import java.util.*;class Solution { List<List<Integer>> res; int n; public List<List<Integer>> permute(int [] nums) { this .n=nums.length; this .res = new LinkedList<>(); List<Integer> output = new LinkedList<Integer>(); for (int n : nums){ output.add(n); } backtrace(output,0 ); return this .res; } void backtrace (List<Integer> output,int first) { if (first==n){ res.add(new ArrayList<Integer>(output)); return ; } for (int i=first;i<n;++i){ Collections.swap(output,first,i); backtrace(output,first+1 ); Collections.swap(output,first,i); } } }
一个简单的回溯 关键是控制配对
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution {public : vector<string> res; void reverse (int left,int right,char ch,string str) { if (ch=='(' ) { left--,right--; str.push_back (ch); if (left>0 ) reverse (left,right,'(' ,str); if (right<0 ) reverse (left,right,')' ,str); } else { right++; str.push_back (')' ); if (left>0 ) reverse (left,right,'(' ,str); if (right<0 ) reverse (left,right,')' ,str); } if (left==0 && right==0 ) res.push_back (str); } vector<string> generateParenthesis (int n) { string str = "" ; reverse (n,0 ,'(' ,str); return res; } };
一开始想用栈做 但是实际情况更复杂 没有考虑括号套娃的情况
dp版:关键在于 完整的子串必定是右括号结尾,所以状态转移只需要关注右括号,左括号结尾的子串长度必定是0(全初始化为0就可以) 有两种转移状态 成对和套娃 小心下标合法性的判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution {public : int longestValidParentheses (string s) { int len = s.size (); int max=0 ; vector<int > dp (len,0 ) ; for (int i=1 ;i<len;++i) { if (s[i]==')' ) { if (s[i-1 ]=='(' ) dp[i]=2 +((i<2 )?0 :dp[i-2 ]); else { if (i-dp[i-1 ]>0 && s[i-1 -dp[i-1 ]]=='(' ) dp[i]=2 +dp[i-1 ]+((i-2 -dp[i-1 ]<0 )?0 :dp[i-2 -dp[i-1 ]]); } } max=(dp[i]>max)?dp[i]:max; } return max; } };
栈:
一开始使用更多的是对符号本身是否匹配,这里改成存下标,以来通过下标的信息可以判断是否匹配,二来下标差可以直接求出子串长
这里栈底是最后一个没有被匹配的右括号的下标,之上都是可以被匹配的左括号初始化采用-1可以在开头不是右括号的情况下完成补全。
需要注意的细节:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution {public : int longestValidParentheses (string s) { int max=0 ,len=s.size (); stack<int > stk; stk.push (-1 ); for (int i=0 ;i<len;++i) { if (s[i]=='(' ) stk.push (i); else { if (!stk.empty ()) { stk.pop (); if (!stk.empty ()) max = (i-stk.top ()>max)?i-stk.top ():max; else stk.push (i); } else stk.push (i); } } return max; } };
卡在了具体实现上(本质就是部分和问题)
动态规划 巧用stl库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution {public : vector<vector<int >> combinationSum (vector<int >& candidates, int target) { unordered_map<int ,set<vector<int >> >dict; for (int i=1 ;i<=target;++i) { for (auto it:candidates) { if (it==i) dict[i].insert ({it}); else { for (auto vec:dict[i-it]) { vec.push_back (it); sort (vec.begin (),vec.end ()); if (dict[i].count (vec)==0 ) dict[i].insert (vec); } } } } vector<vector<int >> ans; for (auto it:dict[target]) ans.push_back (it); return ans; } };
回溯法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution {public : vector<vector<int >> ans; vector<int > candi; void backtrace (vector<int > res,int remain) { if (remain==0 ) { sort (res.begin (),res.end ()); if (find (ans.begin (),ans.end (),res)==ans.end ()) ans.push_back (res); } for (auto it : candi) { if (it>remain) continue ; res.push_back (it); backtrace (res,remain-it); res.pop_back (); } } vector<vector<int >> combinationSum (vector<int >& candidates, int target) { candi = candidates; vector<int > res; backtrace (res,target); return ans; } };
dp和记忆化搜索都可以
dp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution {public : int minPathSum (vector<vector<int >>& grid) { int m=grid.size (),n=grid[0 ].size (); vector<vector<int >> dp (m,vector<int >(n,0 )); dp[m-1 ][n-1 ]=grid[m-1 ][n-1 ]; for (int i=m-2 ;i>=0 ;--i) dp[i][n-1 ]=grid[i][n-1 ]+dp[i+1 ][n-1 ]; for (int i=n-2 ;i>=0 ;--i) dp[m-1 ][i]=grid[m-1 ][i]+dp[m-1 ][i+1 ]; for (int i=m-2 ;i>=0 ;--i) for (int j=n-2 ;j>=0 ;--j) dp[i][j]=min (dp[i+1 ][j],dp[i][j+1 ])+grid[i][j]; return dp[0 ][0 ]; } };
记忆化搜索另外一题:62. Unique Paths
dp的含义:以ij为右下角的正方形的最大边长
依赖于左上方 左方 上方是否是正方形 为了统计数量方便 也可以理解为依赖于左上方 左方 上方正方形边长大小 缺一不可 所以要取三者最小值 注意边界情况(在图形边界)
可以解释为什么要考虑左侧上侧的最小值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution {public : int maximalSquare (vector<vector<char >>& matrix) { int maxSide = 0 ; int rows = matrix.size (), columns = matrix[0 ].size (); vector<vector<int >> dp (rows, vector<int >(columns)); for (int i = 0 ; i < rows; i++) { for (int j = 0 ; j < columns; j++) { if (matrix[i][j] == '1' ) { if (i == 0 || j == 0 ) { dp[i][j] = 1 ; } else { dp[i][j] = min (min (dp[i - 1 ][j], dp[i][j - 1 ]), dp[i - 1 ][j - 1 ]) + 1 ; } maxSide = max (maxSide, dp[i][j]); } } } maxSide * maxSide; return maxSquare; } };
内层循环是在动态规划解法中是不可避免的 相信自己!
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution {public : int numSquares (int n) { vector<int > dp (n+1 ) ; for (int i=1 ;i<=n;++i){ int minn=INT_MAX; for (int j=1 ;j*j<=i;++j) minn = min (minn,dp[i-j*j]); dp[i]=minn+1 ; } return dp[n]; } };
BFS 减去比自己小的完全平方数 成为树的节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution {public : int numSquares (int n) { queue<int > que; set<int > visited; visited.emplace (n); que.push (n); int level=0 ; while (!que.empty ()){ int size = que.size (); level++; for (int i=0 ;i<size;++i){ int num = que.front (); que.pop (); for (int j=1 ;j*j<=num;++j){ int residual = num-j*j; if (residual==0 ) return level; else if (visited.count (residual)==0 ) { visited.emplace (residual); que.push (residual); } } } } return level; } };
最长子序列的变体 关键在于从动态规划的思路理清
最大除了符号反转之外还有重新开始的可能(nums[i]
自身)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution {public : int maxProduct (vector<int >& nums) { int minp=nums[0 ],maxp=nums[0 ]; int ans=nums[0 ]; int tmpmax=0 ,tmpmin=0 ; for (int i=1 ;i<nums.size ();++i) { tmpmax=maxp; tmpmin=minp; maxp = max (nums[i],max (tmpmax*nums[i],tmpmin*nums[i])); minp = min (nums[i],min (tmpmax*nums[i],tmpmin*nums[i])); ans=max (ans,maxp); } return ans; } };
和最大子串和 最大子串积是同类
动态规划 有重复遍历
贪心+二分查找——上升的尽可能慢 长度为j的最小末尾作为状态记录 遍历到一个数用二分查找来更新(可以证明最小末尾数据是递增的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution {public : int lengthOfLIS (vector<int >& nums) { int len=1 ; int n=nums.size (); vector<int > d (n+1 ,0 ) ; d[len]=nums[0 ]; for (int i=1 ;i<n;++i){ if (nums[i]>d[len]) d[++len]=nums[i]; else { int l=1 ,r=len; int pos = 0 ; while (l<=r){ int mid = (l+r)/2 ; if (d[mid]<nums[i]){ pos = mid; l=mid+1 ; } else r=mid-1 ; } d[pos+1 ]=nums[i]; } } return len; } };
How to come up with the idea that using dynamic programming?
multiple states+rely on state of previous day
Because it has three different decisions in one day, dp vector should be two-dimension.
To optimize the space, we notice that every step is only dependent to its previous one step. So we can use single variable instead of vector to memorize states. In this way the memory used is back to one dimension but it is a bit different to my original thought, which only consider the time series.
Another point is to figure out how the state changes with cool-down. Devide the state into three situations: stock, not stock and cool, not stocking and not cool.
For day i, Stock means you have bought stocks previous day, so state is same as day i-1 stock. Or you bought it today, which means you cannot cool down and have stocks yesterday. So it is not stock and not cool plus your cost of buying stocks.
For day i, not stock and not cool means you have no stocks yesterday ( same as day i-1 no-stocks-no-cool);or you are cool down yesterday.
For day i, not stock and cool means you sold your stocks yesterday, which is day i-1 stock plus the price you sold them.
Besides, for initialization, stock means you bought stocks on that day, so it’s minus. It’s impossible for first day to be cool-down but we still set it zero,same as no-stocks-no-cool. We can take it as sell at 0 before.
Last, it’s meaningless to have stocks at last day, so the answer comes from cool-down and no-stocks-no-cool.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution {public : int maxProfit (vector<int >& prices) { int stock=-prices[0 ]; int cool = 0 ; int nsnc = 0 ; for (int i=1 ;i<prices.size ();++i){ int newst = max (stock,nsnc-prices[i]); int newcool = stock+prices[i]; int newnsnc = max (nsnc,cool); stock = newst; cool = newcool; nsnc = newnsnc; } return max (cool,nsnc); } };
小心初始化的时候 第二个位置是取前两个的最大值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution {public : int rob (vector<int >& nums) { int n=nums.size (); if (n==1 ) return nums[0 ]; else if (n==2 ) return max (nums[0 ],nums[1 ]); vector<int > dp (n,0 ) ; dp[0 ]=nums[0 ]; dp[1 ]=max (nums[0 ],nums[1 ]); for (int i=2 ;i<n;++i) { dp[i]=max (nums[i]+dp[i-2 ],dp[i-1 ]); } return dp[n-1 ]; } };
可以发现 实际上只需要存前两栋房屋的最大值就可以了 再往前的不需要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution {public : int rob (vector<int >& nums) { int house1=0 ,house2=0 ; if (nums.size ()<2 ) return nums[0 ]; else { house1 = nums[0 ]; house2 = max (nums[0 ],nums[1 ]); } int ans=house2; for (int i=2 ;i<nums.size ();++i) { ans = max (house1+nums[i],house2); house1 = house2; house2=ans; } return ans; } };
曾经的算法题复习 是否要取状态要记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution {public : unordered_map<TreeNode*,int > yes; unordered_map<TreeNode*,int > no; void robPlan (TreeNode* root) { if (!root) return ; if (!root->left && !root->right) { yes[root]=root->val; no[root]=0 ; } robPlan (root->left); robPlan (root->right); yes[root]=root->val+no[root->left]+no[root->right]; no[root]=max (yes[root->left],no[root->left])+max (yes[root->right],no[root->right]); } int rob (TreeNode* root) { robPlan (root); return max (yes[root],no[root]); } };
memorized search
if deleting ballon, two non-adjacent balloons will become adjacent. So we can take deletion as addition(Last one deleted). And use addtion to partition. Try every possible solution to find out currently max count.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution {public : vector<int > val; vector<vector<int >> rec; int solve (int left,int right) { if (left>=right-1 ) return 0 ; if (rec[left][right]!=-1 ) return rec[left][right]; else { for (int mid=left+1 ;mid<right;++mid) { int sum=val[left]*val[right]*val[mid]; sum+=solve (left,mid)+solve (mid,right); rec[left][right]=max (rec[left][right],sum); } } return rec[left][right]; } int maxCoins (vector<int >& nums) { int n=nums.size (); val.resize (n+2 ); for (int i=1 ;i<=n;++i) val[i]=nums[i-1 ]; val[0 ]=1 ; val[n+1 ]=1 ; rec.resize (n+2 ,vector<int >(n+2 ,-1 )); return solve (0 ,n+1 ); } };
Change the sequence to calculate. So we have dynamic programming.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution {public : int maxCoins (vector<int >& nums) { int n=nums.size (); vector<int > val (n+2 ) ; for (int i=1 ;i<=n;++i) val[i]=nums[i-1 ]; val[0 ]=1 ; val[n+1 ]=1 ; vector<vector<int >> rec (n+2 ,vector<int >(n+2 ,0 )); for (int i=n-1 ;i>=0 ;--i) { for (int j=i+2 ;j<=n+1 ;++j) { for (int k=i+1 ;k<j;++k) { int sum=val[i]*val[k]*val[j]; sum+=rec[i][k]+rec[k][j]; rec[i][j]=max (sum,rec[i][j]); } } } return rec[0 ][n+1 ]; } };
一个背包问题 转换为所有值的一半 应该复习的问题 算法课出现过
背包问题用一维是解决不了的 因为需要记录目前加了什么值
dp[i][j]
的含义 对目标j
数组里的前i
个数是否能满足
由于只求一半 所以j的范围在0~target就可以 即数组建立为target+1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution {public : bool canPartition (vector<int >& nums) { int n=nums.size (); if (n<2 ) return false ; int sum=accumulate (nums.begin (),nums.end (),0 ); int maxNum=*max_element (nums.begin (),nums.end ()); if (sum & 1 ) return false ; int target = sum/2 ; if (maxNum>target) return false ; vector<vector<int >> dp (n,vector<int >(target+1 ,0 )); for (int i=0 ;i<n;++i) dp[i][0 ]=true ; dp[0 ][nums[0 ]]=true ; for (int i=1 ;i<n;++i) { int num=nums[i]; for (int j=1 ;j<=target;++j) { if (j>=num) dp[i][j]=dp[i-1 ][j]|dp[i-1 ][j-num]; else dp[i][j]=dp[i-1 ][j]; } } return dp[n-1 ][target]; } };
一开始想到的逐个和累加没有排除负数
列的大小只与目标有关 转换目标
用公式转换 凑需要是负数的和
dp[i][j]
的含义是前i个数凑出来负数和是j的方法数
neg没有也是一种情况
想明白传一方程:是累加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution {public : int findTargetSumWays (vector<int >& nums, int target) { int sum= accumulate (nums.begin (),nums.end (),0 ); if (sum<target) return 0 ; if ((sum-target)%2 !=0 ) return 0 ; int neg = (sum-target)/2 ; int n=nums.size (); vector<vector<int >> dp (n+1 ,vector<int >(neg+1 )); for (int i=0 ;i<=n;++i) dp[i][0 ]=1 ; for (int i=1 ;i<=n;++i) { for (int j=0 ;j<=neg;++j) { dp[i][j]=dp[i-1 ][j]; if (j-nums[i-1 ]>=0 ) dp[i][j]+=dp[i-1 ][j-nums[i-1 ]]; } } return dp[n][neg]; } };
空间优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution {public : int findTargetSumWays (vector<int >& nums, int target) { int sum=accumulate (nums.begin (),nums.end (),0 ); if (sum<target) return 0 ; int delta = sum-target; if (delta%2 ==1 ) return 0 ; int neg = delta/2 ; vector<int > dp (neg+1 ) ; dp[0 ]=1 ; for (auto n:nums) { for (int j=neg;j>=n;--j) dp[j]+=dp[j-n]; } return dp[neg]; } };
When using dynamic programing, vector can be initialized to be amount+1. Such impossible value can avoid INT_MAX overflow when calculating.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution {public : int coinChange (vector<int >& coins, int amount) { vector<int > dp (amount+1 ,amount+2 ) ; dp[0 ]=0 ; for (int i=1 ;i<=amount;++i) { for (auto n:coins) { if (i-n>=0 ) dp[i]=min (dp[i],dp[i-n]+1 ); } } return dp[amount]==amount+2 ?-1 :dp[amount]; }
注意返回的答案应当是f_n_1(Fn-1);以及按照题目要求每个答案都要取模
一开始想到用回溯 但是在边界情况一直出错 可以用动态规划 避免一开始的是否需要加入的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class Solution { boolean judge (String shorter,String longer) { if (shorter.length()+1 != longer.length()) return false ; int insert=1 ; int i=0 ,j=0 ; while (i<shorter.length() && j < longer.length()){ if (shorter.charAt(i)!=longer.charAt(j)) insert--; else { ++i; } if (insert<0 ) return false ; ++j; } return true ; } public int longestStrChain (String[] words) { int ans = 0 ; List<String> wordsList =Arrays.asList(words); wordsList.sort(new Comparator<String>(){ @Override public int compare (String s1,String s2) { return s1.length()-s2.length(); } }); for (String str:wordsList){ System.out.printf("%s " ,str); } System.out.println("" ); int n = wordsList.size(); int [] dp = new int [n]; Arrays.fill(dp,1 ); for (int i=0 ;i<n;++i){ for (int j=0 ;j<i;++j){ if (judge(wordsList.get(j),wordsList.get(i))){ dp[i] = Math.max(dp[j]+1 ,dp[i]); } } ans = Math.max(dp[i],ans); } for (int i=0 ;i<n;++i) System.out.printf("%d " ,dp[i]); return ans; } }
最开始想到了树,在这过程中想到了区间分治可以用记忆话搜索或者动态规划
预打表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { static int N = 200 ; static int [][] cache = new int [N+10 ][N+10 ]; static { for (int len=2 ;len<=N;++len){ for (int l=1 ;l+len-1 <=N;++l){ int r = l+len-1 ; cache[l][r]=0x3f3f3f3f ; for (int i=l;i<=r;++i){ int cur = Math.max(cache[l][i-1 ],cache[i+1 ][r])+i; cache[l][r]=Math.min(cache[l][r],cur); } } } } public int getMoneyAmount (int n) { return cache[1 ][n]; } }