动态规划(Dynamic Programming,简称DP),虽然抽象后进行求解的思路并不复杂,但具体的形式千差万别,找出问题的子结构以及通过子结构重新构造最优解的过程很难统一,并不像回溯法具有解决绝大多数问题的银弹(本文主要是对一些常见的动态规划题目的收集,希望能有所帮助。难度评级受个人主观影响较大,仅供参考。
目录(点击跳转)
3.最长公共子序列(Longest Common Subsequence,lcs)
4.最长递增子序列(Longest Increasing Subsequence,lis)
判断问题的子结构(也可看作状态),当具有最优子结构时,动态规划可能适用。
求解重叠子问题。一个递归算法不断地调用同一问题,递归可以转化为查表从而利用子问题的解。分治法则不同,每次递归都产生新的问题。
重新构造一个最优解。
动态规划的一种变形,使用自顶向下的策略,更像递归算法。
初始化时表中填入一个特殊值表示待填入,当递归算法第一次遇到一个子问题时,计算并填表;以后每次遇到时只需返回以前填入的值。
实例可以参照矩阵链乘法部分。
难度评级:★
假设有几种硬币,如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<n;j++)
if(i-coin[j]>=0 && sum[i-coin[j]].nCoin+1<sum[i].nCoin)
{
sum[i].nCoin = sum[i-coin[j]].nCoin+1;
sum[i].lastSum = j;
sum[i].addCoin = coin[j];
}
if(sum[total].nCoin == INF)
{
printf("can't make change.\n");
return 0;
}
else
//output
;
通过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.字符串相似度/编辑距离(edit distance)
难度评级:★
对于序列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 <stdio.h>
#include <string.h>
#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]<m[i][j].cost)
{
m[i][j].cost = opt[k];
m[i][j].parent = k;
}
}
}
i--,j--;
//goal_cell(s,t,&i,&j);
return m[i][j].cost;
}
int main() {
char t[] = "you should not";
char p[] = "thou shalt not";
int n = string_compare(t,p);
printf("%d\n",n);
}