这道NOIP2005的题目是道好题,思路多多,编码技巧多多。据说NOI夏令营的时候虎爷
还专门拿出来津津乐道(汗~)。 鉴于好久没做NOI的题了,正巧有人问道,就写个解题报
告吧。
题目描述
在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧。在桥上有一些石
子,青蛙很讨厌踩在这些石子上。由于桥的长度和青蛙一次跳过的距离都是正整数,我们可
以把独木桥上青蛙可能到达的点看成数轴上的一串整点:0,1,……,L(其中L是桥的长度)。
坐标为0的点表示桥的起点,坐标为L的点表示桥的终点。青蛙从桥的起点开始,不停的向终点
方向跳跃。一次跳跃的距离是S到T之间的任意正整数(包括S,T)。当青蛙跳到或跳过坐标为L
的点时,就算青蛙已经跳出了独木桥。
题目给出独木桥的长度L,青蛙跳跃的距离范围S,T,桥上石子的位置。你的任务是确定
青蛙要想过河,最少需要踩到的石子数。
对于全部的数据,L <= 10^9。
输入格式
输入的第一行有一个正整数L(1 <= L <= 10^9),表示独木桥的长度。第二行有三个
正整数S,T,M,分别表示青蛙一次跳跃的最小距离,最大距离,及桥上石子的个数,其中
1 <= S <= T <= 10,1 <= M <= 100。第三行有M个不同的正整数分别表示这M个石子在数
轴上的位置(数据保证桥的起点和终点处没有石子)。所有相邻的整数之间用一个空格隔开。
输出格式
输出只包括一个整数,表示青蛙过河最少需要踩到的石子数。
样例输入
10
2 3 5
2 3 5 6 7
样例输出
2
分析:
首先注意下数据量,maxL=10^9,这是一个即使是线性算法也已经逼近运行时限的规模。
所以要准备好进行极限优化。
显而易见,最纯朴的想法是尝试搜索,从一个点扩展出所有可能跳到的点,依此类推,
最后DFS寻找由顶至下经过石子最少的路径即可。但注意到树的宽度有可能达到maxJump=10,
深度也和L成正比,故蛮力搜索不现实。
可不可以进行优化呢?我们注意到,DFS效率低下的一个很重要原因是,递归遍历的路
径有许多次其实是同一条路径,这些重复工作消耗了太多时间。于是我们自然而然想到要寻
找记忆化的算法。
我们发现,这个问题实际上包含最优子结构,只要青蛙能站在某一个位置,以它的智商
绝对会从前面k个可能的位置中,寻找这么一个位置,这个位置所处在的跳跃路径上所经过的
石子是最少的,从这个位置跳跃而来。显然,这个性质是递归定义的。 额外的,我们看到,
当青蛙站在某条路径上的某个位置,那么它此后跳跃的路径(当然是可能的路径)的选择并
不受之前的结果所干涉,因此问题具备无后效性。
可以动规了,下面直接给出转移方程。
DP[n]=min{DP[k]|n-maxJump<=k<=n-minJump}+t
t=0||t=1
即,设一个位置可以到达,那么必从前面可能的位置选择一个,该位置所处的路径上经
过石子最少。若当前位置有石子,dp结果再加一。
这是一个O(n)的顺推,但是,如果minJump-maxJump=1-10,常数C就会太大造成超时。能
不能再优化呢?仔细观察,发现石子最多有100粒,在整个10^9长度中分布相当稀疏,这是一
个很重要的特点!假设 minJuump!=maxJump,那么若空白位置足够长,dp的结果最终都会趋
向于一个稳定值,这个值是前面所有可能的dp结果中最小的那个(请读者自己想象,若跳跃的
步长可以选择,即跳到某个位置的起跳点可以选择的话,在足够远的未来,一个聪明的青蛙
肯定会在一条最优的路径上屁颠着~ 汗)。
例如在这一数据
25
4 5 5
2 3 5 6 7
0~4位置的初始dp指依次为 [0,0,1,1,0],它将有如下变化,并且趋于稳定值0:
(0~5)=[0,0,1,1,0,1]
(1~6)=[0,1,1,0,1,1]
(2~7)=[1,1,0,1,1,1]
(3~8)=[1,0,1,1,1,0]
(4~9)=[0,1,1,1,0,0]
(5~10)=[1,1,1,0,0,2]
(6~11)=[1,1,0,0,2,2]
(7~12)=[1,0,0,2,2,0]
(8~13)=[0,0,2,2,0,0]
(9~14)=[0,2,2,0,0,0]
(10~15)=[2,2,0,0,0,2]
(11~16)=[2,0,0,0,2,0]
(12~17)=[0,0,0,2,0,0]
(13~18)=[0,0,2,0,0,0]
(14~19)=[0,2,0,0,0,0]
(15~20)=[2,0,0,0,0,0]
......
为了利用这个性质,我们先确定要使用的数据结构。显而易见的,对第i个位置的计算,
只依赖于距离i一定距离的数个已知结果。因此,我们只需关心相对坐标即可,于是使用队列
保存i之前一定距离内的dp结果,计算完毕后 dp[i-maxJump] 出队,dp[i]入队,再利用该队
列计算dp[i+1],如此往复循环。
现在回到之前的讨论,当一个队列中的结果已经达到稳态,那么对下一次计算,它只能产
生两种结果:如果计算位置没有石子,那么队列仍然保持稳定;如果有石子,那么加一的结果
将使得队列再次出现不稳定。 多么令人振奋的结论!
这意味着什么呢?这意味着当你发现队列已经进入稳定的时候,后面所有对不存在石子的
位置进行的计算都是可以忽略的,你不妨直接计算到下一个未访问的且有石子的位置即可。
很多人已经看到了,这是一个状态压缩+DP的做法。
算法终止: 1.dp已计算完所有石子并且结果进入稳定态
2. 队列已经稳定并且下一个落地的位置所有可能的起点都在桥的终点或之外
读者可以自行分析中止的条件,剩下的只需要考虑细节问题就可以了,只要思路清晰本
题就不会太难过。算法的实际运行效率没有问题,单个极限数据只消耗了45ms。
本题据说还有其他解法,例如数学观点、最短路观点等,感兴趣的可以google下。
下面给出算法的main函数框架.
int main(){
scanf("%d%d%d%d",&l,&s,&t,&m);
for(step=0;step<m;step++)
scanf("%d",&stone[step]);
qsort(stone,m,sizeof(stone[0]),cmp);
_que=(Queue)CreatNewQueue;
for(step=0;step<t;step++) Enqueue(_que,0); //初始化DP队列
result=0;
stepTotal=0;
stepStone=-1;
if(s!=t){
while(1){
if(IsStran(_que)){
if(stepTotal-t>=l||stepStone==m-1) break;
stepStone++;
stepTotal=stone[stepStone];
}
else
stepTotal++;
DPCompute(_que,stepTotal,stone);
}
result=_que->head->dpS;
}
else{
// 当s=t 时其实仍然符合转移方程的定义,但简单起见写成了如下形式
for(step=0;step<m;step++)
if(stone[step]%s==0) result++;
}
printf("%d\n",result);
return 0;
}