本文组织结构如下:

  • 前言
  • 最长公共子序列(LCS)
  • 最长不下降子序列(LIS)
  • 最大连续子序列之和
  • 最长回文子串
  • 数塔问题
  • 背包问题(Knapsack-Problem)
  • 矩阵链相乘
  • 总结

前言

在学过的算法当中,DP给我的感觉是最难的了。借着本次写blog好好复习一下这个算法。

众所周知,DP算法的关键点:

  • 抽象出问题的状态表示
  • 定义状态转移方程
  • 填表顺序

最长公共子序列

最长公共子序列(Longest Common Subsequence,LCS),顾名思义,是指在所有的子序列中最长的那一个,子序列要求都出现过并且出现顺序与母串保持一致。

例如,给定字符串 ab:

string a = "cnblog"
string b = "belong";

blog 都出现过,且字母顺序一致,那就一个公共子序列(在这里也是最长的公共子序列)。

状态定义:

dp[i, j] 表示 a[0,...,i] 与 b[0,...j] 的最长公共子序列的长度

那么现在的目的就是求出:dp[alen, blen]

状态转移方程:

        = 0                          if i=0 or j=0
dp[i,j] = dp[i-1,j-1]+1 if a[i]=b[j]
= max(dp[i-1,j], dp[i, j-1]) if a[i]!=b[j]

观察可知,每一个 dp[i,j] 都是依赖于 “左边、上边、左上角” 的三个元素之一,所以对于数组 dp ,可以从前往后填写。

DP算法一个关键的地方就是需要初始化边界。在此处,就是需要初始化 dp 数组的第 0 列,以及第 0 行。

代码如下,其中 plat 数组用于求解最大的公共子序列是什么(回溯法+DFS,具体不展开细讲,其他blog写了很多)。

int lcs(string &a, string &b)
{
string result = "";
int alen = a.length();
int blen = b.length();
vector<vector<int>> dp(alen + 1, vector<int>(blen + 1, 0));
vector<vector<char>> plat(alen + 1, vector<char>(blen + 1, 0));
for (int i = 1; i <= alen; i++)
{
for (int j = 1; j <= blen; j++)
{
if (a[i - 1] == b[j - 1]) //此处为什么是 a[i-1] 和 b[j-1] 呢?
dp[i][j] = dp[i - 1][j - 1] + 1, plat[i][j] = '\\';
else
{
if (dp[i - 1][j] >= dp[i][j - 1])
dp[i][j] = dp[i - 1][j], plat[i][j] = '|';
else
dp[i][j] = dp[i][j - 1], plat[i][j] = '-';
}
}
} print(plat, alen, blen);
return dp.back().back();
}

上面的代码中,需要特别留意和加以理解的地方就是内层循环中的 if 语句。

可以使用下图来理解,实际上当 i=0 or j=0 时,表示的是字符串为空串,即 s="" 。但是在代码当中,字符串的下标是从 0 开始计数的。在上面的状态转移方程当中,dp[1,1] 实际上需要判断两个字符串的第一个字符是否相等,即 a[0] == b[0]

      B D C A B A
0 1 2 3 4 5 6
0 0 0 0 0 0 0
A 1 0
B 2 0
C 3 0
B 4 0
D 5 0
A 6 0
B 7 0

完整代码:

