一,关于“动态规划”的学习建议:
Here is my answer in similar topic
Start with
- wikipedia article about dynamic programming then
- I suggest you read this article in topcoder
- ch6 about dynamic programming in algorithms (Vazirani)
- Dynamic programming chapter in Algorithms Design Manual
- Dynamic programming chapter in algorithms classical book (Introduction to Algorithms)
If you want to test yourself my choices about online judges are
- Uva Dynamic programming problems
- Timus Dynamic programming problems
- Spoj Dynamic programming problems
- TopCoder Dynamic programming problems
and of course
- look at algorithmist dynamic programming category
You can also checks good universities algorithms courses
二,感性认识“动态规划”
1、基本概念
动态规划是一种灵活的方法,不存在一种万能的动态规划算法可以解决各类最优化问题(每种算法都有它的缺陷)。所以除了要对基本概念和方法正确理解外,必须具体问题具体分析处理,用灵活的方法建立数学模型,用创造性的技巧去求解。
2、基本思想与策略
基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
动态规划中的子问题往往不是相互独立的(即子问题重叠)。在求解的过程中,许多子问题的解被反复地使用。为了避免重复计算,动态规划算法采用了填表来保存子问题解的方法。
3、适用的情况
1)两个必备要素
适合应用动态规划方法求解的最优化问题应该具备两个重要的要素:最优子结构和子问题重叠。
(a)最优子结构:问题的最优解由相关子问题的最优解组合而成,并且可以独立求解子问题!
(b)子问题重叠:递归过程反复的在求解相同的子问题。
2)三个性质
能采用动态规划求解的问题的一般要具有3个性质:
(a) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(b) 无后效性:即某阶段状态(定义的新子问题)一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与其以前的状态有关。
(c)有重叠子问题:即子问题之间是不独立的(分治法是独立的),一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
4、求解的基本步骤
实际应用中可以按以下几个简化的步骤进行设计:
(1)分析最优解的性质,并刻画其结构特征,这一步的开始时一定要从子问题入手。
(2)定义最优解变量,定义递归最优解公式。
(3)以自底向上计算出最优值(或自顶向下的记忆化方式(即备忘录法))
(4)根据计算最优值时得到的信息,构造问题的最优解
三,理性认识“动态规划”
1,例子一,钢条切割问题
1),问题描述
长度i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
价格Pi | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
2),分析过程
A)描述最优解的结构
设子问题:规模为n时的最优切割利润为r[n],n值从0到10。显然R[0]=0。
然后我们选取一种在求取子问题时能够囊括所有切割方案的切割方法:我们总是在距离钢条左端j(j可以从0到i-1,到i是没有必要的)英寸处切下,此时我们将钢条切割成了j和n-j两段钢条。
最优子结构描述如下:假设对于子问题Rn我们在其距离钢条左边j的地方切成两段,那么该子问题的最优值必然是两个子问题最优值之和。
B) 递归定义最优解的值
根据上面的分析,显然子问题Rn的最优值为表达式 Rn=max(Rj+Rn-j),1<=j<=n且R0=0
因此,在计算r[n]时,所求值即为r[0]+r[n],r[1]+ r[n- 1],r[2]+ r[n- 2],...,r[n- 1] +r[1]之间的最大值。
C)按自底而上的方式计算最优解的值
在计算过程中,先计算r[0]到r[i-1]的值,在计算r[i]时,因为已经保存好其该子问题的值了,所以计算r[n]时直接取子问题的值即可,从而减少计算量。
3),代码如下:
// ConsoleAppDynamicProTest1.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "iostream"
using namespace std;
int DPBottomUpCutRod(int *p,int n,int *r,int *s);
int _tmain(int argc, _TCHAR* argv[])
{
system("color 0A");
int price[11]={0,1,5,8,9,10,17,17,20,24,30};
int *rb=new int[11];
int *s=new int[10];
for (int i = 0; i < 10; i++)
{
s[i]=0;
}
DPBottomUpCutRod(price,10,rb,s);
cout<<"以下数据分别是钢条长度从1到10下的最优值售卖价格"<<endl;
for (int i = 1; i < 11; i++)
{
cout<<rb[i]<<" ";
}
cout<<endl<<"以下数据分别是钢条长度从1到10下的切割点(距离钢条左端,0为无切割)"<<endl;
for (int i = 0; i < 10; i++)
{
cout<<s[i]<<" ";
}
cout<<endl;
delete[] rb;
rb=NULL;
delete[] s;
s=NULL;
system("pause");
return 0;
}
int DPBottomUpCutRod(int *p,int n,int *r,int *s)
{
r[0]=0;
r[1]=0;//初始化规模为0,1时的最优值
int max=0;
int sum=0;
for (int i=1;i <= n;i++)//规模的遍历从1到n,本例子n=10
{
max=p[i];//不进行切割时
for (int j=0;j < i;j++)//切割方法的遍历,距离钢条左端j(j可以从0到i-1,到i是没有必要的)英寸处切下,钢条将会分成两段
{
sum=r[j]+r[i-j];
if (sum>max)
{
max=sum;
s[i-1]=j;
}
}
r[i]=max;//规模为j时的最优值
}
return 0;
}
2,例子二,矩阵连乘问题
1),问题描述:
给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2 ,…,n-1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。例如:A1={30x35} ; A2={35x15} ;A3={15x5} ;A4={5x10} ;A5={10x20} ;A6={20x25} ;
最后的结果为:((A1(A2A3))((A4A5)A6)) 最小的乘次为15125。
2),分析过程
A)描述最优解的结构
设子问题:计算从Ai到Aj的矩阵相乘的最少乘法次数,我们用A(i,j)来表示。
对乘积AiAi+1...Aj的任意加括号方法都会将序列在某个地方分成两部分,我们将这个位置记为k,也就是说首先计算Ai...Ak和Ak+1...Aj,然后再将这两部分的结果相乘。
最优子结构米描述如下:假设AiAi+1...Aj的一个最优加括号把乘积在Ak和Ak+1间分开,则前缀子链Ai...Ak的加括号方式必定为Ai...Ak的一个最优加括号,后缀子链同理。
B) 递归定义最优解的值
定义m[i][j]为子问题Ai到Aj的最优值,则原问题的最优值解m[1][6].同时我们定义s[i][j]来记录该子问题的断开值k。按照上面的最优子结构描述那么
当i=j时,A[i,j]=Ai, m[i,j]=0;(表示只有一个矩阵,如A1,没有和其他矩阵相乘,故乘的次数为0)
当i<j时,m[i,j]=min{m[i,k]+m[k+1,j] +pi-1*pk*pj} ,其中i<=k<j
C)按自底而上的方式计算最优解的值
3),代码如下:
ConsoleAppDynamicProTest2.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include<iostream>
#define N 6
#define MAX_VALUE 65535
using namespace std;
void MatrixChainOrder(int n, int *p, int **m, int **s);
void traceback(int i, int j,int **s);
//p用来记录矩阵的行列,main函数中有说明
//m[i][j]用来记录第i个矩阵至第j个矩阵的最优解
//s[][]用来记录从哪里断开的才可得到该最优解
int _tmain(int argc, _TCHAR* argv[])
{
system("color 0A");
int p[N+1]={30,35,15,5,10,20,25};
int **m = new int *[N];
int **s = new int *[N];
for (int i=0; i < N; i++)
{
m[i] = new int [N];
s[i] = new int [N];
}
MatrixChainOrder(N, p, m, s);
cout<<"矩阵连乘顺序:"<<endl;
traceback(0, N-1, s);
cout<<endl<<"矩阵相乘最少次数:";
cout<<m[0][N-1]<<endl;
for (int i=0;i<N;i++)
{
delete[] m[i];//删除行指针
m[i]=NULL;
}
delete[] m;
m=NULL;
for (int i=0;i<N;i++)
{
delete[] s[i];
s[i]=NULL;
}
delete[] s;
s=NULL;
system("pause");
return 0;
}
void MatrixChainOrder(int n, int *p, int **m, int **s)
{
int q=0;
for(int i=0; i < n; i++)//意义:单一矩阵A[i,i]的最小乘次都置为0
{
m[i][i]=0;
}
for(int l = 2; l <= n; l++) //矩阵链长度的控制,即Ai~到Aj的长度控制
{
for(int i = 0; i <= n - l; i++) //行的遍历,从第i个矩阵Ai开始,
{
int j = l + i - 1; //列的控制j,到Aj,即矩阵段为(Ai~Aj)
m[i][j]=MAX_VALUE;
for(int k = i; k < j; k++) //断开点k从i到j - 1循环找m[i][j]的最小值
{
//将矩阵段(Ai~Aj)分成左右2部分(左m[i][k],右m[k+1][j]), //再加上左右2部分最后相乘的次数(p[i-1] *p[k]*p[j])
q = m[i][k] + m[k+1][j] + p[i] * p[k+1] * p[j+1];
if(q < m[i][j])
{
m[i][j] = q;
s[i][j] = k;
}
}
}
}
}
//s[][]用来记录在子序列i到j段中,在k位置处断开得到最优解位置k
//根据s[][]记录的各个子段的最优解,将其输出
void traceback(int i, int j,int **s)
{
if(i == j)
{
cout<<"A"<<i;
}
if(i < j)
{
cout<<"(";
traceback(i, s[i][j],s);
traceback(s[i][j]+1, j,s);
cout<<")";
}
}
3,例子三,0/1背包问题
1),问题描述:
2),分析过程
A)描述最优解的结构
设子问题:f[i][v]表示允许前i件物品放入容量为v的背包时可以获得的最大价值。注:这里的i从0到5,v从0到10
为了能够得到已经计算过的,更小规模的子问题,我们可以根据当前限重来只考虑第i件物品放或者不放,那么就可以转化为涉及前i-1件物品的问题,
即:
情况1、如果第i件物品不能放(即这个物品的重量直接大于了当前限重v),则问题转化为“前i-1件物品放入容量为v的背包中”,即f[i-1][v];
情况2、如果放第i件物品是可以放也可以不放,则问题转化为:
1)、如果选择不放第i件物品,则问题转化为“前i-1件物品放入容量为v的背包中”,即变大时f[i-1][v];
2)、如果选择放第i件物品,则问题转化为“前i-1件物品放入剩下的容量为v-w[i]的背包中”,此时能获得的最大价值就是f [i-1][v-w[i]]再加上通过放入第i件物品获得的价值w[i]。
则情况2下,f[i][v]的值就是1),2)中最大的那个值。
最优子结构描述如下:当子问题f[i][v]是最优时,其子问题f[i-1][v]和f[i-1][v-w[i]](中的较大者)显然同样也必须是最优的值,不然在情况1或者情况2下总会矛盾。
B) 递归定义最优解的值
根据上面的分析,显然子问题
f[i][v]=f[i-1][v],这时是情况1
f[i][v]=max{f[i-1][v], f[i-1][v-w[i]]+v[i] },这时是情况2。
C)按自底而上的方式计算最优解的值
在计算过程中,在遍历时先计算子问题的值,再计算f[i][v]时,因为已经保存好其该子问题的值了,所以计算f[i][v]时直接取两个相应子问题的较大者,从而减少计算量。但是规模大时依然慢!
3),代码如下:
// ConsoleAppDynamicProgrammTest3.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "iostream"
#define N 5
using namespace std;
void DPKnapsack(int *v, int *w, int c, int **f);
int max(int a,int b);
int _tmain(int argc, _TCHAR* argv[])
{
system("color 0A");
int cap=10;
int value[N]={6,3,5,4,6};
int weight[N]={2,2,6,5,4};
int **f = new int *[N+1]; //6行
for (int i=0; i < N+1; i++)
{
f[i] = new int[cap+1]; //11列
}
DPKnapsack(value, weight,cap,f);
int count=0;
for (int i=1; i < N+1; i++) //hang
{
for (int j=1;j < cap +1;j++)//lie
{
cout<<f[i][j]<<" ";
count++;
if (count%10==0)
{
cout<<endl;
}
}
}
for (int i=0;i<N+1;i++)
{
delete[] f[i];//删除行指针
f[i]=NULL;
}
delete[] f;
f=NULL;
system("pause");
return 0;
}
void DPKnapsack(int *v, int *w, int cap, int **f)
{
//初始化数据
for (int j=0;j<cap+1;j++)//其意义就是当不允许(即行为0)物品放入时,所能取得的价值肯定为0
{
f[0][j]=0;
}
for (int k=0;k<N+1;k++)//当限制重量为0时无论怎么放入最大价值都会为0
{
f[k][0]=0;
}
for ( int j=1;j<N+1;j++)//当前允许放入的前j个物品,1到5
for ( int i=1;i < cap+1;i++)//当前限制的背包容量,1到10
{
if (w[j-1]>i)//第j个(在数组中的价值实际是j-1的位置)物品的重量与当前限制重量的比较
{
f[j][i]=f[j-1][i];//只能这样放,属于情况1
}else
{
int k1=0,k2=0;
k1=f[j-1][i-w[j-1]]+v[j-1];
k2=f[j-1][i];
f[j][i]=max(k1,k2);//这时有两种选择,属于情况2,则最优值是较大者
}
}
}
int max(int a,int b)
{
if ( a >= b )
{
return a;
}else
{
return b;
}
}
4,动态规划小结
实际上动态规划的问题就是在牺牲一定量的内存存储子问题的计算结果,从而未来需要这些信息时不再重复计算,直接获取计算过的结果即可。其实动态规划的问题不好想,除非递推公式比较明显。
1),动态规划问题的判断
那么什么样的问题是动态规划呢?
比如如下几个经典问题
<LeetCode OJ> 5. Longest Palindromic Substring
<LeetCode OJ> 53. Maximum Subarray
<LeetCode OJ> 62. / 63. Unique Paths(I / II)
<LeetCode OJ> 64. Minimum Path Sum
LeetCode解题报告 70. Climbing Stairs
<LeetCode OJ> 96. Unique Binary Search Trees
<LeetCode OJ> 121. /122. Best Time to Buy and Sell Stock(I/II)
<LeetCode OJ> 123. / 188. Best Time to Buy and Sell Stock (III / IV)
这一系列的问题都是最优值问题,能从题目中露骨的感受到求取最优结果的意思。
比如最长回文...最长上升序列...等等。实际上刷题刷多了之后一看就知道是动态规划问题。
2),归纳设计步骤
实际应用中可以按以下几个简化的步骤进行设计:
(1)定义子问题变量,分析递归最优解的公式。
思考问题时一般都是将问题的规模缩小,即较小的子问题,假定其已经获得了最优值,然后将其扩大一点点成为较大规模的子问题,那么较大规模子问题应该怎样从较小规模的子问题递推公式得到呢?并且使得当前的子问题也是最优值。
(2)分析最优解的性质,并刻画其结构特征。
说白了就是刻意寻找定义的子问题变量与前面更小规模子问题变量的关系,一般用一个关系式表达。
(3)以自底向上计算出最优值
所谓自底向上的递推过程,即由较小规模的子问题遍历到原问题。首先我们的原问题是什么?那么他的较小规模的子问题是什么?然后思考我们应该怎么样遍历才能遍历回原问题(首先思考遍历的最外层)。在以后的遍历过程就是通过某种遍历顺序遍历回原问题。比如,最长回文字符串,我们定义的子问题就是“dp[i][j] 表示子串s[i…j]是否是回文”。子串s[i…j]就是原问题的缩小版本,因为我们始终可以通过控制两个变量变回原问题。
(4)实际编写程序时一定要记得初始化
有的边界问题可能不能由递推公式获得。比如最长回文字符串,必须先初始化,这一步一定要纳入思考的范围。
四,小试牛刀“动态规划”
练习一 ,排队买票问题
初始化数据:歌迷人数 n=6,第i位歌迷需要的时间 T[6]={2,3,4,1,3,1},R[5]={4,5,3,2,2}
1,问题分析
A)描述最优解的结构
设子问题:f(i)表示前i个人买票的最优方式,即所需最短时间。
很显然求取f(i)时需要考虑两种情况,即第i个人的票自己买,第i个人的票由第i-1个人买,即前一个人帮他买。
1)当由自己买时,此时的发f(i)=f(i-1)+T(i),
2)当由前一位买时,此时f(i)=f(i-2)+R(i-1),
显然f(i)的真正结果是情况1,2的较小者。
最优子结构描述如下:当子问题f[i]是最优时,其子问题f[i-1]和f[i-2]显然同样也必须是最优的值,不然在情况1或者情况2下推出的最小值不可能是最优方式。
B) 递归定义最优解的值
当i=0时,显然f(i)=0;
当i=1时,显然f(i)=T(0);
其他情况:f(i)=min{f(i-1)+T(i),f(i-2)+R(i-1)};
C)按自底而上的方式计算最优解的值
至此已经很显然,子问题f(i-1)f(i-2)总是被已经求出
2,代码实现
// ConsoleAppDPTest1.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "iostream"
using namespace std;
#define Persons 6
void DPBuyTickets(int *Ti,int *Ri,int *f);
int min(int a,int b)
{
if (a>b)
{
return b;
}
else
{
return a;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
system("color 0A");
int T[Persons] = { 2, 3, 4, 1, 3, 1 };
int R[Persons-1] = { 4, 5, 3, 2, 2 };
int fi[Persons+1] = { 0 };
DPBuyTickets(T, R,fi);
for (int i = 0; i < Persons+1;i++)
{
cout << fi[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
void DPBuyTickets(int *Ti, int *Ri,int *f)
{
f[0] = 0;
f[1] = Ti[0];
for (int i = 2; i < Persons+1;i++)
{
f[i] = min(f[i-1]+Ti[i-1],f[i-2]+Ri[i-2]);
}
}
练习二 ,最长不降子序列
-
题目描述:
-
给定一个整型数组, 求这个数组的最长严格递增子序列的长度。 譬如序列1 2 2 4 3 的最长严格递增子序列为1,2,4或1,2,3.他们的长度为3。
-
输入:
-
输入可能包含多个测试案例。
对于每个测试案例,输入的第一行为一个整数n(1<=n<=100000):代表将要输入的序列长度
输入的第二行包括n个整数,代表这个数组中的数字。整数均在int范围内。
-
输出:
-
对于每个测试案例,输出其最长严格递增子序列长度。
-
样例输入:
-
4 4 2 1 3 5 1 1 1 1 1
-
样例输出:
-
2 1
1,问题分析
A)描述最优解的结构
设子问题:f(i)表示以第i项为结尾序列的最长度,即最长不下降序列值。(这样定义与原问题等价)
显然f(i)的最优值一定是由b1到bi中的某个满足条件的bj(条件为bi>bj)位置的最长度再+1
如此,计算出其最大的那一个就是当前规模i的最优解
仔细推敲就是:
2· 若从a(2)开始查找,则存在下面的两种可能性:
1)若a(1)<a(2)则存在长度为2的不下降序列a(1),a(2),所以f(2)=2。
2)若a(1)>a(2)则存在长度为1的不下降序列a(1)或a(2),所以f(2)=1。
在a(1),a(2),…,a(i)中,依次从a(1)开始到a(i-1)比较与a(i)的大小,很显然比a(i)大的此时的f(i)不变,然而比a(i)小的不一定是最长的序列中的元素,他还必须满足f(j)(此时比a(i)小的那个元素的位置为j)不比f(i)小,因为一旦小于说明不是最长的,。
2,代码实现(尽然超时了,本题还有一种更好的解决办法)
#include "stdio.h"
#include "vector"
using namespace std;
int LongNoDrop(const vector<int> &Arr, vector<int> &dp);
int main()
{
int n = 0;
while (scanf("%d",&n)!=EOF)
{
vector<int> srcArr(n, 0);
for (size_t i = 0; i < n; i++)
cin >> srcArr[i];
vector<int> dp(n, 0);//记录第1个元素到第i个元素之间的不降长度
//cout << LongNoDrop(srcArr, dp) << endl;
printf("%d", LongNoDrop(srcArr, dp) );
}
return 0;
}
//动态规划法:复杂度O(n2),很遗憾,超时
//令dp(i);//记录第1个元素到第i个元素之间的不降长度
int LongNoDrop(const vector<int> &Arr, vector<int> &dp)
{
int maxLong = 1;
for (int i = 0; i < Arr.size(); i++)
{
dp[i] = 1;//无论计算哪个位置都有1个不降长度
for (int j = 0; j < i; j++)
{//这个遍历的实际意思就是在考虑第i个元素和第j个元素及其以前的元素之间所能形成的不降长度
if (Arr[i] > Arr[j] && dp[j] >= dp[i])
{
dp[i] = dp[j] + 1;//更新该位置的不降长度
if (maxLong < dp[i])
maxLong = dp[i];//更新最大不降长度
}
}
}
return maxLong;
}
/**************************************************************
Problem: 1533
User: EbowTang
Language: C++
Result: Time Limit Exceed
****************************************************************/
练习三,出入栈
-
题目描述:
-
给定一个初始为空的栈,和n个操作组成的操作序列,每个操作只可能是出栈或者入栈。
要求在操作序列的执行过程中不会出现非法的操作,即不会在空栈时执行出栈操作,同时保证当操作序列完成后,栈恰好为一个空栈。
求符合条件的操作序列种类。
例如,4个操作组成的操作序列符合条件的如下:
入栈,出栈,入栈,出栈
入栈,入栈,出栈,出栈
共2种。
-
输入:
-
输入包含多组测试用例,每组测试用例仅包含一个整数n(1<=n<=1000)。
-
输出:
-
输出仅一个整数,表示符合条件的序列总数,为了防止总数过多超出int的范围,结果对1000000007取模(mod 1000000007)。
-
样例输入:
-
2 4 10
-
样例输出:
-
1 2 42
#include <iostream>
using namespace std;
//因为是求最大操作数目,所以考虑动态规划,寻求用子问题推导出较大的子问题
//1,n个操作组成的操作序列,那么为满足题目要求必定是出入栈操作各占一半
//2,第一个操作必为入,最后一个必为出,但是中间情况却的不一定,它既可以是出也可以是入
//3,令dp[i][j](i >= j)表示入栈i次出栈j次的种类数(共i+j个操作数),那么显然dp[i][0]=1
//并且任何时候入必定比出更多(否则出错)
//但是dp[i][j](在i!=j的情况下)必有第i+j个操作是入两种情况,那么dp[i][j]显然就是两者之和
//状态转移方程dp[i][j] = dp[i - 1][j] + dp[i][j - 1],
//这个转移方程就是根据只考虑最后一个操作得出的结果,比如假设最后增加的一步是入栈,方法数就为dp[i][j - 1],
//如果最后一步增加的是出栈,数目就是为dp[i - 1][j],最后加起来就是需要的总数。
int dp[501][501] = {0};
void maxStackTime(int n)//操作数为n时的最大合法序列数
{
//初始化
for (int i = 0; i <= n / 2; i++)
dp[i][0] = 1; //只入不出均只有一种序列数
for (int i = 1; i <= n / 2; i++)
{
for (int j = 1; j <= i; j++)//入栈数一定大于出栈数,否则会非法操作
{
if (i == j)
dp[i][j] = dp[i][j-1] % 1000000007;//(第i+j个操作)即末尾操作必定是出
else
dp[i][j] = (dp[i - 1][j] + dp[i][j - 1]) % 1000000007;//除去第一个和最后一个操作,中间的操作最大数必定是两种情况之和
}
}
cout << dp[n / 2][n / 2] << endl;
}
int main(void)
{
int n = 0;
while (cin >> n)
{
if (n % 2 == 1 || n <= 0)//n不能为奇数
{
cout << "0" << endl;
continue;
}
maxStackTime(n);
}
return 0;
}
/**************************************************************
Problem: 1547
User: EbowTang
Language: C++
Result: Accepted
Time:50 ms
Memory:2500 kb
****************************************************************/
练习四,最大上升子序列和
-
题目描述:
-
一个数的序列bi,当b1 < b2 < ... < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, ...,aN),我们可以得到一些上升的子序列(ai1, ai2, ..., aiK),这里1 <= i1 < i2 < ... < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中序列和最大为18,为子序列(1, 3, 5, 9)的和.
你的任务,就是对于给定的序列,求出最大上升子序列和。注意,最长的上升子序列的和不一定是最大的,比如序列(100, 1, 2, 3)的最大上升子序列和为100,而最长上升子序列为(1, 2, 3)。
-
输入:
-
输入包含多组测试数据。
每组测试数据由两行组成。第一行是序列的长度N (1 <= N <= 1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。
-
输出:
-
对于每组测试数据,输出其最大上升子序列和。
-
样例输入:
-
7 1 7 3 5 9 4 8
-
样例输出:
-
18
#include"iostream"
#include <stdio.h>
using namespace std;
int data[1001];
int dp[1001];//第一个元素到第i个元素的不降和
int N = 0;
int MaxLongNoDrop()
{
int max = -1;
for (int i = 1; i <= N; ++i)
{
dp[i] = data[i];
for (int j = 1; j < i; ++j)
{
if (data[i] > data[j] && dp[i] < dp[j] + data[i])//前面个条件保持“不降”,后一个条件始终保持“最大”
{
dp[i] = dp[j] + data[i];
if (max < dp[i])
max = dp[i];//获取整个数组中最大的不降和
}
}
}
return max;
}
int main(void)
{
while (cin>>N)
{
for (int i = 1; i <= N; ++i)
cin>>data[i];
cout << MaxLongNoDrop() << endl;
}
return 0;
}
练习五,跳台阶
-
题目描述:
-
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
-
输入:
-
输入可能包含多个测试样例,对于每个测试案例,
输入包括一个整数n(1<=n<=70)。
-
输出:
-
对应每个测试案例,
输出该青蛙跳上一个n级的台阶总共有多少种跳法。
-
样例输入:
-
5
-
样例输出:
-
8
#include "vector"
#include "string"
#include "algorithm"
#include <iostream>
#include "stack"
#include <cmath>
using namespace std;
//当n=1时,ans=1,当n=2时,ans=2
//当n=3时,ans=3,当n=4时,ans=5
//当n=5时,ans=8
//令跳到第i个台阶时的方法数为f(i)
//当我们确定选择下一步跳2时,方法数就是f(i-1)(此时f(i-1)+2=f(i+1))
//当我们确定选择下一步跳1时,方法数就是f(i)
//显然两种选择都可以,所以f(i+1)=f(i)+f(i-1)
int main()
{
int n;
while (cin >> n)
{
vector<long long> vec(n+1);
vec[1] = 1;
vec[2] = 2;
for (int i = 3; i <= n; i++)
{
vec[i] = vec[i - 1] + vec[i-2];
}
cout << vec[n] << endl;
}
return 0;
}
练习六,斐波那契数列
-
题目描述:
-
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项。斐波那契数列的定义如下:
-
输入:
-
输入可能包含多个测试样例,对于每个测试案例,
输入包括一个整数n(1<=n<=70)。
-
输出:
-
对应每个测试案例,
输出第n项斐波那契数列的值。
-
样例输入:
-
3
-
样例输出:
-
2
#include "vector"
#include "string"
#include "algorithm"
#include <iostream>
#include "stack"
#include <cmath>
using namespace std;
int main()
{
int n;
while (cin>>n && n >= 1&& n <= 70)
{
vector<long long> vec(71,0);
vec[0] = 0;
vec[1] = 1;
for (int i = 2; i <= n; i++)
vec[i] = vec[i - 1] + vec[i-2];//第i个子问题的解总是由第i-1和i-2个子问题的解求和得到,O(n)的时间复杂度
cout << vec[n]<<endl;
}
return 0;
}
练习七,股票买卖的最大利润
-
题目描述:
-
给定一个大小为n的数组,数组的元素a[i]代表第i天的股票价格。
设计一个算法,计算在最多允许买卖k次(一买一卖记为一次)的条件下的最大收益。
需要注意的是,你不能同时拥有两份股票。也就是说在下次买入前,你必须把手头上原有的股票先卖掉。
-
输入:
-
输入可能包含多个测试案例。
对于每个测试案例,输入的第一行为两个整数n和k(1<=n,k<=1000)。
输入的第二行包括n个整数,范围在[0,10000),代表数组中的元素。
-
输出:
-
对应每个测试案例,输出最大获益。
-
样例输入:
-
5 1 3 4 5 1 4 7 2 1 2 3 5 6 1 7
-
样例输出:
-
3 11
#include "algorithm"
#include <iostream>
#include "stack"
#include <cstring>
#include <cmath>
using namespace std;
//一般思路:暴力获取后面的值减去前面值的差,取最大的一个
//本题必须禁止出现递减的情况,不然你觉得呢。
//本体和背包问题一样,有两个变量,第i天,交易第j次
//用profit[i][j]表示前i天交易j次能得到的最大利润
//对于第i天的物品有两种选择:交易(买或者卖)或者不做任何交易
//如果不做任何交易:显然,此时的最大利润还是前一天的最大利润profit[i-1][j]
//如果交易:
//为了能在这一天获得最大利润如果执行交易只能卖股票(不能买):
//那么假设第j次交易的买进股票是在第z天 ,0<z<i,,那么必定有j-1次交易完成在z天以内
//所以这一次交易的利润就是price[i] - price[z]
//那么这种情况下的最大利润profit[i][j] 就是 profit[z][j-1] + price[i]-price[z]
//那么当我们遍历到i,即在每次加上price[i]这个已知值时,先求出profit[z][j-1]-price[z]这个临时利润值maxtmp
//其实就是模拟:用手头已有的利润减去买股票的支出,对于临时利润它必须也是最大的(可以反正法证明)
//每一次可以买,也可以不买
//若不买:显然还是以前的maxtmp,即不变
//若买:为了能最大显然是,用截止到第i天的最大利润减去前一天的买股票的钱,profit[i][j - 1] - price[i](这样写代码位置要放正确,见下面代码)
//所以临时利润就是两种情况下的较大者
//综上所诉,最大利润显然就是两种情况的较大者
const int MAXN = 1010;
//每一天的股票价格
int price[MAXN];
//profit[i][j] 表示前i天交易j次能得到的最大利润
int profit[MAXN][MAXN];
int main()
{
int n, k;
//输入天数和交易次数
while (cin >> n >> k)
{//接受每天对应的价格
for (int i = 1; i <= n; i++)
cin >> price[i];
//初始化
memset(profit, 0, sizeof(profit));
for (int j = 1; j <= k; j++)
{
int maxTmp = profit[0][j - 1] - price[1];
for (int i = 1; i <= n; i++)
{
//maxTmp保存着已完成的j-1次交易的最大利润减去买股票后的临时利润
profit[i][j] = max(profit[i - 1][j], maxTmp + price[i]);//前者为不卖,后者为卖出时的最大利润
//更新下一次的最大maxTmp:用手头的利润减去前一天(因为i增加了)买股票的钱
maxTmp = max(maxTmp, profit[i][j - 1] - price[i]);//前者不买。后者买
}
}
cout << profit[n][k] << endl;
}
return 0;
}
练习八,座位问题
-
题目描述:
-
计算机学院的男生和女生共n个人要坐成一排玩游戏,因为计算机的女生都非常害羞,男生又很主动,所以活动的组织者要求在任何时候,一个女生的左边或者右边至少有一个女生,即每个女生均不会只与男生相邻。现在活动的组织者想知道,共有多少种可选的座位方案。
例如当n为4时,共有
女女女女, 女女女男, 男女女女, 女女男男, 男女女男, 男男女女, 男男男男
7种。
-
输入:
-
输入包含多组测试用例,每组测试用例仅包含一个整数n(1<=n<=1000)。
-
输出:
-
对于每组测试用例,输出一个数代表可选的方案数,为防止答案过大,答案对1000000007取模。
-
样例输入:
-
1 2 4
-
样例输出:
-
1 2 7
#include "vector"
#include "string"
#include "algorithm"
#include <iostream>
#include "stack"
#include <cmath>
#include <set>
using namespace std;
//最值方案问题,考虑采用动态规划
//令dp[i][0]表示共i个人的座位方式,且最后一个是男生,dp[i][1]则是女生
//无论什么时候最后一个人要么是男要么是女
//如果最后一个是男生,显然dp[i][0] = dp[i - 1][0] + dp[i - 1][1];
//因为前面的女生已经满足座位关系,再加一个男生一样满足座位关系
//如果最后一个是女生,显然它的前一个不能是男生,那么只能是女生,
//此时方式数目就是dp[i-1][1]
//但是dp[i-1][1]中并没包括倒数第二女生前面可以是男生的情况(因为最后一个也是女生,所以此时是可以的),此时数目是dp[i-2][0]
const int maxx = 1001;
const int MOD = 1000000007;
long dp[maxx][2];
void init()
{
long sum = 0;
dp[1][0] = 1;//男生
dp[1][1] = 0;//女生
dp[2][0] = 1;//男生
dp[2][1] = 1;//女生
for (int i = 3; i<1001; ++i)
{
//男生
dp[i][0] = dp[i - 1][0] + dp[i - 1][1];
dp[i][0] %= MOD;
//女生
dp[i][1] = dp[i - 2][0] + dp[i - 1][1];
dp[i][1] %= MOD;
}
}
int main(){
long n, ans;
init();
while (cin>>n)
{
ans = (dp[n][0] + dp[n][1]) % MOD;
cout << ans << endl;
}
return 0;
}
练习九,最小邮票数
-
题目描述:
-
有若干张邮票,要求从中选取最少的邮票张数凑成一个给定的总值。
如,有1分,3分,3分,3分,4分五张邮票,要求凑成10分,则使用3张邮票:3分、3分、4分即可。
-
输入:
-
有多组数据,对于每组数据,首先是要求凑成的邮票总值M,M<100。然后是一个数N,N〈20,表示有N张邮票。接下来是N个正整数,分别表示这N张邮票的面值,且以升序排列。
-
输出:
-
对于每组数据,能够凑成总值M的最少邮票张数。若无解,输出0。
-
样例输入:
-
10 5 1 3 3 3 4
-
样例输出:
-
3
#include "algorithm"
#include <iostream>
#include "stack"
#include <cmath>
#include <set>
using namespace std;
int v, N;
int w[1000];
int dp[1000];
#define inf 1000
//令dp[i]表示邮票总价值为i的凑齐时的最小邮票数
//每一个物品,我们可以选择,也可以不选
//自底向上遍历求解,当总价值为j,在选择第i个物品时
//如果选,显然,dp[j] = dp[j - w[i]] + 1
//如果不选,那就没变化,dp[j] = dp[j]
void MinNumStamp()
{
for (int j = 0; j < 1000; j++)
dp[j] = inf;
dp[0] = 0;//总价值为0,不需要邮票数
for (int i = 1; i <= N; ++i)//遍历物品,1到N
{
for (int j = v; j >= w[i]; --j)//遍历邮票总价值,总价值到w[i]
{
dp[j] = min(dp[j], dp[j - w[i]] + 1);
}
}
if (dp[v] >= 1000)
cout << 0 << endl;
else
cout << dp[v] << endl;
}
int main()
{
while (cin >> v >> N)
{
for (int i = 1; i <= N; ++i)
cin>>w[i];
MinNumStamp();
}
return 0;
}
/**************************************************************
Problem: 1209
User: EbowTang
Language: C++
Result: Accepted
Time:190 ms
Memory:1528 kb
****************************************************************/
练习十,Jobdu MM分水果
-
题目描述:
-
Jobdu团队有俩PPMM,这俩MM干啥都想一样。一天,富强公司给团队赞助了一批水果,胡老板就把水果派发给了这俩MM,由她们自行分配。每个水果都有一个重量,你能告诉她们怎么分才使得分得的重量差值最小吗?
-
输入:
-
输入有多组数据,每组数据第一行输入水果个数n(1<=n<=100),接下来一行输入n个重量wi(0<=wi<=10^5)。
-
输出:
-
对每组输入输出一行,输出可以得到的最小差值。
-
样例输入:
-
510 20 30 10 10
-
样例输出:
-
0
#include "vector"
#include <cstdio>
#include <iostream>
#include "algorithm"
#include<cstring>
#include<cmath>
#include<cstdlib>
using namespace std;
//将问题转化为动态规划:
//假设所有水果总重为sum, 为了两个人分得尽可能相等(差值最小),
//那么其中一个人所能分得的水果重量w范围一定在 (0,sum/2]之间,
//问题就转化为一个人所分得的重量w尽可能接近sum/2。
//令dp[i]表示期望接近的重量值为i时,只对前j个物品进行选择最接近i的值
//(选择出来的重量和不超过i值,但是最接近)
//每一个物品,我们可以选择,也可以不选 ,对物品的选择从一个到所有
//自底向上遍历求解,当期望接近的重量为i,在选择第j个物品时
//如果选择第j个物品,显然就是当前物品的价值fruit[j]加上最接近i - fruit[j]的值(即dp[i - fruit[j]])
//就是最接近i的值了,即dp[i] = dp[i - fruit[j]] + fruit[j](i应该递减)
//如果不选,那就没变化,dp[i] = dp[i] (背包问题里面此时就是f[i][v]=f[i-1][v])
//谁更大,显然就更接近i值
int sum;
int getMinDiff(vector<int> &fruit)
{
int half = (sum >> 1);
vector<int> dp(half+1,0);
for (int i = 0; i < fruit.size(); ++i)//水果的选择
for (int j = half; j >= fruit[i]; --j)//计算最接近j重量的值(在选择前i个物品时)
dp[j] = max(dp[j], dp[j - fruit[i]] + fruit[i]);
return sum - 2 * dp[half];
}
int main(int argc, char *argv[])
{
int n = 0;
while (scanf("%d", &n) != EOF)
{
sum = 0;
vector<int> vecfruit(n, 0);
for (int i = 0; i < n; ++i)
{
scanf("%d", &vecfruit[i]);
sum += vecfruit[i];
}
printf("%d\n", getMinDiff(vecfruit));
}
return 0;
}
练习十一,两船载物问题
-
题目描述:
-
给定n个物品的重量和两艘载重量分别为c1和c2的船,问能否用这两艘船装下所有的物品。
-
输入:
-
输入包含多组测试数据,每组测试数据由若干行数据组成。
第一行为三个整数,n c1 c2,(1 <= n <= 100),(1<=c1,c2<=5000)。
接下去n行,每行一个整数,代表每个物品的重量(重量大小不大于100)。
-
输出:
-
对于每组测试数据,若只使用这两艘船可以装下所有的物品,输出YES。
否则输出NO。
-
样例输入:
-
3 5 8 6 3 3 3 5 8 5 3 4
-
样例输出:
-
NO YES
#include "vector"
#include <iostream>
#include "algorithm"
#include <stdio.h>
#include <string.h>
using namespace std;
//将问题转化为动态规划:
//假设所有物品总重为sum, 为了两个船装的下,必须sum<c1+c2,
//那么其中载物重量较小的那艘船(令其为c1)尽可能装接近其载物量c1的物品总量和
//问题就转化为选择所有物品尽可能接近c1 (意义就是小船尽可能多装,为什么选择小船?内存消耗较小)
//令dp[i]表示期望接近的重量值为i时,只对前j个物品进行选择时最接近i的值
//选择出来的重量和不超过i值,但是是基于前j个物品最接近
//每一个物品,我们可以选择,也可以不选 ,对物品的选择从一个到所有
//自底向上遍历求解,当期望接近的重量为i,在选择第j个物品时
//如果选择第j个物品,显然就是当前物品的重量weightlist[j]加上最接近i - weightlist[j]的值(即dp[i - weightlist[j]])
//就是最接近i的值了,即dp[i] = dp[i - weightlist[j]] + weightlist[j](i应该递减)
//如果不选,那就没变化,dp[i] = dp[i] (背包问题里面此时就是f[i][v]=f[i-1][v])
//最后小船已经装载了dp[c1],那么如果大船只要再装sum-dp[c1],所以大船的载重量c2必须大于这个差值
int main()
{
int c1, c2, sum, n;
while (~scanf("%d %d %d", &n, &c1, &c2))
{
sum = 0;
vector<int> weightlist(n + 1, 0);
//接受输入
for (int i = 1; i <= n; i++)
{
scanf("%d", &weightlist[i]);
sum += weightlist[i];
}
if (c1 + c2<sum)
{ //物品的总重量大于两首船的载重
puts("NO");
continue;
}
//总是让c1成为较小者为了,总是先装载物较小的船
if (c1>c2)
{
int tmp = c1;
c1 = c2;
c2 = tmp;
}
vector<int> dp(c1+1, 0);
for (int i = 1; i <= n; i++)
for (int j = c1; j >= weightlist[i]; j--)
dp[j] = max(dp[j - weightlist[i]] + weightlist[i], dp[j]);
if (c2 + dp[c1]<sum)
puts("NO");
else
puts("YES");
}
return 0;
}
/**************************************************************
Problem: 1462
User: EbowTang
Language: C++
Result: Accepted
Time:10 ms
Memory:1520 kb
****************************************************************/
练习十二,项目安排
-
题目描述:
-
小明每天都在开源社区上做项目,假设每天他都有很多项目可以选,其中每个项目都有一个开始时间和截止时间,假设做完每个项目后,拿到报酬都是不同的。由于小明马上就要硕士毕业了,面临着买房、买车、给女友买各种包包的鸭梨,但是他的钱包却空空如也,他需要足够的money来充实钱包。万能的网友麻烦你来帮帮小明,如何在最短时间内安排自己手中的项目才能保证赚钱最多(注意:做项目的时候,项目不能并行,即两个项目之间不能有时间重叠,但是一个项目刚结束,就可以立即做另一个项目,即项目起止时间点可以重叠)。
-
输入:
-
输入可能包含多个测试样例。
对于每个测试案例,输入的第一行是一个整数n(1<=n<=10000):代表小明手中的项目个数。
接下来共有n行,每行有3个整数st、ed、val,分别表示项目的开始、截至时间和项目的报酬,相邻两数之间用空格隔开。
st、ed、value取值均在32位有符号整数(int)的范围内,输入数据保证所有数据的value总和也在int范围内。
-
输出:
-
对应每个测试案例,输出小明可以获得的最大报酬。
-
样例输入:
-
3 1 3 6 4 8 9 2 5 16 4 1 14 10 5 20 15 15 20 8 18 22 12
-
样例输出:
-
16 22
#include "vector"
#include <iostream>
#include "algorithm"
#include<string>
#include <stdio.h>
#include<cmath>
#include<cstdlib>
using namespace std;
//动态规划问题:
//令dp[i]为在前i个项目下能获得的最大利润
//对于第i个项目,如果前面第j个项目满足时间条件,我们可以做,也可以不做
//如果不做第i个项目,那就不变,和前一次一样,dp[i]=dp[i-1](不是dp[i])
//如果做第i个项目,并且满足第j个项目时间,dp[i]=dp[j] + pro[i].value
//最终结果显然就是两种情况的较大者
class Item
{
public:
int st;
int ed;
int value;
};
int cmp(Item x, Item y)
{
return x.ed<y.ed;//按照结束时间排序
}
int main()
{
int n;
while (scanf("%d", &n) != EOF)
{
vector<int> dp(n+1,0);
Item pro[10001];
int i, j;
for (i = 1; i <= n; i++)
scanf("%d%d%d", &pro[i].st, &pro[i].ed, &pro[i].value);
sort(pro+1, pro+n+1, cmp);//类似贪心处理
dp[1] = pro[1].value;
for (i = 2; i <= n; i++)
{
for (j = i - 1; j >= 1; j--)//必须逆序
if (pro[j].ed <= pro[i].st)//只要第i个项目前面的某个项目满足条件
break;
dp[i] = max(dp[j] + pro[i].value, dp[i - 1]);
}
printf("%d\n", dp[n]);
}
return 0;
}
/**************************************************************
Problem: 1499
User: EbowTang
Language: C++
Result: Accepted
Time:260 ms
Memory:1576 kb
****************************************************************/
练习十三,拦截导弹
-
题目描述:
-
某国为了防御敌国的导弹袭击,开发出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭,并观测到导弹依次飞来的高度,请计算这套系统最多能拦截多少导弹。拦截来袭导弹时,必须按来袭导弹袭击的时间顺序,不允许先拦截后面的导弹,再拦截前面的导弹。
-
输入:
-
每组输入有两行,第一行,输入雷达捕捉到的敌国导弹的数量k(k<=25),
第二行,输入k个正整数,表示k枚导弹的高度,按来袭导弹的袭击时间顺序给出,以空格分隔。
-
输出:
-
每组输出只有一行,包含一个整数,表示最多能拦截多少枚导弹。
-
样例输入:
-
8 300 207 155 300 299 170 158 65
-
样例输出:
-
6
分析:
最大不降子序列的简单变形--------最大不增子序列:
#include "vector"
#include <iostream>
#include "fstream"
#include "algorithm"
#include <stdio.h>
#include "string"
using namespace std;
int maxLongDrop(vector<int>& vec)
{
int maxLong = 1;
vector<int> dp(vec.size(),1);
for (int i = 1; i < vec.size(); i++)
{
for (int j = 0; j < i; j++)
{
if (vec[i] <= vec[j] && dp[j] >= dp[i])
{
dp[i] = dp[j] + 1;
if (dp[i]>maxLong)
maxLong = dp[i];
}
}
}
return maxLong;
}
int main()
{
int n = 0;
while (cin>>n && n != 0)
{
vector<int> vec(n);
for (int i = 0; i < n; i++)
cin >> vec[i];
cout << maxLongDrop(vec) << endl;
}
return 0;
}
/**************************************************************
Problem: 1112
User: EbowTang
Language: C++
Result: Accepted
Time:10 ms
Memory:1520 kb
****************************************************************/
参考资源
【1】《算法导论》
【2】《百度文库》,《百度百科》
【3】《维基百科》
【4】九度OJ:http://ac.jobdu.com/problemset.php
【5】https://en.wikipedia.org/wiki/Dynamic_programming
【6】http://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741374.html
【7】http://shmilyaw-hotmail-com.iteye.com/blog/2009761
【8】http://www.icodeguru.com/cpp/10book/
【9】网友,SJF0115,博客地址,http://blog.csdn.net/SJF0115/article/category/910590
【10】http://www.zhihu.com/question/23995189
注:
本文部分文字学习并整理自网络,代码参考并改写于《算法导论》.
如果侵犯了您的版权,请联系本人tangyibiao520@163.com,本人将及时编辑掉!
注:本博文为EbowTang原创,后续可能继续更新本文。如果转载,请务必复制本条信息!
原文地址:http://blog.csdn.net/ebowtang/article/details/45127659
原作者博客:http://blog.csdn.net/ebowtang