以前写代码都不写解题报告,从现在开始对于比较难的题都提供解题报告
一方面自己以后好查阅,一方面供网友理解学习交流。
---- 江南孤峰 ----
2007--7--30
下面这题调试了很久,主要是状态压缩出错,最后到网上找到了测试数据,终于
AC了,而且还是最优的代码,冲这点,我觉得发点时间写份详细的报告,值得。
代码真的是调试出来的!!!
/////////////////////////////////////////////////////////////////////
青蛙过河
Problem description
在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧。在桥上有一些石子
青蛙很讨厌踩在这些石子上。由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以
把独木桥上青蛙可能到达的点看成数轴上的一串整点:0,1,……,L(其中L是桥的长度)坐标为0的点表示桥的起点,坐标为L的点表示桥的终点。青蛙从桥的起点开始,不停的向终点方向跳跃。一次跳跃的距离是S到T之间的任意正整数(包括S,T)。当青蛙跳到或跳过坐标为L的点时,就算青蛙已经跳出了独木桥。
题目给出独木桥的长度L,青蛙跳跃的距离范围S,T,桥上石子的位置。你的任务是确定
青蛙要想过河,最少需要踩到的石子数。
Input
输入的第一行是一个整数T,测试数据的组数。对于每一组测试数据,第一行是一个正整数
L(1 <= L <= 10^9),表示独木桥的长度。第二行有三个正整数S,T,M,分别表示青蛙一次跳跃的最小距离,最大距离,及桥上石子的个数,其中1 <= S <= T <= 10,1 <= M <= 100。第三行有M个不同的正整数分别表示这M个石子在数轴上的位置(数据保证桥的起点和终点处没有石子)。所有相邻的整数之间用一个空格隔开。
Output
对于每一组测试数据,输出一行只包括一个整数,表示青蛙过河最少需要踩到的石子数。
Sample Input
1
10
2 3 5
2 3 5 6 7
Sample Output
2
///////////////////////////////////////////////////////////////////////////////////
解题报告
这题因为 1 <= L <= 10^9 L非常大,只能进行状态压缩。如果 L小到可以开数组,我们可以设一个数组Dp[L_MAX]保存每个子状态,从后往前DP,Dp[n] 表示从坐标位置 n 跳到或者跳过终点的最优解(局部最优),求Dp[n-1],Dp[n-1] = min{Dp(n-1+minJump ~ n-1+maxJump)},minJump表示青蛙最小的跳数,maxJump表示青蛙最大的跳数,如果 n-1 位置有石头则Dp[n-1]=Dp[n-1]+1.
比如对于示例:
10
2 3 5
2 3 5 6 7
首先我们初始化数组,Dp[]={0},从坐标位置7开始算,Dp[7]=min{Dp(2+7,3+7)}=0,因为7位置有石头所以Dp[7]=Dp[7]+1=1,同理,Dp[6]=1,Dp[5]=1,Dp[4] = min{Dp(4+2,4+3)} = 1,因为5位置没有石头所以Dp[4]不用加1,最后可求得 Dp[] = {2,1,2,2,1,1,1,1,0,0,0 ……},Dp[0]即为全局的最优解,表示从起点跳到或者跳过终点青蛙最少要踩到的石头数目。
这题因为L很大,子状态不能全部保存,我们观察到,当求Dp[n]的值时用到的状态只有
Dp(n+minJump,n+maxJump)所以我们可以不用全部保存每个坐标位置的Dp值,而只要保存从n位置之后最多maxJump个坐标的Dp值即可。为此我们可设Dp[maxJump](对本题可设Dp[10]),注意这里的下标不表示坐标位置,我们可用一个变量nHeadStart跟踪坐标位置,当求n位置的Dp值时,nHeadStart=n+1。到这里我们解决了空间问题,真正难理解的是下面的状态压缩。
假如测试数据是:
1000000000
2 5 5
2 6 5 10 99999999
99999999这个位置的Dp值我们知道是 1,而该位置前面有99999999-10个位置都没有石头,我们不可能也没有时间去求所有这些中间位置的Dp值,但是10位置的值却与这中间的有关,How Can I do ???假如有这样一组Dp值,Dp[]={2 5 1 3} 我们观察到单minJump != maxJump时,因为每次都取最小值所以经过有限步运算后,Dp={1 1 1 1},即所有的Dp值都变成开始计算时的最小值。变化经过如下
数据:minJump=2, maxJump=3,Dp[]={2,5,1,3}
Dp[]={1,2,5,1}
Dp[]={1,2,2,5}
Dp[]={2,1,2,2}
Dp[]={1,2,1,2}
Dp[]={1,1,2,1}
Dp[]={1,1,1,2}
Dp[]={1,1,1,1}
即从当前位置到前面 7 个及 7 个以前的空位(没有石头),我们断定Dp[]={1,1,1,1}
因此,对 2 6 5 10 99999999,我们可以知道从 11 位置开始,Dp[]={0,0,0,0,0}(只需保存maxJump个)前面的就好做了。经过几步的空位可以达到稳定状态(我称Dp数组值不再变化的状态为稳定态),我们有两种方法
其一:每求一个空位后我们求Dp数组中的最值,如果最大和最小值相等就达到稳定态了。这种方 法因为每次都要求最值,因此当maxJump比较大的时候不适合。
其二:我们可以断定如果当前位置之前至少有maxJump*maxJump个空位,就一定会出现稳定态(请读者自己理解 这样我们遇到空位大于等于maxJump*maxJump时直接设Dp数组为最小值。
上面的是minJump != maxJump的情况,当minJump==maxJump时,假如Dp[]={0,0,1}
数据:minJump=3, maxJump=3,Dp[]={0,0,1},Dp的变化情况如下:
Dp[]={1,0,0}
Dp[]={0,1,0}
Dp[]={0,0,1}
......
出现重复的情况,且min{Dp[]} != max{Dp[]},因此不能按minJump != maxJump的情况处理。这种情况只要将重复出现的位去掉即可,比如上面的如果当前位置前有 4 个空位,前三个不用计算,因为到第三个位置的Dp值和当前的Dp值一样。
Dp[]数组的下标变化大家可以参考循环队列的情况。
////////////////////////////////////////////////////////////////////
/// 下面的是 minJump!=maxJump 时采用第一种方法压缩状态的。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_STONE 120
#define JUMP_MAX 20
int ngHead,ngHeadStart,ngMaxJump,ngMinJump,gDp[JUMP_MAX];
int myCmp(const int *a, const int *b){
if(*a > *b)
return 1;
else if(*a < *b)
return -1;
return 0;
}
void GetResult(int nJumpPoint){
int i,j,min,k,t[JUMP_MAX];
if(ngMinJump==ngMaxJump && ngHeadStart>nJumpPoint+ngMinJump)
ngHeadStart = nJumpPoint+(ngHeadStart-nJumpPoint-1)%ngMinJump+1;
for(i=ngHeadStart-1; i>=nJumpPoint; i--){
for(k=ngHead,j=i+1; j<i+ngMinJump;j++)
k = (k+1==ngMaxJump ? 0 : k+1);
for(min=gDp[k]; j<i+ngMaxJump; j++){
k = (k+1==ngMaxJump ? 0 : k+1);
min = gDp[k]<min ? gDp[k] : min;
}
ngHead = ngHead-1<0 ? ngMaxJump-1 : ngHead-1;
gDp[ngHead] = min;
memcpy(t,gDp,sizeof(int)*ngMaxJump);
qsort(t,ngMaxJump,sizeof(int),myCmp);
if(t[0]==t[ngMaxJump-1])
break;
}
ngHeadStart = nJumpPoint;
gDp[ngHead] = min+1;
}
int main(){
int nTest,nStones,i,j,k,s[MAX_STONE];
scanf("%d",&nTest);
for(i=0; i<nTest; i++){
scanf("%d",&k);
scanf("%d %d %d",&ngMinJump,&ngMaxJump,&nStones);
for(s[0]=0,j=1; j<=nStones; j++)
scanf("%d",&s[j]);
qsort(s,j,sizeof(int),myCmp);
for(ngHead=0,ngHeadStart=s[j-1],gDp[0]=k=1; k<ngMaxJump; k++)
gDp[k] = 0;
for(j-=2; j>=0; j--)
GetResult(s[j]);
printf("%d\n",gDp[ngHead]-1);
}
return 0;
}
/// 下面的是 minJump!=maxJump 时采用第二种方法压缩状态的。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_STONE 120
#define JUMP_MAX 20
int ngHead,ngHeadStart,ngMaxJump,ngMinJump,gDp[JUMP_MAX];
int myCmp(const int *a, const int *b){
if(*a > *b)
return 1;
else if(*a < *b)
return -1;
return 0;
}
void GetResult(int nJumpPoint){
int i,j,min,k;
if(ngMinJump==ngMaxJump && ngHeadStart>nJumpPoint+ngMinJump)
ngHeadStart = nJumpPoint+(ngHeadStart-nJumpPoint-1)%ngMinJump+1;
if(ngHeadStart > nJumpPoint+ngMaxJump*ngMaxJump){
for(min=gDp[0],i=1; i<ngMaxJump; i++)
min = gDp[i] < min ? gDp[i] : min;
for(i=0; i<ngMaxJump; i++)
gDp[i] = min;
}
else
for(i=ngHeadStart-1; i>=nJumpPoint; i--){
for(k=ngHead,j=i+1; j<i+ngMinJump;j++)
k = (k+1==ngMaxJump ? 0 : k+1);
for(min=gDp[k]; j<i+ngMaxJump; j++){
k = (k+1==ngMaxJump ? 0 : k+1);
min = gDp[k]<min ? gDp[k] : min;
}
ngHead = ngHead-1<0 ? ngMaxJump-1 : ngHead-1;
gDp[ngHead] = min;
}
ngHeadStart = nJumpPoint;
gDp[ngHead] = min+1;
}
int main(){
int nTest,nLength,nStones,i,j,k,s[MAX_STONE];
scanf("%d",&nTest);
for(i=0; i<nTest; i++){
scanf("%d",&nLength);
scanf("%d %d %d",&ngMinJump,&ngMaxJump,&nStones);
for(s[0]=0,j=1; j<=nStones; j++)
scanf("%d",&s[j]);
qsort(s,j,sizeof(int),myCmp);
for(ngHead=0,ngHeadStart=s[j-1],gDp[0]=k=1; k<ngMaxJump; k++)
gDp[k] = 0;
for(j-=2; j>=0; j--)
GetResult(s[j]);
printf("%d\n",gDp[ngHead]-1);
}
return 0;
}
////////////////////////////////////////////////////////////////////
测试数据下载 (下载后请改后缀名为.txt)
测试数据的答案:6 2 10 10 3 1 3 5 6 25
评论