最长回文子串解题过程
暴力求解
枚举字符串s所有长度大于1的子串,验证其是否为回文子串。
时间复杂度:O(n^3) 其中n=len(s)
class Solution:
def longestPalindrome(self, s: str) -> str:
def isPalindrome(s): #s是否回文
for i in range(0,len(s)//2):
if s[i]!=s[-(1+i)]:
return False
return True
def findinstr(s): #枚举当前起点的所有str
for i in range(len(s),0,-1):
if isPalindrome(s[:i]):
return s[:i]
res_n=1
res_s=s[0]
for j in range(len(s)-1): #不同的str起点
r = findinstr(s[j:])
if len(r)>res_n:
res_n=len(r)
res_s=r
if res_n>=len(s)-1-j:
break
return res_s
动态规划
1.思考状态
状态的定义,先尝试「题目问什么,就把什么设置为状态」;
然后思考「状态如何转移」,如果「状态转移方程」不容易得到,尝试修改定义,目的依然是为了方便得到「状态转移方程」。
定义dp[i][j],(i<=j)
表示子串 s[i..j]
是否为回文子串,这里子串 s[i..j]
定义为左闭右闭区间,可以取到 s[i] 和 s[j]
2.思考状态转移方程(核心、难点)
「状态转移方程」是原始问题的不同规模的子问题的联系。即大问题的最优解如何由小问题的最优解得到。
常见的推导技巧是:分类讨论。即:对状态空间进行分类;
理解动态规划解决问题,是从一个小规模问题出发,逐步得到大问题的解,并记录中间过程;
「动态规划」方法依然是「空间换时间」思想的体现,常见的解决问题的过程很像在「填表」。
状态转移:若一个字符串首尾字符相同,且首尾字符(不含)之间的字符串为回文字串,则该字符串为回文串。
另外,若一个字符串首尾字符相同,且该字符串长度小于4,即j-i+1<4 --> j-i<3
时,该字符串一定是回文串。
即:dp[i][j]=(s[i]==s[j]) and(j-i<3 or dp[i+1][j-1])
3.思考初始化
角度 1:直接从状态的语义出发;
角度 2:如果状态的语义不好思考,就考虑「状态转移方程」的边界需要什么样初始化的条件;
角度 3:从「状态转移方程」方程的下标看是否需要多设置一行、一列表示「哨兵」(sentinel),这样可以避免一些特殊情况的讨论。
初始化的时候,单个字符一定是回文串,因此把对角线先初始化为true
,即 dp[i][i] = true
4.思考输出
有些时候是最后一个状态,有些时候可能会综合之前所有计算过的状态。
得到 dp[i][j] = true
,记录此时的回文子串的「起始位置」和「回文长度」
5.思考优化空间(也可以叫做表格复用)
「优化空间」会使得代码难于理解,且是的「状态」丢失原来的语义,初学的时候可以不一步到位。先把代码写正确是更重要;
「优化空间」在有一种情况下是很有必要的,那就是状态空间非常庞大的时候(处理海量数据),此时空间不够用,就必须「优化空间」;
非常经典的「优化空间」的典型问题是「0-1 背包」问题和「完全背包」问题。
dsbab
class Solution:
def longestPalindrome(self, s: str) -> str:
res=1 #当前最长回文串长度
res_i=0 #当前最长回文串首位索引
dp=[[False for _ in range(len(s))] for _ in range(len(s))]
for i in range(len(s)):
dp[i][i]=True #不初始化也可 用不到
for j in range(1,len(s)):
for i in range(0,j):
if s[i]==s[j] and (j-i<3 or dp[i+1][j-1]):
#首尾相同时 当前字符串len为2或3 不必参考dp[i+1][j-1]
#or 首位相同 参考dp[i+1][j-1]
dp[i][j]=True
if j-i+1>res:
res = j-i+1
res_i = i
return s[res_i:res_i+res]
时间复杂度:
O
(
N
2
)
O(N^2)
O(N2)
dp[i][j]
遍历顺序:01,02,12,03,13,23,04,14,24,34
j属于(1,len(s)
i属于(0,j)
参考:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/
递归与动态规划
class Solution:
def fib(self, n: int) -> int:
#########递归 自顶向下###########
if n==0:
return 0
if n==1:
return 1
else:
return self.fib(n-1)+self.fib(n-2)
##########动态规划 自底向上,存储之前的状态##############
if n<2:
return n
p,q=0,1
for i in range(1,n):
r=p+q
p=q
q=r
return r
分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
分析:实际上是在数组nums中取一些数,它们的和等于target(即sum(nums)//2)。
设置状态:dp[i][j]表示nums从[0]到[i]中存在一些数和为j (i从0到len(nums)-1,j从0到target)
状态转移:
当nums[i]>target,必不能取nums[i],dp[i][j] = dp[i-1][j]
当nums[i]<=target,可能不取nums[i],可能取,有一种情况成立则dp[i][j]成立, dp[i][j]= dp[i-1][j] or dp[i-1][j-nums[i]]
初始化:。。。
class Solution:
def canPartition(self, nums: List[int]) -> bool:
all = sum(nums)
maxnum = max(nums)
if all%2!=0 or len(nums)==1:
return False
target = int(all/2)
# print(target)
if maxnum > target:
return False
dp = [[False]*(target+1) for _ in range(len(nums))]
for i in range(len(nums)):
dp[i][0]=True
dp[0][nums[0]]=True
for i in range(len(nums)):
for j in range(target+1):
if nums[i]>target:
#不取nums[i]
dp[i][j] = dp[i-1][j]
else:
#不取nums[i]或取nums[i]
dp[i][j]= dp[i-1][j] or dp[i-1][j-nums[i]]
# print(dp)
return dp[-1][-1]
目标和
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
问题转化:假设满足条件的表达式中,前面为正号的数字和为p,前面为负号的数字和为n,有p-n=target,p+n=sum(nums)
,所以p=(target+sum)/2
,此时问题转化为:数组nums中可以取出多少种不同的数字组合,满足和为(target+sum)/2
的条件,(target+sum)/2
成为了新的Target。
定义状态:dp[i][j]表示在数组nums的前i个数中选取元素,使得这些元素之和等于j的方案数。i从0到len(nums),j从0到Target+1
状态转移:
nums[i]>j时,必不取nums[i]: dp[i][j]=dp[i-1][j]
nums[i]<=j时,nums[i]可能不取也可能取,两种情况方案数应相加: dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i-1]]
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
all = sum(nums)
if (target+all)%2:
return 0
target = (target+all)//2
dp=[[0]*(target+1) for _ in range(len(nums)+1)]
dp[0][0]=1
for i in range(1,len(nums)+1):
for j in range(target+1):
if nums[i-1]<=j:
dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i-1]]
else:
dp[i][j]=dp[i-1][j]
print(dp)
return dp[-1][-1]
单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
dp=[False]*(len(s)+1)
dp[0]=True
for i in range(1,len(s)+1):
for j in range(0,i):
if dp[j] and s[j:i] in wordDict:
dp[i]=True
break
return dp[-1]
分析:
定义状态:dp[i]表示前i个字符是否可以被拆分为一个或多个在字典中出现的单词。
状态转移:若s[0:i]整个字符串都在Dict中,则dp[i]=True;
若不在,则设置j从0到i-1进行搜索,是否存在s的前j个字符可被拆分为Dict中出现的单词即dp[j]为True 且 s[j:i]在Dict中,若满足则dp[i]=True;(事实上当j=0时,就是在验证s[0:i]整个字符串是否都在Dict中,所以不必再单独拎出来做判断辽)
初始状态:dp[0]=True 空字符串在Dict中可被找到。
剑指 Offer 42. 连续子数组的最大和
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
class Solution {
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
int res = nums[0];
for(int i=1;i<nums.length;i++){
if(dp[i-1]>0){
dp[i]=dp[i-1]+nums[i];
}
else{
dp[i]=nums[i];
}
res=Math.max(res,dp[i]);
}
return res;
}
}
// 定义dp[i]为数组nums[0,i]左闭右闭内子数组和最大值
// if dp[i-1]<=0 dp[i]=nums[i]
// if dp[i-1]>0 dp[i]=nums[i]+dp[i-1]
//初始化 dp[0] = nums[0]
剑指 Offer 48. 最长不含重复字符的子字符串
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
###########动态规划+哈希表 O(N)################
dic={}
res=0
tem=0 #记录dp[j]
for j in range(len(s)):
i=dic.get(s[j],-1) #寻找s[j]左侧最近的相同字符位置,若无返回-1
dic[s[j]]=j #记录s[j]目前最后出现的位置
if j-i>tem:
tem=tem+1
else:
tem=j-i
res=max(tem,res)
# print(j,i,res,dic)
return res
#设dp[j]为以s[j]为结尾的最长不重复子字符串的长度
# 字符s[j]左边距离最近的相同字符为s[i]
#当s[i]不存在或s[i]不在dp[j-1]范围内即j-1-i+1>dp[j-1] dp[j]=dp[j-1]+1
#当s[i]在dp[j-1]范围内即j-1-i+1<=dp[j-1] dp[j]=j-i
#dp[0]=0
剑指 Offer 47. 礼物的最大价值
class Solution {
public int maxValue(int[][] grid) {
int[][] dp=new int[grid.length][];
for (int n=0;n<grid.length;n++)
dp[n]=new int[grid[0].length];
dp[0][0]=grid[0][0];
for (int i=1;i<grid[0].length;i++)
dp[0][i]=dp[0][i-1]+grid[0][i]; //第一行初始化
for (int j=1;j<grid.length;j++)
dp[j][0]=dp[j-1][0]+grid[j][0]; //第一列初始化
for (int j=1;j<grid[0].length;j++){
for (int i=1;i<grid.length;i++){
dp[i][j]=grid[i][j]+Math.max(dp[i-1][j],dp[i][j-1]);
}
}
return dp[grid.length-1][grid[0].length-1];
}
}
//设dp[i][j]为处于grid[i][j]时的最大价值
//dp[i][j]=grid[i][j]+max(dp[i-1][j],do[i][j+1])
//初始化第一行第一列 dp[0][:] dp[:][0]
剑指 Offer 60. n个骰子的点数
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
class Solution:
def dicesProbability(self, n: int) -> List[float]:
dp=[[0 for _ in range(n*6+1)] for _ in range(n+1)]
for i in range(1,n+1): #1...n 相当于第0行是空的
dp[i][i]=1
for j in range(i+1,6*i+1): #第0列也是空的
if i==1: #i=1时初始化第一行的12...6列
dp[1][j]=1
if i>1 :
for t in range(1,min(7,j)): #j-t>0 t<j and 1<=t<7
dp[i][j]+=dp[i-1][j-t]
sum=6**n
res=[]
# print(dp)
for i in range(n,n*6+1):
res.append(dp[n][i]/sum)
return res
i \ j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 5 | 4 | 3 | 2 | 1 |
剑指 Offer 46. 把数字翻译成字符串
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
class Solution:
def translateNum(self, num: int) -> int:
cur=num%10
num=(num-cur)/10
dp=[1,1]
while num!=0:
pre=cur
cur=num%10
num=(num-cur)/10
left2num = cur*10+pre
if left2num>=10 and left2num<=25:
dp.append(dp[-1]+dp[-2])
else:
dp.append(dp[-1])
return dp[-1]
#dp[i]表示i个数的可翻译数量
#dp[i]=dp[i-1] 当num首位两数不能分开翻译
#dp[i]=dp[i-1]+dp[i-2] 当num首位两数可以分开翻译
#初始化dp[0]=1 dp[1]=[1]
剑指 Offer 19. 正则表达式匹配
class Solution:
def isMatch(self, s: str, p: str) -> bool:
dp=[[False for _ in range(len(p)+1)] for _ in range(len(s)+1)]
dp[0][0]=True
for i in range(1,len(p)+1):
if i%2==0 and p[i-1]=='*':
#p[i-2]*为空 p[0]~[i-3]与空字符匹配
dp[0][i]=dp[0][i-2]
for i in range(1,len(s)+1):
for j in range(1,len(p)+1):
if p[j-1]=='*':
#1.p[j-2]*合起来为空 s[0]~s[i-1]与p[0]~p[j-3]匹配 即dp[i][j-2]=True
#2.*让p[j-2]多出现一次 s[i-1]=p[j-2] 且 s[0]~[i-2]与p[0]~p[j-2]*匹配即dp[i-1][j]=True
#3.*让p[j-2]多出现一次 p[j-2]='.'且 s[0]~[i-2]与p[0]~p[j-2]*匹配
# print(i,j)
dp[i][j]=dp[i][j-2] or (dp[i-1][j] and s[i-1]==p[j-2]) or (dp[i-1][j] and p[j-2]=='.')
else: #p[j-1]为'.'或普通字符
#1.s[i-1]=p[j-1] 且 s[0]~s[i-2]与p[0]~p[j-2]匹配 即dp[i-1][j-1]=True
#2.p[j-1]='.'且 s[0]~s[i-2]与p[0]~p[j-2]匹配 即dp[i-1][j-1]=True
dp[i][j]=dp[i-1][j-1] and (s[i-1]==p[j-1] or p[j-1]=='.')
# print(dp)
return dp[-1][-1]
定义状态: 矩阵dp[i][j]代表字符串s的前i个字符 (s[0]~ s[i-1])
和字符串p的前j个字符 (p[0]~ p[i-1])
是否匹配。
状态转移: 见代码注释
初始状态: dp[0][0] = true
代表两个空字符串能够匹配。
dp[0][j] = dp[0][j - 2] 且 p[j - 1] = '*'
: 首行 s 为空字符串,因此当 p 的偶数位(第偶数个位)为 * 时才能够匹配(即让 p 的奇数位出现 0 次,保持 p 是空字符串)。因此,循环遍历字符串 p ,步长为 2(即只看偶数位)。
返回值: dp[-1][-1]
打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums)==1:
return nums[0]
dp=[nums[0],max(nums[0],nums[1])]
for i in range(2,len(nums)):
dp.append(max(dp[i-1],dp[i-2]+nums[i]))
return dp[-1]
定义状态: dp[i]表示偷完(经过)序号i家后的最大金额 i从0开始
状态转移: 若偷序号i家,则序号i-1家必不偷 dp[i]=num[i]+dp[i-2]
若不偷第i家 dp[i]=dp[i-1]
初始状态: dp[0]=nums[0] dp[1]=max(nums[0],nums[1])
打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
class Solution:
def rob(self, nums: List[int]) -> int:
def rob1(nums1):
dp0=nums1[0]
dp1=max(nums1[0],nums1[1])
for i in range(2,len(nums1)):
dp2=max(dp1,dp0+nums1[i])
dp0,dp1 = dp1,dp2
return dp2
if len(nums)==1:
return nums[0]
if len(nums)==2:
return max(nums[0],nums[1])
if len(nums)==3:
return max(nums[0],nums[1],nums[2])
else:
return max(rob1(nums[0:len(nums)-1]),rob1(nums[1:len(nums)]))
核心思想是:如果偷了第0家,必不偷最后一家;如果偷了最后一家,必不偷第0家。
长度为1,2,3时单独讨论,长度大于等于4时,分为[0:n-2]和[1:n-1]讨论(左闭右闭)。
删除并获得点数
给你一个整数数组 nums ,你可以对它进行一些操作。
每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。
开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。
class Solution:
def deleteAndEarn(self, nums: List[int]) -> int:
if len(nums)==1:
return nums[0]
all=[0]*max(nums)
print(all)
for i in nums:
all[i-1]+=1
dp0,dp1 = all[0],max(all[0],all[1]*2)
for i in range(2,len(all)):
dp = max(dp1,dp0+all[i]*(i+1))
dp0,dp1 = dp1,dp
return dp
在原来的 nums 的基础上构造一个临时的数组 all,这个数组,以元素的值来做下标,下标对应的元素是原来的元素的个数。
举个例子:
nums = [2, 2, 3, 3, 3, 4]
构造后:
all=[0, 0, 2, 3, 1];
就是代表着 2 的个数有两个,3 的个数有 3 个,4的个数有 1 个。
剩下和打家劫舍一样了。
最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
dp0=nums[0]
maxnum=dp0
for i in nums[1:]:
if dp0<=0:
dp1=i
else:
dp1=dp0+i
dp0=dp1
# print(dp1)
if dp1>maxnum:
maxnum = dp1
return maxnum
dp[i]表示以nums[i]为结尾的连续子数组的最大和
dp[i-1]>0时,dp[i]=dp[i-1]+nums[i]
dp[i-1]<=0时,dp[i]=nums[i]
初始化 dp[0]=nums[0]
环形子数组的最大和
给定一个由整数数组 A 表示的环形数组 C,求 C 的非空子数组的最大可能和。
class Solution:
def maxSubarraySumCircular(self, nums: List[int]) -> int:
if len(nums)==1:
return nums[0]
dp0=nums[0]
dp0min=nums[0]
maxnum=dp0
minnum=dp0min
for i in nums[1:]:
#最大和序列在中间
if dp0<=0:
dp=i
else:
dp=i+dp0
dp0=dp
if maxnum<dp:
maxnum=dp
#最大和序列在两边 那就找中间的最小和序列
if dp0min>=0:
dpmin=i
else:
dpmin=i+dp0min
dp0min=dpmin
if minnum>dpmin:
minnum=dpmin
print(maxnum,minnum)
if sum(nums)==minnum:
return maxnum
return max(maxnum,sum(nums)-minnum)
分情况讨论:最大和序列在中间;最大和序列在两边,即求中间的最小和序列;
然后比较两种情况的序列和取大者
乘积最大子数组
给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
class Solution:
def maxProduct(self, nums: List[int]) -> int:
dp_pre,dpmin_pre= nums[0],nums[0]
resmax = nums[0]
for i in range(1,len(nums)):
# print(dp_pre*nums[i],dpmin_pre*nums[i],nums[i])
nummax = max(dp_pre*nums[i],dpmin_pre*nums[i],nums[i])
nummin = min(dp_pre*nums[i],dpmin_pre*nums[i],nums[i])
dp_pre = nummax
dpmin_pre = nummin
if resmax<dp_pre:
resmax = dp_pre
return resmax
dp[i]表示以nums[i]为结尾的连续子数组的最大积
与最大和序列不同的是,dp[i]可能等于dp[i-1]*nums[i](如2x3)或nums[i](如-3>2x-3),也可能等于前一位最小积dpmin[i-1]乘以nums[i](如-2x-3),所以需要同时维护寻找连续子数组的最小积,比较三者。
乘积为正数的最长子数组长度
给你一个整数数组 nums ,请你求出乘积为正数的最长子数组的长度。
一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。
请你返回乘积为正数的最长子数组长度。
class Solution:
def getMaxLen(self, nums: List[int]) -> int:
#初始化nums[0]位置
#numz 以nums[i]为结尾的最长正积长度
#numf 以nums[i]为结尾的最长负积长度
if nums[0]>0:
numz,numf,resmax = 1,0,1
elif nums[0]==0:
numz,numf,resmax = 0,0,0
else:
numz,numf,resmax = 0,1,0
for i in range(1,len(nums)):
if nums[i]>0: #[i]为正
numz+=1 #以nums[i]为结尾的最长正积长度dp[i]=dp[i-1]+1
if numf!=0: #当前面存在负积时,否则numf还是0
numf+=1 #以nums[i]为结尾的最长负积长度dpf[i]=dpf[i-1]+1
elif nums[i]<0: #[i]为负
if numz!=0: #最长负积长度取决于前面是否存在正积
numf_temp = numz+1 #存在正积,则等于正积长度加一
else:
numf_temp = 1 #不存在正积,则等于一
if numf!=0: #最长正积长度取决于前面是否存在负积
numz_temp = numf+1 #存在负积则等于负积加一
else:
numz_temp = 0 #不存在负积则等于0
numz,numf = numz_temp,numf_temp
else: #[i]=0
numf=0
numz=0
resmax = max(resmax,numz)
# print(numf,numz)
return resmax