递归程序
#include<iostream>
#include<algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX];
int n;
int MaxSum(int i, int j)
{
if (i == n)
return D[i][j];
int x = MaxSum(i + 1, j);
int y = MaxSum(i + 1, j + 1);
return max(x, y) + D[i][j];
}
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n;i++)
for (j = 1; j <= i; j++)
cin >> D[i][j];
cout << MaxSum(1, 1) << endl;
}
会超时!
怎样避免重复计算呢?
动态规划问题(记忆递归):
代码实现:
#include<iostream>
#include<algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX];
int maxSum[MAX][MAX];
int n;
int MaxSum(int i, int j)
{
if (maxSum[i][j] != -1)//避免重复计算
return maxSum[i][j];
if (i == n)
maxSum[i][j]= D[i][j];
else
{
int x = MaxSum(i + 1, j);
int y = MaxSum(i + 1, j + 1);
maxSum[i][j]= max(x, y) + D[i][j];
}
return maxSum[i][j];
}
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n; i++)
{
for (j = 1; j <= i; j++)
{
cin >> D[i][j];
maxSum[i][j] = -1;
}
}
cout << MaxSum(1, 1) << endl;
}
运行结果
递归转成递推
建立一个5×5的网格,从下到上依次填写相应位置的最大和。
代码实现:
#include<iostream>
#include<algorithm>
using namespace std;
#define MAX 101
int D[MAX][MAX];
int n;
int maxSum[MAX][MAX];
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n;i++)
for (j = 1; j <= i; j++)
cin >> D[i][j];
for (int i = 1; i <= n; i++)
maxSum[n][i] = D[n][i];
for (int i = n - 1; i >= 1;i--)
for (int j = 1; j <= i; j++)
maxSum[i][j] = max(maxSum[i+1][j], maxSum[i+1][j + 1]) + D[i][j];
cout << maxSum[1][1] << endl;
}
空间优化
没必要用二维数组maxSum存储每一个MaxSum(r,j),只要从底层一行行向上递推,那么只要一维数组maxSum[100]即可,即只要存储一行的MaxSum值就可以。因为从下到上计算时,每计算一个最大值,此值就没用了。
进一步考虑,连maxSum数组都可以不要,直接用D的第n行替代maxSum即可。节省空间,时间复杂度不变。
代码实现
#include<iostream>
#include<algorithm>
using namespace std;
#define MAX 101
int D[MAX][MAX];
int n;
int main()
{
int i, j;
cin >> n;
for (i = 1; i <= n;i++)
for (j = 1; j <= i; j++)
cin >> D[i][j];
for (int i = n - 1; i >= 1;i--)
for (int j = 1; j <= i; j++)
D[n][j] = max(D[n][j], D[n][j + 1]) + D[i][j];
cout << D[n][1] << endl;
}
递归到动规的一般转化方法
递归函数有n个参数,就定义一个n维的数组,数组的下标是递归函数参数的取值范围,数组元素的值是递归函数的返回值,这样就可以从边界值开始,逐步填充数组,相当于计算递归函数值的逆过程。
动规解题的一般思路
1.将原问题分解为子问题
- 把原问题分解为若干个子问题,子问题和原问题形式相同或类似,只不过规模变小了。子问题都解决,原问题即解决
- 子问题的解一旦求出就会被保存,所以每个子问题只需求解一次。
2.确定状态
在用动态规划解题时,我们往往将和子问题相关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题,所谓某个“状态”下的“值”,就是这个“状态”所对应的子问题的解。整个问题的时间复杂度是状态数目乘以计算每个状态所需要的时间。
3.确定一些初试状态(边界状态)的值
以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值。
4.确定状态转移方程
定义出什么是“状态”,以及在该“状态”下的“值”后,就要找出不同的状态之间如何迁移——即如何从一个或多个“值已知的“状态”,求出另一个“状态”的“值”(人人为我递推型)。状态的迁移可以用递推公式表示,此递推公式也可以被称为“状态转移方程”。
数字三角形的状态转移方程:
能用动规解决的问题的特点
- 问题具有最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。
- 无后效性。当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪种路径演变到当前的这若干个状态无关。
例 最长上升子序列
解题思路
代码实现:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 1010;
int a[MAXN];
int maxLen[MAXN];
int main()
{
int N;
cin >> N;
for (int i = 1; i <= N; i++)
{
cin >> a[i];
maxLen[i] = 1;
}
for (int i = 2; i <= N; i++)
{//每次求以第i个数为终点的最长上升子序列的长度
for (int j = 1; j < i; j++)
{//查看以第j个数为终点的最长子序列长度
if (a[i]>a[j])
maxLen[i] = max(maxLen[i], maxLen[j] + 1);
}
}
cout << *max_element(maxLen + 1, maxLen + N + 1);//取容器中最大元素
return 0;
}
动规的两种形式
(1)递归型
优点:直观,容易编写
缺点:可能会因递归层数太深导致爆栈,函数调用带来的额外时间开销。无法使用滚动数组节省空间。总体来说,比递推型要慢。
(2)递推型
效率高,有可能使用滚动数组节省空间。但是不直观。
例 最长公共子序列
代码实现
递推型:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
char sz1[1000];
char sz2[1000];
int maxLen[1000][1000];
int main()
{
while (cin >> sz1 >> sz2)
{
int length1 = strlen(sz1);
int length2 = strlen(sz2);
int nTmp;
int i, j;
for (i = 0; i <= length1; i++)
maxLen[i][0] = 0;
for (j = 0; j <= length2; j++)
maxLen[0][j] = 0;
for (i = 1; i <= length1; i++)
{
for (j = 1; j <= length2;j++)
if (sz1[i - 1] == sz2[j - 1])
maxLen[i][j] = maxLen[i - 1][j - 1] + 1;
else
maxLen[i][j] = max(maxLen[i][j - 1], maxLen[i - 1][j]);
}
cout << maxLen[length1][length2] << endl;
}
return 0;
}
递归型:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
char sz1[10];
char sz2[10];
int maxLen(int i, int j)
{
if (i <= 0 || j <= 0)
return 0;
if (sz1[i - 1] == sz2[j - 1])
return maxLen(i - 1, j - 1) + 1;
else
return max(maxLen(i, j - 1), maxLen(i - 1, j));
}
int main()
{
while (cin >> sz1 >> sz2)
{
int length1 = strlen(sz1);
int length2 = strlen(sz2);
cout << maxLen(length1, length2) << endl;
}
return 0;
}
由此可见,递归型的代码数量明显少于递推型。
例 最佳加法表达式
解题思路
代码实现
#include<string>
#include<iostream>
#include<cstring>
#include<stdlib.h>
using namespace std;
const int MaxLen = 55;
const string maxv = "999999999999999999999999999999999999999999999999999999999";
string ret[MaxLen][MaxLen];
string num[MaxLen][MaxLen];
int cmp(string &num1, string &num2)
{
int l1 = num1.length();
int l2 = num2.length();
if (l1 != l2)
{
return l1 - l2;
}
else
{
for (int i = l1 - 1; i >= 0; i--)
{
if (num1[i] != num2[i])
{
return num1[i] - num2[i];
}
}
return 0;
}
}
void add(string &num1, string &num2, string &num3)
{
//加法从低位到高位相加,那么需要将字符串倒过来
int l1 = num1.length();
int l2 = num2.length();
int maxl = MaxLen, c = 0; //c是进位标志
for (int i = 0; i<maxl; i++)
{
int t;
if (i < l1 && i < l2)
{
t = num1[i] + num2[i] - 2 * '0' + c;
}
else if (i < l1 && i >= l2)
{
t = num1[i] - '0' + c;
}
else if (i >= l1 && i < l2)
{
t = num2[i] - '0' + c;
}
else
{
break;
}
num3.append(1, t % 10 + '0');
c = t / 10;
}
while (c)
{
num3.append(1, c % 10 + '0');
c /= 10;
}
}
int main()
{
int m; //加号数目
string str; //输入的字符串
while (cin >> m >> str)
{
//为了之后的加法计算先将这个字符串倒过来
reverse(str.begin(), str.end());
int n = str.length();
for (int i = 0; i<n; i++)
{
num[i + 1][i + 1] = str.substr(i, 1);
}
for (int i = 1; i <= n; i++) //求解对应的num[i][j]
{
for (int j = i + 1; j <= n; j++)
{
num[i][j] = str.substr(i - 1, j - i + 1);
}
}
//当加号数目为0
for (int i = 1; i <= n; i++)
{
ret[0][i] = num[1][i];
}
for (int i = 1; i <= m; i++) //对于加号数目的枚举
{
for (int j = 1; j <= n; j++) //对于长度的枚举
{
string minv = maxv;
string tmp;
for (int k = i; k <= j - 1; k++)
{
tmp.clear();
add(ret[i - 1][k], num[k + 1][j], tmp);
if (cmp(minv, tmp)>0)
{
minv = tmp;
}
}
ret[i][j] = minv;
}
}
//将原先颠倒的字符串倒回来
reverse(ret[m][n].begin(), ret[m][n].end());
cout << ret[m][n] << endl;
}
return 0;
}