[LeetCode]-動的プログラミング-2

序文

LeetCode で質問を解決する際に遭遇した動的プログラミング関連の質問を記録する (パート 2)

115. 異なるサブシーケンス

このアイデアは公式ソリューションに基づいています

今回は従来の考え方と異なり、sの長さをm、tの長さをnとして後ろから前へマッチングを行います。
2 次元 dp 配列を構築します。ここで、 dp[i][j] は、s の i 添字 (i 添字を含む) から開始して先に進む部分文字列の数を表します (つまり、 s[i...m - 1]) 部分列は、t の (添字 j を含む) から始まる部分列と同じです (つまり、t[j...n - 1])

次に、t[j...n - 1] と同じサブシーケンスを見つけるためにs[i] == t[j] の場合、 dp[i][j] はdp[i + 1][と等しくなります。j + 1 ]、つまり、 s[i + 1…m - 1] で見つかった t[j + 1…n - 1] と同じサブシーケンスに基づいて、s[i] を左側に追加した後に t を続けることができます。 [j ...n - 1] は同じです
が、s[i] == t[j] です。t[j...n - 1] と同じ部分列を見つけるために s[i] を選択する必要はありませんs[ i + 1…m - 1] を直接選択して、t[j…n - 1] と同じサブシーケンス、つまり dp[i + 1][j] を検索することもできます。 != t[j]の場合、 t[j…n - 1] と同じになるようにするため、 s[i] は選択できません。 s[i には t[j…n - 1] と同じ部分列のみが見つかります。 + 1…m - 1] (後ろから前に一致するため、後方検索のみ) したがって、この時点ではdp[i][j] = dp[i + 1][j]

すると状態遷移方程式が出てきて、次は状態境界です i == m のときは s の空文字列を意味します このとき t と同じ部分列はあってはならないので dp[ m][0...n] = 0; j == n の場合、t の空の文字列を表します。空の文字列は任意の文字列の部分文字列である必要があるため、dp[0…m][n] = 1

境界も処理され、コードを取得できます。

public int numDistinct(String s, String t) {
    
    
    int m = s.length(), n = t.length();
    if (m < n) {
    
    
        return 0;
    }
    char[] sc = s.toCharArray();
    char[] tc = t.toCharArray();
    int[][] dp = new int[m + 1][n + 1];
    for (int i = 0; i <= m; i++) {
    
    
        dp[i][n] = 1;
    }
    for (int i = m - 1; i >= 0; i--) {
    
    
        for (int j = n - 1; j >= 0; j--) {
    
    
            if (sc[i] == tc[j]) {
    
    
                dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j];
            } else {
    
    
                dp[i][j] = dp[i + 1][j];
            }
        }
    }
    return dp[0][0];
}

上記のコードでは5msかかりますが、事前に文字列を文字配列に変換しておらず、後からcharAt()メソッドで文字を取得すると10msかかりますので、頻繁に特定の文字を取得する必要がある場合は、文字列を操作するには文字配列に変換する必要があります

2 次元 dp を使用する場合は、1 次元 dp を考慮する必要があります。状態遷移方程式を観察してください。 dp[i][j] の値は、次の行の 2 つの要素、添字 j と添字 j + 1 から得られます。変換できます。これは 1 次元の dp ですが、dp 配列の走査方向は右から左ではなく左から右である必要があります。そうしないと、左の値が右の値から派生するため、値が上書きされます。 . 右から左にたどると右側の値が先に変更されると左側で得られる値が異なります。

public int numDistinct(String s, String t) {
    
    
        int m = s.length(), n = t.length();
        if (m < n) {
    
    
            return 0;
        }
        char[] sc = s.toCharArray();
        char[] tc = t.toCharArray();
        int[] dp = new int[n + 1];
        dp[n] = 1;
        for (int i = m - 1; i >= 0; i--) {
    
    
            char sChar = sc[i];
            //从左到右遍历
            for (int j = 0; j < n; j++) {
    
    
                if (sChar == tc[j]) {
    
    
                    dp[j] = dp[j + 1] + dp[j];
                }
            }
        }
        return dp[0];
    }

718. 最長反復部分配列

最初の方法は総当りです。2 層の for ループは、nums1 配列の数値と nums2 配列の数値を列挙します。2 つの数値が等しい場合は、前の数値を一緒に走査して、それらが等しいかどうかを確認します。 2 つの等しくない数を通過する時間計算量が O(n 3 )になるまでの長さを記録します。

2 番目の方法は動的プログラミングです。動的プログラミングは、暴力的な方法と暗記とみなすことができます。2 次元配列 dp、dp[i][j] は、nums1 配列内の部分配列の最後の番号を表します。配列の nums2[j] は部分配列の最後の番号であり、得られた部分配列の繰り返し長は状態方程式になります。

