这一篇博客继续以一些OJ上的题目为载体,对动态规划专题进行整理一下。会陆续的更新。。。
这是动态规划专题整理的第二篇博客,第一篇博客的地址
三、其他一些使用DP来解决的题目
1、HDU 2084
题目与分析:
使用DP来解决的题目具有的一个特点就是:问题的最优解包含了子问题的最优解。
从顶部出发,在每一结点可以选择向左走或是向右走,一直走到底层,要求找出一条路径,使路径上的值最大。
从顶点出发时到底向左走还是向右走应取决于是从左走能取到最大值还是从右走能取到最大值,只要左右两道路径上的最大值求出来了才能作出决策。 同样,下一层的走向又要取决于再下一层上的最大值是否已经求出才能决策。这样一层一层推下去,直到倒数第二层时就非常明了。 如数字2,只要选择它下面较大值的结点19前进就可以了。所以实际求解时,可从底层开始,层层递进,最后得到最大值。
操作,先记录最后一行的DP值,然后从倒数第二行开始向上开始计算取最大值,那么根的的值也就最大,最后输出根节点的DP值就可以了。
/*
* HDU_2084.cpp
*
* Created on: 2014年6月17日
* Author: Administrator
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 105;
int num[maxn][maxn];
int main(){
int t;
scanf("%d",&t);
while(t--){
int n;
scanf("%d",&n);
int i;
int j;
for(i = 1 ; i <= n ; ++i){
for(j = 1 ; j <= i ; ++j){
scanf("%d",&num[i][j]);
}
}
int dp[n+1];
for(i = 1 ; i <= n ; ++i){//记录最后一行的DP值
dp[i] = num[n][i];
}
for(i = n-1 ; i >= 1 ; --i){//从底向上求最大值
for(j = 1 ; j <= i ; ++j){
dp[j] = max(dp[j] + num[i][j],dp[j+1] +num[i][j]);
}
}
printf("%d\n",dp[1]);
}
return 0;
}
四、使用DP来解决最长连续子和问题
问题描述:在a[1]、a[2]......a[n]中找到a[i]、a[i+1]、a[j-1]、a[j]使得a[i]+a[i+1]+...+a[j-1]+a[j]最大
①最简单最容易想到的就是根据定义来枚举。
枚举上下界{i,j | 0<=i<=j<=n},维护一个max值即可。
其中枚举上下界的时间复杂度为O(n^2),求区间和的复杂度为O(n),所以总时间复杂度为O(n^3)。
for ( int i = 1 ; i <= n ; i++ )//i可以理解为区间的起点
for ( int j = i ; j <= n ; j++ )//j可以理解为区间的终点
ans = max(ans,accumulate(a+i,a+j+1,0));
②其实就是第一种方法的优化。
这里有个很容易想到的优化,即预处理出前缀和sum[i]=a[0]+a[1]+...+a[i-1]+a[i],算区间和的时候即可将求区间和的复杂度降到O(1),枚举上下界的复杂度不变,所以总时间复杂度为O(n^2)。
for ( int i = 1 ; i <= n ; i++ )
sum[i]=sum[i-1]+a[i];
for ( int i = 1 ; i <= n ; i++ )
for ( int j = i ; j <= n ; j++ )
ans = max(ans,sum[j]-sum[i-1]);
③可以利用动态规划的思维来继续优化,得到一个线性的算法,也是最大连续区间和的标准算法
定义maxn[i]为以i为结尾的最大连续和,则很容易找到递推关系:maxn[i]=max{0,maxn[i-1]}+a[i]。
所以只需要扫描一遍即可,总时间复杂度为O(n)。
for ( int i = 1 ; i <= n ; i++ )
{
last = max(0,last)+a[i];//last是计算到当前这个数的最长连续子序列的和
ans = max(ans,last);//ans是全局的最场连续子序列的和
}
④同样用到类似的思维。
首先也需要预处理出前缀和sum[i],可以推出ans=max{sum[i]-min{sum[j] } | 0<=j<i<=n }。
而最小前缀和可以动态维护,所以总时间复杂度为O(n)。
for ( int i = 1 ; i <= n ; i++ )
sum[i]=sum[i-1]+a[i];
for ( int i = 1 ; i <= n ; i++ )
{
ans = max(ans,sum[i]-minn);
minn = min(minn,sum[i]);
}
总结:虽然朴素的O(n^3)和前缀和优化的O(n^2)算法很容易想到,但代码实现却反而比方法三麻烦,第四个方法虽然有和方法三相同的复杂度,但需要一个预处理和多出的O(n)的空间,所以,方法三很好很强大。
例题:
1、NYOJ 44
题目分析:
这道题抽象一下就是给出a[1]...a[n],求a[i]、a[i+1]...a[j-1]、a[j],使得a[i]+a[i+1]+...a[j-1]+a[j]最大。输出这个最大值即可。这道题是典型的题目。一下给出算法思想一样(都是采用3中的方法),但可能在实现上有所不同的两种解法.
1)
/*
* NYOJ_44.cpp
*
* Created on: 2014年6月22日
* Author: Administrator
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1000005;
int a[maxn];
int main(){
int t;
scanf("%d",&t);
while(t--){
int n;
scanf("%d",&n);
int i;
for(i = 1 ; i <= n ; ++i){
scanf("%d",&a[i]);
}
int last = a[1];
int ans = a[1];
for(i = 2 ; i <= n ; ++i){
last = max(0,last) + a[i];//last: 以a[i]结尾的最长连续子和
ans = max(last,ans);//ans: 最大的连续子和
}
printf("%d\n",ans);
}
return 0;
}
2)
/*
* NYOJ_44.cpp
*
* Created on: 2014年6月22日
* Author: Administrator
*/
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 1000005;
int a[maxn];
int main(){
int t;
scanf("%d",&t);
while(t--){
int n;
scanf("%d",&n);
int pre;
int cur;
scanf("%d",&pre);
int max = pre;
int i;
for(i = 1 ; i < n ; ++i){
scanf("%d",&cur);
if((cur+pre) > cur){
cur += pre;
}
if(cur > max){
max = cur;
}
pre = cur;
}
printf("%d\n",max);
}
return 0;
}
2、NYOJ 104
题目分析:
这道题与上面的不同之处就在于,这道题是求二维的最长子序列连续和,而上面是求一维的最长子序列连续和...本题的关键就在于将二维的问题转换成一味的问题来解决。。
假设最大子矩阵的结果为从第r行到k行、从第i列到j列的子矩阵,如下所示(ari表示a[r][i],假设数组下标从1开始):
| a11 …… a1i ……a1j ……a1n |
| a21 …… a2i ……a2j ……a2n |
| . . . . . . . |
| . . . . . . . |
| ar1 …… ari ……arj ……arn |
| . . . . . . . |
| . . . . . . . |
| ak1 …… aki ……akj ……akn |
| . . . . . . . |
| an1 …… ani ……anj ……ann |
那么我们将从第r行到第k行的每一行中相同列的加起来,可以得到一个一维数组如下:
(ar1+……+ak1, ar2+……+ak2, ……,arn+……+akn)
由此我们可以看出最后所求的就是此一维数组的最大子段和问题,到此我们已经将问题转化为上面的已经解决了的问题了。
代码如下:
/*
* NYOJ_104.cpp
*
* Created on: 2014年6月22日
* Author: Administrator
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 105;
int a[maxn][maxn];
int ans;
int n,m;
/**
* 求第k行的最长子序列之和
*/
int work(int k){
int j;
int last = a[k][1];
int ansn = a[k][1];
for(j = 2 ; j <= m ; ++j){
last = max(0,last) + a[k][j];
ansn = max(last,ansn);
}
return ansn;
}
int main(){
int t;
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&m);
int i;
int j;
for(i = 1 ; i <= n ; ++i){
for(j = 1 ; j <= m ; ++j){
scanf("%d",&a[i][j]);
}
}
ans = -1000;
int k;
for(i = 1; i <= n ; ++i){
ans = max(ans,work(i));//先求该行没有加上下面的行之前的最长子区间
for(j = i+1 ; j <= n ; ++j){
for(k = 1 ; k <= m ; ++k){
a[i][k] += a[j][k];//将二维转换成一维问题的关键..
}
ans = max(ans,work(i));
}
}
printf("%d\n",ans);
}
return 0;
}
3、POJ 1050
题目分析:
这道题和上面的那一道题是类似的。只是输入数据不一样而已(矩阵由n*m变成了n*n,其他的还是一样的)...
/*
* POJ_1050.cpp
*
* Created on: 2014年6月23日
* Author: Administrator
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 105;
int a[maxn][maxn];
int n;
int ans;
int work(int k){
int last = a[k][1];
int ansn = a[k][1];
int j;
for(j = 2 ; j <= n ; ++j){
last = max(0,last) + a[k][j];
ansn = max(last,ansn);
}
return ansn;
}
int main(){
while(scanf("%d",&n)!=EOF){
int i;
int j;
for(i = 1 ; i <= n ; ++i){
for(j = 1 ; j <= n ; ++j){
scanf("%d",&a[i][j]);
}
}
ans = -100000;
int k;
for(i = 1 ; i <= n ; ++i){
ans = max(ans,work(i));
for(j = i+1 ; j <= n ; ++j){
for(k = 1 ; k <= n ; ++k){
a[i][k] += a[j][k];
}
ans = max(ans,work(i));
}
}
printf("%d\n",ans);
}
return 0;
}
4、HDU 1231
题目与分析:
这一道题抽象一下还是,“最长连续子序列的和”,与前面的题的不同之处在于:本题需要求出最长连续子序列的首元素和尾元素。
/*
* HDU_1231.cpp
*
* Created on: 2014年6月23日
* Author: Administrator
*/
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 10005;
int a[maxn];//用来记录数字序列
int ans[maxn];//ans[i]表示以a[i]结尾的最大连续子序列的和
int start[maxn];//以a[i]结尾的最长连续子序列的首元素
int n;
int main(){
while(scanf("%d",&n),n){
int i;
for(i = 1 ; i <= n ; ++i){
scanf("%d",&a[i]);
}
ans[1] = a[1];
start[1] = a[1];
for(i = 2 ; i <= n ; ++i){
if(ans[i-1] + a[i] > a[i]){
ans[i] = ans[i-1] + a[i];
start[i] = start[i-1];//表明以a[i]结尾的最大连续子序列的首元素和以a[i-1]结尾的最大连续子序列的首元素是一样的
}else{
ans[i] = a[i];
start[i] = a[i];//以a[i]结尾的最大连续子序列的首元素是a[i]自己
}
}
int ansn = -100000;
int pos = -1;
for(i = 1 ; i <= n ; ++i){
if(ans[i] > ansn){
ansn = ans[i];
pos = i;
}
}
if(ansn >= 0){
printf("%d %d %d\n",ansn,start[pos],a[pos]);
}else{
printf("%d %d %d\n",0,a[1],a[n]);
}
}
return 0;
}