dp杂题–数塔问题
1.简单例题:
数塔问题(Hdu_oj2084)DP
数塔
Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 70797 Accepted Submission(s): 40982
Problem Description
在讲述DP算法的时候,一个经典的例子就是数塔问题,它是这样描述的:
有如下所示的数塔,要求从顶层走到底层,若每一步只能走到相邻的结点,则经过的结点的数字之和最大是多少?
已经告诉你了,这是个DP的题目,你能AC吗?
Input
输入数据首先包括一个整数C,表示测试实例的个数,每个测试实例的第一行是一个整数N(1 <= N <= 100),表示数塔的高度,接下来用N行数字表示数塔,其中第i行有个i个整数,且所有的整数均在区间[0,99]内。
Output
对于每个测试实例,输出可能得到的最大和,每个实例的输出占一行。
Sample Input
1
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
Sample Output
30
Source
2006/1/15 ACM程序设计期末考试
题解
1.dp入门题。
2.非dp思路:
其实本题有很多的方法,这里先简单叙述一下不用dp的方法
例:
(1)贪心。当然纯粹的贪心是不能过的,因为贪心会破坏对应的无后效性,那么一次贪心不行我们就贪个一万次,嘛,每次寻找最优解。
(2) 深搜。时间复杂度:2 ^ n;
以上两种算法不一定能够满分,数据如果比较强,那就需要dpd了
(3)随机数法。因为该题是多组数据,所以概率就大打折扣了
该方法代码:
int result=0;
srand(time(NULL));
for(int t=1;t<=10000;t++)
{
int x=0;
int v=0;
for(int i=1;i<=n;i++)
{
double p=(double)rand()/(double)RAND_MAX;
if(p<0.5) x+=0;
else x+=1;
v+=a[i][x];
}
if(v>result) result=v;
}
//随机大法
(4)这里简绍一种求最小值的算法:深搜。当然纯粹的深搜也是不能过的,因为深搜会带来2 ^ n的时间复杂度,所以我们可以通过剪枝来优化。那怎么剪枝呢?goodquestion。我们可以先简单贪心一两次,找到一组较小的合法的解。然后在深搜的过程中,如果我们当前得到的答案已经大于贪心的解了,就直接return
3.dp大法
(1)定义dp状态:dp[i][j] 表示位置为的i ,j的点从底部到该点的最大和
(2)dp方程
a数组是用来存图
f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];
表示从自区间一层一层dp来获得最优解
这里需要注意的是我们for循环中i的顺序是逆推还是正推。
逆推: 从底下向上找最优解,我们可以dp完了之后直接输出 dp[1][1]
顺推:从上到下进行寻找,得到的最优解就存在于最底部,我们就需要在遍历最底层寻找一下最大值
核心代码如下
逆推:
for(int i=n;i>=1;i--)
{
for(int j=1;j<=i;j++)
{
f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];
}
}
cout<<f[1][1];//逆推
顺推:
for(int i=1;i<=n;i--)
{
for(int j=1;j<=i;j++)
{
f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];
}
}
int ans=-10;
for(int i=1;i<=n;i++)
{
if(ans<f[n][i]) ans=f[n][i];
}
cout<<ans;//顺推
4.附上AC代码+注释
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<ctime>
#define maxn 105
using namespace std;
int a[maxn][maxn],f[maxn][maxn];
int t;
int n;
int main()
{
cin>>t;
while(t--)
{
cin>>n;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
{
cin>>a[i][j];
}
}
for(int i=1;i<=n;i++)
{
f[n][i]=a[n][i];
}
for(int i=n;i>=1;i--)
{
for(int j=1;j<=i;j++)
{
f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];//由子区间dp
}
}
cout<<f[1][1]<<endl;//逆推
}
return 0;
}
2.在该题原条件下输出路径
g数组是一个标记数组表示在路径上一点的下一层是经过左边还是右边
void path(int i,int j)
{
if(i==0||j==0)
return ;
else if(g[i][j]==1)
path(i-1,j);
else
path(i-1,j-1);
cout<<a[i][j]<<" ";
}
int ans,p;
for(int i=1;i<=n;i++)
{
ans=p=0;
for(int j=1;j<=i;j++)
{
f[i][j]=f[i-1][j]+a[i][j];
g[i][j]=1;
if(f[i][j]>f[i-1][j-1]+a[i][j])
{
f[i][j]=f[i-1][j-1]+a[i][j]
g[i][j]=2;
}
if(result>f[i][j]) result=f[i][j],p=j;
}
}path(n,p);
//打印路劲
cout<<result;
3.进阶题目
(1)原条件不变,将求解目标改为路径和的个位数最大
求解目标改变会带来dp状态的改变。换汤不换药。但是如果还是按照以前的方法处理进位就会造成错误。
所以面对更加棘手的题目,我们可以升维度,来降难度。
dp[i][j][k](1<=k<=9) 定义: 坐标为(i,j)的点可以/不可以产生个位数为k的和,可以就标记为true
上代码
dp:
if((a[i][j]+t)%10==k)//如果可以构成k
{
if(dp[i+1][j][t]==true||dp[i+1][j+1][t]==true)
//如果子树能够提供t这个常数,那么就可以让该点的dp[][][k]变为true
{
dp[i][j][k]=true;
}
}
AC代码:
//求三角形路劲和的个位数最大值
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<ctime>
#define maxn 100
using namespace std;
bool dp[maxn][maxn][11];
int a[maxn][maxn];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
{
cin>>a[i][j];
}
}
for(int i=1;i<=n;i++)
{
dp[n][i][a[n][i]%10]=true;
}
for(int i=n;i>=1;i--)
{
for(int j=1;j<=i;j++)
{
for(int k=0;k<=9;k++)
{
for(int t=0;t<=9;t++)//这里的循环可以直接剪掉,各位自己实现吧
{
if((a[i][j]+t)%10==k)
{
if(dp[i+1][j][t]==true||dp[i+1][j+1][t]==true)
{
dp[i][j][k]=true;
}
}
}
}
}
}
for(int i=9;i>=0;i--)
{
if(dp[1][1][i])
{
cout<<i;
break;
}
}
return 0;
}
(2)求解目标不变,条件改变为其中某一个点,可以到下一层的任意一个点,并且这个点坐标未知,求最大路径和
后效性改变—升维
为了方便,我们称那个特殊点为x点
dp方程:
dp[i][j][0/1]:坐标为(i,j)的点的最优解,0表示该条路径已经遍历到了的地方没有x点,1表示该点或该条路径上有x点。
max[i]:存第i层的每个d[i][][0]当中的最大值,方便处理x点的计算
d[i][j][0]=max(d[i+1][j][0],d[i+1][j+1][0]);
d[i][j][1]=max(d[i+1][j][1],max(d[i+1][j+1][1],max[i+1]));
核心代码:
memset(max,0,sizeof(max));
for(int i=1;i<=n;j++)
{
d[n][i][1]=d[n][i][0]=a[n][i];
if(a[n][i]>max[n]) max[n]=a[n][i];
}
for(int i=n-1;i>=1;i--)
{
for(j=1;j<=i;j++)
{
d[i][j][0]=max(d[i+1][j][0],d[i+1][j+1][0]);
if(d[i][j][0]>max[i]) max[i]=d[i][j][0];
d[i][j][1]=max(d[i+1][j][1],max(d[i+1][j+1][1],max[i+1]));
}
}
conclusion
** 升维度思考:遇到棘手问题,考虑后效性
降维度思考:考虑空间换时间 **