目录
300.最长上升子序列
376.摆动序列
5.最长回文子串
516.最长回文子序列
最长公共子序列 && 最长公共子串
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18] 输出: 4 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
- 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
- 你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
LIS(i)表示以第 i 个数字为结尾的最长上升子序列的长度
LIS(i)表示在[0...i] 的范围内,选择数组nums[ i ] 可以获得的最长上升子序列的长度
LIS(i) = max(1+ LIS(j)if nums[i] > nums[j])
解法:如示例
(1)初始化所有元素的最长上升子序列为 1
(2)第一个元素10,不变;第二个元素9,9<10,构不成上升序列,依旧为1;第三个元素2,2比10,9都小,也构不成上升子序列,不变
(3)第四个元素为5,5比10,9小,比2大,结果为1+ (以2结尾的最长上升子序列长度为1) = 2
(4)第五个元素为3,3比10,9小,比2大,比5小,结果为1+ (以2结尾的最长上升子序列长度为1) = 2
(5)第六个元素为7,3比10,9小,比2,5,3大,结果为1+ (以2,5,3结尾的最长上升子序列长度为1),取大值为3
(6)第七个元素为101,101比10,9,2,5,3,7大,结果为1+ (以10,9,2,5,3,7结尾的最长上升子序列长度为1),取大值为4;
第八个元素为18,18比10,9,2,5,3,7大,结果为1+ (以10,9,2,5,3,7结尾的最长上升子序列长度为1),取大值为4;
(7)将取得的dp再遍历一次,取其中最大的值即可
class Solution {
/**
* @param Integer[] $nums
* @return Integer
*/
function lengthOfLIS($nums) {
$len = count($nums); //初始化长度
if($len == 0) return 0;
$dp = []; //dp[i] 表示以nums[i]为结尾的最长上升子序列长度
for($i = 0;$i < $len; ++$i){
$dp[$i] = 1; //初始化当前的dp为 1
for($j = 0;$j<$i;++$j){ //再从头遍历一次,到 i 下标
if($nums[$j] < $nums[$i]){ //如果找到比num[i]下的,即有一个上升子序列
$dp[$i] = max($dp[$i], 1+$dp[$j]); //取大值
}
}
}
//取得的dp遍历一遍取最大值
$res = 1;
foreach($dp as $v){
$res = max($res,$v);
}
return $res;
}
}
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5]
是一个摆动序列,因为差值 (6,-3,5,-7,3)
是正负交替出现的。相反, [1,4,7,2,5]
和 [1,7,4,5,5]
不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
示例 1:
输入: [1,7,4,9,2,5] 输出: 6 解释: 整个序列均为摆动序列。
示例 2:
输入: [1,17,5,10,13,15,10,5,16,8] 输出: 7 解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。
示例 3:
输入: [1,2,3,4,5,6,7,8,9] 输出: 2
进阶:
你能否用 O(n) 时间复杂度完成此题?
分析:获得最长的摆动序列,可以获得每一点对应的最长摆动序列,该题需要判断两个状态,新的元素点是要对应摆动序列的上升点还是下降点。因此可以用两个动态规划数组up[i]和down[i]来标记:记录到第i个元素为止以上升沿和下降沿结束的最长“摆动”序列长度。
解法:
(1)初始化所有点的摆动序列为1 ,up[i] = down[i] = 1
(2)遍历数组,如果nums[i]>nums[i-1],表明第i-1到第i个元素是上升的,因此up[i]只需在down[i-1]的基础上加1即可,而down[i]保持down[i-1]不变
(3)如果nums[i]
(4)如果nums[i]==nums[i-1],不考虑进摆动序列中,则up[i]保持up[i-1],down[i]保持down[i-1]
(5)比较最终以上升沿和下降沿结束的最长“摆动”序列长度即可获取最终结果
优化:
每次up和down都只取前一个元素,如nums[i]>nums[i-1]时,up取前一个down+1,且down本身不变,复制给当前的down[i],再前面的数之后都不会在访问了。所以可以把O(n)的空间复杂度,降为O(1)。只使用up和down两个变量。
如果nums[i]
class Solution {
/**
* @param Integer[] $nums
* @return Integer
*/
function wiggleMaxLength($nums) {
$len = count($nums); //初始化长度
if($len < 2) return $len; //长度0:数组空,返回0;长度1,只有一个序列,返回1
$up = $down = 1; //初始化当前摆动序列为1,记录当前位置的摆动序列长度,当前状态为上,还是下
for($i = 1; $i < $len; ++$i){ //从第二个元素开始遍历
//不考虑 前后元素相等的情况
if($nums[$i] > $nums[$i-1]){ //如果当前元素大于后面的元素,则该点为上升沿,状态是下降沿时 + 1
$up = $down + 1;
}elseif($nums[$i] < $nums[$i-1]){ //如果当前元素小于后面的元素,则该点为下降沿,状态是上升沿沿时 + 1
$down = $up + 1;
}
}
//取摆动序列的最大值,结尾的数不管是上升沿还是下降沿
return $up > $down ? $up:$down;
}
}
给定一个字符串 s
,找到 s
中最长的回文子串。你可以假设 s
的最大长度为 1000。
示例 1:
输入: "babad" 输出: "bab" 注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd" 输出: "bb"
该题有多重解法,本文介绍如下四种
解决速度(由慢到快):暴力解法(10836ms) > 动态规划(3816ms)>中心扩展法(436ms)>马拉车算法(36ms)
(1)暴力解法:从第一个字符开始遍历,截取到最后的一个字符,每次获得的字符串翻转,判断是否是回文串,比较大值
(2)动态规划:用dp[i][j]表示从j到i的字符串是否为回文串。
dp[i][j]回文字符串的子串也是回文,即如果dp[i][j]是回文,则dp[i-1][j+1]也是回文子串。
因此可以反过来判断,要判断dp[i][j]是否是回文,则只需判断s[i]==s[j] && dp[i-1][j+1]也是回文。
有特殊情况如有“bb”没有dp[i-1][j+1],因此对于该情况当s[i]==s[j]时,只要 i-j ==1 即可判断为true
(3)中心扩展法:把给定的字符串的每一个字母当做中心,向两边扩展,这样来找最长的子回文串。算法复杂度为O(N^2)。
遍历一遍字符串,求出每个字符串对应的中心扩散回文串最大长度
(4)马拉车算法:最长回文子串——马拉车算法详解 参考其他博文得出答案
/**************** 暴力解法 ****************/
class Solution {
/**
* @param String $s
* @return String
*/
function longestPalindrome($s) {
$len = strlen($s);
if($len < 2) return $s; //初始化判断
$max = $s[0];
for($i=0;$i<$len;$i++){ //从每一个字符开始,截取到最后一个字符
for($j=$i+1;$j<$len;++$j){
$str = substr($s, $i,$j-$i+1); //正向字符串
$restr = strrev($str); //反向字符串
if($str == $restr && strlen($str) > strlen($max)){
$max = $str; //比较是否是回文串,且比当前最大的子串长
}
}
}
return $max;
}
}
/**************** 动态规划 ****************/
class Solution {
/**
* @param String $s
* @return String
*/
function longestPalindrome($s) {
$len = strlen($s);
if($len < 2) return $s; //初始化判断
$dp = []; //初始化动态规划dp数组,dp[i][j]表示从j到i的字符串是否为回文串
$right = $left = 0; //初始化最长的最优节点
for($i=0;$i<$len;++$i){
$dp[$i][$i] = true; //只有一个元素的时候肯定为true
for($j=$i-1;$j>=0;--$j){ //遍历到第i个元素,再倒退判断是否为回文串
//头i尾j两个元素相等,且dp[i-1][j+1]是回文串,即dp[i][j]也是回文串
//特殊情况,“bb”,此时dp[i-1][j+1]=dp[j][i]此时数组不成立,不存在截取的反向字符串
$dp[$i][$j] = $s[$i] == $s[$j] && ($i-$j==1 || $dp[$i-1][$j+1]);
if($dp[$i][$j] && ($i-$j)>($right-$left)){
$right = $i; //截取的字符串的长度大于之前求得的左右长度,则取的左右下标点
$left = $j;
}
}
}
return substr($s,$left,$right-$left+1); //截取字符串
}
}
/**************** 中心扩展法 ****************/
class Solution {
private $s,$len;
/**
* @param String $s
* @return String
*/
function longestPalindrome($s) {
$len = strlen($s);
if($len < 2) return $s; //初始化判断
$this->len = $len; //使其成为成员变量
$this->s = $s;
$left = $right = 0; //定义左右边界
for($i=0;$i<$len;++$i){
$lenji = $this->centerExpand($i,$i); //奇数中心扩散,判断该点的回文长度
$lenou = $this->centerExpand($i,$i+1); //偶数中心扩散
$maxLen = max($lenji,$lenou); //取最大
if($maxLen > $right-$left+1){
$right = $i + floor($maxLen/2); //取新的左右值
$left = $i - floor(($maxLen-1)/2); //其本身也包含在内,因此要($maxLen-1)
}
}
return substr($s,$left,$right-$left+1); //截取字符串
}
private function centerExpand($left,$right){
while($left>=0 && $right<$this->len && $this->s[$left] == $this->s[$right]){
$left--;$right++;
}
//当不满足条件是,左右都再进了一位,此时不是常规的$right-$left+1,而是要-1
return $right-$left-1;
}
}
/**************** 马拉车算法 ****************/
class Solution {
/**
* @param String $s
* @return String
*/
function longestPalindrome($s) {
$len = strlen($s);
if($len < 2) return $s; //初始化判断
$str = '^#'.implode('#', str_split($s)).'#$'; //分割字符串,使奇偶性统一
$len = strlen($str); //计算改好的字符串长度
$r = array_fill(0, $len, 0); //初始化半径数组
$center = $maxRight = 0; //初始化偏移量:中心点和回文串最大右点
$maxStr = ''; //结果,最长的回文串
for($i=1;$i<$len;++$i){
if($i<$maxRight){
$r[$i] = min($maxRight-$i,$r[2*$center-$i]); //计算当前回文路径的长度
}
while ($str[$i-$r[$i]-1] == $str[$i+$r[$i]+1]) { //扩展回文子串南京
$r[$i] ++;
}
if($i+$r[$i] > $maxRight){ //如果超出最右的点,则更新中心点和右节点
$maxRight = $i+$r[$i];
$center = $i;
}
if(1+2*$r[$i] > strlen($maxStr)){ //计算当前回文子串是否大于记录的结果
$maxStr = substr($str,$i-$r[$i],2*$r[$i]+1);
}
}
return str_replace('#', '', $maxStr);
}
}
给定一个字符串s
,找到其中最长的回文子序列。可以假设s
的最大长度为1000
。
示例 1:
输入:"bbbab"输出:4
一个可能的最长回文子序列为 "bbbb"。
示例 2:
输入:"cbbd"输出:2
一个可能的最长回文子序列为 "bb"。
分析:遍历每个子串,构造dp数组,dp[i][j]表示从i到j的最长回文子序列长度。
状态转移方程:当某子串s(i,j)中s[i]与s[j]相等时,即长到一个回文子序列,因此该子串的最长回文子序列长度等于去掉头尾两字符后新子串的最长回文子序列长度+2。方程:dp[i][j] = dp[i + 1][j - 1] + 2
当s[i]与s[j]不相等时,则dp[i][j] = max(de[i + 1][j],dp[i][j - 1]),取任一端较长的回文子序列作为新的长度
class Solution {
/**
* @param String $s
* @return Integer
*/
function longestPalindromeSubseq($s) {
$len = strlen($s); //初始化长度判断
if($len<2) return $len;
$dp = []; //初始化动态规划数组
for($i=0;$i<$len;$i++){
$dp[$i][$i] = 1; //一个字符肯定是字符串
for($j=$i-1;$j>=0;$j--){
if($s[$i] == $s[$j]){
$dp[$i][$j] = $dp[$i-1][$j+1]+2; //找到一个回文子序列,为前一个坐标 + 2(两个字符)
}else{
$dp[$i][$j] = max($dp[$i-1][$j],$dp[$i][$j+1]); //取两端较大值
}
}
}
return $dp[$len-1][0];
}
}
LCS:Longest Common Sequence (LCS)
由两个维度进行动态规划
LCS(m,n):S1[0...m] 和 S2[0...n]的最长公共子序列的长度
两种情况:
(1)S1[m] == S2[n] :LCS(m,n) = 1+LCS(m-1,n-1)是公共子序列的一部分,计入公共子序列+1
(2)S1[m] ! = S2[n] :LCS(m,n) = max(LCS(m-1,n),LCS(m,n-1)) 两个字符串往前缩一个,取较大值
解法:如下图ABCD,AEBD
(1)ABCD 和 AEBD,首先找到了一个D,最长子序列+1,下一步只需要考察ABC和AEB
(2)C和B不等,则可以考察ABC往前缩一缩,AB和AEB ; AEB往前缩一缩,ABC和AE比较。这两个比较的最长公共子序列
(3)以左边为例,AB和AEB 找到B相等,最长子序列+1,下一步考虑A和AE
(4)A和E不相等,则可以考察A往前缩一缩,‘’和AEB,空字符串不比较,直接返回 ; AE往前缩一缩,A和A比较。这两个比较的最长公共子序列
(5)A和A相等,最长子序列+1,下一步考虑‘’和‘’,两个空字符串,直接返回。以此类推,求出最长公共子序列
class Solution {
function LCS($s1,$s2) {
$len1 = strlen($s1); //计算出两个字符串的长度
$len2 = strlen($s2);
$dp = [];
for($i=0;$i<$len1;$i++){ //开始遍历两个字符串,求出dp[i][j] 字符串s1到i和字符串s2到j的最长公共子序列
for($j=0;$j<$len2;$j++){
if($s1[$i] == $s2[$j]){
$dp[$i][$j] = $dp[$i-1][$j-1]+1; //子序列加1
}else{
$dp[$i][$j] = max($dp[$i-1][$j],$dp[$i][$j-1]); //求出一侧的最大子序列即可
}
}
}
return $dp[$len1-1][$len2-1];
}
}
最长公共子串是最长公共子序列的特殊形式,它必须要有连续性,此时的dp[i][j]不是记录字符串s1到i和字符串s2到j的最长公共子序列,而是记录字符串s1到i和字符串s2到j的最长公共子串
LCS(m,n):S1[0...m] 和 S2[0...n]的最长公共子串的长度
两种情况:
(1)S1[m] == S2[n] :LCS(m,n) = 1+LCS(m-1,n-1)是公共子串的一部分,计入公共子序列+1
(2)S1[m] ! = S2[n] :LCS(m,n) = 0 考虑连续性,该点肯定不是公共子串
class Solution {
function LCS($s1,$s2) {
$len1 = strlen($s1); //计算出两个字符串的长度
$len2 = strlen($s2);
$dp = [];
$res = 0;
for($i=0;$i<$len1;$i++){ //开始遍历两个字符串,求出dp[i][j] 字符串s1到i和字符串s2到j的最长公共子串长度
for($j=0;$j<$len2;$j++){
if($s1[$i] == $s2[$j]){
$dp[$i][$j] = $dp[$i-1][$j-1]+1; //最长公共子串长度加1
$res = max($dp[$i][$j],$res); //记录最长的子串
}else{
$dp[$i][$j] = 0; //如果不相等,该位置节点为0,肯定不是公共子串
}
}
}
return $res;
}
}