#include <iostream>
#include <cassert>
#include <string>
#include "leetcode.h"
#include <vector>
using namespace std;
string a = "ABCBDAB", b = "BDCABA";
// string a = "xyxxzxyzxy", b = "zxzyyzxxyxxz";
void print(vector<vector<char>> &plat, int i, int j)
{
if (i <= 0 || j <= 0)
{
return;
}
if (plat[i][j] == '-')
{
print(plat, i, j - 1);
}
else if (plat[i][j] == '|')
{
print(plat, i - 1, j);
}
else if (plat[i][j] == '\\')
{
print(plat, i - 1, j - 1);
cout << a[i - 1];
}
else
{
}
}
int lcs(string &a, string &b)
{
string result = "";
int alen = a.length();
int blen = b.length();
vector<vector<int>> dp(alen + 1, vector<int>(blen + 1, 0));
vector<vector<char>> plat(alen + 1, vector<char>(blen + 1, 0));
for (int i = 1; i <= alen; i++)
{
for (int j = 1; j <= blen; j++)
{
if (a[i - 1] == b[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1, plat[i][j] = '\\';
else
{
if (dp[i - 1][j] >= dp[i][j - 1])
dp[i][j] = dp[i - 1][j], plat[i][j] = '|';
else
dp[i][j] = dp[i][j - 1], plat[i][j] = '-';
}
}
} print(plat, alen, blen);
return dp.back().back();
} int main()
{
cout << lcs(a, b);
} /*
= 0 if i=0 or j=0
dp[i,j] = dp[i-1,j-1]+1 if a[i]=b[j]
= max(dp[i-1,j], dp[i, j-1]) if a[i]!=b[j]
*/

最长不下降子序列

不下降子序列就是说:从一个数组当中选出若干个元素,按照下标顺序排列,该序列要求非降序排列。

例如有数组:

int a[] = {1, 2, 3, -9, 3, 9, 0, 11};

那么不下降子序列可以是:

1 2 3 3 9 11
1 2 3
...

求这些序列的最大长度。

我们令 dp[i] 表示:a[i] 结尾的,最长不下降子序列的长度。

那么对于 dp[i],可以有以下状态转移方程:

dp[0] = 1
dp[i] = 1
= max(1, max(dp[j]+1)), 0<=j<=(i-1) and a[i]>=a[j]

(其实这里的状态转移方程比代码还难理解,还是看代码好 = =)

代码:

int solve(int a[], int len)
{
vector<int> dp(len, 0);
dp[0]=1;
for (int i=1;i<len;i++)
{
dp[i] = 1;
for (int j=0;j<i;j++)
{
if (a[i]>=a[j])
dp[i]=max(dp[i], dp[j]+1);
}
}
int val = -1;
for (auto x:dp)
val=max(val,x);
return val;
}
int main()
{
int a[] = {1, 2, 3, -9, 3, 9, 0, 11};
cout << solve(a, 8);
}

最大连续子序列之和

这又是一道最XX的题目。

给定 A[1,...,n] ,求 ij , 1<=i<=j<=n,使得 sum(A[i,...,j]) 最大,输出这个最大和。

比如

-2 11 -4 13 -5 2

最大的、连续的子序列就是:

11 -4 13

最大和是 20 。

正常的思路是穷举每一个左端点和右端点,但是这样的复杂度是 O(n*n),其次每次对区间求和又需要 O(n) 的复杂度,只要数据量大,这种方法是不明智的。

来看一下DP的解法。

状态定义:

dp[i]: 以 a[i] 结尾的,最大连续子序列的和。

在这种情况下,在 i 位置,要么只取 a[i],要么取 a[i] 加上“前面”的。

转移方程:

dp[0] = a[0]
dp[i] = max(a[i], dp[i-1]+a[i])

代码如下:

int solve(int a[], int len)
{
vector<int> dp(len, 0);
dp[0] = a[0];
for (int i=1;i<len;i++)
{
dp[i]=max(a[i], dp[i-1]+a[i]);
}
int val = -1;
for (auto x:dp)
val = max(val, x);
return val;
}
int main()
{
int a[] = {-2, 11, -4, 13, -5, -2};
cout << solve(a, 6) << endl;
}
/*
常见错误:
dp[i]是[0,i]的最大连续子序列之和
dp[i+1] = max(dp[i], dp[i]+a[i])
(这么定义没法保证连续)
正解:
dp[i]表示必须以a[i]结尾的连续序列最大和,为什么“必须”?(因为要求连续)
那么:
dp[i] = max(a[i], dp[i-1]+a[i])
*/

最长回文子串

子串要求是连续的。

求出字符串S的所有子串中,最长的子串。

还是直接说怎么用DP求解。

定义状态:

dp[i,j]表示:S[i,...,j] 是否为回文串。

边界条件:

dp[i,i] = true				//只有一个字符的字符串
dp[i,i+1] = (s[i]==s[i+1])

状态转移方程:

dp[i,j] = dp[i+1,j-1] && (s[i]==s[j])

细心的你可能会发现,这次的 “填表” 不能从前往后填了,因为 dp[i, j] 依赖于它的左下角的元素。这次需要从 “左上” 到 “右下” 这样斜着填表。(先算左下的,后算右上的)

图解说明一下为什么斜着填,假设 s = "abba", len = 4

dp数组初始状态:
a b b a
a 1
b 0 1
b 0 0 1
a 0 0 0 1 执行s[i]==s[i+1]:
a b b a
a 1 0
b 0 1 1
b 0 0 1 0
a 0 0 0 1
==>
a b b a
a 1 0 0
b 0 1 1 0
b 0 0 1 0
a 0 0 0 1
==>
a b b a
a 1 0 0 1
b 0 1 1 0
b 0 0 1 0
a 0 0 0 1

代码:

int solve(string &s)
{
int len = s.length();
if (len == 0 || len == 1)
return len;
if (len == 2)
return (s[0] == s[1]) + 1;
vector<vector<bool>> dp(len, vector<bool>(len, 0));
int maxlen = 1;
for (int i = 0; i <= len - 2; i++)
{
dp[i][i] = 1, dp[i][i + 1] = (s[i] == s[i + 1]);
if (dp[i][i + 1])
maxlen = 2;
}
dp[len - 1][len - 1] = 1;
int i = 0;
int j = 2;
int ti, tj;
do
{
ti = i, tj = j;
while (i < len && j < len)
{
dp[i][j] = dp[i + 1][j - 1] && (s[i] == s[j]);
if (dp[i][j] == true)
maxlen = max(maxlen, j - i + 1);
i++, j++;
}
i = 0, j = tj + 1;
} while (!(j == len));
return maxlen;
}
int main()
{
string s[] = {"PATZJUJZTACCBCC", "", "1", "12", "22", "232", "2332"};
for (int i = 0; i < 7; i++)
cout << solve(s[i]) << endl;
} /*
dp[i][j]表示str[i,j]是否为一个回文串,若是则1,若否则0
那么:
= 1 if i=j
dp[i,j] = (s[i]==s[j]) if i+1=j
= dp[i+1,j-1]&&s[i]==s[j] other
*/

数塔问题

给定如下的树塔 :

level = 5
5
/ \
8 3
/ \ / \
12 7 16
/ \/ \/ \
4 10 11 6
/ \ / \ / \ / \
9 5 3 9 4

i 层有 i 个数字, 从第 1 层到第 n 层,每次只能向下走一个数字, 求解所有路径中, 和最大是多少?

首先,我们使用一个二维数组去存储这个树塔,那么对于某个节点 a[i,j] ,它的左右子树如下:

a[i][j]
| \
a[i+1][j] a[i+1][j+1]

定义状态:

dp[i,j] 表示从(i,j)出发,到达底层的所有路径中得到的最大和

显然,边界条件为:

dp[level-1][j] = a[level-1][j], 0<=j<level

状态转移函数:

dp[i,j] = a[i,j] + max(dp[i+1,j], dp[i+1,j+1]), 0<=i<levl-1

显然,填表的顺序是自底向上。

完整代码:

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <vector>
#define MAXR 100
#define MAXC 100
using namespace std;
int a[MAXR][MAXC] = {{0}};
int solve(int level)
{
vector<vector<int>> dp(level + 1, vector<int>(level + 1, 0));
auto &v = dp.back();
for (int j = 1; j <= level; j++)
v[j] = a[level][j];
for (int i = level - 1; i >= 1; i--)
{
for (int j = 1; j <= i; j++)
{
dp[i][j] = max(dp[i + 1][j], dp[i + 1][j + 1]) + a[i][j];
}
}
for (int i = 1; i <= level; i++)
{
for (int j = 1; j <= i; j++)
{
cout << setw(3) << dp[i][j];
}
cout << endl;
}
return dp[1][1];
}
void print(int level)
{
for (int i = 1; i <= level; i++)
{
for (int j = 1; j <= i; j++)
cout << setw(4) << a[i][j];
cout << endl;
}
}
int main()
{
int level;
cin >> level;
for (int i = 1; i <= level; i++)
{
for (int j = 1; j <= i; j++)
cin >> a[i][j];
}
cout << solve(level);
} /*
a[i][j]
/ \
a[i+1][j] a[i+1][j+1] dp[i,j] 表示从(i,j)出发,到达底层的所有路径中得到的最大和
dp[1,1] = max(dp[2,1], dp[2,2])+a[1,1]
dp[i,j] = max(dp[i+1,j], dp[i+1,i+1])+a[i,j]
...
dp[r,c] = a[r,c]
*/ /*
Sample1:
5
5
8 3
12 7 16
4 10 11 6
9 5 3 9 4
*/

0/1背包问题

给定背包容量 C ,物品体积 volume[n],物品价值 value[n]每个物品只有一个

求:在背包容量允许的情况下,装入背包物品的最大价值。

定义状态:

dp[i,j] 表示: 在背包容量为 j 的, 供选择物品为前 i 项, 装入背包的最大价值。

状态转移方程:

dp[i,j] = 0                                               if i=0 or j=0
= dp[i-1,j] if j < volume[i]
= max(dp[i-1,j], dp[i-1,j-volume[i]] + value[i]) if j > volume[i]

完整代码:

#include <iostream>
#include <vector>
#include "leetcode.h"
using namespace std;
const int items = 4;
const int C = 9;
int volume[items + 1] = {-1, 2, 3, 4, 5};
int values[items + 1] = {-1, 3, 4, 5, 7};
vector<vector<int>> dp(items + 1, vector<int>(C + 1, 0));
int solve()
{
for (int i = 1; i <= items; i++)
{
for (int j = 1; j <= C; j++)
{
if (j < volume[i])
dp[i][j] = dp[i - 1][j];
else
{
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - volume[i]] + values[i]);
}
}
}
printvv(dp);
return dp.back().back();
}
int main()
{ cout << solve() << endl;
}

