前言
解决问题的一个重要思想就是归纳,即减小问题的规模。如果小规模在一定范围内可解,则求解返回,若还是太大,继续往小分解。在返回大问题的过程中,有个弊端,多个大问题与同一个小问题有联系,这个小问题反复求解。解决办法就是动态规划,存下这个小问题的解,不就省事多了。
动态规划
维基百科说得好,动态规划就是通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
说白了就是:
1. 这个问题可以分解为子问题
2. 原问题与子问题有关系
3. 子问题可以求解并记录
说到这里,你是不是想到了递归,想到了数学归纳法,其实他们都是相通的。(哇哦,瞬间觉得学科之间原来真的是有联系的)
看到这里,你是不是觉得动态规划挺简单的嘛,就是三个步骤嘛:
1. 把原问题分解为子问题
2. 找到原问题与子问题的关系
3. 记录下子问题的解
emmm,就是这三个简单的步骤,难倒了多少好汉啊(也包括我),下面我们就通过分析一些使用动态规划解决的问题来摸索这三步怎么找。
动态规划的常见应用:
1. 斐波那契数列
1 1 2 3 5 8 13。。。
我们要求第n个数,即f(n)
a. 分解问题:求f(n)就是求f(n - 1) 与 f(n - 2)
b. 找关系:f(n) = f(n - 1) + f(n - 2)
c.找个数组把f(n)存起来,这样调用时就不用再次计算了
这样,在f(1) 与 f(2) 已知的情况下,我们便可以求得f(n)。
看到这里,你可能会说这不是明显的递归吗,怎么是动态规划呢?这里便要提出动态规划与递归的不同,若用递归,求f(100)就要求f(99) 与 f(98), 求f(99)就要求f(98) 与f(97),此时f(98)算了两遍,而使用动态规划,它存储了f(98),后面用的时候便不需要再重新计算,能够省下来许多时间,这便是传说中动态规划记忆化存储的性质。
2. 钢条切割问题
设f(n)为长度为n的钢条切割后得到的最大价值
a.分解问题:
求f(n)即需要知道f(n - 1),f(n - 2), ....及p(n), p(n -1), p(n -2)....
b.找关系:
f(n) = max(p(n), f(1) + f(n -1), f(2) + f(n -2), ......., f(n/2) + f(n/2) )
c.存储f(n),节省时间
这样便可找到最值。
int cut(int []p) { int *r=new int[p.length+1]; r[1] = p[1]; for(int i = 1; i <= p.length; i++) { int q = p[i]; for(int j=1;j<=i;j++){ q = max(q, r[j] + r[i-j]); } r[i]=q; } return r[p.length]; }
3. 小朋友过河问题
在一个夜黑风高的晚上,有n(n <= 50)个小朋友在桥的这边,现在他们需要过桥,但是由于桥很窄,每次只允许不大于两人通过,他们只有一个手电筒,所以每次过桥的两个人需要把手电筒带回来,i号小朋友过桥的时间为T[i],两个人过桥的总时间为二者中时间长者。问所有小朋友过桥的总时间最短是多少。
输入:
两行数据:第一行为小朋友个数n
第二行有n个数,用空格隔开,分别是每个小朋友过桥的时间。
输出:
一行数据:所有小朋友过桥花费的最少时间。
a. 分解问题
i个人过河可看作是前i-1个人过河的情况再多一个人,或是前i-2个人再做两个人情况,这两种的时间不一样,需要分开讨论。
b. 找关系
我们先将所有人按花费时间递增进行排序,假设前i个人过河花费的最少时间为opt[i]。
(1)考虑前i-1个人过河的情况,即河这边还有1个人,河那边有i-1个人,并且这时候手电筒肯定在对岸,所以opt[i] = opt[i-1] + a[1] + a[i] (让花费时间最少的人把手电筒送过来,然后和第i个人一起过河)
(2)如果河这边还有两个人,一个是第i号,另外一个无所谓,河那边有i-2个人,并且手电筒肯定在对岸,所以opt[i] = opt[i-2] + a[1] + a[i] + 2*a[2] (让花费时间最少的人把电筒送过来,然后第i个人和另外一个人一起过河,由于花费时间最少的人在这边,所以下一次送手电筒过来的一定是花费次少的,送过来后花费最少的和花费次少的一起过河,解决问题)
所以 opt[i] = min{opt[i-1] + a[1] + a[i] , opt[i-2] + a[1] + a[i] + 2*a[2] }。
c.记录每一个opt[n], 节省时间。
#include<iostream> #include<vector> #include<algorithm> using namespace std; int main() { int childNum, i; cin >> childNum; int *child = new int[childNum]; for (i = 0;i < childNum; i++) { cin >> child[i]; } sort(child, child + childNum); vector<int> vect(childNum); vect[0] = 0; vect[1] = child[1]; for (i = 2; i < childNum; i++) { vect[i] = min(vect[i-1] + child[0] + child[i],vect[i-2] + child[0] + child[i]+2 * child[1]); } cout << vect[childNum - 1]; return 0; }
4. 求T和P的最长公共子序列(LCS)
定义C[i][j]表示X[i]与Y[j]的最大子序列的长度
其中X[i]表示A序列前i个
其中Y[j]表示B序列前j个
Algorithm LCS Input: A[N], B[N] Output: length begin int c[i][j] for(i = 0; i <= N; i++){ c[i][0] = 0; c[0][i] = 0; } for(i = 1; i <= N; i++){ for(j = 1; j <= N; j++){ if(A[i] == B[j]){ c[i][j] = c[i - 1][j - 1]; } else{ c[i][j] = max{c[i - 1][j], c[i][j - 1]}; } } } end
5. 计算最短公共超序列SCS
最短公共超序列(SCS)S定义为所有以T和P为子序列的序列中最短的一个。
定义a[i][j]表示s1[i]与s2[j]的最大子序列的长度
其中s1[i]表示A序列前i个
其中s2[j]表示B序列前j个
Algorithm SCS Input: A[N], B[N] Output: length begin int c[i][j] for(i = 0; i <= N; i++){ c[i][0] = 0; c[0][i] = 0; } for(i = 1; i <= N; i++){ for(j = 1; j <= N; j++){ int case1, case2, case3; case1 = c[i - 1][j] + 1; case2 = c[i][j - 1] + 1; if(A[i] == B[j]){ case3= c[i - 1][j - 1] + 1; } else{ case3 = c[i - 1][j - 1] + 2; } c[i][j] = min(case1, case2, case3); } } return c[m][n]; end
总结
动态规划算法在归纳的基础上对数值小问题的解进行存储,这样可以在重复使用的时候减小开销,一般动态规划在解决日常问题时很常见。