硬币模型
有n种硬币,面值 V1,V2,...,Vn 且有无限多个。给定S,可以选出多少硬币,使得总额为S?求出该数量的最大值和最小值。
硬币模型的区别在于,这里的起点和终点都是确定的。我们求的就是确定起点、终点的DAG最短路。
以最短路为例,转移方程:dp[i]=min{dp[i-V[j]]},这里的i表示从0出发到i的最短路径长度。
实现:
#include <iostream>
#include <climits>
#include <algorithm>
#define MAXN INT_MAX-10003
using namespace std;
int V[103],n;
int maxd[103],mind[103];
void print(int* dp,int S){
for(int i=1;i<=n;++i){
if(V[i]<=S&&dp[S]==dp[S-V[i]]+1){
cout<<i<<" ";
print(dp,S-V[i]);
break;
}
}
}
int main(){
int S;
cin>>n>>S;
for(int i=1;i<=n;++i)
cin>>V[i];
for(int i=1;i<=S;++i)
mind[i]=MAXN,maxd[i]=-MAXN;
for(int i=1;i<=S;++i){
for(int j=1;j<=n;++j){
if(i>=V[j]){
mind[i]=min(mind[i],mind[i-V[j]]+1);
maxd[i]=max(maxd[i],maxd[i-V[j]]+1);
}
}
}
if(mind[i]==MAXN&&maxd[i]==-MAXN) cout<<"Undefined"<<endl;
else{
cout<<mind[S]<<" "<<maxd[S]<<endl;
print(mind,S);
cout<<endl;
print(maxd,S);
}
}
[千万不要太作死的用limts.h…我惨痛的+1溢出了]
[虽说刘书还提供了一种字典序打印方法…不过这里就不列了,我觉得这种就挺好用的。]
DAG问题小结
(1)DAG问题的基本规律
DAG是有向无环图。也就是说,对于一类可以将状态转换到图上求解最短路/最长路的题目可以用这种方式解决。
动态转移方程:
最短路:dp[i]=min{dp[j]+1} (1)
最长路:dp[i]=max{dp[j]+1} (2)
复杂度O(VE)
[可能你会奇怪,之前我们在处理硬币问题的时候,转移并不是长这个样子的啊。其实不然。这里的j,指的是顶点]
无论是(1)还是(2),本质都是一致的。但是有个问题:dp[i]的含义是什么?有两种情况:第一种是以i为起点的路径,第二种是以i为终点的路径。
不妨来思考一下,对于这两种解释,上述转移方程是否成立…显然是成立的。不过显然,对于第一种情况,限制条件应该为(j,i)
∈
E,而第二种情况是(i,j)
∈
E。
从正常情况下讲,人类的思想会偏向于选择第二种情况。事实上,这种情况写起来也要顺一些。我们两道题都是用的这种思想。
(2)刷表法
很多时候,我们的递推公式是建立在i与i之前状态的。这样我们用类似填鸦的方法,完成一张dp表格。我们称这种方法叫填表法。
但是有个很麻烦的问题:我们并不一定能确定所有i依赖的状态。
这时有一个看上去很蛋疼的方法:对于每个状态i更新i影响的状态。这样的方法就叫刷表法。
[说实话光光是概念并不能让我理解,然后我就百度了一下。]
http://blog.youkuaiyun.com/good_night_sion_/article/details/54868978
先贴原po,这道例题可以比较清楚的阐释刷表法的做法。
给的几道例题
9-1 UVa1025
看上去超级复杂浑身难受,不过其实可以意外很简单的思考。
用dp[i][j]表示到第i时刻在车站j的最短等待时间,那么我们求的就是起点为0,终点为T的DAG最短路。
因此可以写出状态转移方程:
dp[i][j]=min(dp[i+1][j]+1,dp[i+t[j]][j+1],dp[i+t[j-1]][j-1])
其中取到状态二的条件是此时有正向的车,条件三是有反向的车。
9-2 UVa437
因为有无穷多个,所以不怂,将每个长方体变成三个。这就是一个DAG。可连通的一条路权值就是高度,求最长路即可。
说实话不是很懂刘书的dp[idx,k]…
不过其实我想问如果这里的长方体只有一个会怎么办?
9-3 UVa1347
非常丧心病狂的转移…不过一旦接受了这个设定还挺带感的..
来换一个思考的角度,既然是一个人折返就不如看成两个人单向。
现在的问题是,如何确定两个人没有经过同一个点?
不妨设现在一个人在横坐标i的点,一个在横坐标j的点。我们用dp[i][j]表示这一状态。
想要走向下一步,有两种转移。一种是第一个人走向第i+1个点,一种是第二个人走向第i+1个点。转移:
dp[i][j]=min(dp[i+1][j]+dist(i,i+1),dp[i][i+1]+dist(j,i+1))
dist表示距离,用两点之间坐标公式处理即可。
不过这样还有些复杂。显然,dp[i][j]=dp[j][i],因此不妨设i>j,则可以将dp[i][i+1]转化为dp[i+1][i]。
临界条件为dp[n-1][j]=dist(n-1,n)+dist(j,n)。
现在的问题只有一个:为什么我们的转移只有两种呢?明明第一个人可以走向i+1,也可以走向i+2啊!
秘诀在于:这里的dp[i][j]是1~i全部走过(i>j),两个人的位置为i和j。换言之,此时如果我转移到i+2那么i+1就是一个未走过的部分,矛盾!这就是这个状态设计的精妙之处。
实现可以使用记忆化搜索的方法,免去处理for方向的麻烦。