矩阵链相乘

如果两个矩阵可以相乘,那么必然满足:

M1[a,b]
M2[b,c]

此外, M1与M2相乘, 所计算的乘法次数为: a*b*c。所得到的矩阵为 M3[a,c]

给定矩阵:

M1 2*10
M2 10*2
M3 2*10

计算:(M1M2)M3,乘法次数为:2*2*10+2*10*2 = 80

计算:M1(M2M3),乘法次数为:10*10*2+2*10*10 = 400

在这里,我们想找到一种计算顺序,使得计算:

M1 M2 M3 ... Mn

的乘法次数达到最小。

首先,由于矩阵相乘必须满足:M1的列数等于M2的行数。

我们采用下面的数据结构去存储 n 个矩阵的行数与列数。

int nums[N+1]
nums[i] 表示 Mi 的行数,
nums[i+1] 表示 Mi 的列数.
nums[n] 表示 M(n-1) 的列数

然后,定义问题的状态:

dp[i,j] 表示 Mi ... Mj 的最小乘法次数

边界条件:

dp[i,i] = 0

下面推导状态转移方程,考虑整数 k (i<k<=j),将 M[i,j] 的乘法拆分为三步:

  • 计算 A = M[i,k-1] ,A的行列分别为:nums[i], nums[k]
  • 计算 B = M[k,j],B的行列分别为:nums[k], nums[j+1]
  • 计算 A*B

