P1868 饥饿的奶牛+C. 青蛙国王(类子序列线性dp+二分优化)

https://www.luogu.com.cn/problem/P1868

题目描述

有一条奶牛冲出了围栏,来到了一处圣地(对于奶牛来说),上面用牛语写着一段文字。

现用汉语翻译为:

有 NN 个区间,每个区间 x,yx,y 表示提供的 x\sim yx∼y 共 y-x+1y−x+1 堆优质牧草。你可以选择任意区间但不能有重复的部分。

对于奶牛来说,自然是吃的越多越好,然而奶牛智商有限,现在请你帮助他。

输入格式

第一行一个整数 N。

接下来 N 行,每行两个数 x,y,描述一个区间。

输出格式

输出最多能吃到的牧草堆数。

输入输出样例

输入 #1复制

3
1 3
7 8
3 4

输出 #1复制

5

说明/提示

1≤n≤1.5×1e5,0≤x≤y≤3×1e6。



其实这两个题。思路是一样的。而且第二题是第一题的暴力版本。

思路:枚举第i个段选的话,从前面哪个状态转移过来。类似最长上升子序列的模型。当然这样暴力n^2的。

不过在这之前需要sort,来消除dp的后效性。有个样例就很好的说明了后效性。如果不按照左端点或者右端点排序,会导致后效性没法转移到最优解。

关于排序:按左或者按右是一样的,与之对应的转移方程互相倒过来。(好比算面积,你先算长再乘宽和先算宽再乘长是一样的)

按左小的话,对于i找前面的j。

这里采用按左端点排序。

先上青蛙国王的暴力代码。

#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<cstdio>
#include<algorithm>
#define debug(a) cout<<#a<<"="<<a<<endl;
using namespace std;
const int maxn=9e3+100;
typedef long long LL;
inline LL read(){LL x=0,f=1;char ch=getchar();	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;}
LL dp[maxn];
LL cnt=0;
struct Stone{
    LL l,w,h;
}stone[maxn];
bool cmp(Stone A,Stone B){
    if(A.l==B.l){
        if(A.w==B.w) return A.h>B.h;
        return A.w>B.w;
    }
    return A.l>B.l;
}
void add(LL a,LL b,LL c){
    stone[++cnt].l=a;
    stone[cnt].w=b;
    stone[cnt].h=c;
}
int main(void)
{
  cin.tie(0);std::ios::sync_with_stdio(false);
  LL n;n=read();
  for(LL i=1;i<=n;i++){
     LL l,r,h;l=read();r=read();h=read();
     add(max(r,h),min(r,h),l);
     add(max(l,h),min(l,h),r);
     add(max(l,r),min(l,r),h);
  }
  sort(stone+1,stone+1+cnt,cmp);
  for(LL i=1;i<=cnt;i++){
    dp[i]=stone[i].h;
  }
  /*
  for(LL i=1;i<=cnt;i++){
    cout<<stone[i].l<<" "<<stone[i].w<<" "<<stone[i].h<<'\n';
  }*/

  for(LL i=1;i<=cnt;i++){
    for(LL j=1;j<i;j++){
        if(stone[j].l>stone[i].l&&stone[j].w>stone[i].w){
            dp[i]=max(dp[i],dp[j]+stone[i].h);
        }
    }
  }
  LL ans=-1e18;
  /*
  for(LL i=1;i<=cnt;i++){
    cout<<dp[i]<<"\n";
  }*/
  for(LL i=1;i<=cnt;i++){
    ans=max(ans,dp[i]);
  }
  cout<<ans<<"\n";
return 0;
}

现在来说说优化。优化的方法是用二分找到前面第一个能转移的位置,找不到的话就取前面一个的最后和提前预处理好的本身长度比较转移下去。

可能比较有点怪的为什么按一个维度排序,另一个可能乱序,却能单调性二分。另一个维度不是单调的。

这里用青蛙国王的例子说明。

比如

长: 8   7  6  5   4

宽  4   3  3   3   2

如果有相同的宽度,其实转移的最优的是最后面的那个dp状态(长为5的)

原因:按长度排序,如果出现了宽度的波峰/平峰,那么由于其长度小于前面的,宽大于/等于前面的,其dp状态肯定选择继承。 

dp[1]=4;dp[2]=max(4,4);dp[3]=max(3,4)=4;dp[4]=max(2,4)=4;

此时dp[5]去二分找的第一个大于等于2的位置,其就是dp[4],其最优是从第一个dp[1]转移过来的。所以能最优更新。

即dp[i]最后更新的是[1~i]的最优解。

那我二分找第一个宽度大于本身的是符合最优的。
 

这里附上奶牛的暴力和奶牛的二分优化代码:

暴力:

#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<cstdio>
#include<algorithm>
#define debug(a) cout<<#a<<"="<<a<<endl;
using namespace std;
const int maxn=2e3+100;
typedef int LL;
inline LL read(){LL x=0,f=1;char ch=getchar();	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;}
struct P{
    LL l,r;
}a[maxn];
bool cmp(P A,P B){
    if(A.l==B.l) return A.r<B.r;
    return A.l<B.l;
}
LL dp[maxn];
int main(void)
{
  cin.tie(0);std::ios::sync_with_stdio(false);
  LL n;n=read();
  for(LL i=1;i<=n;i++){
    a[i].l=read();a[i].r=read();
  }
  sort(a+1,a+1+n,cmp);
  for(LL i=1;i<=n;i++){
    dp[i]=a[i].r-a[i].l+1;
  }
  for(LL i=1;i<=n;i++){
     ///dp[i]=max(dp[i],dp[i-1]);会导致先拿了前一个状态的大数,但是这个大数可能在接下来的转移会碰撞区间线段
     for(LL j=1;j<i;j++){
        if(a[j].r<a[i].l){
            dp[i]=max(dp[i],dp[j]+a[i].r-a[i].l+1);
        }
     }
  }
  LL ans=0;
  for(LL i=1;i<=n;i++){
    ans=max(ans,dp[i]);
  }
  cout<<ans<<endl;
return 0;
}

二分优化: 

#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<cstdio>
#include<algorithm>
#define debug(a) cout<<#a<<"="<<a<<endl;
using namespace std;
const int maxn=2e5;
typedef long long LL;
struct Query{
    LL l,r,id;
}q[maxn];
LL dp[maxn];///注意dp[i]要维护到第i个段时,1-i最优解
bool cmp(Query A,Query B){
    ///if(A.l==B.l) return A.r<B.r;
///return A.l<B.l;
    return A.r<B.r;
}
bool check(LL mid,LL maxx){
    if(q[mid].r<=maxx) return true;
    else return false;
}
LL bsearch(LL L,LL R,LL maxx){
    LL l=L;LL r=R;
    while(l<r){
        LL mid=(l+r+1)>>1;
        if(check(mid,maxx)) l=mid;
        else r=mid-1;
    }
    return l;
}
int main(void)
{
  cin.tie(0);std::ios::sync_with_stdio(false);
  LL n;cin>>n;
  for(LL i=1;i<=n;i++){
    cin>>q[i].l>>q[i].r;
  }
  sort(q+1,q+1+n,cmp);
  for(LL i=1;i<=n;i++){
     LL l=0;LL r=i-1;LL maxx=q[i].l-1;
     LL t=bsearch(l,r,maxx);
   ///  debug(t);
     if(t==0){
        dp[i]=max(dp[i-1],q[i].r-q[i].l+1);
     }
     else dp[i]=max(dp[i-1],dp[t]+q[i].r-q[i].l+1);
  }
  cout<<dp[n]<<endl;
return 0;
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值