重拾算法第一天,选择从dp开始入门。
对于dp(动态规划)的理解:dp算法高效的原因在于记忆化,dp算法适用于解决具有最优子结构,重叠子问题,无后效性的问题。这时候记忆化的优势就可以体现出来,具体来说就是由于存在重叠子问题,可以把子问题的解保存下来,在求解其他问题时不必再求,可以减少时间复杂度。其实递推求解也是一种特殊的dp,特殊在于,递推的子结构只有一种情况。而dp来说,是要通过比较找到子问题的最优子结构(子问题的最优解)。
用dp解题的关键在于找到状态转移方程,这非常重要。
以上都是个人理解,不保真。
dp入门题:
hdu-2084 数塔
在讲述DP算法的时候,一个经典的例子就是数塔问题,它是这样描述的:
有如下所示的数塔,要求从顶层走到底层,若每一步只能走到相邻的结点,则经过的结点的数字之和最大是多少?
这道题是dp的模板题,从dp的三个特点来分析,重叠的子问题:对于一个5层数塔来说,要求得它的最优解是找到它对应的四层数塔的最优解,这就是重叠子问题。最优子结构:求解一个n层的数塔的最优解,我们将n层数塔对应的最优解设为f[n],那么就有f[n]=f[n-1]+该节点的值。那么f[n-1]又可以递归下去,当n=1时,f[1]就是节点的值。无后效性:显然是不存在后效性的。
具体实现:
维护一个二维数组dp[][],用于保存当前节点对应的数塔的最优解,由最优子结构,我们显然是从数塔底部向上维护dp数组。
for(int j=1;j<=n;j++)
{
dp[n][j]=a[n][j];//一层数塔最优解是节点的值
}
for(int i=n-1;i>=1;i--)
{
for(int j=1;j<=i;j++)
{
dp[i][j]=a[i][j]+max(dp[i+1][j],dp[i+1][j+1]);//选择最优子结构,并存入dp数组中
}
}
hdu-1176 免费馅饼
这道题可以说是数塔的变形题,只需要通过预处理将输入变成与数塔类似的二维数组里。然后的dp【】【】数组的构建思路和数塔完全相同,不过需要注意边界的状态转移方程略有不同。
hdu-1257 最少拦截系统
本题实际上也可以用贪心的思路做,但是根据Dilworth定理,可以将问题转化为求最长上升子序列,用dp求解最长上升子序列是非常简便的。
int ma=0;
for(int i=0;i<n;i++)
{
f[i]=1;
for(int j=0;j<i;j++)
{
if(a[j]<a[i])
{
f[i]=max(f[i],f[j]+1);
}
}
ma=max(ma,f[i]);
}
printf("%d\n",ma);
hdu-1160 FatMouse's Speed
本题实质就是求最长下降子序列,但是需要降维和记录dp的路径,(有点难。。。),降维可以用sort排序实现,记录路径是通过构造一个pre【】数组,记录某个点的前驱,最后可以使用递归输出来实现倒着输出,(这是我的for循环设计的问题,导致我要记录的是前驱)(思路确实巧妙)
附上代码:
#include<bits/stdc++.h>
using namespace std;
struct node
{
int n;
int wei;
int sp;
} a[1001];
int f[1001];
int pre[1001]={0};
void output(int pos)
{
if(pos==0)
return;
output(pre[pos]);
printf("%d\n",a[pos].n);
}
bool cmp(node a, node b)
{
if(a.wei==b.wei)
return a.sp>b.sp;
return a.wei<b.wei;
}
int main()
{
int count=0;
while(cin>>a[count].wei>>a[count].sp)
{
a[count].n=count+1;
count++;
}
sort(a,a+count,cmp);
int res=0;
int id=0;
for(int i=0; i<count; i++)
{
f[i]=1;
for(int j=0; j<i; j++)
{
if(a[i].sp<a[j].sp&&a[i].wei>a[j].wei)
{
if(f[i]<f[j]+1)
pre[i]=j;
f[i]=max(f[i],f[j]+1);
}
}
if(res<f[i])
{
res=f[i];
id=i;
}
}
printf("%d\n",res);
output(id);
}