Leetcode ブラッシング ノート (C++) - 動的プログラミング
質問をブラッシュアップする過程でアイデアを整理し、ここにまとめて共有します。
github アドレス: https://github.com/lvjian0706/Leetcode-solutions
github プロジェクトは新しく作成されたばかりで、C++ と Python をベースに整理されたコードやアイデアが次々とアップロードされます。同時に、基本的な並べ替えアルゴリズムも並べ替えてアップロードされます。
45. ジャンピングゲームⅡ
負でない整数の配列を指定すると、最初は配列の最初の位置にいます。
配列内の各要素は、その位置でジャンプできる最大長を表します。
目標は、最小限のジャンプ数で配列の最後の位置に到達することです。
例:
入力: [2,3,1,1,4]
出力: 2
説明: 最後の位置に到達するための最小ホップ数は 2 です。
インデックス 0 からインデックス 1 にジャンプし、1 ステップジャンプしてから 3 ステップジャンプして、配列の最後の位置に到達します。
class Solution {
public:
/*
最少跳跃次数:动态规划和贪心算法都可以(贪心比较容易但是思路不好想)
动态规划:dp数组存放到达该位置的最少跳跃次数;(超出时间限制)
1. 初始条件:在位置0出发,dp[0]=0;
2. 状态方程:
2.1 首先判断从哪些位置可以到达位置n:判断位置n之前的任意一个位置走对应步数是否可以到达位置n,nums[i]>=n-i;
2.2 查找上述位置中的最小跳跃次数加1即为到位置n的最小跳跃次数:dp[n]=min(dp[*])+1;
*/
int jump(vector<int>& nums) {
vector<int> dp(nums.size());
dp[0]=0;
int minTimes;
for(int i=1; i<nums.size(); i++){
/*
初始化最小跳跃次数为i,即每次都只跳1步
*/
minTimes=i;
for(int j=0; j<i; j++){
if(nums[j]>=i-j && dp[j]<minTimes) minTimes = dp[j];
}
dp[i] = minTimes + 1;
}
return dp[nums.size()-1];
}
};
class Solution {
public:
/*
最少跳跃次数:动态规划和贪心算法都可以(贪心比较容易但是思路不好想)
贪心算法:使用preMax记录目前可以到达的最远位置,使用maxNum记录可以到达的最远位置
1. preMax和maxNum的初始值设为nums[0],表示第一步能走的最大距离,minJump代表最小跳跃次数,初始化为1;
2. 循环遍历数组,当i+nums[i]>maxNum时,更新maxNum为i+nums[i];
3. 当超过preMax的范围时,更新preMax的值为maxNum,最少跳跃次数加1;
*/
int jump(vector<int>& nums) {
/*
当数组中小于两个元素时,不用跳跃即可到达
*/
if(nums.size()<2) return 0;
int preMax=nums[0], maxNum=nums[0];
int minJump=1;
for(int i=0; i<nums.size(); i++){
if(i>preMax){
minJump++;
preMax = maxNum;
}
if(i+nums[i]>maxNum) maxNum=i+nums[i];
}
return minJump;
}
};
55. ジャンプゲーム
負でない整数の配列を指定すると、最初は配列の最初の位置にいます。
配列内の各要素は、その位置でジャンプできる最大長を表します。
最後の位置に到達できるかどうかを判断します。
例 1:
入力: [2,3,1,1,4]
出力: true
説明: 最初に位置 0 から位置 1 まで 1 ステップジャンプし、次に位置 1 から最後の位置まで 3 ステップジャンプできます。
例 2:
入力: [3,2,1,0,4]
出力: false
説明: 何があっても、インデックス 3 の位置に必ず到達します。ただし、その位置の最大ジャンプ長は 0 であるため、最後の位置に到達することはできません。
class Solution {
public:
/*
是否能够到达:动态规划和贪心算法都可以(贪心比较容易但是思路不好想)
动态规划:dp数组存放能否到达该位置,0:不可以1:可以;
1. 初始条件:在位置0出发,所以一定可以到达位置0,dp[0]=1;
2. 状态方程:判断是否可以到达位置n之前的任意一个位置,且在该位置走对应步数可以到达位置n,如果存在则可以到达该位置,dp[n]=dp[i]&&nums[i]>=n-i;
*/
bool canJump(vector<int>& nums) {
vector<int> dp(nums.size());
dp[0]=1;
for(int i=1; i<nums.size(); i++){
for(int j=0; j<i; j++){
if(dp[j]&&nums[j]>=i-j){
dp[i] = 1;
break;
}
}
}
return dp[nums.size()-1];
}
};
class Solution {
public:
/*
是否能够到达:动态规划和贪心算法都可以(贪心比较容易但是思路不好想)
贪心算法:使用maxNum记录当前可以到达的最远位置
1. maxNum<i时,代表当前可以到达的最远位置比i小,因此无法到达i,返回false
2. (i+nums[i])>maxNums时,代表在位置i可以到达比maxNums更远的位置,更新maxNums
*/
bool canJump(vector<int>& nums) {
int maxNums = 0;
for(int i=0; i<nums.size(); i++){
if(maxNums<i) return false;
if((i+nums[i])>maxNums) maxNums = i+nums[i];
}
return true;
}
};
62. 異なる道
ロボットは、mxn グリッドの左上隅に配置されます (下図では開始点に「開始」とマークされています)。
ロボットは一度に 1 ステップ下または右へのみ移動できます。ロボットはグリッドの右下隅 (下の画像で「終了」とマークされている) に到達しようとします。
異なるパスは合計で何通りありますか?
例 1:
入力: m = 3、n = 2
出力: 3
説明:
左上隅から開始して、右下隅に到達するまでに合計 3 つのパスがあります。
- 右 -> 右 -> 下
- 右 -> 下 -> 右
- 下 -> 右 -> 右
例 2:
入力: m = 7、n = 3
出力: 28
class Solution {
public:
/*
动态规划问题:新建dp数组存储到达该点的路径数
1. 初始状态:由于只能只能向下或者向右移动,dp数组第一列和第一行元素均为1:
2. 状态方程:dp[i][j] = dp[i-1][j]+dp[i][j-1];
3. 因为想走到右下角,所以返回答案为dp[m-1][n-1];
*/
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n));
dp[0][0] = 1;
for(int i=0; i<m; i++){
dp[i][0] = 1;
}
for(int i=0; i<n; i++){
dp[0][i] = 1;
}
for(int i=1; i<m; i++){
for(int j=1; j<n; j++){
dp[i][j] = dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
63. 異なる道 II
ロボットは、mxn グリッドの左上隅に配置されます (下図では開始点に「開始」とマークされています)。
ロボットは一度に 1 ステップ下または右へのみ移動できます。ロボットはグリッドの右下隅 (下の画像で「終了」とマークされている) に到達しようとします。
ここで、グリッド内に障害物があると考えてみましょう。では、左上から右下までの異なるパスは何通りあるでしょうか?
グリッド内の障害物と空き位置は、それぞれ 1 と 0 で表されます。
注: m 値も n 値も 100 を超えません。
例 1:
入力:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
出力: 2
説明:
3x3 グリッドの真ん中に障害物があります。
左上隅から右下隅までの 2 つの異なるパスがあります。
- 右 -> 右 -> 下 -> 下
- 下 -> 下 -> 右 -> 右
class Solution {
public:
/*
动态规划问题:新建dp数组存储到达该点的路径数
1. 初始状态:由于只能只能向下或者向右移动,dp数组第一列和第一行元素均为1,
(其中,考虑到如果有障碍,从该点往后的所有点都无法到达,因此,定义flag变量,碰到障碍则flag=1,dp数组从该点开始全都赋0)
2. 状态方程:dp[i][j] = dp[i-1][j]+dp[i][j-1];
(其中,考虑到如果有障碍,到障碍点的路径数应该设置为0,表明该点不通且无法到达)
3. 因为想走到右下角,所以返回答案为dp[m-1][n-1];
*/
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
vector<vector<int>> dp(m, vector<int>(n));
int flag = 0;
for(int i=0; i<m; i++){
if(obstacleGrid[i][0]==1){
flag = 1;
}
if(!flag) dp[i][0] = 1;
else dp[i][0] = 0;
}
flag = 0;
for(int i=0; i<n; i++){
if(obstacleGrid[0][i]==1){
flag = 1;
}
if(!flag) dp[0][i] = 1;
else dp[0][i] = 0;
}
for(int i=1; i<m; i++){
for(int j=1; j<n; j++){
if(obstacleGrid[i][j]==1) dp[i][j] = 0;
else dp[i][j] = dp[i][j-1] + dp[i-1][j];
}
}
return dp[m-1][n-1];
}
};
64. 最小パス合計
非負の整数の mxn グリッドが与えられた場合、パス上の数値の合計を最小にする左上隅から右下隅までのパスを見つけます。
注: 一度に下または右に 1 ステップずつのみ移動してください。
例:
入力:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
出力: 7
説明: パス 1→3→1→1→1 の合計は一番小さい。
class Solution {
public:
/*
动态规划问题:新建dp数组存储路径上的最小数字和
1. 初始状态:由于只能只能向下或者向右移动,dp数组第一列和第一行元素计算方式:
1.1 dp数组第一列的元素计算方式为dp[i][0] = dp[i-1][0] + grid[i][0];
1.2 dp数组第一行的元素计算方式为dp[0][i] = dp[0][i-1] + grid[0][i];
2. 状态方程:dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
3. 因为想走到右下角,所以返回答案为dp[m-1][n-1];
*/
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> dp(m, vector<int>(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] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
return dp[m-1][n-1];
}
};
70. 階段を登る
あなたが階段を登っているとします。建物の屋上に到達するまでに n 歩かかります。
一度に1段か2段ずつ登ることができます。建物の屋上に行くには、何通りの方法がありますか?
注: 指定された n は正の整数です。
例 1:
入力: 2
出力: 2
説明: 建物の屋上に行くには 2 つの方法があります。
- ティア 1 + ティア 1
- 2 ステップ
例 2:
入力: 3
出力: 3
説明: 建物の頂上に登るには 3 つの方法があります。 - ティア 1 + ティア 1 + ティア 1
- ティア 1 + ティア 2
- 2段目+1段目
class Solution {
public:
/*
求可行个数:动态规划
1. 初始条件:爬到0层有1种方法,爬到1层有1种方法(爬1阶),dp[0]=1,dp[1]=1,;
2. 状态方程:爬到第n层可以在n-1层爬1阶,也可以在n-2层爬2阶,因此dp[n]=dp[n-1]+dp[n-2];
*/
int climbStairs(int n) {
if(n==0) return 1;
vector<int> dp(n+1);
dp[0] = 1, dp[1] = 1;
for(int i=2;i<n+1;i++){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};
120. 三角形の最小パス和
三角形が与えられた場合、トップダウンの最小パス合計を見つけます。各ステップは、次の行の隣接するノードにのみ移動できます。
ここでの隣接ノードとは、添字が前のノードの添字と同じか、前のノードの添字 + 1 に等しい 2 つのノードを指します。
たとえば、三角形
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]の
場合、トップダウンの最小パス合計は 11 (つまり、2 + 3 + 5 + 1 = 11)。
class Solution {
public:
/*
动态规划问题,建立dp数组;
由于是三角形,求自顶向下的最小路径和,为了计算方便,适合从下向上计算,最后dp[0][0]即为最终答案;
1. 初始状态:dp数组的每一个值代表从下向上的最小路径和,其中,dp数组最后一行为原始三角形中最下一行元素;
2. 状态方程:dp[i][j] = triangle[i][j] + min(dp[i+1][j], dp[i+1][j+1]);
*/
int minimumTotal(vector<vector<int>>& triangle) {
/*
三角形最后一行索引以及三角形最后一行长度
*/
int lastRaw = triangle.size(), maxLen = triangle[triangle.size()-1].size();
vector<vector<int>> dp(lastRaw, vector<int>(maxLen));
for(int i=0; i<maxLen; i++){
dp[lastRaw-1][i] = triangle[lastRaw-1][i];
}
/*
从倒数第二行开始往上计算,每行的数组元素个数比下边一行少1;
*/
for(int i=lastRaw-2; i>=0; i--){
maxLen--;
for(int j=0; j<maxLen; j++){
dp[i][j] = triangle[i][j] + min(dp[i+1][j], dp[i+1][j+1]);
}
}
return dp[0][0];
}
};
300. 最長の昇順サブシーケンス
整数の順序なし配列を指定して、その中で最も長い昇順のサブシーケンスの長さを見つけます。
例:
入力: [10,9,2,5,3,7,101,18]
出力: 4
説明: 最長の昇順サブシーケンスは [2,3,7,101] で、その長さは 4 です。
説明:
最長の昇順サブシーケンスの組み合わせは複数ある場合があります。対応する長さを出力するだけで済みます。
アルゴリズムの時間計算量は O(n2) である必要があります。
class Solution {
public:
/*
要求最长上升子序列的长度:动态规划,dp数组存放最长上升子序列的长度
1. 初始状态:每个位置上升子序列的长度最少为1(元素本身),因此,dp数组所有元素初始化为1;
2. 状态方程:求解以每个位置元素为最后一个元素的最长上升子序列的长度,首先判断之前元素中小于该元素的位置,再找到dp数组中这些位置的最大值,dp[i]=max(dp[*])+1;
3. 返回值:dp数组的最大值;
*/
int lengthOfLIS(vector<int>& nums) {
/*
数组为空时,返回0;
*/
if(nums.size()==0) return 0;
vector<int> dp(nums.size(), 1);
int maxLen;
for(int i=0; i<nums.size(); i++){
maxLen = dp[i];
for(int j=0; j<i; j++){
if(nums[j]<nums[i] && dp[j]+1>maxLen) maxLen=dp[j]+1;
}
dp[i] = maxLen;
}
int ans = dp[0];
for(int i=1; i<dp.size(); i++){
if(dp[i]>ans) ans = dp[i];
}
return ans;
}
};
322. 小銭の交換
さまざまな金種のコインと合計金額が表示されます。合計金額を構成するために必要なコインの最小数を計算する関数を作成します。どのコインの組み合わせも合計金額を構成しない場合は、-1 を返します。
例 1:
入力: コイン = [1, 2, 5]、金額 = 11
出力: 3
説明: 11 = 5 + 5 + 1
例 2:
入力: コイン = [2]、金額 = 3
出力: -1
説明:
各コインの数は無限であると考えることができます。
class Solution {
public:
/*
最少的组合个数:动态规划问题,dp数组中存放凑成金额i所需要的最少硬币个数
1. 初始状态:初始状态时,默认除了0之外所有面额都不能凑成,都赋值为-1,面额为0时,可使用0枚硬币凑成,因此,dp[0]=0;
2. 状态方程:面额i可以根据dp[i-coins[j]]的最小值加1凑成,因此,dp[i]]=min(dp[i-coins[j]])+1;(其中,需要判断边界值i>=coins[j]以及dp[i-coins[j]]!=-1即面额i是否可以用coins中的硬币凑成)
3. 返回值:dp[amount]
*/
int coinChange(vector<int>& coins, int amount) {
/*
如果没有硬币,返回-1;
*/
if(coins.size()==0) return -1;
vector<int> dp(amount+1, -1);
dp[0] = 0;
for(int i=1; i<amount+1; i++){
for(int j=0; j<coins.size(); j++){
if(i>=coins[j] && dp[i-coins[j]]!=-1){
if(dp[i]==-1) dp[i] = dp[i-coins[j]] + 1;
else dp[i] = min(dp[i], dp[i-coins[j]] + 1);
}
}
}
return dp[amount];
}
};
1143. 最長共通部分列
2 つの文字列 text1 と text2 を指定すると、これら 2 つの文字列の最長の共通部分列の長さを返します。
文字列のサブシーケンスとは、そのような新しい文字列を指します。これは、文字の相対的な順序を変更せずに、元の文字列から一部の文字を削除する (または文字をまったく削除しない) ことによって形成される新しい文字列です。
たとえば、「ace」は「abcde」の部分列ですが、「aec」は「abcde」の部分列ではありません。2 つの文字列の「共通部分列」とは、両方の文字列に共通する部分列です。
2 つの文字列に共通のサブシーケンスがない場合は 0 を返します。
例 1:
入力: text1 = "abcde"、text2 = "ace"
出力: 3
説明: 最長の共通部分列は長さ 3 の "ace" です。
例 2:
入力: text1 = "abc"、text2 = "abc"
出力: 3
説明: 最長の共通部分列は長さ 3 の "abc" です。
例 3:
入力: text1 = "abc"、text2 = "def"
出力: 0
説明: 2 つの文字列に共通の部分シーケンスがない場合は、0 を返します。
ヒント:
1 <= text1.length <= 1000
1 <= text2.length <= 1000
入力文字列には英小文字のみが含まれます。
class Solution {
public:
/*
最长公共子序列的长度:动态规划问题,dp数组存放text1[0:i]与text2[0:j]的最长公共子序列的长度(二维数组)
1. 初始状态:当text1[0:i]中有元素与text2[0]相等时,dp[i][0]=1;当text2[0:j]中有元素与text1[0]相等时,dp[0][j]=1;
2. 状态方程:
2.1 当text1[i]==text2[j]时,dp[i][j] = dp[i-1][j-1]+1;
2.2 否则,dp[i][j] = max(dp[i-1][j], dp[i][j-1])
3. 返回值:dp[text1.size()-1][text2.size()-1]
*/
int longestCommonSubsequence(string text1, string text2) {
int m=text1.length(), n=text2.length();
vector<vector<int>> dp(m, vector<int>(n));
int flag = 0;
for(int i=0; i<m; i++){
if(text1[i]==text2[0]) flag=1;
if(flag) dp[i][0]=1;
else dp[i][0]=0;
}
flag = 0;
for(int j=0; j<n; j++){
if(text2[j]==text1[0]) flag=1;
if(flag) dp[0][j]=1;
else dp[0][j]=0;
}
for(int i=1; i<m; i++){
for(int j=1; j<n; j++){
if(text1[i]==text2[j]) dp[i][j] = dp[i-1][j-1]+1;
else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
return dp[m-1][n-1];
}
};