Ø 动态规划基本思想:
将待求解的问题分解为若干个子问题(阶段),按顺序求解子问题,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个表中。
Ø 与分治的区别:
适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的,即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解。
Ø 与搜索的区别:
搜索是遍历每一个可能的分支,动态规划是记忆化搜索,重叠子问题只计算一次
Ø 与贪心的区别:
在动态规划算法中,每步所做的选择往往依赖于相关子问题的解。因而只有在解出相关子问题后,才能做出选择。而在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择。
Ø 动态规划算法的难点
从实际问题中抽象出动态规划表dp,dp表一般是一个数组,这个数组可能是一维的也可能是二维的,也可能是其他的数据结构。
Ø 动态规划求解问题的3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3) 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)。动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
Ø 经典案例:
1. 有n级台阶,一个人每次上一级或者两级,问有多少种走完n级台阶的方法。
int fun(int n)
{
if (n == 1 || n == 2) {
return n;
}
if (!dp[n - 1]) {
dp[n - 1] = fun(n - 1);
}
if (!dp[n -2]) {
dp[n - 2] = fun(n -2);
}
return dp[n -1] + dp[n - 2];
}
2. 给定一个矩阵m,从左上角开始每次只能向右走或者向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和,如果给定的m如下,那么路径1,3,1,0,6,1,0就是最小路径和,返回12。
#include <iostream>
#include <algorithm>
using namespace std;
int dp[4][4] = {};
int main(){
int arr[4][4] = {1,3,5,9,8,1,3,4,5,0,6,1,8,8,4,0};
//cout << fun(arr,4,4) << endl;
const int oo = ~0U>>2;
for (int i = 0;i<4;i++)
for (int j = 0; j < 4;j++)
dp[i][j] = oo;
//dp[0][0] = oo;
for (int i = 0; i < 4;i++){
for (int j = 0; j<4;j++){
if (dp[i][j] == oo){
if (i==0&&j==0)
dp[i][j] = arr[i][j];
else if (i==0&&j!=0)
dp[i][j] = arr[i][j] + dp[i][j-1];
else if(i!=0&&j==0)
dp[i][j] = arr[i][j] + dp[i-1][j];
else{
dp[i][j] = arr[i][j]+min(dp[i-1][j],dp[i][j-1]);
}
}
}
}
// cout << dp[3][3] << endl;
for (int i = 0; i< 4;i++){
for (int j = 0; j<4;j++){
cout << dp[i][j] << " ";
}
cout << endl;
}
}
3. 给定数组arr,返回arr的最长递增子序列的长度,比如arr=[2,1,5,3,6,4,8,9,7],最长递增子序列为[1,3,4,8,9]返回其长度为5.
#include <iostream>
#include <algorithm>
using namespace std;
/*动态规划表*/
int dp[5] = {};
int main(){
int arr[5] = {2,4,5,3,1};
dp[0] = 1;
const int oo = 0;
for (int i = 1;i<5;i++){
int _max = oo;
for (int j=0;j<i;j++)
if(dp[j]>_max&&arr[i]>arr[j])
_max = dp[j];
dp[i] = _max+1;
}
int maxlist=0;
for (int i = 0; i < 5;i++)
if (dp[i] > maxlist)
maxlist=dp[i];
cout << maxlist << endl;
}
4. 给定两个字符串str1和str2,返回两个字符串的最长公共子序列,例如:str1="1A2C3D4B56",str2="B1D23CA45B6A","123456"和"12C4B6"都是最长公共子序列,返回哪一个都行。
分析:假设str1的长度为M,str2的长度为N,则生成M*N的二维数组dp,dp[i][j]的含义是str1[0..i]与str2[0..j]的最长公共子序列的长度。
int findLCS(string A, int n, string B, int m) {
// n表示字符串A的长度,m表示字符串B的长度
int dp[500][500] = {};
for (int i = 0;i < n;i++)
{
for (int j = 0; j<m;j++)
{
if (A[i]==B[j])
dp[i+1][j+1] = dp[i][j]+1;
else
dp[i+1][j+1] = max(dp[i+1][j],dp[i][j+1]);
}
}
return dp[n][m];
}
5. 背包问题,动态规划经典问题,一个背包有滴定的承重W,有N件物品,每件物品都有自己的价值,记录在数组V中,也都有自己的重量,记录在数组W中,每件物品只能选择要装入还是不装入背包,要求在不超过背包承重的前提下,选出的物品总价值最大。
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
int main(){
/*物品数量*/
int n = 4;
/*背包承重*/
int cap = 10;
int v[4] = {42,12,40,25};
int w[4] = {7,3,4,5};
/*二维动态规划表*/
vector<int> p(cap+1,0);
vector<vector<int>> dp(n+1,p);
for (int i = 1;i <=n;i++){/*枚举物品*/
for (int j = 1;j<cap+1;j++){/*枚举重量*/
/*判断枚举的重量和当前选择的物品重量的关系
如果枚举的和总量大于等于选择物品,则需要判断是否选择当前物品*/
if (j-w[i-1]>=0)
dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i-1]]+v[i-1]);
else
/*如果枚举的重量还没有当前选择物品的重量大,那就只能是不取当前物品*/
dp[i][j] = dp[i-1][j];
}
}
cout << dp[n][cap] << endl;
}
6. 给定两个字符串str1,str2,在给定三个整数ic,dc,rc,分别代表插入,删除和替换一个字符的代价, 这里假设均为1。返回将str1
编辑成str2的代价,比如,str1="sea",str2="ea",ic=1,dc=1,rc=1,从str1到str2,删除's'代价最小,所以返回1.
分析:在构建出动态规划表的时候,关键是搞清楚每个位置上数值的来源。首先我们生成dp[M+1][N+1]的动态规划表,M代表str1的长度,N代表str2的长度,那么dp[i][j]就是str1[0..i-1]变成str2[0...j-1]的最小代价,则dp[i][j]的来源分别来自以下四种情况:
a、首先将str1[i-1]删除,变成str1[0...i-2],然后将str1[0...i-2]变成str2[0...j-1],那么dp[i-1][j]就代表从str1[0..i-2]到str2[0...j-1]的最小代价,所以:dp[i][j] = dp[i-1][j]+dc;
b、同理也可以是从str1[0...i-1]变成str2[0...j-2],然后在插入str2[j-1],dp[i][j-1]就代表从str1[0...i-1]变成str2[0...j-2]的最小代价,所以:dp[i][j] = dp[i][j-1]+ic;
c、如果str[i-1] == str2[j-1],则只需要将str1[0...i-2]变成str2[0...j-2],此时dp[i][j] = dp[i-1][j-1];
d、如果str1[i-1]!=str2[j-1],则我们只需要将str1[i-1]替换成str2[j-1],此时dp[i][j] = dp[i-1][j-1]+rc;
在这四种情况当中,我们选取最小的一个,即为最小代价。
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <stdlib.h>
int Min(int x, int y, int z)
{
if (x <= y && x <= z) {
return x;
}
if (y <= z && y <= x) {
return y;
}
if (z <= x && z <= y) {
return z;
}
return 0;
}
int minDistance(char* str1, char* str2)
{
int row = strlen(str1) + 1;
int col = strlen(str2) + 1;
int minDist = 0;
if (row == 0) {
return col;
}
if (col == 0) {
return row;
}
// dp[i][j] <==> dp[col * i + j];
int* dp = (int*)malloc(sizeof(int) * row * col);
// for example :"sea" and "ea"
// row = 4 , strlen("sea") + 1
// col = 3 , strlen("ea") + 1
// init dp :
// 0 1 2
// 1 0 0
// 2 0 0
// 3 0 0
dp[col * 0 + 0] = 0;
for (int i = 1; i < row; i++) {
dp[col * i + 0] = i;
}
for (int j = 1; j < col; j++) {
dp[col * 0 + j] = j;
}
// loop for dp
// dp[i][j] is minimum value of str1[0...i-1] converting to str2[0...j-1]
// beacuse i, j in dp is length, but i, j in str1 and str2 is index
for (int i = 0; i < row - 1; i++) {
for (int j = 0; j < col - 1; j++) {
if (str1[i] == str2[j]) {
dp[col * (i + 1) + (j + 1)] = dp[col * i + j];
} else {
// dp[i][j] + 1, replace
// dp[i][j + 1], insert
// dp[i + 1][j], delete
dp[col * (i + 1) + (j + 1)] = Min(dp[col * i + j], dp[col * (i + 1) + j], dp[col * i + (j + 1)]) + 1;
}
}
}
// return dp[row - 1][col - 1]
// for "sea" and "ea", return dp[3][2]
minDist = dp[col * (row - 1) + (col - 1)];
free(dp);
return minDist;
}
int main()
{
char str1[] = "sea";
char str2[] = "ea";
int answer = minDistance(str1, str2);
printf("MinimulDistance : %d\n", answer);
return 0;
}