1002


 描述 Description
    在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧。在桥上有一些石子,青蛙很讨厌踩在这些石子上。由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可能到达的点看成数轴上的一串整点:0,1,……,L(其中L是桥的长度)。坐标为0的点表示桥的起点,坐标为L的点表示桥的终点。青蛙从桥的起点开始,不停的向终点方向跳跃。一次跳跃的距离是S到T之间的任意正整数(包括S,T)。当青蛙跳到或跳过坐标为L的点时,就算青蛙已经跳出了独木桥。 
   题目给出独木桥的长度L,青蛙跳跃的距离范围S,T,桥上石子的位置。你的任务是确定青蛙要想过河,最少需要踩到的石子数。 
   对于30%的数据,L <= 10000; 
   对于全部的数据,L <= 10^9。 
 输入格式 Input Format
    输入的第一行有一个正整数L(1 <= L <= 10^9),表示独木桥的长度。第二行有三个正整数S,T,M,分别表示青蛙一次跳跃的最小距离,最大距离,及桥上石子的个数,其中1 <= S <= T <= 10,1 <= M <= 100。第三行有M个不同的正整数分别表示这M个石子在数轴上的位置(数据保证桥的起点和终点处没有石子)。所有相邻的整数之间用一个空格隔开。 
 输出格式 Output Format

    输出只包括一个整数,表示青蛙过河最少需要踩到的石子数。


解析:本题目是自己碰到的第一个动态规划题目,遇到的难度也是前所未有。自己只能窘迫的说水平太低了,连续提交了十几遍才通过,这还是在参考别人代码的基础上AC的。顺别吐槽一下,Vijos的系统也实在太差了,自动删除回车换行打乱格式导致编译通不过,最后还逼着我换了firefox才可以顺利提交。

思路一:也是自己最开始的思路,我想到了递归求解此问题。虽然对于样例很容易的通过,但是提交之后10组测试数据,没有一对是正确的,各种窘迫,考虑到数据规模还以为是自己的数据类型定义的太小,还专门换成了long类型,但是提交之后仍然出错,这是关注错误信息,发现给出的是堆栈溢出,这时候就是傻子也会想到应该是递归层次太多导致的,看到那个L的范围,溢出也是理所应当的。不得不放弃这种思路,但是自己有不会别的思路,只能参考网友上各种牛们提供的代码。下面是自己的递归代码,还是后者脸皮贴出来吧。

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
 
 int NumStone = 101;
 int IsStone(int destinaton,unsigned int Stone[],int M)
 {
 	int high = M -1;
 	int low = 0;
     while(low <= high)
 	{
 		int mid = (low + high) / 2;
 		if(destinaton == Stone[mid])
 			return mid+1;
 		if (destinaton <Stone[mid])
 			high = mid - 1;
 		else
 			low = mid + 1;
 	}
 	return 0;
 }
 void select(unsigned int a[], int count)   /* 将石子出现的位置排序 */
 
 {
 
 	int i,j,temp;
 
 	for(i=0;i<count;i++)         
 
 	{
 
 		temp=a[i];
 
 		j=i-1;
 
 		while(j>=0 && temp<a[j])
 
 		{
 
 			a[j+1]=a[j];
 
 			j--;
 
 		}
 
 		a[j+1]=temp;
 
 	}
 
 }
 
 void cal(int location,int num,long int L,int S,int T,unsigned int Stone[],int M)
 {
 	int swap;
 	if(location >= L)
 	{
 		if(num < NumStone)
 		{
 			NumStone = num;
 		}
 	}
 	else
 	{
 		for(swap=S;swap<=T;swap++)
 		{
 			if (IsStone(location,Stone,M))
 			{
 				num++;
 			}
 			cal(location+swap,num,L,S,T,Stone,M);
 		}
 	}
 	
 }
 int main()
 {
 	unsigned int L;
 	int S,T,M,Temp,n;
 	scanf("%ld",&L);
 	scanf("%d %d %d",&S,&T,&M);
 	unsigned int *Stone = (unsigned int *)malloc(sizeof(unsigned int)*M);
 	for (n=0;n<M;n++)
 		scanf("%u",&Stone[n]);
 	select(Stone,M);
 	cal(0,0,L,S,T,Stone,M);
 	printf("%d",NumStone);
 	system("pause");
 	return 0;
 }
 


