タイトル- [LeetCode-5から](中程度)
文字列sが与えられた場合、sの最大長が1000を超えないと仮定して、sで最長の回文部分文字列を見つけます。
ヒント:回文文字列:順方向と逆方向の両方の読み取りは同じです
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 同样是符合题意的答案。
示例 2:
输入: "cbbd"
输出: "bb"
文章
この古典的な質問は、LeetCodeで中程度の難易度としてマークされています。質問の意味を理解できる限り、作成するのは難しくありませんが、最適なアルゴリズムを作成するのは簡単ではありません。私自身の問題解決のアイデアによると、3種類をまとめました:
方法1:ブルートフォースの列挙
私の先生はかつて、「2つのforループで解決できないアルゴリズムは世界にありません」と言いました。バブルソートの概念に基づいて、私が考えた最初で最も暴力的な方法は、すべての部分文字列を列挙し、最後に決定することでした。回文の最長の文字列。
public String longestPalindrome(String s){
if (s.isEmpty()) return null;
if (s.length() == 1) return s;
String ret = s.substring(0,1);
for (int i = 0; i < s.length(); i++) {
for (int j = i+1; j < s.length(); j++) {
String temp = s.substring(i,j+1);
String rev = new StringBuilder(temp).reverse().toString();
// 如果是回文串,且长度大于ret,替换
if (temp.equals(rev) && temp.length() > ret.length()) {
ret = temp;
}
}
}
return ret;
}
実行しました...LeetCodeプログラムに「制限時間を超えました」と表示され、暴力的すぎる解決策は許可されていません。!
IDEAで実行すると、メソッドは問題ありません。
方法2:動的計画法
分割統治法、欲張り法、バックトラッキング法、動的計画法の4つの一般的に使用されるアルゴリズム。その中で、動的計画法を最初に習得する必要があります。このアルゴリズムの問題を通して、一緒に学びましょう。
動的計画法プロセス:各決定は現在の状態に依存し、それが次に状態遷移を引き起こします。意思決定シーケンスは変化する状態で生成されるため、この多段階の最適化意思決定プロセスは動的計画法と呼ばれます。
基本的な考え方は分割統治法に似ていますが、解決する問題をいくつかのサブ問題(ステージ)に分解し、サブステージを順番に解決します。前者のサブ問題の解決は有用です。後者のサブ問題の解決のための情報。
この質問では、文字列strに対して、新しい2桁の配列dp [] [] arrをメモとして作成し、サブ問題の結果を記録します。ここで、dp [i] [j]は、i番目の問題かどうかを示します。 jへの文字列は回文です。trueははいを意味し、falseはいいえを意味します。dp [i、j] = trueとすると、dp [i + 1、j-1]=trueである必要があります。このようにして、最長の回文サブストリングを一連のサブ問題に分解できます。これは、動的計画法によって解決できます。これは動的計画法の本質でもありますが、このアプローチのアルゴリズムの複雑さもO(n ^ 2)です。
まず、状態遷移方程式を作成します。
上記の状態遷移方程式は、str [i] = str [j]の場合、次のようになります。
- str [i + 1 ... j-1]が回文である場合、str [i...j]も回文です。
- str [i + 1 ... j-1]が回文ではない場合、str [i...j]も回文ではありません。
public String longestPalindrome(String s) {
if (s.isEmpty()) return s;
int len = s.length();
boolean[][] dp = new boolean[len][len];
int left = 0, right = 0;
// 倒序向前遍历
for (int i = len - 2; i >= 0; i--) {
for (int j = i + 1; j < len; j++) {
// 小于3一定是回文,aa或aba
dp[i][j] = s.charAt(i) == s.charAt(j) && ( j-i < 3 || dp[i+1][j-1]);
// 更新左右边界
if(dp[i][j] && right-left < j-i){
left = i;
right = j;
}
}
}
return s.substring(left,right+1);
}
それを実行しました...そして結果は悪くありません。
注意深い学生は、上記のアルゴリズムが逆の順序で便利であり、正の順序の原理が次のように同じであることに気付くでしょう。
public String longestPalindrome_1(String s){
if (s.isEmpty()) return null;
if (s.length() == 1) return s;
int len = s.length();
boolean[][] arr = new boolean[len][len];
int left = 0, right = 0;
// 正序向后遍历,注意i和j的位置
for (int i = 1; i < len; i++) {
for (int j = i - 1; j >= 0; j--) {
arr[i][j] = s.charAt(i) == s.charAt(j) && ( i-j < 3 || arr[i-1][j+1]);
if (arr[i][j] && (i - j > right - left)) {
right = i;
left = j;
}
}
}
return s.substring(left, right+1);
}
方法3:再帰
オンラインのOOM事故を目撃したので、問題を解決するために再帰を使用することはめったにありません。簡単に思えますが、時間とメモリを消費します。ただし、次の方法では、再帰のアイデアを使用して最初の方法を最適化しました。開始点は、「不要なクエリと判断を減らして操作時間を短縮する」ことです。
基本的な考え方:外側のループは1回トラバースして、ターゲット位置iの対称位置にある要素が等しいかどうかを判断します。等しい場合、whileループは両側の境界を取り、左右の境界値を保存します。等しくない場合は、トラバースを続行して不要な検索値の操作を減らします。
class Solution {
private int stt = 0;
private int end = 0;
public String longestPalindrome_3(String s) {
char[] str = s.toCharArray();
find(str, 0);
return s.substring(stt, end);
}
private void find(char[] str, int i) {
if(i >= str.length) return;
int left = i - 1;
int right = i + 1;
// 处理右侧连续的相同字符
while(right < str.length && str[i] == str[right]) right++;
// 重新定义下次循环的起点
i = right;
// 查找左右边缘
while(left >= 0 && right < str.length && str[left] == str[right]) {
left--;
right++;
}
// 左边恢复到回文字符串端点
left++;
// 刷新记录
if(right - left > end - stt) {
stt = left;
end = right;
}
find(str, i);
}
}
しばらくの間それを実行しました...hohohoho、私はネチズンの100%を打ち負かすとは思っていませんでした。この書き込み方法は最短時間であり、メモリ消費量は最適化されていません。