1 算法思想
动态规划
1.1含义
把问题分解成多阶段或多个子问题,顺序求解各个子问题,最后一个子问题就是初始问题的解。
概念
阶段: 问题分成的顺序的几个环节。例如最长递增子序列中每个字符就是一个阶段。
状态: 描述问题当前状况的数字量。可以表示状态特征,例如最长递增子序列中dp[x]表示以x结尾的字符串的最长递增子序列长度,就是一个状态。
决策:从某阶段状态到下一阶段某状态的选择。例如数塔问题中取第i行第j个数有两种方案: 取第i-1行第j1个数或取第i-1行第j个数后再取第i行第j个数。
状态转移方程:数字量之间的递推关系。例如最长递增子序列中状态转移方程为:
F[i]={1, i=1
{max{1, F[j] + 1}, j < i && aj>=ai
1.2 性质
最优子结构: 问题最优解包含子问题的最优解。
无后效性:某阶段状态(数字量例如dp[x])确定后,就不受这个状态以后决策的影响。即以后不影响以前。
解释:
最优子结构: 数塔问题中,假设9->12->10->8->10是最优路径,那么12->10->8->1也是12到达最后一层的最大和
1.3适用
子问题重叠+无后效性+最优子结构的问题
为了解决子问题重叠的问题,可以采用备忘录法,用表格记录到已经计算过的状态值。
1.4通用解法
动态规划算法: 1 确定状态转移方程 2 初始化边界状态的值 3 设定循环来递推状态的值 4 返回目标状态的值 总结: 方程->边界->循环 |
1.5经典例题讲解
0-1背包问题
不同草药,采每一株需要一些时间,每一株有自己价值,如何让采到的价值最大。
输入:第一行有两个整数T(1<=T<=1000)和M(1<=M<=100),T代表总共能够用来采药的时间,M代表山洞里的草药数目。
接下来的M行,每行包括两个在1到100之间(包括1和100)的整数,分别表示采摘某株草药的时间和这株草药的价值
输出:在规定时间内,可以采到的草药的最大总价值
问题抽象:
有一个容量为V的背包,和一些物品。这些物品分别有两个属性,体积w和价值v,每种物品只有1个。
要求背包装下价值尽可能多的物品,求该最大价值,背包可以不被装满。
分析状态转移方程:
用dp[i][j]表示总体积不超过j的情况下,前i个物品能达到的最大价值。
边界状态:
dp[0][j] = 0,(0<=j<=V)
根据每个物品是否放入背包,每个状态有两个状态转移来源
若物品i放入背包,设其体积为w,价值为v,则有
dp[i][j]=dp[i-1][j-w] + v
不放入背包,则有
dp[i][j]=dp[i-1][j]
所以有:
dp[i][j]=max{ dp[i-1][j-w] + v, dp[i-1][j]}
注意: j-w的值不能为负数
//定义背包 typedef struct List { int w;//体积 int v;//价值 }List;
int max(int a,int b) { return a > b ? a:b; }
int main(int argc,char* argv[]) { int s;//总容积 int n;//n行 int i,j; while(EOF!=scanf("%d %d",&s,&n)) { List list[101]; //int dp[101][1001]; int dp[1001];//优化 for(i = 1 ; i <= n ; i++) { scanf("%d %d",&list[i].w,&list[i].v); } //初始化边界状态的值 for(i = 0 ; i <= s ; i++) { //dp[0][i] = 0; dp[i] = 0; }
//设定循环来递推状态的值 //对于时间足够的情况,状态来源是:dp[i][j]为两者之中的最大值 for(i = 1 ; i <= n ; i++) { for(j = s; j >= list[i].w ; j--) { //dp[i][j] = max(dp[i-1][j],dp[i-1][j-list[i].w] + list[i].v); //优化:必须倒序更新每个dp[j]的值,j小于list[i].w的各dp[j]不做更新,保持原值,即等价与dp[i][j] = dp[i-1][j] dp[j] = max(dp[j],dp[j-list[i].w] + list[i].v); } /* for(j = list[i].w-1; j >= 0 ; j--) { dp[i][j] = dp[i-1][j]; } */ }
//返回目标状态的值 //printf("%d\n",dp[n][s]); printf("%d\n",dp[s]); } system("pause"); getchar(); return 0; } |
1.6动态规划与其他算法的区别
动态规划与贪心的区别:
贪心是在当前状态进行局部最优的选择,这种选择以来过去所做的选择。与贪心算法不同的是,动态规划分解的子问题往往不独立
动态规划与分治的联系:
基本思想都是将待求解问题分解成若干个子问题,先求解子问题,然后从子问题的解得到原问题的解。
1.7时间复杂度与空间复杂度
时间复杂度=状态数量*每次状态转移的时间复杂度
空间复杂度=申请的数组大小
2 动态规划系列
类别-编号 |
题目 |
遁去的1 |
1 |
N阶楼梯上楼问题 N阶楼梯上楼问题:一次可以走两阶或者一阶,问有多少种上楼方式(要求采用非递归) 输入:一个整数N(1<=N<90) 输出:输出阶数为N时的上楼方式的个数 输入: 4 输出: 5 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47186333 思路: 当n>2时,最后一步的行走方式可能:1)走到N-1阶台阶,走一步形成, 上楼方式与原问题中到达n-1阶方式数量相同为F[n-1]。2)走到N-2阶台阶, 走两步形成,上楼方式与原问题中到达n-2阶台阶的方式数量相同,为F[n-2] 关键: 1 递推求解在于确定数列的递推关系 F[n]={n,n=1或2 {F[n-1]+F[n-2],n>2
代码: #define N 91 //关键,不能使用递归,则我们需要预处理
int main(int argc,char* argv[]) { _int64 F[N]; F[1] = 1; F[2] = 2; for(int i = 3 ; i <= 90 ; i++) { F[i] = F[i-1] + F[i-2]; } int n; while(EOF!=scanf("%d",&n)) { printf("%ld\n",F[n]); } system("pause"); getchar(); return 0; } |
2 |
不容易系列之一 给N个网友写信,所有信全部装错信封有多少种可能的错误方式 输入:n(1<n<=20) 输出:错误的方式 输入: 2 3 输出: 1 2 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47186349 递推求解:n=1,0种,n=2,1种,n=3,2种,n=4,8种。设F[n]为n个信封的装错方式总数。 假设n号信封装的是k号信封,而n号信封中的信装在m号信封里。将k==m分为两类 1)若k!=m,交换n号信封和m号信封,则n号信封对了,m号信封中是装的k号信,即除n号信封外, 其余n-1个信封全部装错。装错方式为F[n-1]。又由于m的n-1个可能取值 这类装错方式总数为(n-1)*F[n-1]。在n-1个信封装错的F[n-1]基础上, 将n号信封所装的信与n-1个信封中任意一个信封(n-1中选择)所装的新交换,所得信全部错误。 2)若k=m,交换n号信和m号信后,n号信封和m号中恰好对了,除它们之外剩余的n-2个信封全部装错, 装错方式为F[n-2],又由于m的n-1个取值,装错方式总数为 (n-1)*F[n-2]。可理解为在n-2个信封全部装错基础上,交换n号信封和1到n-1号信封中任意1刚和,共有n-1种选择。 关键: 1 错排公式F[n]={0,n=1 {1,n=2 {(n-1)*F[n-1] + (n-1)*F[n-2],n>=2 2 递推求解技巧:逆推,从倒数第二步或者倒数第一步如何到达最终状态,找到递推关系式 3 递推求解往往数值较大,用long long 或 _int64 ,打印用%ld 4 分析问题,将每一个问题分割成规模较小的几个问题,分割过程要不遗漏和不重复,要得到递推关系式
代码: #define N 21
int main(int argc,char* argv[]) { _int64 F[N]; F[1] = 0; F[2] = 1; for(int i = 3 ; i <= N ; i++) { F[i] = (i-1)*F[i-1] + (i-1)*F[i-2]; } int n; while(EOF!=scanf("%d",&n)) { printf("%ld\n",F[n]); } system("pause"); getchar(); return 0; } |
3 |
最长递增子序列问题 在一个已知的序列{a1,a2,...,an}中,取出若干数组组成的序列{ai1,ai2,..,aim},其中下标i1,i2,...,im保持递增,即新增数列中的各个数之间依旧保持原数列中的先后顺序,那么称新的序列为原序列的一个子序列。若在序列中,下标ix > iy时,aix > aiy,称这个子序列为原序列的一个递增子序列。 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47186395 思路:F[i]表示递增子序列以ai结尾的最长长度,F[1] = 1.假设F[1]到F[x-1]值都已经确定,以ax结尾的递增子序列, 除了长度为1情况,其他情况中ax都是紧跟在由ai(i<x)组成的子序列之后。要求以ax结尾最长递增子序列, 一次比较ax与其之前所有的ai(i<x),若ai<ax,则ax跟在ai之后新递增子序列。又因为以ai结尾的递增子序列长度已经求得, 由以ai结尾的最长递增子序列再加上ax得到新的序列,长度也可以确定,取所有这些长度的最大值。当没有ai<ax时,以ax结尾的长度为1 关键: 1 动态规划公式:F[x] = max{1,F[i]+1 | ai <ax && i<x} F[1] = 1 F[i] = max{1,F[j] + 1} | aj < ai && j <i |
4 |
拦截导弹 导弹系统有缺陷,后面炮弹高度<=前一发高度。计算系统能拦截多少导弹。拦截时,必须按照时间顺序,不允许先拦截后面的导弹再拦截前面的导弹。 输入:每组输入两行。第一行:导弹数量k(k<=25)。第二行:k个整数,表示第k枚导弹的高度,按时间顺序,空格分隔 输出:每组输出一行,包含一个整数,表示能拦截多少导弹。 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47186409 思路:其实就是求最长非递增子序列(子序列中排在前面的数字不比排在后面的数字小,前面>=后面,降序)。递推关系为: F[1] = 1 F[i] = max{1,F[j]+1} | j < i && aj >= ai 关键: 1 求最长非递增子序列的方法是:遍历整个数组,以当前下标开始,遍历该下标之前的数,将两个数做比较,一旦前面的>=后面的,就将后面的加入到以前面结尾的子序列中,同时 更新子序列长度 2 定义数组大小时,一定要比题目规定的容量大小加1,因为用累加方式会超过规定数组的大小 3 易错,需要将初始iMax = 1 放在大循环下面,每次i做变动之后重新计数。输入时发生错误:原因是忘记加上&符号 4 时间复杂度为O(n*n),空间复杂度为O(n) 5 最长递增子序列问题特点:将问题分割为许多子问题,每个子问题为确定以第i个数字结束的递增子序列长度,与以排在该数字之前所有比它小的元素结尾的最长递增子序列长度 有关,且仅与其数字量有关。
代码: int main(int argc,char* argv[]) { int iElemArr[N];//保存输入的元素 int iLenArr[N];//保存以第i号元素结尾的最长非递增子序列长度 int n,iCnt = 1; int i,j; while(EOF!=scanf("%d",&n)) { //接受输入信息 int iTemp = n; while(iTemp--) { scanf("%d",&iElemArr[iCnt++]); } //int iMax= 1; //对每个元素进行遍历 for(i = 1 ; i <= n ; i++) { //易错,这里需要将初始值iMax放在每次i做变动之后 int iMax = 1; //遍历当前元素之前的元素 for(j = 1; j < i ; j++) { //如果前面的元素>=后面的元素,就更新以第i个元素结尾的子序列的长度 if(iElemArr[j] >= iElemArr[i]) { iMax = max(iMax,iLenArr[j] + 1); } } iLenArr[i] = iMax;//更新以第i号元素结尾的子序列的长度 } int iMMax = -123123123; for(i = 1 ; i <= n ; i++) { if(iLenArr[i] > iMMax) { iMMax = iLenArr[i]; } } printf("%d\n",iMMax); } system("pause"); getchar(); return 0; } |
5 |
最长公共子序列 字符串S中按照先后顺序依次取出若干字符,并将它们排列成一个新的字符串,这个字符串就称为原字符串的子串。有2个字符串S1和S2,求字符串S3同时为S1和S2的子串,且要求它的长度最长,确定这个长度。 输入: abcd cxbydz 输出: 2 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47186443 思路:dp[i][j]表示S1中前i个字符与S2中的前j个字符分别组成的两个前缀字符串的最长公共子串长度, 当i.j较小时,dp[0][j]=0。假设已经求得dp[i][j](0<=i<x,0<=j<y)的所有值 如何由这些值推得dp[x][y]。若S1[x]==S2[y],即S1中第x个字符和S2中第y个字符相同, 且它们各是各自前缀子串的最后字符,那么必存在一个最长公共子串以S1[x]或S2[y] 结尾,其他部分等价于S1中前x-1个字符和S2中前y-1个字符的最长公共子串。 dp[x][y]=dp[x-1][y-1] +1.若S1[x]!=S2[y],最长公共子串长度为S1中前x-1个字符和S2中前y, 个字符的最长公共子串长度 与 S1中前x个字符和S2中前y-1个字符的最长公共子串长度的较大者。 即在两种情况下得到最长公共子串不会因为其中一个字符串又增加了一个 字符长度而发生改变。dp[x][y]=max{dp[x-1][y],dp[x][y-1]} 关键: 1 递推条件:{dp[i][0] = 0 (0<=i <=n) {dp[0][j] = 0 (0<=j <=m) {dp[i][j] = dp[i-1][j-1] + 1,(S1[i]==S2[j]) {dp[i][j] = max{dp[i-1][j],dp[i][j-1]},(S1[i]!=S2[j]) 2 时间,空间复杂度均为O(L1*L2) 3 易错,这里会遗漏对初始字符的处理,所以比较的是s1[i-1]==s2[j-1] //if(s1[i]==s2[j]) 4 dp[i][j]表示两个字符串中前i,j个字符的最长公共子序列 问题:求两个完全小写的字符串的最长公共子串长度。第一行和第二行分别是两个字符串,没有空格间隔,每个字符串的长度不超过100,输出:最长公共子串长度个数
代码: int main(int argc,char* argv[]) { char s1[N],s2[N]; int dp[N][N];//用于存放S1中以某个字符结尾,S2中以某个字符结尾的最长公共子串长度 int i,j; while(EOF!=scanf("%s %s",s1,s2)) { int iLen1 = strlen(s1); int iLen2 = strlen(s2); //设定初始状态 //for(i = 0 ; i < iLen1 ; i++) for(i = 0 ; i <= iLen1 ; i++) { dp[i][0] = 0; } //for(j = 0 ; j < iLen2 ; j++) for(j = 0 ; j <= iLen2 ; j++) { dp[0][j] = 0; }
//for(i = 1 ; i < iLen1 ; i++) for(i = 1; i <= iLen1;i++) { //for(j = 1 ; j < iLen2 ; j++) for(j = 1 ; j <= iLen2 ; j++) { //如果两个结尾字符相同,则计算其余长度 //易错,这里会遗漏对初始字符的处理,所以s1[i-1]==s2[j-1] //if(s1[i]==s2[j]) if(s1[i-1]==s2[j-1]) { dp[i][j] = dp[i-1][j-1] + 1; } else { dp[i][j] = max(dp[i-1][j],dp[i][j-1]); } }//for }//for printf("%d\n",dp[iLen1][iLen2]); }//while system("pause"); getchar(); return 0; } |
6 |
搬寝室 n件物品,n<2000.准备搬2*k(<=n)件物品。每搬一次的疲劳度和左右手之间的重量差的平方成正比。请求出搬完这2*k件物品后的最低疲劳度是多少 输入:每组输入数据有2行,第一行有2个数n,k(2<=2*k<=n<2000),第二行有n个整数,分别表示n件物品的重量(<2的15次方) 输出:对应每组输入数据,输出数据只有一个表示他的最少的疲劳度,每个一行. 输入: 2 1 1 3 输出: 4 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47186477 思路:设a<=b<=c<=d经计算ab,cd配对方案的累计疲劳度<=ac,bd配对方案的累计疲劳度。结论:每一对组合的两个物品重量,是原物品中重量相邻的两个物品。 先对物品排序。设dp[i][j]表示前j件物品中选择i对物品的最小疲劳度。状态来源: 1)物品j和j-1未配对,则物品j每被选择,dp[i][j]=dp[i][j-1] 2) 物品j和j-1配对,等价于配对i-1对,将j-1与j进行配对dp[i][j]=dp[i-1][j-2] + (list[j]-list[j-1])的平方 关键: 1 状态转移方程dp[i][j]= min{dp[i][j-1] ,dp[i-1][j-2] + (list[j]-list[j-1])的平方},其中dp[0][n]=0 2 dp时间复杂度=状态数量*状态转移复杂度=(k*n)*O(1)=O(k*n) 3 你要求出的值是dp[k][n] 4 注意两边的i,j需要配对。i表征k,j表征n,j=2*i 5 程序还没有运行就退出,说明你声明了一个全局变量,而且申请的空间很大,导致崩溃 6 程序发生错误,粗心:scanf("%d",&Arr[n]);dp[0][n] = 0;应该把n改成i。 7 本题技巧,j > 2*i dp[i][j]=dp[i-1][j],j<=2*i,dp[i][j]=INT_MAX,比较dp[i][j]和dp[i-1][j-2]的大小
代码: #define INT_MAX 123123123 #define NN 201
int partition(int* Arr,int low,int high) { int iPos = Arr[low]; while(low < high) { while(low < high && Arr[high] >= iPos) { high--; } Arr[low] = Arr[high]; while(low < high && Arr[low] <= iPos) { low++; } Arr[high] = Arr[low]; } Arr[low] = iPos; return low; }
void quickSort(int* Arr,int low,int high) { if(low < high) { int iPos = partition(Arr,low,high); quickSort(Arr,low,iPos-1); quickSort(Arr,iPos+1,high); } }
int main(int argc,char* argv[]) { int n,k; int dp[NN][NN]; int Arr[NN]; int i,j; while(EOF!=scanf("%d %d",&n,&k)) { //对状态数组进行初始化 for(i = 0; i <= n ; i++) { dp[0][i] = 0; }
//接受输入信息 //for(i = 0; i < n ; i++) //易错,这里的i与j要与下面的进行比对质量时配对 for(i = 1 ; i <= n ; i++) { scanf("%d",&Arr[i]); }
//对重量进行快速排序 quickSort(Arr,1,n); //sort(Arr,Arr+1,Arr+1+n);
//开始进行动态规划 for(i = 1 ; i <= k ; i++) { //易错,j必须从2i开始 //for(j = 2 ; j <=n ; j++) for(j=2*i ; j <= n ; j++) { //如果j > 2*i表示,最后2个物品可以不配对 if(j > 2*i) { dp[i][j] = dp[i-1][j]; } //j<=2*i,最后2个物品必须配对 else { dp[i][j]=INT_MAX;//否则前j件物品配不成i对,所以其状态不能由dp[i][j-1]转移而来,dp[i][j]先设置为正无穷大 } //判定是dp[i-1][j-2]的值与其他的值比较,更新这个值 if(dp[i][j] > dp[i-1][j-2] + (Arr[j]-Arr[j-1])*(Arr[j]-Arr[j-1])) { dp[i][j] = dp[i-1][j-2] + (Arr[j]-Arr[j-1])*(Arr[j]-Arr[j-1]); } } } //最终你所要求的是dp[k][n] printf("%d\n",dp[k][n]);
} system("pause"); getchar(); return 0; } |
7 |
Greedy Tino 有一堆柑橘,重量为0到2000,总重量不大于2000。从中取出两堆放在扁担两头,且两头重量相等,问符合条件的的每堆重量最大是多少。没有符合条件的分堆方式则输出-1 输入:第一行时t,表示t个测试用例,对每个测试用例,包含一个数字n,表示橘子数。第二行有n个数字,表明每个橘子的重量,1<=n<=100.如果是因为存在重量为0的橘子,导致扁担两边重量为0,那么应该输出0,否则输出-1
输入: 1 5 1 2 3 4 5 输出: 7 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47186505 思路:橘子放在第一堆或者第二堆造成两堆之间的重量动态变化,设dp[i][j]:表示第i个橘子放入后,第一堆比第二堆重j时, |