动态规划算法(@背包问题)
动态规划(Dynamic programming)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许 多 子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同 一个 子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
关于动态规划最经典的问题当属背包问题。
算法步骤:
1. 最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
2. 子 问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多 次。 动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题 时,只是 在表格中简单地查看一下结果,从而获得较高的效率。
eg:
1 #include <iostream> 2 #include <iomanip> 3 #include <vector> 4 #include <cmath> 5 using namespace std; 6 class Item 7 { 8 public: 9 int weight; 10 int value; 11 Item(int w,int v) 12 { 13 weight=w; 14 value=v; 15 } 16 }; 17 vector<Item> items; 18 const int PACKAGE_WEIGHT=10; 19 20 void init() 21 { 22 Item item(5,10); 23 items.push_back(item); 24 item.weight=4;item.value=40; 25 items.push_back(item); 26 item.weight=6;item.value=30; 27 items.push_back(item); 28 item.weight=3;item.value=50; 29 items.push_back(item); 30 } 31 void calResult(vector<Item> &itemlist) 32 { 33 cout<<endl; 34 cout<<setw(5)<<"w"<<setw(5)<<"v"<<endl; 35 for(int i=0;i<itemlist.size();i++) 36 { 37 Item tItem=itemlist.at(i); 38 cout<<setw(5)<<tItem.weight<<setw(5)<<tItem.value<<endl; 39 } 40 41 int item_count=itemlist.size(); 42 int **resultArray=new int*[item_count+1]; 43 for(int i=0;i<item_count+1;i++) 44 { 45 resultArray[i]= new int[PACKAGE_WEIGHT+1]; 46 } 47 48 for(int row=0;row<item_count+1;row++) 49 { 50 for(int col=0;col<PACKAGE_WEIGHT+1;col++) 51 { 52 resultArray[row][col]=0; 53 } 54 } 55 56 //real calculate 57 for(int row=1;row<item_count+1;row++) 58 { 59 for(int col=1;col<PACKAGE_WEIGHT+1;col++) 60 { 61 if(col<itemlist[row-1].weight) 62 resultArray[row][col]=resultArray[row-1][col]; 63 else 64 { 65 resultArray[row][col]=max(resultArray[row-1][col],\ 66 resultArray[row-1][col-itemlist[row-1].weight]+itemlist[row-1].value); 67 68 } 69 } 70 } 71 72 //print result 73 cout<<endl; 74 for(int row=0;row<item_count+1;row++) 75 { 76 for(int col=0;col<PACKAGE_WEIGHT+1;col++) 77 { 78 cout<<setw(5)<<resultArray[row][col]; 79 } 80 cout<<endl; 81 } 82 } 83 int main(int argc,char *argv[]) 84 { 85 init(); 86 calResult(items); 87 88 return 0; 89 }
游船费问题
问题描述
某旅游城市在长江边开辟了若干个旅游景点。一个游船俱乐部在这些景点都设置了游艇出租站。游客可在这些游船出租站租用游船,并在下游的任何一个游船出租站 归还游船,从一个游船出租站到下游的游船出租站间的租金明码标价。你的任务是为游客计算从起点到终点站间的最小租船费用。
输入
输入文件有若干组数据,每组测试数据的第一行上有一个整数n(1<=n<=100),表示上游的起点站0到下游有n个游船出租站
1,2,。。。,n。接下来有n行,这n行中的第1行有n个整数,分别表示第0站到第1,2,3,。。。,n站间的游船租金;第2行有n-1个整 数,分别表示第1站到第2,3,4,。。。n站间的游船租金;。。。。;第n-1行有2个整数,表示第n-2站到第n-1、n站间的游船租金;第n行有1 个整数,表示第n-1站到第n站间的游船租金。一行上有两个整数之间是用空格隔开的。两组测试数据之间无空行。
输出
对输入文件中的每组测试数据,先在一行上输出“Case #:”,其中“#”测试数据的编号(从1开始)。再输出一行,内容是该情况下游客从起点站到终点站间的最少租船费用。
输入样例
3
2 3 6
1 3
2
输出样例
Case 1:
5
1 #include<iostream> 2 #include<cstring> 3 #include<climits> 4 using namespace std; 5 int main() 6 { 7 int cost[100][101]; 8 int mincost[101]; 9 int n; 10 memset(cost,0,sizeof(cost)); 11 cin >> n; 12 13 for(int i = 0;i < n; i++) 14 for(int j = i + 1; j <= n; j++) 15 cin >> cost[i][j]; 16 17 mincost[0] = 0; 18 for(int i = 1; i < n+1; i++) 19 mincost[i] = LONG_MAX; 20 21 for(int i=1;i<=n;i++) 22 for(int j=0;j<i;j++) 23 if(mincost[j]+cost[j][i]<mincost[i]) 24 mincost[i]=mincost[j]+cost[j][i]; 25 cout << endl << mincost[n]; 26 }
详细解释文:http://blog.jobbole.com/83949/ 感谢章主.
一、基本概念
动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。
二、基本思想与策略
基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
三、适用的情况
能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
四、求解的基本步骤
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
图1 动态规划决策过程示意图
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。
实际应用中可以按以下几个简化的步骤进行设计:
(1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
五、算法实现的说明
动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。
使用动态规划求解问题,最重要的就是确定动态规划三要素:
(1)问题的阶段 (2)每个阶段的状态
(3)从前一个阶段转化到后一个阶段之间的递推关系。
递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。
确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}
六、动态规划算法基本框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
for (j=1; j<=m; j=j+1) // 第一个阶段 xn[j] = 初始值; for (i=n-1; i>=1; i=i-1) // 其他n-1个阶段 for (j=1; j>=f(i); j=j+1) //f(i)与i有关的表达式 xi[j]=j=max(或min){g(xi-1[j1:j2]), ......, g(xi-1[jk:jk+1])}; t = g(x1[j1:j2]); // 由子问题的最优解求解整个问题的最优解的方案 print(x1[j1]); for (i=2; i<=n-1; i=i+1) { t = t-xi-1[ji]; for (j=1; j>=f(i); j=j+1) if (t=xi[ji]) break ; } |