动态规划算法介绍与相关问题。

动态规划(dynamic programming DP)是个很广的一个话题,在除了在我们本身计算机方面起着很重要的作用之外,在其它学科技术领域应用很广,如在经济管理、生产调度、工程技术和最优控制等方面得到了广泛的应用。例如最短路线、库存管理、资源分配、设备更新、排序、装载等问题,用动态规划方法比用其它方法求解更为方便。然而,作为最重要的算法之一,学好这一算法,不仅对以后解决某些实际问题,通过DP方法,建模,编程。可以很好锻炼自己解决问题的思维方式。所以在以后的学习中,一步步提高自己用DP方法建模编程来解决实际中的问题,(PS:其实就是所谓的笔试面试中,遇到考官给的实际应用题,如何采用DP方法在规定的时间内有效的解决这些实际问题,或许在以后工作生活中,也会用到DP这种思维方式吧)。好的,进入正题。

历史:(参考:http://www.cppblog.com/Fox/archive/2008/05/07/Dynamic_programming.html对应的英文原版维基百科:http://en.wikipedia.org/wiki/Dynamic_programming)

在数学与计算机科学领域,动态规划用于解决那些可分解为重复子问题(overlapping subproblems,想想递归求阶乘吧)并具有最优子结构(optimal substructure,想想最短路径算法)(如下所述)的问题,动态规划比通常算法花费更少时间。

上世纪40年代,Richard Bellman最早使用动态规划这一概念表述通过遍历寻找最优决策解问题的求解过程。1953年,Richard Bellman将动态规划赋予现代意义,该领域被IEEE纳入系统分析和工程中。为纪念Bellman的贡献,动态规划的核心方程被命名为贝尔曼方程,该方程以递归形式重申了一个优化问题。

在“动态规划”(dynamic programming)一词中,programming与“计算机编程”(computer programming)中的programming并无关联,而是来自“数学规划”(mathematical programming),也称优化。因此,规划是指对生成活动的优化策略。举个例子,编制一场展览的日程可称为规划。 在此意义上,规划意味着找到一个可行的活动计划。

由于时间关系就直接给出参看链接了:详细介绍DP算法:(以后有时间,看完后,自己总结总结)http://www.ahhf45.com/info/Data_Structures_and_Algorithms/algorithm/technique/dynamic_programming/index.htm

相关问题:(代码归作者所有,请声明。相互参考学习)

背包问题:(0/1背包)

/***************************************************************************
 *@coder:刘抟羽 wanda1416 @editor:weedge
 *date:2011/6/28 
 *comment:
 背包问题作为NP完全问题,暂时不存在多项式时间算法。动态规划属于背包问题求解最优解的可行方法之一。
 此外,求解背包问题最优解还有搜索法等,近似解还有贪心法等,分数背包问题有最优贪心解等。 背包问题具有最优子结构和重叠子问题。
 动态规划一般用于求解背包问题中的整数背包问题(即每种物品所选的个数必须是整数)。

题目:(0/1背包问题)
  有N件物品和一个容量为V的背包。第i件物品的重量是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

基本思路:(建模)
  这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
  用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移(DP)方程式便是:
									f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]} 。 
	可以压缩空间,f[v]=max{f[v],f[v-c[i]]+w[i]}这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。
	所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),
	那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,
	价值为f[i-1][v];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,
	此时能获得的最大价值就是f [i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。
  注意f[v]有意义当且仅当存在一个前i件物品的子集,其费用总和为v。所以按照这个方程递推完毕后,最终的答案并不一定是f[N] [V],
	而是f[N][0..V]的最大值。如果将状态的定义中的“恰”字去掉,在转移方程中就要再加入一项f[v-1],这样就可以保证f[N] [V]就是最后的答案。
	至于为什么这样就可以,由你自己来体会了。

优化空间复杂度:
  以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(N)。
  先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1..N,每次算出来二维数组f[i][0..V]的所有值。
	那么,如果只用一个数组f [0..V],能不能保证第i次循环结束后f[v]中表示的就是我们定义的状态f[i][v]呢?
  f[i][v]是由f[i-1][v]和f [i-1][v-c[i]]两个子问题递推而来,能否保证在推f[v]时(也即在第i次主循环中推f[v]时)
	能够得到f[v]和f[v -c[i]]的值呢?事实上,这要求在每次主循环中我们以v=V..0的顺序推f[v],
	这样才能保证推f[v]时f[v-c[i]]保存的是状态f[i-1][v-c[i]]的值。伪代码如下:
  for i=1..N
  for v=V..0
  f[v]=max{f[v],f[v-c[i]]+w[i]};
  其中的f[v]=max{f[v],f[v-c[i]]}一句恰就相当于我们的转移方程f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]},因为现在的
  f[v-c[i]]就相当于原来的f[i-1][v-c[i]]。如果将v的循环顺序从上面的逆序改成顺序的话,
	那么则成了f[i][v]由f[i][v-c[i]]推知,与本题意不符,但它却是另一个重要的背包问题P02最简捷的解决方案,
	故学习只用一维数组解01背包问题是十分必要的。
****************************************************************************************/

#include <iostream>
using namespace std;
#define digui
#define MAXSIZE 1000
/****************************
递归实现背包问题://BY wanda1416
	现在设A[i][v]表示在剩余空间为v时选取当前物品i的最大值,
	B[i][v]表示不选取当前物品i的最大值,
	所以总的最大值必然是max(A[n][v], B[n][v])
********************************/
int A[MAXSIZE+1][MAXSIZE+1],B[MAXSIZE+1][MAXSIZE+1];
int c[MAXSIZE+1];//第i件物品的重量
int	w[MAXSIZE+1];//第i件物品的价值

int F(int n,int v){
	if(n==0) return 0;
	if( !A[n][v] && v >= c[n])
		A[n][v] = F(n-1,v-c[n]) +w[n];
	if(!B[n][v])
		B[n][v] = F(n-1,v);
	return A[n][v]>B[n][v]?A[n][v]:B[n][v];
}

/*非递归实现 //BY 刘抟羽*/
int f[MAXSIZE + 1];//f[i][v]:前i件物品恰放入一个容量为v的背包可以获得的最大价值
int main()
{
	int N, V;
	cin >> N >> V;
	int i = 1;
	for (; i <= N; ++i)
	{
		cin >> c[i] >> w[i];
	}
	memset(f, 0, sizeof(f));//初始矩阵f中的值全为0
	//f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]} 
	for (i = 1; i <= N; ++i)
	{
		for (int v = V; v >= c[i]; --v) //c[i]可优化为bound, bound = max {V - sum c[i,...n], c[i]}
		{
			f[v] = (f[v] > f[v - c[i]] + w[i] ? f[v] : f[v - c[i]] + w[i]);
		}
	}
	//当i=N时,可以跳出循环单独计算F[V]
	cout << f[V] << '\n';
#ifndef digui
	cout<<F(N,V)<<endl;
#endif
	return 0;
}

 

邮局问题:

/****************************************************************************************************************
 *@author: 上善若水qin.yusen  @editor:weedge
 *@date: 2011/06/28
 *@comment:
	邮局问题的一般DP问题解法
	题目来源(阿里云的那个原题):
	12个工厂分布在一条东西向高速公路的两侧,工厂距离公路最西端的距离分别是0、4、5、10、12、18、27、30、31、38、39、47.
	在这12个工厂中选取3个原料供应厂,使得剩余工厂到最近的原料供应厂距离之和最短,问应该选哪三个厂。

从模型上来看,应该来自于IOI2000 (International Olympiad in Informatics)国际信息学奥林匹克竞赛的一个经典DP题目,
原题表述为英文,可以直接搜IOI200 POST OFFICE查看

原题描述,大意如下:
	在一条高速公路边上有V个村庄,在这些村庄中选出P个建立邮局,邮局建立在村庄里,每个村庄使用离他最近的那个邮局。
求一种方案,令所有村庄到各自所使用的邮局的距离总和最小。
	输入:村庄数目V和邮局数目P,1<=V<=300,1<=P<=30,P<=V;按递增顺序给出V个村庄的坐标x1,x2,…xV,1<=xi<=10000。
	输出:所有村庄到各自所使用的邮局的距离总和S; 

一。前提:
OK,既然说到了DP,就简单说一嘴,DP,即动态规划,从底向上或者从顶向下递推,需要有无后性和确定性,具体的,
可以去找些DP的资料来看,作为一个编程的人,不会DP会让人笑话的。
也就是将原问题分解为相似的子问题,在求解的过程中通过子问题的解求出原问题的解。问题进行分解归纳总结的过程:

另外提一嘴一个简单的不是小学就是初中的一个小数学奥利匹克的题目和结论
(不知道的面壁思过,或者问问儿子孙子之类的……,也可以自己证明)
题目:数轴上已知N个点x[1]~x[n],从中选择一个,使得这个点到其他各点之和最小,求这个点是哪个?
结论:当N=2k的时候,x[k]和x[k+1]两个点到其他各点距离之和最小,而且相等。
      当N=2k+1的时候,x[k+1]处到其他各点距离之和最小(为了方便计算和下标设置,
	  此结论被等同于当N=2k-1的时候,x[k]处到其他各点距离之和最小)

二。归纳总结的过程:(先从简单入口着手,逐步找最优解。)(n个村庄,m个邮局)

1.n个村庄,1个邮局的情况:
	我们已知,在1个村庄建立1个邮局必然是最优的,2个村庄建立1个邮局必然是最优的。
那么我们也可以通过上面结论,从此推算,3个村庄建立1个在中间的村庄建立是最优的,
	依次类推:对于n个村庄,建立1个邮局在中间的村庄建立是最优的。这是最初始最容易的情况(如上所述)。

2.n个村庄,2个邮局的情况:
	例如:4个村庄如何建立2个邮局是最优的呢?4个村庄建立2个邮局可以拆成,第1个村庄和后3个村庄,
前2个村庄和后2个村庄,前3个村庄和第4个村庄,3种情况,那么,4个村庄建立2个邮局的最优解,
一定是上述3种情况中的最优的那一个。
	依次类推:对于n个村庄,2个邮局的情况如下:
	n个村庄建立2个邮局可以拆成,前1个村庄和后n-1个村庄,...,前i个村庄建立1个邮局最优和后n-i个村庄建立1个邮局最优,
...,前n-1个村庄和后1个村庄;总共n-1种情况。那么,n个村庄建立2个邮局的最优解,一定是上述n-1种情况中的最优的那一个。

3.n个村庄,m个邮局的情况:
	依次类推,归纳总结得 前n个村庄,m个邮局最优解(即cost(n,m))的情况分为二个部分,
	第一部分在前面i个村庄中找到m-1个邮局的最优解;
	第二部分在剩余村庄中找到1个邮局的最优解。而i的范围(range)必须满足闭区间【m-1,n-1】;
	最后考虑i在这些情况下的最小值即最优的那一个。
如下的DP方程式,这样就会知道我们的DP方程是什么样的了。
	Cost[n,m] = min{Cost[i,m-1]+D[i+1,n]},其中i=m-1,m,…,n-1
	Cost[n,m]:在前n个村庄中建立m个邮局
	D[i,j]:在第i个到第j个村庄中建立1个邮局得到的最短距离总和。
	D[i,j]=D[i,j-1]+x[j]-x[(i+j)/2]数轴上算距离,如果不清楚,代入数字比划下,归纳总结证明下就知道了,初中知识。

当然在设计程序时,存在这么一个问题:Cost[n][m]只与Cost[n][m-1]有关,那么2个一维数组就可以替代,即:
Cost[n][2]: Cost[n][0]表示n个村庄中建立1个邮局; Cost[n][1]表示n个村庄建立2个邮局,
			。。。。。。。
			Cost[n][m-1]表示n个村庄中建立m-1个邮局; Cost[n][m]表示n个村庄建立m个邮局。(这么表示主要是节约空间)

题目对应POJ的地址,可以用这个提交测试代码和阅读原题。
http://poj.org/problem?id=1160
***********************************************************************************************************/

#include <iostream>
#define MAX_N 310
using namespace std;

/*非递归实现*/
int main()
{
	/*
	char a[8]; // holds two 4-byte ints

	//初始分配内存块:0xffffffff 0x00000000
	memset(a,0,4*sizeof(char));
	memset(a+4,0xff,4*sizeof(char));

	a[0]=0x01;
	a[1]=0xff;
	a[2]=0xff;
	a[3]=0xff;
	cout<<a[0]<<endl;//asc码。
	cout<<((int*)a)[0]<<endl;//-255的补码:0xffffff01  (machine is little endian)
	cout<<((int*)a)[1]<<endl;//初始化了,所以为:0xffffffff(-1的补码)
	cout<<((int*)a)[2]<<endl;//未初始化,(是否由编译器初始化了?)越界了,如果内存消耗完,程序崩溃。
	*/

    int i, j;
    int v, p, minCost = INT_MAX;
    int x[MAX_N + 1];//v个村庄的坐标
    int D[MAX_N + 1][MAX_N + 1]; //D[a, b]表示在a村庄和b村庄之间建立一个邮局可以得到的最短总距离,其保存在该数组中。(非递归)
	int last = 0, cur = 1;
    int cost[MAX_N + 1][2];//cost[range][last]和cost[range][cur] 即 保存上次最优解Cost[n][i-1] =》 Cost[n][i]
    
    //村庄数目V和邮局数目P
    cin>>v>>p;
	//按递增顺序给出V个村庄的坐标x1,x2,…xV,1<=xi<=10000。
    for(i = 1; i <= v; i++)
        cin>>x[i];
    
    memset(D, 0, sizeof(D));//初始矩阵D中的值全为0

	/**第一步首先计算D[i][j]:在第i个村庄到第j个村庄中选择一个村庄建立一个邮局到各个邮局的距离和最短.
	通过数轴上算距离公式:D[i,j]=D[i,j-1]+x[j]-x[(i+j)/2] 得到距离和最短。
	初始cost[i][0]:在i个村庄中选择一个村庄建立一个邮局的最优解,即各村庄到这个邮局的距离和最短D[1][i].*/
    for(i = 1; i <= v; i++)
    {
        for(j = i + 1; j <= v; j++)
            D[i][j] = D[i][j - 1] + x[j] - x[int((i + j) / 2)];
        //初始化cost[range][p - 1]表示前p-1个邮局可以最优覆盖range个村庄, range范围是[p - 1, v - 1]
        //cost[i][0]相当于分析中的cost[i][1],只不过这里用第二维数组用2取代p进行了空间压缩
		//cout<<D[1][i]<<endl;
        cost[i][0] = D[1][i];
    }

	/**第二步计算cost[v,p];首先计算在v个村庄中建立2个邮局开始遍历.(一个邮局的情况就是D[1][i])
	通过DP方程式: Cost[v,p] = min{Cost[range,m-1]+D[range+1,n]} range属于闭区间【p-1, v-1】,
	最后得到在v个村庄中建立p个邮局的最优解*/	
    for(int pn = 2; pn <= p; pn++)
    {
        cur = 1 - last;
        //村庄数量至少等于邮局数量。
        for(int vn = pn; vn <= v; vn++)
        {
            int curMin = INT_MAX;
            //前pn - 1个邮局可以覆盖的村庄范围
            for(int range = pn - 1; range <= vn - 1; range++)
            {
                int cura = cost[range][last] + D[range + 1][vn];
                if(cura < curMin) curMin = cura;
            }
            cost[vn][cur] = curMin;
			cout<<cost[vn][cur]<<endl;
        }
        last = cur;
    }
    if(p <= 1) cur = 0;
    cout<<cost[v][cur]<<endl;
    return 0;
}


 

最大子序列问题:

/**************************************************
	@date:2011/05/21
	@author:weedge
	@comment:
	数组的最大子序列问题:给定一个数组,其中元素有正,也有负,找去其中一个连续子序列,使和最大。
	这个问题可以可以动态规划的思想解决:
	设b[i]:表示以第i个元素a[i]结尾的最大子序列,那么显然
	b[i+1]=b[i]>0?b[i]+a[i+1]:a[i+1]
************************************/

#include <stdio.h>
#include <stdlib.h>


int maxsum(int num[], int n)
{
	int max_sum = num[0];
	int max = num[0];
	int i = 1;/*gcc*/
	for(; i<n; i++)
	{
		if(max_sum>0)
		{
			max_sum += num[i];
		}
		else
		{
			max_sum = num[i];
		}
		if(max_sum>max)
		{
			max = max_sum;
		}
	}
	return max;
}

int main()
{
	/*test*/
	int a[8] = {1,-2,3,10,-4,7,2,-5};
	
	int max_sum = maxsum(a,8);
	
	printf("the max sum of the numbers is %i",max_sum);
	
	return 0;
}



你可能感兴趣的:(动态规划算法介绍与相关问题。)