思路二:这是人家正统的思想分析,我就直接贴过来吧,代码也是根据理解写的。方法为动态规划+路径压缩

本题是一类很有意思的动态规划题;不同于决策优化,本题要求优化状态,这就使题目增加了很多的灵活性。 
  朴素的动态规划注意到当前状态只与其前面的T个状态有关;所以说采用滚筒法可以在O(LT)的时间内解决本题。但是,本题中L非常大,因此我们希望算法的时间复杂度与L无关。

 
  一种想法是:将当前状态s与其前面的T个状态看作一个长度为T+1的状态数组,如果在一次滚筒更新中新旧两个数组完全一样,则在遇到下一块石头之前,状态将会完全不变。这个原则是很简单的,因为除非遇到一块石头,否则每一个决策的前提都是不变的,所以说滚筒更新下去,状态一定不变。 
  那么,我们就需要问一个问题:究竟会不会出现这种更新前后状态不变的情况呢?如果不会出现这些情况,那么算法的优化也就无从谈起了。事实上,只要S < T,就一定会出现这种情况。
  这是很好理解的。假设S < T,则青蛙可以跳T步或T-1步,这两个步长是互质的。根据扩展的欧几里德定理,当路程足够长的时候,一定会出现这样一种情况:前后T步全部被同一个数覆盖;这就可以直接应用优化了。

时间复杂度是O(NT)。 从桥的一侧到另一侧,中间最多只有100个石子。假设桥长为最大值(10^9),石头数也为最大值(100),则中间一定会有很多“空长条” (两个石子中的空地),关键是如何在处理时把这些“空长条”跳过,使得运算次数降到M次。

结论:
 若(采用跳跃距离p和p+1时可以跳至任何位置Q),则在Q≥P*(P-1)时是一定有解的。
 Because证明
 由于p与p+1间隔1,故方程px+(p+1)y=Q有整数解,设其解为
  x=x0+(p+1)t,y=y0-pt(t是整数)
 取适当的t,使得0≤x≤p(只需在x0上加上或减去若干个p+1),则当Q>p(p-1)-1时,有
 (p+1)y=Q-px>p(p-1)-1-px≥p(p-1)-1-p*p=-(p+1)
 于是y>-1,故y≥0也是非负整数。证毕.
 由于题目给出的一个区间是1≤S≤T≤10,于是当相邻的两个石子之间的距离不小于9*10=90时,则后面的距离都可以到达,我们就可以认为它们之间的距离就是90。如此一来,我们就将原题L的范围缩小为了100*90=9000,动态规划算法完全可以承受了。但是当S=T时,上述等式是无法使用的,在这种情况下,只需要在所有石子中,统计出坐标是S倍数的石子个数就可以了. 

  当然,如果S = T,这个性质显然就不成立了,这种情况下我们可以特判。 
  如果青蛙能够跳得步数不是连续的,这种优化还可以用吗? 
  可以的!如果青蛙跳得步数中有两个数是互质的,则优化立即生效;否则,我们将所有的石头的位置除以步数的最大公约数(不能被最大公约数整除的显然不可能被跳到),对总长度也做类似的变化,就可以套用优化方法了。 
 评价:这是NOIP中第一道DP优化题,虽然其难度远不如NOI,但其灵活度也不小,需要仔细地考察状态转移方程的特征,利用状态空间的稀疏性来进行优化。尤其是当S = 
