バックトラッキングを使用した文字列分割のトリックから前方検索まで

トピック 131. 回文文字列の分割

文字列 s が与えられた場合、各部分文字列が回文になるように s をいくつかの部分文字列に分割してください。の可能なすべての分割スキームを返します。

回文は、前から見ても後ろから見ても同じように読める文字列です。

答え:

class Solution {
    
    
    boolean[][] f;
    List<List<String>> ret = new ArrayList<List<String>>();
    List<String> ans = new ArrayList<String>();
    int n;

    public List<List<String>> partition(String s) {
    
    
        n = s.length();
        f = new boolean[n][n];
        for (int i = 0; i < n; ++i) {
    
    
            Arrays.fill(f[i], true);
        }

        for (int i = n - 1; i >= 0; --i) {
    
    
            for (int j = i + 1; j < n; ++j) {
    
    
                f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
            }
        }

        dfs(s, 0);
        return ret;
    }

    public void dfs(String s, int i) {
    
    
        if (i == n) {
    
    
            ret.add(new ArrayList<String>(ans));
            return;
        }
        for (int j = i; j < n; ++j) {
    
    
            if (f[i][j]) {
    
    
                ans.add(s.substring(i, j + 1)); 
                dfs(s, j + 1); // #A 为什么这里入参是j+1而不是i+1
                ans.remove(ans.size() - 1);
            }
        }
    }
}

作者:力扣官方题解
链接:https://leetcode.cn/problems/palindrome-partitioning/solutions/639633/fen-ge-hui-wen-chuan-by-leetcode-solutio-6jkv/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

質問: dfs(s, j + 1); ここでの入力パラメータが i+1 ではなく j+1 なのはなぜですか?

まず dfs(s, j + 1) を見てください。エントリ パラメータは j+1 のバックトラック プロセスです。

以aab为例,我们dfs的流程是:
添加"a"--->
继续添加"a"--->
继续添加"b"--->
继续dfs发现i==len越界,将答案["a","a","b"]加入到ret里然后返回-->
在res中删除最后添加的"b",并且发现当前层dfs的for循环不能再执行了于是自然返回--->
res继续删除末尾的"a",再次for循环更长的回文串,但发现"ab"不是回文串,而且for循环执行不下去了遂返回--->
res继续删除末尾的"a"(空了),然后for循环(i,j)(0,0)变为(0,1),将对应回文串"aa"添加--->
继续添加"b"-->继续dfs发现i==len越界,将答案["aa", "b"]加入到ret然后返回--->
在res中删除最后添加的"b",发现for不能执行了自然返回--->
res继续删除"aa",再次for循环找更长的回文串,(i,j)(0,1)变成了(0,2),但"子串aab"不是回文串---->
继续for循环,发现j==len越界,for循环结束,最外层调用的dfs函数自然返回。主函数返回结果集ret...

印刷パスは次のとおりです。

(7个节点)
i:0,j:0,s.substring(i,j+1):a
i:1,j:1,s.substring(i,j+1):a
i:2,j:2,s.substring(i,j+1):b
i:1,j:2,s.substring(i,j+1):ab
i:0,j:1,s.substring(i,j+1):aa
i:2,j:2,s.substring(i,j+1):b
i:0,j:2,s.substring(i,j+1):aab

次のバックトラッキング ツリーをもう一度見てください。

root
|_ 'a' (0,0)
   |_ 'a' (1,1)
      |_ 'b' (2,2)
   |_ 'ab' (1,2)
|_ 'aa' (0,1)
   |_ 'b' (2,2)
|_ 'aab' (0,2)

それをグラフとしてプロットします。
ここに画像の説明を挿入

j+1 を使用するツリーに |_ 'aab' (0,2) のサブツリーがないのはなぜですか

j+1 を使用する方法では、コード ロジックはまず文字列をより小さな部分に分割しようとします。つまり、最初に aab を a と ab に分割し、次に a、a、b に分割しようとします。したがって、このプロセスでは、実際には aab の文字列全体が試行されるわけではなく、ノード 'aab' (0,2) は存在しません。これは、回文が見つかると、より大きな文字列を試行することなく、すぐに遡って検索するためです。根本原因はif(f[i][j])の判定

dfs(s, j + 1); エントリパラメータは i+1 のバックトラック履歴です。

同じ文字列「aab」の印刷パスは次のとおりです。

(共经过9个节点)
i:0,j:0,s.substring(i,j+1):a
i:1,j:1,s.substring(i,j+1):a
i:2,j:2,s.substring(i,j+1):b
i:1,j:2,s.substring(i,j+1):ab
i:0,j:1,s.substring(i,j+1):aa
i:1,j:1,s.substring(i,j+1):a
i:2,j:2,s.substring(i,j+1):b
i:1,j:2,s.substring(i,j+1):ab
i:0,j:2,s.substring(i,j+1):aab

