1.变成回文串的最少插入次数
题目:1312. Minimum Insertion Steps to Make a String Palindrome
状态转移: f(i,j) = min{f(i+1,j), f(i,j-1)}
class Solution {
int n;
int[][] dp;
String s;
public int minInsertions(String s) {
this.s = s;
n = s.length();
dp = new int[n][n];
for(int i=0;i<n;++i){
for(int j=0;j<n;++j){
dp[i][j]=-1;
}
}
return f(0,n-1);
// 如果不同 f(xXy) -> y + f(xX) +y
// x + f(Xy) + x
//
// f(i,j) -> f(i+1,j) + 1, f(i,j-1)+1
}
public int f(int i,int j){
if(i>=j){
return 0;
}
if(dp[i][j]!=-1){
return dp[i][j];
}
if(s.charAt(i)==s.charAt(j)){
return dp[i][j] = f(i+1,j-1);
}
return dp[i][j] = Math.min(f(i+1,j),f(i,j-1)) + 1;
}
}
使用自底向上优化:
TODO
2. 编码方式
题目: 639. Decode Ways II
解:转移方程
f(i) = c1*f(i+1) + c2*f(i+2)
其中c1和c2根据 A[i], A[i+1]的情况进行讨论
自底向上:从 i=n-1 to 0逐次计算 f(i), 然后根据A[i-1], A[i-2]生成 f(i-1),f(i-2)
空间优化:在整个过程中只需要保持两个数即可
class Solution {
static final long M = (long)Math.pow(10,9) + 7;
public int numDecodings(String s) {
int n = s.length();
char cn = s.charAt(n-1);
long x=1;
long y=1;
for(int i=n-1;i>=0;--i){
int c1=1;
if(s.charAt(i)=='*'){
c1 = 9;
}else if(s.charAt(i)=='0'){
c1=0;
}
int c2=0;
if(i+1 < n){
if(s.charAt(i+1)=='*'){
if(s.charAt(i)=='*'){
c2 = 15;
}else{
if(s.charAt(i)=='1')c2=9;
if(s.charAt(i)=='2')c2=6;
}
}else{
if(s.charAt(i)=='*'){
if(s.charAt(i+1)<='6')c2=2;
else c2=1;
}else{
if(s.charAt(i)!='0'){
int num = (s.charAt(i)-'0')*10 + (s.charAt(i+1)-'0');
if(num<=26){
c2=1;
}
}
}
}
}
long r = ((c1*x%M) + (c2*y%M))%M;
y = x;
x = r;
}
return (int)x;
}
}
字母表构成的子集最大得数
题目:1255. Maximum Score Words Formed by Letters
给定一个单词列表words, 一个字母列表letters(字母存在重复), 一个字母分数映射表scores, 求出使用letters可组成字母构成的words的子集中,最大的分数。每个单词的分数是所有使用的字母的分数之和。
提示:words.length<=14,letters.length <=100
解:状态转移方程:
显然,对于第i个单词,words[i]要么包含在解集中,要么不在,因此
f(i) = max { f(i+1), f(i+1) + score[i] }
// just a simple backtrack trick
class Solution {
int n;
String[] words;
int[] score;
int[] count;
int[] scoreMap;
int letterCount;
// use letter in letters to form a set of words
// each word will be used at most once
// count[c] count of c
//
// words[i] -> wcount[i] and score[i]
// f(i) = score[i] + f(i+1) or ...
public int maxScoreWords(String[] words, char[] letters, int[] score) {
// since words.length <= 14, we can represent each word as a binary bit
// each time we mask or unmask every bit
// and letters can be encoded also?
n = words.length;
this.words =words;
scoreMap = new int[n];
for(int i=0;i<n;++i){
int c = 0;
for(int j=0,jn=words[i].length();j<jn;++j){
c+=score[words[i].charAt(j)-'a'];
}
scoreMap[i] = c;
}
letterCount = letters.length;
count = new int[26];
for(char ch:letters){
++count[ch-'a'];
}
this.score = score;
return f(0);
}
public int f(int i){
if(i==n){
return 0;
}
if(letterCount==0){
return 0;
}
int m1 = f(i+1);
int m2 = scoreMap[i];
for(int k=0,kn=words[i].length();k<kn;++k){
int c = words[i].charAt(k)-'a';
if(count[c]==0){
for(int j=0;j<k;++j){
++count[words[i].charAt(j)-'a'];
}
m2=-1;
break;
}
--count[c];
}
if(m2>0){
letterCount -= words[i].length();
m2 = m2 + f(i+1);
letterCount += words[i].length();
for(int k=0,kn=words[i].length();k<kn;++k){
++count[words[i].charAt(k)-'a'];
}
if(m2 > m1){
m1 = m2;
}
}
return m1;
}
}
3.最小覆盖问题
题目:1125. Smallest Sufficient Team
假设一个团队需要n个技能,现在给定一组候选人,每个人有不同的技能,寻找一个能构成该团队的最小人数
解:这道题目有点类似于精确覆盖问题,即Knuth所提出的Dancing Links算法解决的问题。但本题不是精确覆盖,同一列可以有多行覆盖,只要这两行不是完全相同。
使用set表示需要覆盖的技能,则该问题的转移方程是: f(set) -> update f(set| p) for each p in candidates
假定set的所有分解是 p1|set1, p2|set2, …, pi|seti
则f(set) = min { f(p1|set1), f(p2|set2), …}
然后我们思考如何自底向上优化这个问题的空间复杂度。当set=0时,显然f(set) = 0. 假定当set的位数是i时,i-1位的所有set已经求解完毕,set的所有分解中,因为pi|seti至少能够使seti增加1位(否则就是已经解决的问题),所以只需要对集合中的所有元素seti,将其与pj求或,即可得到下一个状态,对其进行更新即可。
所以i位的set都将在这一轮中产生。
4.选择和问题
解:转移方程
f(i,S) = f(i+1,S-A[i]) + f(i+1,S+A[i])
when i==n-1, 0:2, other:1 or 0
class Solution {
int[] nums;
int[][] dp;
int d;
int n;
// f(i,S) = f(i+1,S-A[i]) + f(i+1,S+A[i])
// when i==n-1, 0:2, other:1 or 0
int f(int i,int s){
if(i==n-1){
return s==nums[i] || s==-nums[i]?(s==0?2:1):0;
}
if(dp[i][s-d]!=-1){
return dp[i][s-d];
}
return dp[i][s-d] = f(i+1,s + nums[i]) + f(i+1,s - nums[i]);
}
public int findTargetSumWays(int[] nums, int S) {
this.nums = nums;
n = nums.length;
int sum = 0;
for(int i=0;i<n;++i){
sum += nums[i];
}
// [S-sum,S+sum],
dp = new int[n][2*sum + 1];
for(int i=0;i<n;++i){
Arrays.fill(dp[i],-1);
}
d = S - sum;
if(S>sum || S <-sum){
return 0;
}
return f(0,S);
}
}
自底向上优化:TODO
选择操作符(回溯)
题目:282. Expression Add Operators
解:Naive Recursion(回溯)
状态转移方程:
将合并的问题看成 f(i, target, c,x,y)
其中i表示num的[i,n)的问题范围,c表示当前系数,x表示已经确定的乘数,y表示当前数
比如c=-1,x=2,y=3
表示的是 cxy = -23
c=-1,x=23,y=456
表示的是 cxy = -23456
则转移方程如下:
f(i,target,c,x,y) = {
f(i+1,target,c,x*y,A[i]) + // 1*2
f(i+1,target - c*x*y,1, 1,A[i]) + // 1+2
f(i+1,target - c*x*y,-1, 1,A[i]) + // 1-2
if(y!=0){
f(i+1,target, c,x, 10*y + A[i]) // 12
}
xhd2015 - Leetcode解析 - 282. Expression Add Operators
class Solution {
int n;
String num;
List<String> list;
public List<String> addOperators(String num, int target) {
n = num.length();
this.num = num;
list = new ArrayList<>();
if(n>0){
f(0,target,1,1,num.charAt(0)-'0',"" + num.charAt(0));
}
return list;
}
void f(int i,long target,int c,long x,long y,String expr){
if(i==n-1){
if(target == c*x*y){
list.add(expr);
}
return;
}
int p = num.charAt(i+1) - '0';
long prd = y*x;
f(i+1,target ,c,prd,p,expr + "*" + num.charAt(i+1));
f(i+1,target - prd*c,1, 1,p,expr + "+" + num.charAt(i+1));
f(i+1,target - prd*c,-1, 1,p,expr + "-" + num.charAt(i+1));
if(y!=0){
f(i+1,target, c,x, 10*y + p,expr + num.charAt(i+1));
}
}
}
5.访问图中所有的点
题目:847. Shortest Path Visiting All Nodes
解:状态转移方程
将状态定义为 (S,i),表示从i点出发,经过S中的所有点的最小路径
则 f(S,i) = max {
f(S - {i}, e)
} for each e adjacent to i,
i和e都不必在S中
当S为空集时,返回0
TODO
6.矩阵子问题
最大矩形(TODO:提交)
解:以点(i,j)作为左上角的最大矩形,可用于更新 (i-1,j)和(i,j-1)的矩形。
设(i,j)的矩形高是H(i,j),宽是W(i,j), 则对于(i-1,j)的矩形,如果 A[i-1,j]==‘0’,则无法形成矩形;否则, H(i-1,j) = 1 + H(i,j), W(i-1,j) = min { W(i,j), S(i-1,j) }
其中,S(i-1,j)是以(i-1,j)为起点的最长连续1的长度.
S(i,j) = 若A[i,j]==‘0’: 0, 否则: S(i,j+1)+1
同理,对于(i,j-1), 若A[i,j-1]==‘0’,则无法构成矩形;否则,
H(i,j-1) = min { H(i,j), T(i,j-1)}, W(i,j-1) = 1 + W(i,j), 其中T(i,j)表示以(i,j)为起点的最长连续1的高度.
自底向上优化:我们可以使用一维数组优化H,W, S,T.
首先,我们考虑第n-1行,第n-1列, H,W,S,T都初始化为合理值。
对于第n-1行,第n-2列, 可以更新W[j-1] (1+),H[j-1] (min), S[j-1], H[j-1].
对于第n-2行, 仍然从第n-1列开始,S清空, H累积。W[j] (min更新), H[j] (1+), W[j-1], H[j-1]同前
每次换行时,S都需要重新清零。
伪代码:
H=[]
W=[]
S=[]
T=[]
for i=n-1 to 0:
for j=m-1 to 0:
if A[i][j]=='0':
S[j]=0, T[j]=0,W[j]=0,H[j]=0
continue
S[j] = 1 + (j+1==m?0:S[j+1])
T[j] = 1 + T[j]
W[j] = min( W[j],S[j])
H[j] = 1 + H[j]
W[j-1] = 1 + W[j]
H[j-1] = min(H[j],T[j-1])
// calculate area and update maximum here
为了证明一维数组是有效的,我们可以保证W[j]在第i次循环中,使用的是第i+1次循环的值;同理,H[j]在第i次循环中,使用的也是第i+1次的值。产生更新后,它们就从W[i+1,j] 过度到W[i,j], 然后用于更新W[j-1].
最大正方形
解:解决正方形的问题关键是找到正确的转移方程,其实正方形的问题是具有子问题结构的,以(i,j)为左上角顶点的正方形,其(i+1,j+1)如果非0,必然也是这个正方形,因此有
H(i,j) = max( H(i+1,j+1)+1, S[i],T[j])
使用自底向上的空间优化,我们每次更新用H[j]更新H[j-1],因为H[j]恰好就是H[j-1]的第一个右下角对角点。
class Solution {
public int maximalSquare(char[][] matrix) {
int n = matrix.length;
if(n==0)return 0;
int m =matrix[0].length;
if(m==0)return 0;
int[] H = new int[m];
// S=1's count in row,T=1's count in column
int[] S = new int[m];
int[] T = new int[m];
int maxS = 0;
for(int i=n-1;i>=0;--i){
// scan row and column count reversly
for(int j=m-1;j>=0;--j){
if(matrix[i][j]=='0'){
S[j]=T[j]=0;
continue;
}
S[j] = 1 + (j==m-1?0:S[j+1]);
++T[j];
}
for(int j=0;j<m-1;++j){
H[j] = Math.min(Math.min(H[j+1] + 1, T[j]),S[j]);
maxS = Math.max(maxS,H[j]*H[j]);
}
// update m-1 lastly
H[m-1]=0;
if(matrix[i][m-1]=='1'){
H[m-1]=1;
if(maxS<1)maxS=1;
}
}
return maxS;
}
}
7.子数组问题
K-重复最大子数组和问题
问题:1191. K-Concatenation Maximum Sum
给定一个数组A和数字k,将A重复k次得到的数组,求其最大子数组和
解:首先求出第一个数组的最大和,则后续数组的最大和的计算与第一个数组的最大和计算的唯一区别就是数组的第一个元素之前存在一个值,我们称为lastSum。
如果这个值小于等于0,则第二个数组的最大和和第一个数组的最大和相同,后面的所有数组都一样。
实际上,如果要使得第3个数组和第2个数组的最大和不一样,就需要第2个数组的最后一个元素的最大和发生改变,否则其计算过程就是重复第2个。发生改变的唯一可能就是将lastSum和第2个数组的最后一个元素组合起来,因此第2个数组的最后一个元素的最大和S = lastSum + sumOfArray.
由于最后一个元素发生了更新,所以所有的元素都发生了更新,我们可以证明Si=lastSum + sumOfArrayUntil_i。
第3个数组的计算过程重复第2个数组计算过程,Si=lastSum + sumOfArray + sumOfArrayUntil_i
最后一个数组的计算结果就是: Si = lastSum + sumOfArray*(k-2) + sumOfArrayUntil_i
显然,最大值就是 sumOfArrayUntil_i中的最大值,也就是下面代码中使用的sumMax.
class Solution {
static final long M=(long)Math.pow(10,9)+7;
public int kConcatenationMaxSum(int[] arr, int k) {
int n=arr.length;
long res=0;
long last=0;
long sum=0;
long sumMax=0;
// ordinary maximum subarray sum problem to find maximum
for(int j=0;j<n;++j){
long r = Math.max((long)arr[j],arr[j]+last);
res=Math.max(res,r);
last=r;
sum+=arr[j];
if(sumMax<sum){
sumMax=sum;
}
}
// if there were more repeatations, we recaucluate the res
if(k>1 && sumMax>0){
if(sum>0){
res = Math.max(res, last+(k-2)*sum+sumMax);
}else{
res =Math.max(res,last+sumMax);
}
}
return (int)(res%M);
}
}
xhd2015 - Leetcode Solution - 1191. K-Concatenation Maximum Sum
8.括号匹配问题
最长的有效括号
题目:32. Longest Valid Parentheses
给定一个括号对,形如 “(())))(()”, 找出其中最长的有效括号对
解:(基于栈)在括号的匹配问题中,如果遇到 “(”,则表示需要与后面的元素进行合并,如果遇到 “)”,则表明需要与前面的元素合并。
显然,当遇到 ")"但是前面的序列中没有一个 "("与之匹配时,此时这个括号后面的所有括号都不可能再与 ")“以及之前的序列合并。也就是说,一个未合并的”)"能够将序列完全划分为两个不相关的部分。
我们使用栈存储我们遇到的符号,
1.当遇到 "(“时,我们往栈中压入0,后面如果有”)"遇到0,就能进行合并;2.当遇到 ")“时,我们将其与栈中最近的未合并的”("进行合并,也就是说,与栈中最近的0进行合并。如果找不到这个0,则说明这个括号是不能进行合并的,因此此时栈中的所有序列的长度已经确定,清空栈,重复上面的过程即可。
class Solution {
public int longestValidParentheses(String s) {
// two adjacent valid seq can be merged
int n=s.length();
Stack<Integer> st=new Stack<>();
int max = 0;
// note the special case when i==n, we should pop all sums from the stack
for(int i=0;i<=n;++i){
char c=i==n?')':s.charAt(i);
if(c=='('){
st.push(0);
}else if(c==')'){
int sum=0;
boolean found=false;
while(!st.isEmpty()){
int e = st.pop();
// "("放入0,因此")"找到0才能匹配
if(e==0){
if(i<n){
++sum;
found=true;
break;
}else{
max = Math.max(max,sum);
sum = 0;
continue;
}
}
sum+=e;
}
if(!found){
max = Math.max(max, sum);
}else{
st.push(sum);
}
}
}
return max*2;
}
}
解2:设dp[i]是离S[i]最近的有效长度,则转移方程如下
dp[0]=0
dp[i] =
if S[i]=='(' : 0
else S[i]==')': let k be the nearest position, where k= i - dp[i-1],
if S[k-1]=='(', then dp[i] = dp[i-1] +2 + dp[k-2]
otherwise 0
class Solution {
public int longestValidParentheses(String s) {
int [] dp = new int[s.length()];
int maxLength = 0;
char [] str = s.toCharArray();
for(int i = 1 ; i < dp.length ; i++){
if(str[i] == ')'){
int k = i-dp[i-1]-1;
if(k >= 0 && str[k] == '('){
dp[i]=2+dp[i-1];
if(k > 0 )
dp[i]+=dp[k-1];
}
}
maxLength = Math.max(maxLength,dp[i]);
}
return maxLength;
}
}
Leetcode Solution - 32. Longest Valid Parentheses - Java DP
9.分词
Word Break
给定一个非空字符串S和一个单词表W,请确定是否能用空格将S分成多个部分,每个部分的单词都出现在W中
解:我们从最后一个字符考虑。令f(i)表示S[0,i]是否能够以W中的单词划分问题,则
f(i-k) = f(i) && w==S[i-k+1,i], w是W中能够匹配S[i-k+1,i]的所有单词。
我们使用自底向上更新每一个k即可。
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
boolean[] dp=new boolean[n+1];
dp[n]=true;
for(int i=n;i>0;--i){
if(!dp[i])continue;
for(String w:wordDict){
int k = i - w.length();
if(k<0)continue;
if(dp[k])continue;
dp[k]=true;
for(int j=k;j<i;++j){
if(w.charAt(j-k)!=s.charAt(j)){
dp[k] = false;
break;
}
}
}
}
return dp[0];
}
}
使用后缀优化:为W中的每个单词,以结尾的字符作为索引建立列表
时间从2ms降到1ms,beats从93%升到99%
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n =s.length();
List<String>[] mp = new List[128];
for(String w:wordDict){
int c=w.charAt(w.length()-1);
List list=mp[c];
if(list==null)list=mp[c]=new ArrayList<>(n);
list.add(w);
}
boolean[] dp=new boolean[n+1];
dp[n]=true;
for(int i=n;i>0;--i){
if(!dp[i])continue;
int c=s.charAt(i-1);
if(mp[c]==null){
continue;
}
for(String w:mp[c]){
int k=i-w.length();
if(k<0)continue;
if(dp[k])continue;
dp[k]=true;
for(int j=k;j<i-1;++j){
if(w.charAt(j-k)!=s.charAt(j)){
dp[k]=false;
break;
}
}
}
}
return dp[0];
}
}
Word Break II
题目:140. Word Break II
转移方程
f(i) = f(i+k1) + f(i+k2) + … for ki matches S[i,i+ki]
class Solution {
int n;
String s;
List<String> wordDict;
List[] memo;
String cur;
public List<String> wordBreak(String s, List<String> wordDict) {
this.s= s;
n = s.length();
this.wordDict = wordDict;
memo = new List[n];
cur = "";
return f(0);
}
List<String> f(int i){
if(i==n){
return Collections.singletonList(cur);
}
if(memo[i]!=null){
return memo[i];
}
List<String> list = new ArrayList<>(n);
for(String word:wordDict){
int k = i + word.length();
if(k<=n){
boolean match = true;
for(int j=i;j<k;++j){
if(s.charAt(j)!=word.charAt(j-i)){
match = false;
break;
}
}
if(match){
String tmp = cur;
cur = "";
for(String w:f(k)){
list.add(tmp + (tmp.isEmpty()?"":" ") + word + (w.isEmpty()?"":" ")+ w);
}
cur = tmp;
}
}
}
return memo[i] = list;
}
}
同样此题也可以使用第一个字符做索引,但是提升不明显,只有1ms的提升。
单词组合(TODO待提交)
给定一个单词列表W,求出W中所有能够由两个或多个更短的单词组成的单词,这些更短的单词也在W中, 比如,[“a”, “b”, “aab”,“ab”], "aab"能够由 “a”, “a”, "b"组成.
解: 状态转移方程
设f(w)表示w的组成长度
f(w) = max { f(w-s) +1 } for s in words && w.startsWith(s)
注意,如果w不能组成,则返回-1
class Solution {
Map<String,Integer> memo;
String[] words;
public List<String> findAllConcatenatedWordsInADict(String[] words) {
int n = words.length;
this.words= words;
memo = new HashMap<>(n);
List<String> res = new ArrayList<>(100);
for(String w:words){
int cnt = f(w);
if(cnt>=2){
res.add(w);
}
}
return res;
}
int f(String w){
if(w.isEmpty()){
return 0;
}
Integer r = memo.get(w);
if(r!=null){
return r;
}
// a=1,b=1
// ab=2
// abc =
// c
int res = -1;
for(String s:words){
if(w.startsWith(s) && w.length() >= s.length()){
int r = f(w.substring(s.length()));
if(r==-1){
continue;
}
if(res==-1 || res < r+1){
res = r+1;
if(res>=2){
break;
}
}
}
}
memo.put(w,res);
return res;
}
}
10.排列问题
一般而言,排列问题比较恶心,因为你很难知道下一个是什么。
生成所有序列
解:不需要使用计数器,直接可以在原序列上进行生成。
对于第i位,我们可以依次将S中i后面的所有数字交换到i处,然后进行回溯即可。需要注意的,对于第j个元素,如果在 [i,j)不包含j的范围内出现过,则不必交换j。
生成下一个序列
找到第K个序列
给定1~n个数字,返回其全排列的第k个
class Solution {
int n;
char[] arr;
public String getPermutation(int n, int k) {
this.n=n;
arr = new char[n];
// the initial arr is ['1','2','3',....]
// it's the minimal permutation
for(int i=0;i<n;++i){
arr[i]= (char)(i+'1');
}
findKthPermutation(k);
return new String(arr);
}
void findKthPermutation(int k){
if(k==1){
return;
}
// find the smallest segment as [n-h,n-1]
int h=1;
int c=1;
while(k>=c){
c*=++h;
}
c/=h;
--h;
if(n==h){
reverse(0,n-1);
return;
}
// locate the multiple of c and the modulus r
// and shift the segment to generate next permutation
// reverse the remaining to generate the last permuation
int x=k/c;
int r=k%c;
if(r==0){
shift(n-h-1,n-h-1+x-1);
reverse(n-h,n-1);
}else{
shift(n-h-1,n-h-1+x);
findKthPermutation(r);
}
}
/**
* shift array from left to right as a cycle, to generate the nearsest permutation
*/
void shift(int i,int j){
char t=arr[j];
for(int r=j;r>i;--r){
arr[r]=arr[r-1];
}
arr[i]=t;
}
/**
* reverse the array to get the maximal permutation
*/
void reverse(int i,int j){
for(int r=i,x=j;r<x;++r,--x){
char t=arr[r];
arr[r]=arr[x];
arr[x]=t;
}
}
}
解法2:使用数字运算
TODO
找到第K个序列(元素可重复)
可重复序列的排列中,排列的数量等于 n!/(n1!n2!..ni!), ni表示第i个数字的重复,则查找第k个排列等价于在未重复的序列中查找第 k*(n1!n2!..ni!)项, 因为每个唯一的序列都会重复n1!n2!..ni!次.