序文
LeetCode で質問を解決する際に遭遇した動的プログラミング関連の質問を記録する (パート 3)
322. 両替交換
dp[i] は、i ドルを稼ぐために必要なコインの最小数を表します。各コインの額面はcoins[j]であり、dp[i]はi - Coins[j]に1を加えたものを作るのに必要なコインの最小数です。追加された「1」は、額面がコインであることを意味します[jのコイン]
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
//后续需要Math.min来选较小数,所以dp数组每个元素先初始化为一个较大值。
//注意到下面有“dp[i - coins[j]] + 1”,
//所以为了防止溢出,不能直接使用Integer.MAX_VALUE,需要小一点,这里只减一即可
Arrays.fill(dp, Integer.MAX_VALUE - 1);
dp[0] = 0; //边界,凑0块钱需要0个硬币
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
//coins[j]小于等于i才能尝试用coins[j]来凑出i块钱
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
//如果dp[amount]没有改变值说明无法凑出amount,返回-1
return dp[amount] == Integer.MAX_VALUE - 1 ? -1 : dp[amount];
}
518.チェンジエクスチェンジⅡ
dp[i]は金額iに対する交換方法の数を表します。金額 i について、額面がそれ以下のすべてのコインを列挙すると、このコインを i からこのコインの残額を差し引いたものと交換できるため、dp[i] += dp[i - コイン] となります。この考え方に基づいて、次のコードを書くことができます
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1;
for(int i = 1;i <= amount;i++){
for(int j = 0;j < coins.length;j++){
if(i - coins[j] >= 0) dp[i] += dp[i - coins[j]];
}
}
return dp[amount];
}
このコードは間違っています。例: 金額 = 5、コイン = [1, 2, 5]、金額 1 には 1 つの交換方法 1 があり、金額 2 には 2 つの交換方法 1 + 1, 2 があります。その後、金額 3 が計算されるときは、上記のアルゴリズムに従います。 1 + 1 + 1、1 + 2、2 + 1 の 3 つの引き換え方法が得られます。交換方法が繰り返されていることがわかります。つまり、一部のコインが異なる交換方法を使用して複数回使用されていることがわかります。
では、コインを一度だけ使用できるようにするにはどうすればよいでしょうか? for ループの 2 つのレベルの順序を入れ替えて、最初にコイン、次に金額を反復処理するだけです。
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1;
for(int coin : coins){
for(int i = coin;i <= amount;i++){
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
97. インターリーブされた文字列
公式ソリューションを参照してください
状態定義: dp[i][j] は、s1 の最初の i 文字と s2 の最初の j 文字 (添え字は、元の文字列の文字のインデックスではなく文字数を示すことに注意してください) が、 s3 の最初の i 文字 i + j 文字
状態転送: s1 の最初の i - 1 文字と s2 の最初の j 文字が s3 の最初の i + j - 1 文字を形成できる場合、s1 の i - 1 (添え字に従って 0 から始まる) 文字がこれは、s3 の i + j - 1 番目の文字と同じです。つまり、s1 の最初の i 文字と s2 の最初の j 文字が、s3 の最初の i + j 文字、つまり dp[i][ を形成できることを意味します。 j] | = dp[i - 1][j] && c1[i - 1] == c3[i + j - 1]
同様に、 dp[i][j] |= dp[i][j - 1] && c2[j - 1] == c3[i + j - 1]
境界: dp[0][0] = true、2 つの空の文字列をインターリーブして空の文字列を形成できることを示します
public boolean isInterleave(String s1, String s2, String s3) {
char[] c1 = s1.toCharArray();
char[] c2 = s2.toCharArray();
char[] c3 = s3.toCharArray();
int l1 = c1.length;
int l2 = c2.length;
//s1跟s2的长度和不等于s3的长度的话肯定就不能交错形成s3
if(l1 + l2 != c3.length) return false;
/*
boolean[][] dp = new boolean[l1 + 1][l2 + 1];
dp[0][0] = true;
for(int i = 0;i <= l1;i++){
for(int j = 0;j <= l2;j++){
//根据状态转移方程,可以使用状态压缩
if(i > 0){
//这里的dp[i][j]一定是false,所以没必要使用|=
dp[i][j] = dp[i - 1][j] && c1[i - 1] == c3[i + j - 1];
}
//原本dp[i][j]都是false(除了(0,0)),在判断判断j>0之前先判断了i>0,所以在
//此处dp[i][j]可能已经被修改了,所以要使用|=
if(j > 0){
dp[i][j] |= dp[i][j - 1] && c2[j - 1] == c3[i + j - 1];
}
}
}
return dp[l1][l2];
*/
boolean[] dp = new boolean[l2 + 1];
dp[0] = true;
for(int i = 0;i <= l1;i++){
for(int j = 0;j <= l2;j++){
if(i > 0){
dp[j] = dp[j] && c1[i - 1] == c3[i + j - 1];
}
if(j > 0){
dp[j] |= dp[j - 1] && c2[j - 1] == c3[i + j - 1];
}
}
}
return dp[l2];
}
221. 最大の広場
ステータス: dp[i][j]は、matrix[i][j]を右下隅とする1だけの正方形の辺の長さの最大値を表す
状態遷移:行列[i][j]が0の場合、行列[i][j]を右下隅として1だけを含む正方形を持つことは不可能であり、dp[i][j]は0です。行列[i] ][j] が 1 の場合、状態遷移方程式は dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j]) となるはずです。 - 1])、dp [i - 1][j - 1]) + 1、つまり、左のグリッドは右下隅、上のグリッドは右下隅、左上のグリッドは右下です。コーナー. 1 だけを含む正方形の最小辺の長さ 値のうちの最小値。なぜこれを選んだのですか?
現在 dp[3][3] が推定されていると仮定すると、左側のグリッド行列 [3][2] が 1 だけを含む正方形の右下隅として使用される場合、最大辺の長さは 2 になります。 dp[i] [j] が 3 であることを取得したい場合は、matrix[1][1]、matrix[1][2]、matrix[1][3]、matrix[2] の値が必要です[3] はすべて 1、つまり、dp[2][2]、dp[2][3] は 2 以上でなければなりません。同様に、上のグリッドと左上のグリッドが右下隅である状況を分析できます。そして最終的に、右下隅が行列 [i][j] である 1 だけを含む正方形の最大辺の長さは dp[i - 1][j], dp[i][j - 1] でなければならないことがわかります。 , dp[i - 1][j - 1] 3 つの値に 1 を加えた値の最小値
境界: 右下隅が 1 行 1 列の正方形の最大辺の長さは 1 のみであるため、 dp[i][j] = 1 (i == 0 || j == 0)
public int maximalSquare(char[][] matrix) {
int maxSide = 0;
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return maxSide;
}
int rows = matrix.length, columns = matrix[0].length;
int[][] dp = new int[rows][columns];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (matrix[i][j] == '1') {
if (i == 0 || j == 0) {
//边界上的点的最大边长只能为1
dp[i][j] = 1;
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
maxSide = Math.max(maxSide, dp[i][j]);
}
}
}
return maxSide * maxSide;
}
983. 最低運賃
問題の解決策に基づく簡単な要約:
まず、同じ日に n 日間のチケットを購入した場合、お金の無駄を避けるために、n 日後にチケットを購入する必要があります。
したがって、状態 dp[i] を使用して i 日目を導出するために必要な最小コストを表す場合、i 日目については 3 つの決定があります: 1 日パスを購入するか 7 日パスを購入する/30 日パスを購入する場合、dp [i] はこれら 3 つの決定によって得られる最低運賃値と等しくなければなりません。1
日パスを購入する場合、dp[i] はコスト[0] に最低コストを加えたものと等しくなります。 1 日後 dp[i + 1]、つまり dp [ i + 1 ] + コスト [ 0 ] dp[i + 1] + コスト [0]d p [ i+1 ]+cos t s [ 0 ]
同様に、決定 2 が使用される場合、dp [ i ] = dp [ i + 7 ] +cost [ 1 ] dp[i] = dp[i + 7] +cost[1]d p [ i ]=d p [ i+7 ]+cos t [ 1 ]
決定 3 を使用する場合、dp [ i ] = dp [ i + 30 ] + コスト [ 2 ] dp[i] = dp[i + 30] + コスト [2]d p [ i ]=d p [ i+30 ]+Cost [ 2 ]なので、
後ろから前に導出する必要があります
実際の導出プロセスでは、i は最大日数 (日単位) から始まり、最小日数まで減少します。日数単位ではない数値に遭遇した場合、dp[i] は翌日の dp のコストに直接等しくなります。 [i + 1]
public int mincostTickets(int[] days, int[] costs) {
int len = days.length, maxDay = days[len - 1], minDay = days[0];
//在后续推导时需要对 cur+30,可以先判断cur的范围,也可以像下面这样直接把数组调大
int[] dp = new int[maxDay + 31];
int index = len - 1;
for (int cur = maxDay;cur >= minDay;cur--) {
if (cur == days[index]) {
dp[cur] = Math.min(dp[cur + 1] + costs[0], dp[cur + 7] + costs[1]);
dp[cur] = Math.min(dp[cur], dp[cur + 30] + costs[2]);
index--;
} else {
dp[cur] = dp[cur + 1];
}
}
return dp[minDay];
}
1143. 最長共通部分列
このような配列または文字列型の動的計画問題の場合、配列または文字列が 1 つしかない場合は、1 次元配列 dp を使用できます (dp[i] は区間 [0,i] を表します)。2 つある場合は、dp[i] が区間 [0,i] を表します。配列または文字列の場合は、2 次元配列を使用します。 dp[i][j] は、配列 1/文字列 1 の間隔 [0,i] と配列 2/文字列 2 の間隔 [0,j] を表します。次元削減を考慮するかどうか
この問題では、状態 dp[i][j] は、text1 の最初の i 文字と text2 の最初の j 文字の間の最長共通部分列の長さを表し、状態遷移方程式は dp[i][j] = となります。
dp [i - 1][j - 1] + 1, text1[i - 1] == text2[j - 1] dp [
i][j] = max(dp[i][j - 1],dp[ i - 1][j])、その他の状況
最終的な答えは dp[len1][len2] です。
public int longestCommonSubsequence(String text1, String text2) {
char[] cs1 = text1.toCharArray();
char[] cs2 = text2.toCharArray();
int len1 = cs1.length;
int len2 = cs2.length;
int[][] dp = new int[len1 + 1][len2 + 1];
for(int i = 1;i <= len1;i++){
for(int j = 1;j <= len2;j++){
if(cs1[i - 1] == cs2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else{
dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
}
}
}
return dp[len1][len2];
}
64. 最小パス合計
動的プログラミングを使用すると、2 次元 dp 配列の dp[i][j] は、grid[i][j] への最小パスを表し、
点grid[i][j]に行きたい場合は、grid[i - 1][j]から1歩下に進むか、grid[i][j - 1]から1歩右に進むことしかできません。 。したがって、grid[i][j] への最小パス合計は、前と同様に、当然 min(dp[i - 1][j],dp[i][j - 1]) + Grid[i][j] になります。ルートが何であろうと気にしない、これが動的プログラミングの力です
public int minPathSum(int[][] grid) {
int m = grid.length,n = grid[0].length;
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
//对于第一列的点只能从上一个点向下走一步到达
for(int i = 1;i < m;i++){
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
//对于第一行的点只能从左边的点向右走一步到达
for(int i = 1;i < n;i++){
dp[0][i] = dp[0][i - 1] + grid[0][i];
}
//开始状态转移
for(int i = 1;i < m;i++){
for(int j = 1;j < n;j++){
dp[i][j] = Math.min(dp[i - 1][j],dp[i][j - 1]) + grid[i][j];
}
}
//最后返回走到右下角即grid[m - 1][n - 1]的最小路径和
return dp[m - 1][n - 1];
}
次元削減
古いルールである 2 次元 dp は、状態遷移方程式に従って 1 次元 dp に還元できます。
public int minPathSum(int[][] grid) {
int m = grid.length,n = grid[0].length;
int[] dp = new int[n];
dp[0] = grid[0][0];
for(int i = 1;i < n;i++){
dp[i] = dp[i - 1] + grid[0][i];
}
for(int i = 1;i < m;i++){
for(int j = 0;j < n;j++){
if(j == 0) dp[j] = dp[j] + grid[i][j];
else dp[j] = Math.min(dp[j],dp[j - 1]) + grid[i][j];
}
}
return dp[n - 1];
}
152.積最大部分配列
状態 dp[i][0] は、nums[i] を最後の要素とする部分配列から取得できる最大の積を表し、dp[i][1] は取得できる最小の積を表します。常に dp[i][0] >= dp[i][1]境界が最初の要素です。 nums[i ] の場合
、 dp[0][0] = dp[0][1] = nums[0]:
- nums[i] > 0 の場合: dp[i - 1][0] も 0 より大きい場合、dp[i][0] は dp[i - 1][0] * nums[i に等しくなります。 ]; それ以外の場合、dp [i - 1][0] が 0 以下の場合は、dp[i - 1][1] の方が小さくなり、nums[i] に累積できる最大積は nums のみになります。 [私]。dp[i - 1][1] が 0 以下の場合、最小の積は dp[i - 1][1] * nums[i] になります。それ以外の場合は、dp[i - 1][1] の方が大きくなります。 0 よりも大きい、dp[i - 1][0] の方が大きい、最小積は nums[i] 自体である必要があります
- nums[i] == 0 の場合、前の数値が何であっても、最大積と最小積は 0 でなければなりません。
- 最後に、nums[i] < 0 です。最大の積を得るには、負の数を乗算する必要があるため、dp[i - 1][1] が 0 未満の場合、最大の積は当然 dp[i - 1] になります。 ][1] * 数値[i];
public int maxProduct(int[] nums) {
int len = nums.length;
int max = nums[0];
int[][] dp = new int[len][2];
dp[0][0] = nums[0];
dp[0][1] = nums[0];
for(int i = 1;i < len;i++){
if(nums[i] > 0){
dp[i][0] = dp[i - 1][0] > 0 ? dp[i - 1][0] * nums[i] : nums[i];
max = Math.max(max,dp[i][0]);
dp[i][1] = dp[i - 1][1] < 0 ? dp[i - 1][1] * nums[i] : nums[i];
}else if(nums[i] == 0){
max = Math.max(max,0);
dp[i][0] = 0;
dp[i][1] = 0;
}else{
dp[i][0] = dp[i - 1][1] < 0 ? dp[i - 1][1] * nums[i] : nums[i];
max = Math.max(max,dp[i][0]);
dp[i][1] = dp[i - 1][0] > 0 ? dp[i - 1][0] * nums[i] : nums[i];
}
}
return max;
}