由此可知,M[i,j]的乘法次数为:

[i,k-1] + [k,j] + nums[i] * nums[j+1] * nums[k]

因此 ,状态转移方程为:

dp[i,j] = min(dp[i,k] + dp[k,j] + nums[i] * nums[j+1] * nums[k]),	i<k<=j

看到这里,可能有点头大,不知道怎么去填 dp 表。其实是按“斜线”的顺序填入,与 最长回文子串 类似。

下面来简单看一下过程,假设有五个矩阵( M0 M1 M2 M3 M4 )相乘,其行列存储如下:

int nums[N + 1] = {5, 10, 4, 6, 10, 2};

边界条件初始化:

M 0 1 2 3 4
0 0
1 * 0
2 * * 0
3 * * * 0
4 * * * * 0
==>
M 0 1 2 3 4
0 0 200 320 ?
1 * # ### 640
2 * * # 240 ###
3 * * * 0 ###
4 * * * * #

以上述 dp[0,3] 为例,说明递推过程:

dp[0,3] = min
(
dp[0,0] + dp[1,3] + ...
dp[0,1] + dp[2,3] + ...
dp[0,2] + dp[3,3] + ...
)

其中,... 代表三个 nums[i] 相乘的常数项,可自行对照递推式代入。在这里,需要特别注意 dp[i,j]的依赖项(实际上是“同行”与“同列” 的所有元素)。

