自顶向下解法
分解:将长度为n的钢条切割问题转为在i下标切割后长度为n-1的子问题
解决:递归求解子问题,每次长度参数-1,在n=0即不用切时递归终止
合并:
1. 每个子问题的解+前一段切割长度的arr[i]值 成为前一子问题的最优解
2. 原规模问题中的最优解是所有子问题解中值最大的
//自顶向下
int max_sum(int arr[], int n)
{
if (n == 0) {
return 0;
}
int i = 0;
int maxn = INT_MIN;
int sum = 0;
//边界,最后一次切割点是arr[n-1]+0
for (i = 1; i <= n; i++) {
sum = arr[i-1] + max_sum(arr, n - i);
if (maxn < sum) {
maxn = sum;
}
}
return maxn;
}
为什么慢?
反复使用相同参数对自身进行递归调用,反复求解相同子问题。O(2^n)
带备忘的自顶向下
仍按自顶向下的方法编写,但过程中保存每个子问题的解。
敲重点:在过程中保存子问题解,在每次递归之前检查是否保存了解。
//带备忘的自顶向下
#define N 100
int maxArr[N];
void init(int maxArr[N])
{
for (int i = 0; i < N; i++) {
maxArr[i] = INT_MIN;
}
}
int m_max_sum(int arr[], int n)
{
//检查
if (maxArr[n] > INT_MIN) {
return maxArr[n];
}
if (n == 0) {
return 0;
}
int i = 0;
int maxn = INT_MIN;
int sum = 0;
for (i = 1; i <= n; i++) {
sum = arr[i - 1] + max_sum(arr, n - i);
if (maxn < sum) {
maxn = sum;
}
}
//保存
maxArr[n] = maxn;
return maxn;
}
自底向上
这种方法一般需要恰当定义子问题“规模”,使得任何子问题的求解都只依赖于“更小的”子问题的求解。因此我们将子问题按规模从小到大进行求解。当求解某个子问题时,它所依赖的更小子问题已经求解完毕,结果已经保存,所以每个子问题只求解一次,当我们求解它时,它的所有前提子问题都已求解完成。
以上是我手打的书上原话,每一句都很精辟,字字珠玑。
//自底向上
int btm_up(int arr[], int n)
{
int j = 0, i = 0;
int maxnn = INT_MIN;
int sum = 0;
//最小规模的解
maxArr[0] = 0;
//规模为j的子问题
for (j = 1; j <= n; j++) {
//求解规模为i的子问题与原来方法一样,只是规模i < j,所以在求规模j的时候不需要递归
for (i = 1; i <= j; i++) {
sum = arr[i - 1] + maxArr[j - i];
if (sum > maxnn) {
maxnn = sum;
}
}
maxArr[j] = maxnn;
}
return maxArr[n];
}
重构解
对每个子问题不仅保存最优值,还保存切割点,利用这些信息输出最优解。
//最后pos[n]就是最优的切割点
int pos[N] = { 0 };
//自底向上重构解
int btm_up_op(int arr[], int n)
{
int j = 0, i = 0;
int maxnn = INT_MIN;
int sum = 0;
//最小规模的解
maxArr[0] = 0;
//规模为j的子问题
for (j = 1; j <= n; j++) {
//在每个j规模下,maxnn是最优价格,pos[j]存的是前一段最优切割长度
for (i = 1; i <= j; i++) {
sum = arr[i - 1] + maxArr[j - i];
if (sum > maxnn) {
maxnn = sum;
pos[j] = i;
}
}
maxArr[j] = maxnn;
}
return maxArr[n];
}