青蛙过桥 (逆序dp字典序)

问题重述

一座长度为n的桥,起点的一端坐标为0,且在整数坐标i处有a[i]个石头【0<=a[i]<=4】, 一只青蛙从坐标0处开始起跳,一步可以跳的距离为1或2或3【即每一步都会落在整数点处】, 青蛙落在i处会踩着该点的所有石头,求青蛙跳出这座桥最少踩多少个石头?并且输出依次跳 过的坐标点路线,如果存在多种路线,输出字典序最小的那一条。

思路

  • 正序dp(dp[i]=dp[i-1]+num[i])每次记录最有祖先时,当一个点同时有两个及以上的祖先时按照最小字典无法抉择谁是最优祖先,因为记录的是前驱节点前驱节点先小的不一定整个序列的字典序就最先,因为可能前前一个还要大一些如1-3-5-7/1-2-6-7,所以无法通过留最优前驱节点来记录最小路径,因为有多个最优前驱时无法选择
  • 而我们倒着过来看正序是s->e,若从e->s逆序dp,则逆序dp的前驱就是正序dp的后继。定义dp[i]表示从e到i的最小代价,则每次逆序选择i点的前驱时相当于选择从i点开始出发的i的后继,因为起点相同所以后继越小字典序越小

代码

dp

#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int n;
//逆序遍历的fa里面存的就是正序的后继所以直接前序遍历
//而不像正序dp里面存的是正序的前驱所以还要后序遍历先递归再打印
void print(vector<int>& fa,int x){
    if(x) cout<<" ";
    cout<<x;
    if(x>n-3) return;
    else print(fa,fa[x]);
}
int main(){	
    cin>>n;
    vector<int> num(n+1);//后面三个坐标补零
    for(int i=0;i<=n;i++) cin>>num[i];
    vector<int> dp(n+1);//dp[i]表示跳到第i个坐标踩的最少石头数
    vector<int> s(n+1);
    dp[n]=num[n];//注意这里不是0,就算是终点也要算终点本身这一点的代价
    dp[n-1]=num[n-1];//以后面的三个可能的终点为终点
    dp[n-2]=num[n-2];//已经到了终点后,三个终点之间就不会再继续跳转了,因为这样会增加成本,所以只要到了三个终点中的一个就行
    //dp[n-3]=0;//因为必须要跳出桥所以n-3还不能算终点,因为最后还要跳3步到n-1才可以算跳出
    for(int i=n-3;i>=0;i--){//从3个可能起点到i的最小代价!!错的终点只有两个
        int pre=i+3;//默认先选后继最大的再慢慢调小
        // if(dp[i+1]>dp[i+2]) pre=i+2;
        // if(dp[i+2]>dp[i+3]) pre=i+3;
        if(dp[i+2]<=dp[i+3]) pre=i+2;//因为当前是确定落在了i并且是到着找的所以现在实际上找i的前驱是
        //正序中i的后继,因为i已经确定所以现在找的后继i就相当于正序中的第二个数,它越小则正序字典序越小,所以要先找大的再找小的
        if(dp[i+1]<=dp[pre]) pre=i+1;
        dp[i]=dp[pre]+num[i];
        s[i]=pre;
        //为什么这里可以直接记录前驱?
        //因为这里逆序dp(e->s)的前驱实际上是正序的后继,而字典序就是要后继尽量的小
        //所以如果出现多个后继代价相同时,我们在这里选择最小的后继点则一定是最小字典序
        //但如果是正序递推则记录的是前驱节点,当有多个最优前驱节点时,此时选择最小的前驱不一定就是最小字典序
        //因为如1-3-5-7/1-2-6-7
    }
    for(int i=0;i<=n;i++) cout<<dp[i]<<" ";
    cout<<endl;
    cout<<dp[0]<<'\n';
    print(s,0);
    system("pause");
    return 0;
}