完整代码:

#include "leetcode.h"
#include <cmath>
#define N 5
#define MAXINT 9999
int nums[N + 1] = {5, 10, 4, 6, 10, 2};
vector<vector<int>> dp(N, vector<int>(N, MAXINT));
int solve()
{
for (int i = 0; i < N; i++)
dp[i][i] = 0;
for (int d = 1; d < N; d++)
{
for (int i = 0; i < (N - d); i++)
{
int j = i + d;
for (int k = i + 1; k <= j; k++)
{
dp[i][j] = min(dp[i][j], dp[i][k - 1] + dp[k][j] + nums[i] * nums[k] * nums[j + 1]);
}
}
}
for (auto &v : dp)
{
for (auto x : v)
{
cout << setw(4);
if (x == MAXINT)
cout << '*';
else
{
cout << x;
}
}
cout << endl;
}
return dp[0][N - 1];
}
int main()
{
cout << solve() << endl;
} /*
M0...M(n-1): nums[i] 表示 Mi 的行数,
nums[i+1] 表示 Mi 的列数.
nums[n] 表示 M(n-1) 的列数
*/
/*
dp[i,j] 表示 Mi ... Mj 的最小乘法次数
边界: dp[i,i] = 0;
递推: dp[i,j] = min(dp[i,k-1]+dp[k,j] + a[i]*a[k]*a[j+1]), i<k<=j
==>
dp[0,n-1] = min(dp[0, k-1] + dp[k,n-1] + a[0]*a[k]*a[n]), 0<k<=n-1
*/

总结

没总结,有空再补充。

