NOIP 2005提高组 过河(状压DP) 详解

题目

描述

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

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

对于30%的数据,L <= 10000;
对于全部的数据,L <= 10^9。
格式
输入格式

输入的第一行有一个正整数L(1 <= L <= 10^9),表示独木桥的长度。第二行有三个正整数S,T,M,分别表示青蛙一次跳跃的最小距离,最大距离,及桥上石子的个数,其中1 <= S <= T <= 10,1 <= M <= 100。第三行有M个不同的正整数分别表示这M个石子在数轴上的位置(数据保证桥的起点和终点处没有石子)。所有相邻的整数之间用一个空格隔开。
输出格式

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

10
2 3 5
2 3 5 6 7

样例输出1

2

思路

这道题是状态压缩dp的经典例题,至于状压DP,就是将没用的距离缩短,名字很高级,其实并不难,后面我们详细解释
本来这道题是一道十分普通的DP题,但大家可能都注意到L <= 10^9,对于如此大的数据,如果暴力DP,时间空间都会爆,所以就需要状压把没用的长度压缩掉

1.基本dp

首先,我们不考虑长度的问题,看一下dp
这道题的dp思路并不难,就是循环枚举所有距离,每个距离再循环当前这步跳的距离s-t就行了。
a[i]表示位置i上有没有石头,有就是1,否则是0
f[i]表示跳到位置i的过程中踩到的最小石头数
j表示当前这步跳的距离
状态转移方程:

f[i]=min(f[i],f[i-j]+a[i]);//十分简单,f[i-j]+a[i]就是相当于这一步跳j踩到的石头数

2.状态压缩(重点)

我们看到题目就会发现,l最大为10^9,而m最大只有100,可见石头与石头之间的距离是很大的,所以就要想办法把每两个石头之间的距离缩短。
而压缩石头间距离的依据是:由于每次跳能跳s-t,所以从某一固定位置到某一位置p后,任意位置都可以被跳到。
我们用一个图来理解这句话(下图中设s=2,t=4,线段为跳跃过程)
NOIP 2005提高组 过河(状压DP) 详解_第1张图片
可见对于这种情况,p=3,也就是3以后的所有点都能跳到
对应到题目中,也就是说假如从某一石头(就是刚刚提到的固定位置)开始跳,有可能在到达下一石头前,就到达了P(也就是在到达下一石头前,就能够跳到所有点),这样子的话,在p到下一石头之间的所有距离都是没有用的,可以被压缩。
而经过考证,因为s,t都小于10,对于这个范围最大p值为90,所以我们就把所有石头两两间的距离缩短到90以下(不追求完美主义,90不一定是最优的,但肯定是没错的)

AC代码

#include 
#include 
#include 
#include 
using namespace std; 

int l,s,t,m,rock[101],f[1100000],a[1100000];
//a[i]表示位置i上有没有石头,有就是1,否则是0
//f[i]表示跳到位置i的过程中踩到的最小石头数 

void specialjudge(){ //当s=t时,也就是说每次跳的距离是固定的,就进行特判(一定要特判,否则会错) 
    int ans=0;
    for (int i=1;i<=m;i++){
        if (rock[i]%s==0){
            ans++;
        }
    }
    cout<void compress() //核心代码,状态压缩 
{
    for (int i=1;i<=m;i++){ //每两块石头之间进行状态压缩,距离缩短为90以内 
        int dis=rock[i]-rock[i-1];
        rock[i]=rock[i-1]+dis%90;
    }
    l=(l-rock[m])%90+rock[m];//把最后的l也往前移动 
    for (int i=1;i<=m;i++){ //a数组用于判断石头位置,在后面的dp函数中起作用 
        a[rock[i]]=1;
    }
}

void dp() //dp,实际上思想十分基础 
{
    memset(f,0x7f,sizeof(f));
    f[0]=0;

    for (int i=s;i<=l+t;i++){//第一步至少要跳s,所以i=s;最后不一定刚好跳到l,有可能跳过,所以i<=l+t 
        for (int j=s;j<=t;j++){//枚举一次跳的距离 
            if (i>=j){
                f[i]=min(f[i],f[i-j]+a[i]);//状态转移方程,十分简单,f[i-j]+a[i]就是相当于这一步跳j踩到的石头数 
            }
        }
    }
    int ans=0x7f;
    for (int i=l;i<=l+t;i++){ //找到答案,同样因为可能跳过l,所以循环范围这么定 
        if (f[i]cout<int main()
{
    memset(a,0,sizeof(a));
    cin>>l>>s>>t>>m;
    for (int i=1;i<=m;i++){
        cin>>rock[i];
    }
    sort(rock+1,rock+m+1);

    if (s==t){
        specialjudge();
        return 0;
    }

    compress();
    dp();
    return 0;
}

你可能感兴趣的:(题目解析,noip,动态规划,详解)