この質問も決まり文句であり、非常に古典的な質問ですが、この質問に関するさまざまなアルゴリズムのアイデアを学ぶことができます。この記事では主に、暴力的な再帰から動的計画法、そして数学的な驚異的なスキルに至る解法プロセスの進化を要約します。
トピックは次のとおりです:
ロボットは m、n m、n に位置します m、nグリッドの左上隅。一度に右または下に 1 マスしか移動できません。右下に到達するには、いくつの異なるパスが必要ですか写真の隅?
暴力的な再発
最初に簡単に思いつくのは、右下隅に到達できるすべてのパスを列挙してカウントする再帰的書き込み方法です。 。次の図に示すように、再帰ツリーを描画します。アルゴリズムの複雑さは O ( 2 m n ) O(2^{mn}) です。O(2mn)、2 次元配列であるため、空間計算量は O(mn) です。 。
コードは以下のように表示されます
int ans;
int m, n;
int dx[3] = {
0, 1, 0};
vector<vector<int>> grid;
void dfs(int x, int y){
if(x == m-1 && y == n-1){
ans++;
return;
}
for(int i = 0;i < 2;i++){
int nx = x + dx[i];
int ny = y + dx[i+1];
if(nx >= m || ny >= n || nx < 0 || ny < 0 || grid[nx][ny] != 0) continue;
grid[nx][ny] = 1;
dfs(nx, ny);
grid[nx][ny] = 0;
}
}
int uniquePaths(int m, int n) {
ans = 0;
this->m = m;
this->n = n;
grid = vector<vector<int>>(m, vector<int>(n, 0));
grid[0][0] = 1;
dfs(0, 0);
return ans;
}
ただし、このようなコードが直接送信されると、再帰的プロセス中に二重計算が発生しやすいため、TLE タイムアウトが報告されます。例えば、下図のように、Cに到達するすべての状況の合計数を計算する場合、Bに到達する状況の数がdfsで再計算されます。
したがって、最適化したい場合は、再帰ツリーの分岐を減らす必要があります。より簡単な方法は、計算されたパスの総数を dict を使用して保存することです。記述したコードは以下の通りです。
int ans;
int m, n;
int dx[3] = {
0, 1, 0};
vector<vector<int>> grid;
vector<vector<int>> memo; // 记录算过的路径情况数
int dfs(int x, int y){
int res = 0;
if(x == m-1 && y == n-1) return 1;
if(memo[x][y] != -1) return memo[x][y];
// int tmp = grid[x][y];
for(int i = 0;i < 2;i++){
int nx = x + dx[i];
int ny = y + dx[i+1];
if(nx >= m || ny >= n || nx < 0 || ny < 0 || grid[nx][ny] != 0) continue;
grid[nx][ny] = 1;
res += dfs(nx, ny);
grid[nx][ny] = 0;
}
return memo[x][y] = res;
}
int uniquePaths(int m, int n) {
ans = 0;
this->m = m;
this->n = n;
grid = vector<vector<int>>(m, vector<int>(n, 0));
memo = vector<vector<int>>(m, vector<int>(n, -1));
grid[0][0] = 1;
return dfs(0, 0);
}
このソリューションには、メモリ再帰というハイエンドの名前が付けられています。名前が示すように、最適化の目的を達成するために再帰的なプロセス中に見つかった解決策を記録します。このようなソリューションは、leetcode の困難を克服することができます。ただし、この問題についてはさらに最適化できるため、決まり文句の動的プログラミングを使用する必要があります。動的計画法ソリューションでは、各位置について次のように問題を検討します (i, j) (i, j) (i,j)、この位置に到達するパスの総数は ( i , j − 1 ) (i, j-1) (i,j−1)的情况总数加上 ( i − 1 , j ) (i-1, j) (i−1、j) のケースの合計数。これは問題の条件によって決まり、毎回右か下にしか移動できません。すると、現在位置と前位置の解法にはどのような練習問題があるのかがわかり、そのような再帰式を考えることができます。最初の行と列の場合、これらの位置に到達するパスは明らかに 1 つだけあり、これが dp の初期条件です。したがって、 次のような認識が自然に得られます
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 0));
for(int i = 0;i < n;i++){
dp[0][i] = 1;
}
for(int i = 0;i < m;i++){
dp[i][0] = 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];
}
突然、かなりすっきりしたと思いませんか? この方法で解決されたアルゴリズムの複雑さは O(mn) で、空間の複雑さも O(mn) ですが、それは何倍も優れています。暴力的な再発よりも。もちろん、dp の状態遷移プロセスは ( i , j ) (i,j) からのみ開始されるため、このような解決策も最適化することができます。(i,j) を左または上から順に並べるので、これら 2 つの場所の値をそれぞれ保存および更新するだけで済みます。時間。これは、時間計算量が変化せず、空間計算量が O(n) に変換される次の解決策に簡略化できます。
int uniquePaths(int m, int n) {
int left = 1;
int cur;
if(m == 1) return 1;
vector<int> top(n, 0); // 表示列
for(int i = 0;i < n;i++){
top[i] = 1;
}
for(int i = 1;i < m;i++){
left = 1;
for(int j = 1;j < n;j++){
cur = left + top[j];
left = cur;
top[j] = cur;
}
}
return cur;
}
最後に、インターネット上にある優れた数学的解決策を紹介します。スタートからゴールまでは、右横に n-1 回歩き、次に垂直に下に m-1 回、合計 m+n-2 歩歩く必要があります。次に到着します ( m − 1 , n − 1 ) (m-1, n-1) (m−1、n−1)的解等于 C m + n − 2 m − 1 C m − 1 m − 1 C_{m+n-2}^{m-1}C_{m-1}^{m-1} Cm+n−2m−1 Cm−1m−1 または C m + n − 2 n − 1 C n − 1 n − 1 C_{m+n-2}^{n-1}C_{n-1 }^{n-1} Cm+n−2n−1 Cn−1n−1 、これら 2 つの解決策の結果は同じでなければなりません。これは、合計 m+n-2 歩の同じ合計数の歩数の中から、垂直/水平に歩く可能性のある状況を選択し、残りの歩数を選択することに相当します。コードは以下のように表示されます
int uniquePaths(int m, int n) {
int N = n + m - 2;
double res = 1;
for (int i = 1; i < m; i++)
res = res * (N - (m - 1) + i) / i;
return int(res);
}
上記の方法と比較すると、単純ですが、各数値の階乗数と組み合わせ数を事前に計算できるため、頻繁に使用される解法シナリオでは動的計画法アルゴリズムよりも高速です。