动态规划中递推式的求解方法不是动态规划的本质。
dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.动态规划是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。
给定一个数列,长度为N,要解决这个问题,我们首先要定义这个问题和这个问题的子问题。
求这个数列的最长上升(递增)子数列(LIS)的长度.
以 1 7 2 8 3 4 为例。
这个数列的最长递增子数列是 1 2 3 4,长度为4;
次长的长度为3, 包括 1 7 8; 1 2 3 等.
给定一个数列,长度为N,显然,这个新问题与原问题等价。
设为:以数列中第k项结尾的最长递增子序列的长度.
求 中的最大值.
给定一个数列,长度为N,这个新定义与原问题的等价性也不难证明,请读者体会一下。
设为:
在前i项中的,长度为k的最长递增子序列中,最后一位的最小值. .
若在前i项中,不存在长度为k的最长递增子序列,则为正无穷.
求最大的x,使得不为正无穷。
设为:以数列中第k项结尾的最长递增子序列的长度.设A为题中数列,状态转移方程为:
(根据状态定义导出边界情况)用文字解释一下是:
设为:在数列前i项中,长度为k的递增子序列中,最后一位的最小值设A为题中数列,状态转移方程为:
若则(边界情况需要分类讨论较多,在此不列出,需要根据状态定义导出边界情况。)
否则:
分治在求解每个子问题的时候,都要进行一遍计算这就像说多加辣椒的菜就叫川菜,多加酱油的菜就叫鲁菜一样,是存在误解的。
动态规划则存储了子问题的结果,查表时间为常数
文艺的说,动态规划是寻找一种对问题的观察角度,让问题能够以递推(或者说分治)的方式去解决。寻找看问题的角度,才是动态规划中最耀眼的宝石!(大雾)
#include
using namespace std;
int lis(int A[], int n)
{
int *d = new int[n];
int len = 1;
for(int i=0; id[i])
d[i] = d[j] + 1;
if(d[i]>len) len = d[i];
}
delete[] d;
return len;
}
int main(){
int A[] = {5, 3, 4, 8, 6, 7};
cout<
该算法的时间复杂度是O(n^2 ),并不是最优的解法。还有一种很巧妙的算法可以将时间复杂度降到O(nlogn),网上已经有各种文章介绍它,主要是维护一个上升序列表,然后对每一个新加入的数字尝试去二分插入。
动态规划(Dynamic Programming,简称DP),虽然抽象后进行求解的思路并不复杂,但具体的形式千差万别,找出问题的子结构以及通过子结构重新构造最优解的过程很难统一,并不像回溯法具有解决绝大多数问题的银弹(全面解析回溯法:算法框架与问题求解)。为了解决动态规划问题,只能靠多练习、多思考了。本文主要是对一些常见的动态规划题目的收集,希望能有所帮助。难度评级受个人主观影响较大,仅供参考。
目录(点击跳转)
动态规划求解的一般思路
备忘录法
1.硬币找零
扩展1:单路取苹果
扩展2:装配线调度
2.字符串相似度/编辑距离(edit distance)
应用1:子串匹配
应用2:最长公共子序列
3.最长公共子序列(Longest Common Subsequence,lcs)
扩展1:输出所有lcs
扩展2:通过LCS获得最长递增自子序列
4.最长递增子序列(Longest Increasing Subsequence,lis)
扩展:求解lis的加速
5.最大连续子序列和/积
扩展1:正浮点数数组求最大连续子序列积
扩展2:任意浮点数数组求最大连续子序列积
6.矩阵链乘法
扩展:矩阵链乘法的备忘录解法(伪码)
7.0-1背包问题
8.有代价的最短路径
9.瓷砖覆盖(状态压缩DP)
10.工作量划分
11.三路取苹果
参考资料
附录1:其他的一些动态规划问题与解答(链接)
附录2:《算法设计手册》第八章 动态规划 面试题解答
动态规划求解的一般思路:
判断问题的子结构(也可看作状态),当具有最优子结构时,动态规划可能适用。
求解重叠子问题。一个递归算法不断地调用同一问题,递归可以转化为查表从而利用子问题的解。分治法则不同,每次递归都产生新的问题。
重新构造一个最优解。
备忘录法:
动态规划的一种变形,使用自顶向下的策略,更像递归算法。
初始化时表中填入一个特殊值表示待填入,当递归算法第一次遇到一个子问题时,计算并填表;以后每次遇到时只需返回以前填入的值。
实例可以参照矩阵链乘法部分。
难度评级:★
假设有几种硬币,如1、3、5,并且数量无限。请找出能够组成某个数目的找零所使用最少的硬币数。
解法:
用待找零的数值k描述子结构/状态,记作sum[k],其值为所需的最小硬币数。对于不同的硬币面值coin[0...n],有sum[k] = min(sum[k-coin[0]] , sum[k-coin[1]], ...)+1。对应于给定数目的找零total,需要求解sum[total]的值。
typedef struct {
int nCoin; //使用硬币数量
//以下两个成员是为了便于构造出求解过程的展示
int lastSum;//上一个状态
int addCoin;//从上一个状态达到当前状态所用的硬币种类
} state;
state *sum = malloc(sizeof(state)*(total+1));
//init
for(i=0;i<=total;i++)
sum[i].nCoin = INF;
sum[0].nCoin = 0;
sum[0].lastSum = 0;
for(i=1;i<=total;i++)
for(j=0;j=0 && sum[i-coin[j]].nCoin+1
通过sum[total].lastSum和sum[total].addCoin,很容易通过循环逆序地或者编写递归调用的函数正序地输出从结束状态到开始状态使用的硬币种类。以下各题输出状态转换的方法同样,不再赘述。下面为了方便起见,有的题没有在构造子结构的解时记录状态转换,如果需要请类似地完成。
扩展:
(1)一个矩形区域被划分为N*M个小矩形格子,在格子(i,j)中有A[i][j]个苹果。现在从左上角的格子(1,1)出发,要求每次只能向右走一步或向下走一步,最后到达(N,M),每经过一个格子就把其中的苹果全部拿走。请找出能拿到最多苹果数的路线。
难度评级:★
分析:
这道题中,当前位置(i,j)是状态,用M[i][j]来表示到达状态(i,j)所能得到的最多苹果数,那么M[i][j] = max(M[i-1][j],M[i][j-1]) + A[i][j] 。特殊情况是M[1][1]=A[1][1],当i=1且j!=1时,M[i][j] = M[i][j-1] + A[i][j];当i!=1且j=1时M[i][j] = M[i-1][j] + A[i][j]。
求解程序略。
(2)装配线调度(《算法导论》15.1)
难度评级:★
难度评级:★
对于序列S和T,它们之间距离定义为:对二者其一进行几次以下的操作(1)删去一个字符;(2)插入一个字符;(3)改变一个字符。每进行一次操作,计数增加1。将S和T变为同一个字符串的最小计数即为它们的距离。给出相应算法。
解法:
将S和T的长度分别记为len(S)和len(T),并把S和T的距离记为m[len(S)][len(T)],有以下几种情况:
如果末尾字符相同,那么m[len(S)][len(T)]=m[len(S)-1][len(T)-1];
如果末尾字符不同,有以下处理方式
修改S或T末尾字符使其与另一个一致来完成,m[len(S)][len(T)]=m[len(S)-1][len(T)-1]+1;
在S末尾插入T末尾的字符,比较S[1...len(S)]和S[1...len(T)-1];
在T末尾插入S末尾的字符,比较S[1...len(S)-1]和S[1...len(T)];
删除S末尾的字符,比较S[1...len(S)-1]和S[1...len(T)];
删除T末尾的字符,比较S[1...len(S)]和S[1...len(T)-1];
总结为,对于i>0,j>0的状态(i,j),m[i][j] = min( m[i-1][j-1]+(s[i]==s[j])?0:1 , m[i-1][j]+1, m[i][j-1] +1)。
这里的重叠子结构是S[1...i],T[1...j]。
以下是相应代码。考虑到C语言数组下标从0开始,做了一个转化将字符串后移一位。
#include
#include
#define MAXLEN 20
#define MATCH 0
#define INSERT 1
#define DELETE 2
typedef struct {
int cost;
int parent;
} cell;
cell m[MAXLEN+1][MAXLEN+1];
int match(char a,char b)
{
//cost of match
//match: 0
//not match:1
return (a==b)?0:1;
}
int string_compare(char *s,char *t)
{
int i,j,k;
int opt[3];
//row_init(i);
for(i=0;i<=MAXLEN;i++) {
m[i][0].cost = i;
if(i==0)
m[i][0].parent = -1;
else
m[i][0].parent = INSERT;
}
//column_init(i);
for(i=0;i<=MAXLEN;i++) {
m[0][i].cost = i;
if(i==0)
continue;
else
m[0][i].parent = INSERT;
}
char m_s[MAXLEN+1] = " ",m_t[MAXLEN+1] =" ";
strcat(m_s,s);
strcat(m_t,t);
for(i=1;i<=strlen(s);i++)
{
for(j=1;j<=strlen(t);j++) {
opt[MATCH] = m[i-1][j-1].cost + match(m_s[i],m_t[j]);
opt[INSERT] = m[i][j-1].cost + 1;
opt[DELETE] = m[i-1][j].cost + 1;
m[i][j].cost = opt[MATCH];
m[i][j].parent = MATCH;
for(k=INSERT;k<=DELETE;k++)
if(opt[k]
应用:
(1)子串匹配
难度评级:★★
修改两处即可进行子串匹配:
row_init(int i)
{
m[0][i].cost = 0; /* note change */
m[0][i].parent = -1; /* note change */
}
goal_cell(char *s, char *t, int *i, int *j)
{
int k; /* counter */
*i = strlen(s) - 1;
*j = 0;
for (k=1; k修改部分
如果j= strlen(S) - strlen(T),那么说明T是S的一个子串。
(这部分是根据《算法设计手册》8.2.4和具体实例Skiena与Skienaa、Skiena与somta的分析获得的,解释不够全面,可能有误,请注意)
(2)最长公共子序列
难度评级:★★
将match时不匹配的代价转化为最大长度即可:
int match(char c, char d)
{
if (c == d) return(0);
else return(MAXLEN);
}
此时,最小值是两者不同部分的距离。
(这部分同样也不好理解,对于最长公共子序列,建议直接使用下一部分中的解法)
扩展:
如果在编辑距离中各个操作的代价不同,如何寻找最小代价?