记忆化搜索

  • dp填表法必须考虑顺序,即考虑哪个为起点就必须从这个点开始动态规划
    但例如在DAG有向无环图中,记忆化搜索可以随便选择一个点开始进行递归,就算该点不是起点,即该点不是边界还没有被计算出来,所以会继续递归下去直到遍历完以该节点为根的所有子节点边界,因为他会一直往着子树向下递归直到找到可以算的边界值再倒回来算以前没算过的点
  • 而如果直接用dp的话则必须从入度为0的起点开始递归,所以必须还要用top排序找到起点再来用dp递推
  • 所以记忆化搜索不用考虑顺序直接从任何点开始都可以遍历完该点为根的子树,若有多个入度为0的点(根)则直接for循环f(i)每一个节点即可保证全部都被计算,而dp要考虑顺序即开始递推点的选择
//dp填表法必须考虑顺序,即考虑哪个为起点就必须从这个点开始动态规划
//但例如在DAG有向无环图中,记忆化搜索可以随便选择一个点开始进行递归,
//就算该点不是起点,即该点不是边界还没有被计算出来,所以会继续递归下去直到找到边界的值才会倒回来算以前没算过的点
//而如果直接用dp的话则必须从入度为0的起点开始递归,所以必须还要用top排序找到起点再来用dp递推
//所以记忆化搜索不用考虑顺序,而dp要考虑顺序即开始递推点的选择
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int n;
vector<int> dp,num,fa;
//逆序遍历的fa里面存的就是正序的后继所以直接前序遍历
//而不像正序dp里面存的是正序的前驱所以还要后序遍历先递归再打印
void print(vector<int>& fa,int x){
    if(x) cout<<" ";
    cout<<x;
    if(x>n-3) return;
    else print(fa,fa[x]);
}
int f(int x){
    if(x<=n&&x>=n-2){
        return dp[x]=num[x];
    }
    if(dp[x]!=-1) return dp[x];
    int pre=x+3;
    if(f(x+2)<=f(x+3)) pre=x+2;//注意这里只能写f(x+1)不能写dp[x+1],因为这不是像dp那样按照顺序填表的
    //计算现在的dp[x]的时候可能dp[x+1]还根本没有计算出来
    if(f(x+1)<=f(pre)) pre=x+1;
    fa[x]=pre;
    return dp[x]=f(pre)+num[x];
}
int main(){	
    cin>>n;
    num=vector<int>(n+1);//后面三个坐标补零
    for(int i=0;i<=n;i++) cin>>num[i];
    fa=vector<int>(n+1);
    dp=vector<int>(n+1,-1);//dp[i]表示跳到第i个坐标踩的最少石头数
    //因为是递归所以可以选择任意一个起点开始,而不用像之前一样必须从3个可能的终点倒着推
    f(4);//因为是向后递归的所以f(4)解决的是4之后的有向图,但4之前的还没有算,所以如果要算4之前的还得把所有的i都f一下
    //或者在多起点有向图图时不知道起点是谁时,可以把所有i都f一下,但这道题我们已知了0是起点所以直接0打头f一次即可
    //-1 -1 -1 -1 2 4 2 3 1 1 2
    for(int i=0;i<=n;i++) f(i);
    cout<<dp[0]<<endl;
    print(fa,0);
    //cout<<f(0);
    system("pause");
    return 0;
}

std

#include<iostream>
#include<string.h>
#include<vector>
#include<algorithm>
using namespace std;
const int MAX_N = 150005;
int n;
int v[MAX_N];
int fa[MAX_N];

void show(int i){
    // cout << i << "[" << v[i] << "]" << " ";
    if(i) cout << " ";
    cout << i;
    if(i > n-3) return;
    else show(fa[i]);
}

int main(){
    ios::sync_with_stdio(false);
    cin >> n;
    memset(v, 0, sizeof(v));
    for(int i = 0; i <= n; i++){
        cin >> v[i];
    }

    for(int i = n-3; i >= 0; i--){
        int tmp = i+3;
        if(v[i+2] <= v[i+3]) tmp = i+2;
        if(v[tmp] >= v[i+1]) tmp = i+1;
        v[i] = v[i] + v[tmp];
        fa[i] = tmp;
    }
    cout << v[0] << endl;
    show(0);
    return 0;
}


  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值