dp [ i ] [ j ] = dp [ i − 1 ] [ j − 1 ] + 1 、 nums 1 [ i ] = = nums 2 [ j ] dp[i][j] = dp[i - 1][j - 1] + 1,nums1[i] == nums2[j]d p [ i ] [ j ]=d p [ i1 ] [ j1 ]+1 数値1 [ i ] _ _ _==nums 2 [ j ] dp [ i ] [ j ] = 0 、 nums 1 [ i ] ≠ nums 2 [ j ] dp[i][j] = 0、nums1[i] ≠ nums2 [ j ]
d p [ i ] [ j ]=0 数値1 [ i ] _ _ _=数値2 [ j ] _ _ _

次元削減はここで直接行われます。

public int findLength(int[] nums1, int[] nums2) {
    
    
    int len1 = nums1.length;
    int len2 = nums2.length;
    int[] dp = new int[len2 + 1];
    int res = 0;
    for(int i = 1;i <= len1;i++){
    
    
        for(int j = len2;j > 0;j--){
    
    
            dp[j] = nums1[i - 1] == nums2[j - 1] ? 1 + dp[j - 1] : 0;
            res = Math.max(res,dp[j]);
        }
    }
    return res;
}

139. 単語の分割

ブール配列 dp、dp[i] は、文字列 s の最初の i 文字で構成される部分文字列を、辞書に表示される単語を使用して結合できるかどうかを示します。

次に、この部分文字列が [0,k)、[k,i)、[0,k)] の 2 つの部分に分割できる場合、この部分文字列は辞書に載っている単語を使用して結合できます。 dp[k] == true、および [k,i) 部分文字列が辞書に含まれる単語である場合、[0,i) 部分文字列全体を辞書内の単語と結合できます。

列挙長 i は 1 から len までであり、dp[i] の値は順番に計算されます。各長さについて、0 から i - 1 までの長さ j を列挙します。 dp[j] が true を満たす j が見つかり、残りの i - j 文字で構成される文字列が辞書内の単語である限り、それはdp[i] が true、複雑度が O(n 2 )であることを意味します

public boolean wordBreak(String s, List<String> wordDict) {
    
    
    Set<String> wordDictSet = new HashSet(wordDict);
    int len = s.length();
    boolean[] dp = new boolean[len + 1];
    dp[0] = true; //前 0 个字符,表示空串
    for(int i = 1;i <= len;i++){
    
    
        for(int j = 0;j <= i - 1;j++){
    
    
        	//枚举前 j 个字符构成的子串以及对应的剩余的 i - j 个字符构成的单词
        	//只要有一种情况满足就可以判定 dp[i] 为 true
            if(dp[j] && wordDictSet.contains(s.substring(j,i))){
    
    
                dp[i] = true;
                break;
            }
        }
    }
    return dp[len];
}

140. 単語分割 II

まず問題 139 の方法で単語に分割できる部分を見つけ、次にバックトラッキングを使用して文を取得できるすべての組み合わせを見つけます。

public class Solution {
    
    
    public List<String> wordBreak(String s, List<String> wordDict) {
    
    
        //先找出可以拆分的子串,方法跟139题一样
        Set<String> wordSet = new HashSet<>(wordDict);
        int len = s.length();
        boolean[] dp = new boolean[len + 1];
        dp[0] = true;
        for (int right = 1; right <= len; right++) {
    
    
            for (int left = right - 1; left >= 0; left--) {
    
    
                if (wordSet.contains(s.substring(left, right)) && dp[left]) {
    
    
                    dp[right] = true;
                    break;
                }
            }
        }
        List<String> res = new ArrayList<>();
        //如果整个字符串不能拆分就不用继续算了
        if (dp[len]) {
    
    
            LinkedList<String> cur = new LinkedList<>();
            backTrack(s, len, wordSet, dp, cur, res);
            return res;
        }
        return res;
    }
    //回溯方法,将s中[0,len)拆分出单词,并将不同的拆分方案加到res中
    private void backTrack(String s, int len, Set<String> wordSet, boolean[] dp, Deque<String> cur, List<String> res) {
    
    
        if (len == 0) {
    
    
            //String.join() 方法用于在数组/集合/多个字符串中每个字符串之间添加一个符号,然后将最后得到的整个字符串返回
            res.add(String.join(" ",cur)); 
            return;
        }
        //从后往前找是否有能拆分出来的单词
        for (int i = len - 1; i >= 0; i--) {
    
    
            String suffix = s.substring(i, len);
            if (wordSet.contains(suffix) && dp[i]) {
    
    
                //找到能拆分出来的单词,就加到 cur 最前面的位置。然后对前面的长度i的字串继续找
                cur.addFirst(suffix);
                backTrack(s, i, wordSet, dp, cur, res);
                cur.removeFirst();//回退
            }
        }
    }
}