T时这种特殊情况的讨论极易被遗漏。写代码的时候仍然暴露出很多问题,包括头文件的理解混乱,字符长度书写错误,边界问题考虑不周等。

下面是写出的代码:

 

#include <stdio.h>
 #include <string.h>
 int main()
 {
 	long int L ,temp,k;
 	int S,T,M,i,j,min;
 	long stone[102],b[10000];
 	int Num[10000];
 	scanf("%ld",&L);
 	scanf("%d %d %d",&S,&T,&M);
 	for (i=0;i<M;i++)
 		scanf("%ld",&stone[i]);
 
 
 	if (S!=T)
 	{
 		for (i=0;i<M-1;i++)
 		{
 			for (j=0;j<M-i-1;j++)
 				if(stone[j]>stone[j+1])
 				{
 					temp = stone[j];
 					stone[j]=stone[j+1];
 					stone[j+1]=temp;
 				}
 		}
 		stone[M] = L;
 
 		if (stone[0]>90)
 		{
 			k = stone[0]-90;
 			for(i=0;i<=M;i++)
 				stone[i] -= k;
 		}
 		for (i=1;i<=M;i++)
 		{
 			if (stone[i]-stone[i-1]>90)
 			{
 				k = stone[i]-stone[i-1]-90;
 				for(j=i;j<=M;j++)
 					stone[j] -= k;
 			}
 		}
 
 
 		memset(Num,-1,sizeof(Num));
 		memset(b,0,sizeof(b));
 		//标记石头
 		for (i=0;i<M;i++)
 			b[stone[i]] = 1;
 
 
 		Num[0] = 0;
 
 		for (i=S;i<stone[M]+T;i++)
 		{
 			min = 101;
 			for (j=i-T;j<=i-S;j++)
 			{
 				if (Num[j]<min && Num[j]!=-1 && j>=0)
 					min = Num[j];
 			}
 			if (min != 101)
 				Num[i] = min+b[i];
 		}
 		min = 101;
 		for (i=stone[M];i<stone[M]+T;i++)
 		{
 			if (Num[i] < min && Num[i]!= -1)
 				min = Num[i];
 		}
 		printf("%d",min);
 	}
 	else
 	{
 		min = 0;
 		for (i=0;i<M;i++)
 		{
 			if (stone[i] % S == 0)
 				min++;
 		}
 		printf("%d",min);
 	}
 	return 0;
 }

今天看到一个小规模的动态规划,取消了路径压缩的过程,作者的代码页比较好。

#include<stdio.h>
#include<string.h>
const int MAXN=100020;
int flag[MAXN];
int dp[MAXN];
int main()
{
    int L,s,t,n;
    int a;
    while(scanf("%d%d%d%d",&L,&s,&t,&n)!=EOF)
    {
        memset(flag,0,sizeof(flag));
        memset(dp,-1,sizeof(dp));//初始化,-1为不能到达的 
        //dp[i]表示到底 i  点需要经过的最少石子数,-1表示不能到达 
        for(int i=0;i<n;i++)
        {
            scanf("%d",&a);
            flag[a]=1;//有石子为1,否则为0 
        }    
        dp[0]=0;
        for(int i=s;i<=L+t-1;i++)
        {
            for(int j=i-t;j<=i-s;j++)// j 点跳到 i 点 
            {
                if(j>=0&&dp[j]!=-1)//j 点能够跳到 
                {
                    if(dp[i]==-1)dp[i]=dp[j]+flag[i]; //第一次 直 接 给 值 
                    else if(dp[i]>dp[j]+flag[i]) dp[i]=dp[j]+flag[i];//找小的值 
                    
                }    
            }    
        }  
        int res=10000;
        for(int i=L;i<=L+t-1;i++)//L 到 L+t-1 中最小的非 -1 值 
        {
            if(dp[i]!=-1&&dp[i]<res) res=dp[i];
        }      
        printf("%d\n",res);
    }    
    return 0;
}




 



 

 
 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值