対応するツリーは次のとおりです。
ここに画像の説明を挿入

i+1 を使用するツリーに |_ 'aab' (0,2) サブツリーがないのはなぜですか?

aab の場合、「aab」(0,2) は文字列全体であり、これ以上分割できないため、サブツリーはありません。i+1 を使用するツリーに aab ノードがある理由は、dfs の入力パラメーターが毎回 1 ずつ増加するためです。そのため、前のステップで完全な回文文字列 (aa など) が見つかった場合でも、ステップにより、aab がツリーに追加されます。しかし、aab は回文ではないため、対応する有効な分割、つまりサブツリーは存在しません。

違い

dfs(s, j + 1) を dfs(s, i + 1) に変更すると、ツリー内にさらにいくつかのノードが存在することがわかります。j+1 を入力パラメータとして使用した後は、前の部分がそれと何の関係もないことを示すカットのように見え、後者はいくつかの判断を繰り返すように見えます。

dfs(s, j + 1)と の使用の違いはdfs(s, i + 1)主に検索方向と繰り返し判定に反映されます。

  1. dfs(s, j + 1): これは前方探索戦略です。回文が見つかるたびに、回文の背後にあるすべての可能な分割が直ちに検索されます。つまり、次の再帰呼び出しを行うときには、現在位置までのすべての検索がすでに完了しているため、それらの部分について再度考える必要はありません。つまり、この戦略は繰り返しの判断を効果的に回避します。

  2. dfs(s, i + 1): これは、一度に 1 つのポジションだけを進めて、考えられるすべての分割を試みる奥深い検索戦略です。これは、各場所で、以前に検討されたパーティションであっても、考えられるすべてのパーティションを考慮する必要があることを意味します。したがって、この戦略では、いくつかの繰り返し判断と冗長ノードが生成されます。

そのため、 に変更しdfs(s, j + 1)た後dfs(s, i + 1)、ツリー内に余分なノードがいくつか存在します。使用する戦略によりj + 1、この重複を減らし、検索をより効率的にすることができます。

前方検索とは

前方検索は、開始状態から開始して、到達可能なすべての状態を徐々に探索する検索戦略です。この戦略は、グラフ理論、ツリー、動的計画法などの問題でよく使用されます。コードでは、前方検索は dfs(s, j + 1) 行に反映されます。つまり、回文が見つかるたびに、回文以降の考えられるすべての除算が直ちに検索されます。

前方検索は枝刈り戦略ですか?

前方検索自体は枝刈り戦略ではなく、検索戦略です。これは、検索の方向とステップを決定します。つまり、現在の状態から開始して、直接到達できるすべての状態を探索します。ただし、場合によっては、前方検索によって検索スペースが削減され、検索効率が向上する場合があります。これは暗黙的な枝刈りとみなされます。たとえば、この問題では、dfs(s, j+1) は前方検索戦略と同等であり、不必要な繰り返し検索を回避し、検索スペースを削減します。

関連するバックルのトピックの前方検索

LeetCode では、次のような多くの質問で前方検索戦略を使用できます。

トピック 22. ブラケットの生成

トピック 39. 組み合わせ和

class Solution {
    
    
    List<List<Integer>>res;
    List<Integer>tmp;
    public List<List<Integer>> subsets(int[] nums) {
    
    
        int n=nums.length;
        res=new ArrayList<>();
        tmp=new ArrayList<>();
        dfs(nums,0);
        return res;
    }

    void dfs(int[] nums,int i){
    
    
        res.add(new ArrayList<>(tmp));
        for(int j=i;j<nums.length;j++){
    
    
            tmp.add(nums[j]);
            dfs(nums,j+1);
            tmp.remove(tmp.size()-1);
        }
    }
}

項目 46. フルアレンジメント
項目 79. 単語検索
これらの項目は、1 つの状態から開始して、可能なすべての状態を徐々に探索して前方に検索できます。

i+1 を使用して検索する場合、完全検索になるはずですよね? 関連トピックをいくつかリストします。

はい、i+1 を使用する方法は全検索と同等です。実際には必要のないパーティションも含め、考えられるすべてのパーティションが試行されます。LeetCode では、このメソッドは、考えられるすべての解決策を検討する必要がある多くの問題に使用できます。次に例を示します。

トピック 78. サブセット
トピック 90. サブセット II
トピック 216. 合計の結合 III
トピック 377. 合計の結合 IV

おすすめ

転載: blog.csdn.net/yxg520s/article/details/132007313