[NOIP2005]过河(超详细推导空档压缩距离的计算公式,提供两种100%算法)

原题

在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧。在桥上有一些石子,青蛙很讨厌踩在这些石子上。由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可能到达的点看成数轴上的一串整点:0,1,…,L(其中L是桥的长度)。坐标为0的点表示桥的起点,坐标为L的点表示桥的终点。青蛙从桥的起点开始,不停的向终点方向跳跃。一次跳跃的距离是S到T之间的任意正整数(包括S,T)。当青蛙跳到或跳过坐标为L的点时,就算青蛙已经跳出了独木桥。

题目给出独木桥的长度L,青蛙跳跃的距离范围S,T,桥上石子的位置。你的任务是确定青蛙要想过河,最少需要踩到的石子数。

数据范围:
1≤S≤T≤10,1≤M≤100,即跳跃距离在1和10之间,石子数不超过100;
对于30%的数据,L≤10000;
对于全部的数据,L≤109

分析

这题明显是考动态规划,状态和动态转移方程都容易写,即dp[i]表示青蛙从起点跳到坐标i时最少踩到的石子数,dp[i]=min{dp[i-j] S<=j<=T i-j>=0}+a[i](a[i]表示当前位置是否有石子),初始状态dp[0]=0,最终答案min{dp[L…L+S-1]}。

问题是这样时间复杂度是O(L),可以过30%的数据,但对于100%的数据,显然是不行的,如何优化?

首先要从数据规模看出,桥的长度比石子的个数要大得多得多,意味着中间会有一些很长的空档,计算这些空档的状态花费了绝大多数的时间。当然,为了保证动态规划的正确性,这些状态是不可以不算的,不可以跳过的。只是有个问题,这些状态值是否有规律可循?若有规律可循,我们就不必一个个计算。

不妨构造一组L较大的数据,跑完后打印出所有的状态值,观察,会发现一个惊人的现象,空档中的那么多状态值几乎全是一样的,为什么?这是一个比较深奥的话题,等等我们再深入分析。其实发现这个现象已经是个巨大的收获,让我们意识到哪怕L再大,但大多数的状态都是重复的,只要我们知道什么时候连续重复状态会出现,什么时候会停止,就可以跳过不计算。(注意,当S=T>1时,这种大量连续重复状态不会出现,而是会呈现周期性变化的特点,这种情况无法跳过,但S=T反而使得题目变得无比简单,看各个石子位置能否被S整除就可以了。)

从感性上猜测,重复状态开始于一段长空档的前期,结束于该空档末尾。从理性上分析,这段空档中做的计算都是dp[i]=min{dp[i-T…i-S]}(没有石子),也就是取前几个状态求min值。容易看出,假如前几个状态值是一样的,那么接下来的状态值也全都会是一样的,直到空档结束,(下一个石子处公式形式发生变化,出现+1)。理解了这一点,在遇到状态值不断重复时,就可以省去接下来的非常多的重复状态计算,其实加上这个优化已经可以漂亮地解决这个问题了。

解法一(简单好写,不需离散化和空档压缩,过100%数据)

优化根据是:这题的动态转移方程只依赖于最近T个状态,而在空档期中的绝大多数时候,最近T个状态值和方程不变,后续状态也都不会变,因此可以快进时间,“跨越”空档。

从运行结果来看,在L达到109时,可以省去99.9999%以上的计算量,绝对不会超时。

编程时,用滚动数组保存近T个状态,在迭代过程中一旦发现近T个状态值完全一致,且当前位于石子间的间隙中,直接将位置调整到间隙末尾

时间复杂度O(S2TM)(后面会证明),空间复杂度O(M+T)。

解法一代码

以下是不考虑S=T的情况的核心代码。

