暑期学习7.20
按计划是学习红书9.1,9.2的内容。留点记录。
- 9.1数字三角形
- 9.2DAG上的动态规划
9.1数字三角形
前言
数字三角形也不是第一次接触,这里也不在赘述。而书上这个例子也是为了引出:动态规划的核心是状态和状态转移方程。书上给了三种方法,第一种是个反例,不多说,说一下方法二递推计算以及方法三记忆化搜索。
1.递推计算
个人理解,相较于方法一的函数递归,方法二更体现了状态的保存,让计算的次数减小了许多,这是我能体会到的。说一些细节上的东西吧。
1.在操作上,例如数组的开始可以从1开始,这样为边界处理带来了方便。
2.抄一下书上的内容,“递推的关键是边界和计算顺序。在多数情况下,递推法的时间复杂度是:状态总数X每个状态的决策个数X决策时间。如果不同状态的决策个数不同,需具体问题,具体分析。
3.就这道题来说,递推还是很容易的,但如果换了个情况我还行吗?总之,我先记住,清楚计算顺序以及边界。
/*
memset(dp,0,sizeof(dp));
for(int i=1; i<=n; i++)
{
dp[n][i]=a[n][i];
}
for(int i=n-1; i>0; i--)
{
for(int j=1; j<=i; j++)
{
dp[i][j]=a[i][j]+max(dp[i+1][j],dp[i+1][j+1]);
}
}
cout<<dp[1][1]<<endl;//注意从一开始留出边界 注意i+1
*/
2.记忆优化搜索
1.记忆优化搜索,程序虽然是递归的,但是关键在于,它把计算结果保存在数组d中,根据题目的不同,可以用一些条件判断出该状态是否还需要计算,减少了许多重复的问题。
2.用一下书上的话“它虽然不像递推法那样显示地指明了计算顺序,但仍然可以保证每个结点只能访问一次”。既可以用一些东西作为记忆,避免重复计算。“采用记忆优化搜索时,不必事先确定各状态的计算顺序,但需要记录每个状态是否已经计算过。”
int solve(int i,int j)
{
if(dp[i][j]>=0)
return dp[i][j];
return dp[i][j]=a[i][j]+(i==n?0:max(solve(i+1,j),solve(i+1,j+1)));
}
。。。
memset(dp,-1,sizeof(dp));
cout<<solve(1,1)<<endl;
总结
总的来说这一节是引出了两个典型的方法,前者递推注意边界以及计算顺序,后者注意如何判断已经计算过然后再递归。
9.2DAG上的动态规划
前言
DAG即Directed Acyclic Graph,是有向无环图的意思,很多问题都可以转化为DAG上的最长路,最短路,或路径计数问题。希望我能看出来:)
1.最长路及其字典序
这一章节主要讲的就是“嵌套矩形”的问题,概括起来,它是“不固定起点的最长路径”,d(i)=max{d(j)+1|(i,j)属于E},我们可以采用记忆优化搜索。其中有一个技巧,对于d[i],我们可以声明它的一个引用ans来进行操作,会方便许多。这个关键是输出字典序让我头疼了不少,我们可以选择最大d[i]所对应的i,如果i有多个,则选择最小的,接下来选择d(i)=d(j)+1合情合理。而如果要打印全部,那么必须要用一个数组记录,而不是简单的去掉break。
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
using namespace std;
int n;
int x[1010],y[1010],d[1010];
vector<int>ma[1005];
int a[1010];
int max1=0;
int maxnum;
int dp(int i)
{
int &ans=d[i];
if(ans>0)
return ans;
ans=1;
for(int j=0; j<ma[i].size(); j++)
{
ans=max(ans,dp(ma[i][j])+1);
}
return ans;
}
void print_all(int p)
{
if(p==0)
{
for(int i=max1; i>0; i--)
printf("%d ",a[i]);
printf("\n");
return;
}
for(int i=0; i<n; i++)
{
if(p==d[i])
{
a[p]=i;
print_all(p-1);
}
}
}
void print_ans(int i)
{
printf("%d ",i);
for(int j=0; j<ma[i].size(); j++)
if(d[i]==d[ma[i][j]]+1)
{
print_ans(ma[i][j]);
break;
}
}
int main()
{
int kase;
scanf("%d",&kase);
while(kase--)
{
scanf("%d",&n);
for(int i=0; i<n; i++)
scanf("%d%d",&x[i],&y[i]);
memset(d,0,sizeof(d));
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
{
if((x[i]<x[j]&&y[i]<y[j])||(x[i]<y[j]&&y[i]<x[j]))
ma[i].push_back(j);
}
for(int i=0; i<n; i++)
max1=max(max1,dp(i));
for(int i=0; i<n; i++)
{
if(d[i]==max1)
maxnum=i;
break;
}
printf("****%d\n",max1);
print_all(max1);
//print_ans(maxnum);
for(int i=0; i<n; i++)
{
if(ma[i].size()!=0)
ma[i].clear();
}
}
return 0;
}
2.固定终点的最长路和最短路
这部分主要讲的是硬币问题。d(i)的确切含义变为“从结点i出发到结点0的最长路径长度”。要注意的是,s可以为0,所以用-1来表示没有算过。还需要注意的是,有可能d[s]无法到达0。如果用特殊值表示还没算过,则必须和其他特殊值区分开。当然啦,我们还可以用vis[s]来记录有没有算过。
minv[0]=maxv[0]=0;
for(int i=1; i<=S; i++)
{
minv[i]=INF;
maxv[i]=-INF;
}
for(int i=1; i<=S; i++)
for(int j=1; j<=n; j++)
if(i>=V[j])
{
minv[i]=min(minv[i],minv[i-V[j]]+1);
maxv[i]=max(maxv[i],maxv[i-V[j]]+1);
}
就这样吧,感觉这么搞效率挺低的,以后改一下。