HDU 复试DP题目记录
HDU2059 龟兔赛跑
解决思路
首先思考DP[]数组在这个问题里的含义,dp[i]代表到达第i个充电站的最短时间,所以DP[0]代表起点,DP[N+1]就是最终的答案。
考虑到特殊情况,可能从起点不充电就能抵达终点,所以DP[i]数组的初值为从起点开始,不充电,到第i个加油站的时间.(需要计算没电推车的情况)
状态转移方程:
dp[i]=min(dp[i],dp[j]+x)
dp[i]本身存储着全程不充电到达第i个站的时间
x代表在第j个充电站充电然后直接开到i的时间。(中途不充电)
选择两个方案中时间小的那个
代码实现
#include<stdio.h>
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
double L,C,T,VR,VT1,VT2;
int N;
while(~scanf("%lf %d %lf %lf %lf %lf %lf",&L,&N,&C,&T,&VR,&VT1,&VT2))
{
double p[121];
double dp[121];//dp[i]代表到达第i个站的最短时间
for(int i=1;i<=N;i++)
{
scanf("%lf",&p[i]);
}
p[0] = 0;
p[N+1]=L;
dp[0]=0;
for(int i=1;i<=N+1;i++)
{//判断从起点开始不充电的特殊情况
//从起点开始,不充电,到第i个加油站的时间
if(p[i]<=C)
{
dp[i] = p[i]*1.0/VT1;
}
else
dp[i] = (C*1.0/VT1)+(p[i]-C)*1.0/VT2;
}
//此时dp[i]存储的是从起点开始,不充电,到达第i个加油站的时间
//式子表达为dp[i]=min(dp[i],dp[j]+x,dp[j]+y])
//x代表在第J个充电站充电然后直接开到i的时间。(中途不充电)
//y代表在第J个充电站不充电直接裸开到i充电站的时间。
//PS:当然这里dp[j]+y实际不必要考虑,因为你在j-1号充电站判断是不是要充电的时候已经把在J号充电站不充电的情况考虑进去了。所以加不加问题不大,思路都是正确的。加了更容易理解,如果你熟练了以后可以直接去掉。
for(int i=2;i<=N+1;i++)
{
for(int j=1;j<i;j++)
{//在第j个站充电后,到达第i个站的时间
double Ti = T;
if(p[i]-p[j] <= C)
Ti += dp[j]+((p[i]-p[j])*1.0/VT1);
else
Ti += dp[j]+(C*1.0/VT1)+((p[i]-p[j]-C)*1.0/VT2);
dp[i] = min(Ti,dp[i]);//此时dp[i]里面存着从起点出发到加油站i的时间,若选了dp[i]则代表不充电
}
}
//cout << dp[N+1] <<endl;
float time = L/VR*1.0;
if(dp[N+1]>time)
printf("Good job,rabbit!\n");
else
printf("What a pity rabbit!\n");
}
}
HDU2084 数塔
问题描述
问题思路
思路一:
dp[i][j]代表从该点出发到达底层的最大值,那么边界则是最后一层,因为最后一层的每个dp[i][j]都等于它本身,dp[1][1]则为最终答案
状态转移方程:
dp[i][j] = max(dp[i+1][j],dp[i+1][j+1]) + a[i][j];
第i层第j个节点应该选取下一层它的两个分支中大的数来加上自身的值。
思路二:
dp[i][j]代表从dp[1][1]出发到达dp[i][j]的最大值,边界是第一层第一个,dp[i][n]中最大的一个则为答案
状态转移方程:
dp[i][j] = max(dp[i-1][j],dp[i-1][j+1]) + a[i][j];
第i层第j个节点应该选取上一层它的两个前驱中大的数来加上自身的值。
代码实现
此处仅给出第一种思路的代码
#include<stdio.h>
#include<math.h>
#include<iostream>
#include<string.h>
#include<algorithm>
using namespace std;
int dp[110];
int a[110][110];
int main()
{
int c;
while(~scanf("%d",&c))
{
while(c--)
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
scanf("%d",&a[i][j]);
}
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
{//初始化为最后一层的数
dp[i] = a[n][i];
}
for(int i=n-1;i>=1;i--)
{
for(int j=1;j<=i;j++)
{//从最后一层往回推,把下一层两个分支较大的一个与当前结点相加
dp[j] = max(dp[j],dp[j+1]) + a[i][j];
}
}
printf("%d\n",dp[1]);
}
return 0 ;
}
}
最长连续子序列和
问题描述
解题思路
令DP[i]表示以A[i]为结尾的连续序列的最大和
那么
所以只有两种情况
1、序列只有一个元素,即本身
2、序列有多个连续的元素,即dp[i-1]+A[i]
那么状态转移方程为
dp[i] = max(A[i],dp[i-1]+A[i])
代码实现
#include<stdio.h>
#include<algorithm>
using namespace std;
const int maxn = 10010;
int main()
{
int A[maxn],dp[maxn];
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%d",&A[i]);
}
//边界
dp[0] = A[0];
for(int i=1;i<n;i++)
dp[i] = max(A[i],dp[i-1]+A[i]);//状态转移方程
//遍历DP数组得到答案
int ans = -10000;
for(int i=0;i<n;i++)
{
if(ans < dp[i])
ans = dp[i];
}
printf("%d\n",ans);
return 0;
}
最长不下降子序列(LIS)
问题描述
解题思路
用dp[i]表示以A[i]结尾的最长不下降子序列长度,这样有两种可能
1、如果存在A[j](j<i)使得A[j]<=A[i]且dp[j]+1>dp[i],那么久把A[i]加入到A[j]后面
2、如果不存在这样的A[j],则DP[i] = 1即只有一个A[i]
状态转移方程为
dp[i] = max(1,dp[j]+1)
边界为
dp[i] = 1;
初始假设每个元素自成一个序列
代码实现
#include<stdio.h>
#include<algorithm>
using namespace std;
//8 1 2 3 -9 3 9 0 11
//最长不减子序列(可以不连续)
const int maxn = 1000;
int f[maxn],dp[maxn];
//f存放数,dp存放以该点为右端点的最优值
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{//[1,n]
scanf("%d",&f[i]);//输入数列
}
int ans = -1;//记录最大的dp[i];
//然后往右计算dp[i]
for(int i=1;i<=n;i++)
{//[1,n]
dp[i] = 1;//边界初始条件,假设每个元素自成一个序列
for(int j=1;j<i;j++)
{//用f[i]去询问之前的节点,f[i]是否能加入它们的队列 && 加入f[j]的队列后,以f[i]结尾的序列长度可以变大
if(f[i] >= f[j] && (dp[j]+1 > dp[i]))
dp[i] = dp[j] + 1;//更新dp[i]
}
ans = max(ans,dp[i]);//取得最大的dp[]
}
printf("%d\n",ans);
return 0;
}
最长公共子序列(LCS)
问题描述
解题思路
令dp[i][j]表示字符串A的i号位和B的j号位之前的LCS长度,如dp[4][5]表示“sads”和“admin”之间的LCS长度。分为两种情况
1、若A[i] == B[j] ,则LCS增加1位,即dp[i][j] = dp[i-1][j-1] + 1
2、若A[i] != B[j],则LCS将会继承dp[i-1][j]和dp[i][j-1]中的较大值,即dp[i][j] = max(dp[i-1][j],dp[i][j-1])
状态转移方程为:
代码实现
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
//8 1 2 3 -9 3 9 0 11
//最长公共子序列(可以不连续)
const int maxn = 1000;
char A[maxn],B[maxn];
int dp[maxn][maxn];
//f存放数,dp存放以该点为右端点的最优值
int main()
{
int n;
gets(A+1);//从下标1开始读入
gets(B+1);
int lenA = strlen(A+1);//因为从1开始读入,长度也从+1开始
int lenB = strlen(B+1);
//边界
for(int i=0;i<=lenA;i++)
dp[i][0] = 0;
for(int j=0;j<=lenB;j++)
dp[0][j] = 0;
//状态转移方程
for(int i=1;i<=lenA;i++)
{
for(int j=1;j<=lenB;j++)
{//
if(A[i] == B[j])//dp[i-1][j-1]存放的是i,j之前最长的公共子序列
dp[i][j] = dp[i-1][j-1] + 1;//更新dp[i][j]
else//如果不等,则赋值为A串目前最大的长度或B串目前最大的长度
dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
printf("%d\n",dp[lenA][lenB]);
return 0;
}
最长回文字串
问题描述
解题思路
令dp[i][j]表示S[i]到S[j]之间的子串是不是回文串,是为1,否则为0.这样根据S[i]是否等于S[j]分成两类:
1、若S[i] == S[j] 那么只要S[i+1] 到 S[j-1]是回文串的话,S[i] 到 S[j]就是回文串,如果S[i+1] 到 S[j-1]不是回文串,就不是
2、若S[i] != S[j]则一定不是回文子串
状态转移方程为
边界为
dp[i][i] = 1,dp[i][i+1] = (S[i]==S[i+1])?1:0
还有一个问题:当使用从小到大的方式枚举时,无法保证dp[i+1][j-1]已经被计算过。如:固定i=0,j从2开始枚举,列举到4时,dp[0][4] = dp[1][3],而dp[1][3]是没有被计算过的。
所以,我们需要按照字串的长度的初始位置进行枚举,即第一遍将长度为3的子串的dp值全部求出,第二遍通过第一遍的结果计算出长度为4的子串的dp值
代码实现
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
//PATZJUJZTACCBCC
//PATZJUJZTACCBCC
//最长回文子串(可以不连续)
const int maxn = 1000;
char S[maxn];
int dp[maxn][maxn];//dp[i][j] = 1代表从S[i]到S[j]是回文字串
//f存放数,dp存放以该点为右端点的最优值
int main()
{
gets(S);
int ans = 1;//保存最长回文串的长度
int len = strlen(S);
memset(dp,0,sizeof(dp));
//边界
for(int i=0;i<len;i++)
{
dp[i][i] = 1;//单个字符长度为1
if(i < len-1)
{//在i枚举到最后一位字符之前
if(S[i] == S[i+1])
{//如果有连续的相同的字符
dp[i][i+1] = 1;//是回文串
ans = 2;
}
}
}
//状态转移方程
for(int L=3;L<=len;L++)
{//枚举字串的长度,从3开始,因为2的情况在边界已经处理了
for(int i=0;i+L-1 < len;i++)
{//i代表左端点,i+L-1是右端点
int j = i+L-1;
if(S[i] == S[j] && dp[i+1][j-1] == 1)
{//如果两端点相同 且内部为回文串
dp[i][j] = 1;//更新dp[i][j]
ans = L;//更新长度
}
}
}
printf("%d\n",ans);
return 0;
}
DAG最长路(关键路径)
问题描述
DAG最长路可以分为两类
1、不固定起点和终点,求整个DAG中的最长路径
2、固定终点,求最长路径
解题思路
dp[i]表示从定点i出发能获得的最长路径,这样dp数组的最大值就是答案。
dp[i]数组的求解就是遍历i出发能到达的所有顶点,找出最长的路径。
边界就是出度为0的顶点,它们的路径长度为0。
代码实现
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
using namespace std;
const int inf = 100000000;
const int maxv = 1000;
//4 4 3 0 1 1 0 2 2 1 3 2 2 3 2
//邻接矩阵版。适用于点不多的情况小于1000
int n,G[maxv][maxv];//n为顶点数,maxv为最大顶点数
int dp[maxv];//dp[i]表示从i号节点出发能获得的最长路径的长度
//仅计算最长路径的长度
/*
int DP(int i)
{//计算dp数组
if(dp[i] > 0)//如果这个dp[i]已经算过了
return dp[i];
for(int j=0;j<n;j++)
{//遍历i的所有出边
if(G[i][j] != inf)
{//DP(j)是j号节点出发能获得的最长路径的长度
dp[i] = max(dp[i],DP(j)+G[i][j]);
}
}
return dp[i];//返回计算完毕的dp[i]
}
*/
//具体打印出最长路径
int choice[maxv];
int DP(int i)
{//计算dp数组
if(dp[i] > 0)//如果这个dp[i]已经算过了
return dp[i];
for(int j=0;j<n;j++)
{//遍历i的所有出边
if(G[i][j] != inf)
{//DP(j)是j号节点出发能获得的最长路径的长度
int temp = DP(j) + G[i][j];
if(temp > dp[i])
{
dp[i] = temp;//覆盖dp[i]
choice[i] = j;//i的后继是j
}
}
}
return dp[i];//返回计算完毕的dp[i]
}
//得到最大的dp[i]之后
void printpath(int i)
{
printf("%d",i);
while(choice[i] != -1)
{//choice数组初始化为-1
i = choice[i];//迭代
printf("->%d",i);
}
}
//规定了终点为T
bool vis[maxv];//定义一个访问数组,因为dp[i]>0
int DP_T(int i)
{//计算dp数组
if(vis[i])//如果这个dp[i]已经算过了
return dp[i];
vis[i] = true;
for(int j=0;j<n;j++)
{//遍历i的所有出边
if(G[i][j] != inf)
{//DP(j)是j号节点出发能获得的最长路径的长度
int temp = DP_T(j) + G[i][j];
if(temp > dp[i])
{
dp[i] = temp;//覆盖dp[i]
choice[i] = j;//i的后继是j
}
}
}
return dp[i];//返回计算完毕的dp[i]
}
int main()
{
int u,v,w,t,m;
scanf("%d%d%d",&n,&m,&t);//顶点个数,边数,终点编号
fill(G[0],G[0]+maxv*maxv,inf);//初始化图G
memset(choice,-1,sizeof(choice));//初始化路径数组
for(int i=0;i<m;i++)
{
scanf("%d%d%d",&u,&v,&w);//输入u,v,及u->v的边权
G[u][v] = w;
}
//边界
//memset(dp,0,sizeof(dp));//(不知道终点的情况下)出度为0的点出发的路径长度为0,遇到出度不是0的点,递归求解
fill(dp,dp+maxv,-inf);//知道终点为t的情况
dp[t] = 0;//终点要初始化为0
int maxlong = -1;//记录长度
int path = -1;//记录最长路径的出发点
for(int i=0;i<n;i++)
{//枚举所有顶点
int templen = DP_T(i);
if(maxlong < templen)
{
maxlong = templen;
path = i;//更新
}
}
printpath(path);
printf("\n");
return 0;
}
总结
以下列举部分常见的动态规划模型
1.最长连续子序列和
dp[i]表示以A[i]作为结尾的连续序列的最大和
2.最长不下降子序列LIS
dp[i]表示以A[i]作为结尾的最长不下降子序列的最大和
3.最长公共子序列LCS
令dp[i][j]表示字符串A的i号位和B的j号位之前的LCS长度
4.最长回文子串
令dp[i][j]表示S[i]到S[j]之间的子串是不是回文串
5.数塔DP
令dp[i][j]表示从第i行第j个数字出发到达底层的所以路径上得到的最大和
6.DAG最长路
dp[i]表示从定点i出发能获得的最长路径
7.01背包
令dp[i][v]表示前i件物品恰好装入容量为v的背包中所获得的最大价值
8.完全背包
令dp[i][v]表示前i件物品恰好装入容量为v的背包中所获得的最大价值
TIPS
当题目与序列或字符串有关时,常常是
1、令dp[i]表示以A[i]结尾(或开头)的XXX
2、dp[i][j]表示以A[i]到A[j]间的XXX
当题目的状态是多维的时候,对其中的每一维度采取以下其中一种描述:
1、恰好为i
2、前i
这样dp数组的含义就是“令dp数组表示恰好为i(或前i)、恰好为j(或前j)…的XXX”
最后,大部分情况下可以把动态规划的问题看作一个有向无环图DAG,图中的节点就是状态,边就是状态转移方程,求解的顺序就是按照DAG的拓扑排序进行求解。