無後效性問題:
- 不管通過什麼方式到達某個狀態,其返回值都一樣。
1. 換錢的方法數
暴力遞歸
- 以arr = [5, 10, 25, 1],aim = 15爲例:
節點中的內容表示:
- 從下標爲[i~j]的數組中取任意張,可以換到aim的方法數
class Solution{
public:
int coins(int *arr, int len, int aim)
{
if (!arr || len == 0 || aim < 0)
return 0;
return help(arr, len, 0, aim);
}
private:
//從下標爲[index~len-1]的數組中取任意張,可以換到aim的方法數
int help(int *arr, int len, int index, int aim)
{
int ret = 0;
if (index == len)
ret = (aim == 0) ? 1 : 0;
else{
for (int i = 0; arr[index] * i <= aim; i++)
{
ret += help(arr, len, index + 1, aim - arr[index] * i);
}
}
return ret;
}
};
可以發現 暴力遞歸 有許多重複性計算,可以使用記憶化搜索
class Solution{
public:
int coins(int *arr, int len, int aim)
{
if (!arr || len == 0 || aim < 0)
return 0;
memo = vector<vector<int>>(len + 1, vector<int>(aim + 1, -1));
return help(arr, len, 0, aim);
}
private:
vector<vector<int>> memo; //memo[i][j]:從下標爲[i...len-1]處選取任意張,可以換到j的方法數
//從下標爲[index~len-1]的數組中取任意張,可以換到aim的方法數
int help(int *arr, int len, int index, int aim)
{
int ret = 0;
if (index == len)
ret = (aim == 0) ? 1 : 0;
else{
for (int i = 0; arr[index] * i <= aim; i++)
{
if (memo[index + 1][aim - arr[index] * i] != -1)
ret += memo[index + 1][aim - arr[index] * i];
else
ret += help(arr, len, index + 1, aim - arr[index] * i);
}
}
memo[index][aim] = ret;
return ret;
}
};
動態規劃
從暴力遞歸的遞歸函數可以看出:
- 要計算當前(index,aim)的返回值,需要通過(index+1, aim-xxx)來獲得
以arr=[5,3,2],aim = 5爲例
最終要求得綠色位置的值。
1.首先填寫最後一排
2.依次往上填
比如想計算 index=2,aim=4 的值:
memo[2][4] = memo[3][4] + memo[3][2] + memo[3][0]
(前一個是arr[2]取0張,中間是arr[2]取1張,後一個是arr[2]取2張)
- 最終結果:
class Solution{
public:
int coins(int *arr, int len, int aim)
{
if (!arr || len == 0 || aim < 0)
return 0;
memo = vector<vector<int>>(len + 1, vector<int>(aim + 1, 0));
memo[len][0] = 1;
for (int i = len - 1; i >= 0; i--)
{
for (int j = 0; j <= aim; j++)
{
for (int k = 0; j - k*arr[i] >= 0; k++)
memo[i][j] += memo[i + 1][j - k*arr[i]];
}
}
return memo[0][aim];
}
private:
vector<vector<int>> memo; //memo[i][j]:從下標爲[i...len-1]處選取任意張,可以換到j的方法數
};
假設現在要計算 memo[i][j] 位置上的值,按照上述方法,要通過橙色方塊求得。
- 優化:可以通過下圖橙色方塊求得
class Solution{
public:
int coins(int *arr, int len, int aim)
{
if (!arr || len == 0 || aim < 0)
return 0;
memo = vector<vector<int>>(len + 1, vector<int>(aim + 1, 0));
memo[len][0] = 1;
for (int i = len - 1; i >= 0; i--)
{
for (int j = 0; j <= aim; j++)
{
/*for (int k = 0; j - k*arr[i] >= 0; k++)
memo[i][j] += memo[i + 1][j - k*arr[i]];
*/
int n = (j - arr[i] >= 0) ? memo[i][j - arr[i]] : 0;
memo[i][j] = memo[i + 1][j] + n;
}
}
return memo[0][aim];
}
private:
vector<vector<int>> memo; //memo[i][j]:從下標爲[i...len-1]處選取任意張,可以換到j的方法數
};
2. 機器人走到指定位置的方法數
在一個長度爲N的路上,下標爲[1~N]。一個機器人初始在M位置,它可以走P步,如果在1位置則只能往右走,在N位置則只能往左走,請問機器人走P步後,停在K位置上的走法有多少種
暴力遞歸
int ways(int N, int M, int P, int K)
{
if (N < 2 || M < 1 || M > N || P < 0 || K < 1 || K > N)
return 0;
if (P == 0)
return (M == K) ? 1 : 0;
int ret = 0;
if (M == 1)
ret = ways(N, M + 1, P - 1, K);
else if (M == N)
ret = ways(N, M - 1, P - 1, K);
else
ret = ways(N, M + 1, P - 1, K) + ways(N, M - 1, P - 1, K);
return ret;
}
動態規劃
memo[P][M] = memo[P-1][M-1] + memo[P-1][M+1]
int ways(int N, int M, int P, int K)
{
if (N < 2 || M < 1 || M > N || P < 0 || K < 1 || K > N)
return 0;
vector<vector<int>> memo = vector<vector<int>>(P + 1, vector<int>(N + 1, 0)); //memo[i][j]:走了i步,到達j位置的方法數
memo[0][K] = 1;
for (int i = 1; i <= P; i++)
{
for (int j = 1; j <= N; j++)
{
memo[i][j] += (j - 1 < 1) ? 0 : memo[i - 1][j - 1]; //從左邊一個位置往右走
memo[i][j] += (j + 1 > N) ? 0 : memo[i - 1][j + 1]; //從右邊一個位置往左走
}
}
return memo[P][K];
}