546.ボックスを削除する

解に基づく要約: dp(l,r,k)
は、区間 [l, r] 内の要素と、box[r に等しい区間の右側の k 要素の削除で構成されるこのシーケンスの最大積分を表します。 ]
. では、このシーケンスに対して何をすべきでしょうか? 戦略には次のようなものがあります。

  1. box[r] と、box[r] に等しい右側の k 個の要素をまとめて削除し、残りの [l,r - 1] をまとめて削除すると、得られる解は dp[l][r - 1] になります。 [0 ] + (k + 1)²
  2. [l,r] を 2 つの部分に分割し、それらを別々に削除することを検討してください。 i ∈ [l,r) をとり、[l,r] を [l,i] と ( i,r] に分割ます
class Solution {
    
    
    int[][][] dp;
    public int removeBoxes(int[] boxes) {
    
    
        int len = boxes.length;
        dp = new int[len][len][len];
        return calculatePoints(boxes,0,len - 1,0);
    }
    public int calculatePoints(int[] boxes,int l,int r,int k) {
    
    
        if (l > r) {
    
    
            return 0;
        }
        if (dp[l][r][k] == 0) {
    
     //不为 0 说明已经计算过了,避免重复计算
            dp[l][r][k] = calculatePoints(boxes,l,r - 1,0) + (k + 1) * (k + 1); //将boxes[r]跟区间右边那 k 个元素一起消除
            for (int i = l;i < r;i++) {
    
    
                if (boxes[i] == boxes[r]) {
    
    
                    //boxes[i]==boxes[r],就可以先移除[i + 1,r - 1],让boxes[i]跟boxes[r]跟boxes[r]右边那k个元素连续,
                    //这样boxes[i]右边就有k + 1个相同的连续元素,就可以进行dp[l,i,k + 1]的移除了
                    dp[l][r][k] = Math.max(dp[l][r][k],calculatePoints(boxes,i + 1,r - 1,0)  + calculatePoints(boxes,l,i,k + 1));
                }else{
    
    
                    //boxes[i]!=boxes[r],就不需要让boxes[i]跟boxes[r]连续了,
                    //直接移除[l,i],再进行dp[i+1,r,k]的移除
                    dp[l][r][k] = Math.max(dp[l][r][k],calculatePoints(boxes,l,i,0) + calculatePoints(boxes,i + 1,r,k));
                }
            }
        }
        return dp[l][r][k];
    }
}

688. 盤上の騎士の確率

チェス盤上の特定の点を k 回移動した後にチェス盤上に留まる確率の寄与の一部は、チェス盤上の別の点に k - 1 回移動した後にチェス盤上に留まる確率から得られます。これは分解できます。 -問題、動的計画法が考えられる

class Solution {
    
    
    //八种移动方式
    int[][] move = new int[][]{
    
    {
    
    -1,-2},{
    
    -1,2},{
    
    1,-2},{
    
    1,2},{
    
    -2,1},{
    
    -2,-1},{
    
    2,1},{
    
    2,-1}};
    public double knightProbability(int n, int k, int row, int column) {
    
    
        double[][][] f = new double[n][n][k + 1]; //f[i][j][k] 表示在 (i,j) 处移动 k 次后能留在棋盘内的概率
        //边界,在棋盘上任意位置,移动 0 次后都一定留在棋盘内
        for (int i = 0; i < n; i++) {
    
    
            for (int j = 0; j < n; j++) {
    
    
                f[i][j][0] = 1;
            }
        }
        for (int p = 1; p <= k; p++) {
    
      //移动p次时
            for (int i = 0; i < n; i++) {
    
     
                for (int j = 0; j < n; j++) {
    
     //在(i,j)点开始移动
                    for (int[] m : move) {
    
    
                        int nx = i + m[0], ny = j + m[1];
                        //尝试某种移动方式并判断是否可行
                        if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
                        //从(i,j)移动到(nx,ny)是可行的,反过来在(nx,ny)跳第p次到达(i,j)是可行的
                        //而要到达(i.j)有1/8的可能是从(nx,ny)到达
                        //所以最终(nx,ny)对能否在第p次到达(i,j)的概率的贡献为 f[nx][ny][p - 1] * 1/8
                        f[i][j][p] += f[nx][ny][p - 1] / 8; 
                    }
                }
            }
        }
        //状态推导完毕,最后就是返回从(row,column)移动k次后能留在棋盘上的概率
        return f[row][column][k];
    }
}

おすすめ

転載: blog.csdn.net/Pacifica_/article/details/123685970