[算法总结] 动态规划 (Dynamic Programming)的更多相关文章

  1. Python算法之动态规划(Dynamic Programming)解析:二维矩阵中的醉汉(魔改版leetcode出界的路径数)

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_168 现在很多互联网企业学聪明了,知道应聘者有目的性的刷Leetcode原题,用来应付算法题面试,所以开始对这些题进行" ...

  2. 动态规划(Dynamic Programming)算法与LC实例的理解

    动态规划(Dynamic Programming)算法与LC实例的理解 希望通过写下来自己学习历程的方式帮助自己加深对知识的理解,也帮助其他人更好地学习,少走弯路.也欢迎大家来给我的Github的Le ...

  3. 算法导论学习-Dynamic Programming

    转载自:http://blog.csdn.net/speedme/article/details/24231197 1. 什么是动态规划 ------------------------------- ...

  4. 动态规划Dynamic Programming

    动态规划Dynamic Programming code教你做人:DP其实不算是一种算法,而是一种思想/思路,分阶段决策的思路 理解动态规划: 递归与动态规划的联系与区别 -> 记忆化搜索 -& ...

  5. 6专题总结-动态规划dynamic programming

    专题6--动态规划 1.动态规划基础知识 什么情况下可能是动态规划?满足下面三个条件之一:1. Maximum/Minimum -- 最大最小,最长,最短:写程序一般有max/min.2. Yes/N ...

  6. [算法]动态规划(Dynamic programming)

    转载请注明原创:http://www.cnblogs.com/StartoverX/p/4603173.html Dynamic Programming的Programming指的不是程序而是一种表格 ...

  7. 动态规划算法详解 Dynamic Programming

    博客出处: https://blog.csdn.net/u013309870/article/details/75193592 前言 最近在牛客网上做了几套公司的真题,发现有关动态规划(Dynamic ...

  8. 动态规划 Dynamic Programming

    March 26, 2013 作者:Hawstein 出处:http://hawstein.com/posts/dp-novice-to-advanced.html 声明:本文采用以下协议进行授权: ...

  9. 最优化问题 Optimization Problems & 动态规划 Dynamic Programming

    2018-01-12 22:50:06 一.优化问题 优化问题用数学的角度来分析就是去求一个函数或者说方程的极大值或者极小值,通常这种优化问题是有约束条件的,所以也被称为约束优化问题. 约束优化问题( ...

随机推荐

  1. selenium+requests进行cookies保存读取操作

    看这篇文章之前大家可以先看下我的上一篇文章:cookies详解 本篇我们就针对上一篇来说一下cookies的基本应用 使用selenium模拟登陆百度 from selenium import web ...

  2. C:指针习题

    1. 请指出以下程序段中的错误. 程序中的错误有:(1)p=i:类型不匹配.(2)q=*p:q 是指针,*p 是指针 p 指向变量的值.(3)t='b':t 是指针类型. 解释:指针变量是一种存放地址 ...

  3. sed 分组替换

    将文件以help开头的句子前加# [root@localhost]# cat a.txthelp b helphelp1helphelp2help c help[root@localhost]# se ...

  4. Redis list实现原理 - 双向循环链表

    双向链表 双向表示每个节点知道自己的直接前驱和直接后继,每个节点需要三个域 查找方向可以是从左往右也可以是从右往左,但是要实现从右往左还需要终端节点的地址,所以通常会设计成双向的循环链表; 双向的循环 ...

  5. 放弃了程序员互联网高薪,跑去事业单位做IT的尴尬

    “你是程序员对吧?”“是啊,怎么了?”“那你帮我修一下电脑吧.”我原来也是一个重点大学毕业,基本上事业里面搞IT就干这些事情,要是以前,我肯定会想,我是程序员和修电脑有啥关系. 但是自从进了事业单位, ...

  6. 从0开发3D引擎(补充):介绍领域驱动设计

    我们使用领域驱动设计(英文缩写为DDD)的方法来设计引擎,在引擎开发的过程中,领域模型会不断地演化. 本文介绍本系列使用的领域驱动设计思想的相关概念和知识点,给出了相关的资料. 上一篇博文 从0开发3 ...

  7. Spring 事务传播机制和数据库的事务隔离级别

    Propagation(事务传播属性) 类别 传播类型 说明 支持当前事务 REQUIRED 如果当前没有事务,就新建一个事务.@Transaction的默认选择 支持当前事务 SUPPORTS 就以 ...

  8. docker部署tensorflow serving以及模型替换

    Using TensorFlow Serving with Docker 1.Ubuntu16.04下安装docker ce 1-1:卸载旧版本的docker sudo apt-get remove ...

  9. js中的预编译

    预编译 js执行顺序: 词法/语法分析 预编译 解释执行 js中存在预编译 function demo() { console.log('I am demo'); } demo(); //I am d ...

  10. 【原创】(四)Linux进程调度-组调度及带宽控制

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...