现在要把这几种常见的算法给理清弄明白了,要不然只能做个低级程序员了。
动态规划DP是求解决策过程的最优化的数学方式。动态规划一般分为线性动规,区域动规,树形动规,背包动规。
动态规划是一种方法,但不是一种算法,一般用于多决策中的最优化问题,具有递推的思想。动态规划与分治法类似,基本思想都是把待解问题分解成若干个子问题,先求解子问题,然后由这些子问题的解得到原问题的解。但分治法中分解得到的子问题是相互独立的,但动态规划中不是。动态规划的基本思路与分治法相似,也是用一个表记录所有已解子问题的答案,不管子问题以后是否被用到,都将其填入表中。多决策中,各个阶段的决策依赖于当前状态,又随即引起了状态的转移,一个决策序列是在变化的状态中产生出来的,故为动态。
使用动态规划解决问题:
1. 01背包。
有一个包和n个物品,包的容量为m,每个物品都有各自的体积和价值,问当从这n个物品中选择多个物品放在包里而物品体积总数不超过包的容量m时,能够得到的最大价值是多少?[对于每个物品不可以取多次,最多只能取一次,之所以叫做01背包,0表示不取,1表示取]
先做简单版本,即物品没有价值,只计算能放入的最多物品时的总体积。
则很容易获得一个简单的递归版本:
int getMax(int* p ,int beglast,int arraylast){ if(arraylast<1)return 0; int put,noput; noput = getMax(p+1,beglast,arraylast-1); if(*p <= beglast){ put = *p + getMax(p+1,beglast - *p,arraylast-1); }else put = noput; if(put > noput) return put; else return noput; } int main(){ int a[] = {3,3,5,1,1,2,5,4,3,3}; cout<<getMax(a,10,10); system("pause"); }这里将问题化小,对第一个物品,假如放入,那计算得到放入第一个物品后的最大容量,加上第一个物品,与假设不放入第一个物品而获得的最大容量进行比较,则最大值即为当前背包中放入的最多物品的总体积。这里重要的是对任何一个物体,其加入与不加入背包,会影响之后的物品最大容量的值,而动态规划就体现在这里,所以其实这里会遍历所有情况,即复杂度为O(n * n)。实际上这就是这类问题的最简单求解的方法,就是遍历全部可能计算最大值,只是这里使用递归,使其可读性较高。
而要想使其效率更高,则要将递归改成循环,递推储存数据一般有两种做法,
一种是使用Data1和Data2两个数组,一个记录上一阶段的结果,然后根据Data1推出对应的Data2,然后再将Data2复制给Data1,然后继续,这样如果每次都是简单的递推一级时比较适合,但频繁赋值比较麻烦,可以使用STL中的deque, 而且这种方法不适合递推与之前一级以上的阶段有联系的问题。
另一种是对于与之前N阶相关的问题,建立数组Data[0...N],记录最近N阶的保存数据,而当当前阶段需要方法 阶段k的数据时,访问 data[ k MOD(N+1) ] 即可。
尝试自己用自己的想法将这个递归过程改为循环过程:
int getMaxBegA(int obj[], int const objn,int value[],int begsize){ //对于这个填表,填的应该是剩下背包量以及对于第n个石头时的 int** p = new int* [++begsize]; int** v = new int* [begsize]; for(int i=0; i< begsize;i++){ p[i] = new int[objn]; v[i] = new int[objn]; } for(int i = 0; i < begsize;i++){ if( i >= obj[objn-1]) { v[i][objn-1] = value[objn-1]; p[i][objn-1] = obj[objn-1]; } else { v[i][objn-1] = 0; p[i][objn-1] = 0; } } int a,b; for(int i = objn -2; i > -1 ;i--){ for(int j = 0 ; j <begsize;j++){ if(j < obj[i]){ p[j][i] = p[j][i+1]; v[j][i] = v[j][i+1]; } else{ a = value[i] + v[j-obj[i]][i+1]; b = v[j][i+1]; if(a > b){ v[j][i] = a; p[j][i] = obj[i] + p[j-obj[i]][i+1]; }else{ p[j][i] = p[j][i+1]; v[j][i] = v[j][i+1]; } } } } for(int i = 0 ; i < objn;i++){ cout<<i<<": "; for(int j = 0 ; j < begsize;j++){ cout<<p[j][i]<<" "; } cout<<endl; } a = v[begsize-1][0]; for(int i=0;i<begsize;i++){ delete[] p[i]; delete[] v[i]; } delete[] p; delete[] v; return a; }而事实上动态规划最好的填表大致如此,建一个二维表,行表示决策的阶段,而列表示决策的状态,只不过我这里刚好反了。通过填表,对于重复问题就不需要重复计算,但是需要大量的空间。如果想要更好的空间复杂度的话,进行之前所说的优化,即这里每一个新的阶段都取决与上一个阶段的 不同状态的结果,而在从后倒推到第一阶段时,其只需要之后的第二阶段的数据即可,则不需要保存多余的数据,但每次转向新的阶段都要重新赋值之前的结果,时间上会更加复杂一点。
在看了编程之美的关于最长子序列的算法后,尝试写一个类似思路的解法:
int L10(int obj[],int value[],int maxbeg,int size){ int* LW = new int [size]; int* LV = new int[size]; for(int i = 0 ; i < size;i++){ if(obj[i] <= maxbeg){ LW[i] = obj[i]; LV[i] = value[i]; }else{ LW[i] = 0; LV[i] = 0; } for(int j = 0 ; j < i;j++){ if(LV[j]+ value[i] > LV[i] && obj[i]+LW[j] <= maxbeg){ LV[i] = LV[j] + value[i]; LW[i] = LW[j] + obj[i]; } } } int MAX = 0; for(int i =0; i < size;i++){ if(LV[i] > MAX) MAX = LV[i]; } return MAX; }发现这样也可以获得正确的结果,难道这种方法就是解决大部分的动态规划的常用方式?
然后在进行考虑一个动态规划的常见题目:
设L=<a1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。
简单的递归算法:
int getMaxLengthArray(int stack[],int stacktop,int array[],int arraysize){ int length = 0,mis,re ; if(arraysize<1)return 0; if( *array > stack[stacktop] ){ mis = getMaxLengthArray(stack,stacktop,array+1,arraysize-1); stack[stacktop+1] = *array++; re = 1+getMaxLengthArray(stack,stacktop+1,array,arraysize-1); if(mis > re)return mis; else return re; } if( *array == stack[stacktop]){ return getMaxLengthArray(stack,stacktop,array+1,arraysize-1); } if(*array < stack[stacktop]){ mis = getMaxLengthArray(stack,stacktop,array+1,arraysize-1); int n = 0; while( *array <= stack[stacktop] ){ stacktop--; n++; } stack[++stacktop] = *array; re = getMaxLengthArray(stack,stacktop,array+1,arraysize-1) - n ; if(re>mis)return re; else return mis; } } int getMax(int array[],int arraysize){ int* stack = new int[arraysize+1]; stack[0] = 0xFFFFFFFF; return getMaxLengthArray(stack,0,array,arraysize); }这里还需要用到回溯,由于我暂时还没有研究回溯,只能用一种简单的方法来实现回溯,即用栈来记录计算的过程,然后使用出栈来模拟回溯。
这里的算法是不够合理和简洁的,编程之美中如此写:
int LISA(int array[],int size){ int* LIS = new int[size]; int MAX,i,j; for(i= 0; i < size;i++){ LIS[i] = 1; for(j = 0; j < i;j++){ //这里对于遍历到第i个元素,遍历其前i个元素,若当前第i个元素加在之前的第j个元素后能导致其子 //串长度增加,则记录这个新的第i个节点当前最长字段数量。 if(array[j] < array[i] && LIS[j] + 1 >LIS[i]) LIS[i] = LIS[j] + 1; } } MAX = 1; for(i = 0 ; i < size;i++){ if(LIS[i] > MAX ) MAX = LIS[i]; } delete[] LIS; return MAX; }
这样已经很简洁了,但是编程之美表示还不够,编程之美是如此做的,使用一个新的数组来储存当前最长子序列以及最长子序列时对应的最大值的最小值,如果用一个数组MaxV表示,则MAXV[i] 的值为当前遍历到第j个字符串时,之前长度为i的子段的最大值即最右点 的最小值。由于对于点j,其寻找适合自己的子序列时必定从之前最长子序列开始找起,当最长子序列的最大值 小于 j的值时,就表示j点可以接在这个最长子序列之后,然后更新信息,将续表的最长子序列的最右点值改为j的值。这个记录还有一个要更新的地方是当j点并不比当前最大子序列长时,即在得到的长度依然是之前已存在的子序列长度,这时要更新这个长度对应的最大值的最小值,即判断j点的值释放比这个长度的当前最大值小。由于这个最长子序列的当前最大长度是一级一级增加的,所以这个记录当前子序列长度的数组是连续,记录当前最大值,而查找时只要从最大值开始向下查找。但这样依然是o(N*N),。
而编程之美的进一步优化是将这个向下的 o(N)的查找改为二分查找,这样这个查找的复杂度就成了 O(log2 N) 。而整个过程的复杂度也变成了 O(N*log2 N)了,这里太麻烦,我就不实现了。