int ans=-1;
a[M+1]=-1;
// 滚动数组存前T个状态
int s[10];
memset(s,-1,sizeof(s)); // 注意坐标0之前的状态置为-1
s[T-1]=0; // 坐标0的状态 dp[0]
for (int i=1,m=1;i<L+T;i++) // i从1开始;m表示下一个石子编号
{
   
	if (i!=a[m]) // 当前位置没有石子才考虑跳跃
	{
   
		bool repeat=true;
		for (int k=1;k<T;k++)
		{
   
			if (s[0]!=s[k])
			{
   
				repeat=false;
				break;
			}
		}
		if (repeat) // 发现之前T个状态重复
		{
   
			//printf("found repeat previous states at %d\n", i);
			// skip
			if (a[m]!=-1) // 处于非最后一段空隙,跳至下个石子处
				i=a[m];
			else if (i<L) // 最后一段空隙,跳至桥尾L处
				i=L;
			//printf("moved to %d\n", i);
		}
	}
	int temp=-1; // 本次状态值
	for (int k=T-T;k<=T-S;k++) // 状态转移
	{
   
		if (s[k]!=-1 && (temp==-1 || s[k]<temp)) temp=s[k];
	}
	if (temp!=-1)
	{
   
		temp+=(a[m]==i?1:0);
		if (i>=L && i<L+S && (ans==-1 || temp<ans)) ans=temp; // 到达目标状态时更新答案
	}
	
	// shift 滚动数组
	for (int k=0;k<T-1;k++)
	{
   
		s[k]=s[k+1];
	}
	// insert 插入新状态
	s[T-1]=temp;
	
	if (a[m]==i) m++;
}
		
printf("%d\n",ans);

理论推导

刚才并没有解答什么时候会开始出现重复状态,也没有分析为什么会产生重复。现在我们就来好好探讨。

假设我们正处在空档中,用f表示空档期间的状态序列,f[0]表示空档第一个位置的状态,这些状态满足f[i]=min{f[i-T…i-S]} i>=0。

设f[-T],f[-T+1],f[-T+2]…f[-1]为空档之前的T个状态(值不确定)。

先证两个简单的定理,

  1. i>=0时,任意一个f[i]值都与f[-T]…f[-1]中的某一个值相同。
    非常简单,如f[0]=min{f[-T…-S]},min值一定会与其中一个值相同。也就是,所有f[i]的值都来源于f[-T]…f[-1]这T个值。
  2. i>=0时,f[i]>=min{f[-T…-1]}}。
    也很明显,因规律1,f[i]与集合中的某一个数相同,那么一定不会小于集合中的最小值。

为了分析重复产生的根源,我们需要展开f[i],
假设S=4,T=5,i>=0:
f[i]
=min{f[i-4],f[i-5]}

=min{f[i-4-4],f[i-4-5],f[i-5-4],f[i-5-5]}
=min{f[i-8],f[i-9],f[i-10]}
=min{f[i-8-4],f[i-8-5],f[i-9-4],f[i-9-5],f[i-10-4],f[i-10-5]}
=min{f[i-12],f[i-13],f[i-14],f[i-15]}
=min{f[i-16],f[i-17],f[i-18],f[i-19],f[i-20]}
=min{f[i-20],f[i-21],f[i-22],f[i-23],f[i-24],f[i-25]}

整理得f[i]=min{f[ i - {4,5,8,9,10,12,13,14,15,16,17,18,19,20,21,22,23,24,25,…} ]},注意这里从12开始变成连续整数。这意味着f[i]小于等于f[i-12]及之前的所有元素。

如果f[i-12]之前包括f[-T…-1],也就是说当i>=11时,f[i]<=min{f[-T…-1]},因规律2,f[i]=min{f[-T…-1]}。因此i>=11时f[i]均相同,导致出现无数重复状态。

现在我们证明了S=4、T=5时,每个空档开端的第12个状态起会有重复值。

对于其他的S和T,是否都会出现类似现象呢? 若是在比赛当中,可以通过手工模拟或者自动化程序验证(见本文最后)。


现在我们要通过理论证明。其实刚才的4、5、8、9……这个序列是由任意倍数的 S、T及S、T之间的数字 任意相加形成的序列,可疑规律是这个序列从某个数字(不妨设为X,如刚才的12)开始,变成了连续递增数字序列。现在的目标是证明对于任意S、T(S!=T,S,T>=0),上述的连续递增无限子序列一定存在,并找到这个起始X的值

为了简化问题,不妨限定T=S+1。(当T>S+1时,实际X可能比我们求解的要小一些,但不影响证明规律正确性,对之后的空档压缩效果也没有质的影响)

我们设 X = n S + m ( S + 1 ) n , m ∈ N X=nS+m(S+1) \quad n,m \in N X=nS+m(S+1)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值