1. 程式人生 > >luogu P3657 (NOIP2017) 跳房子(二分+DP+單調佇列)

luogu P3657 (NOIP2017) 跳房子(二分+DP+單調佇列)

題面

傳送門

分析

顯然答案有單調性,可以二分答案,設當前二分值為g,根據題意我們可以求出跳躍長度的範圍[l,r]
考慮DP
子狀態: dp[i]表示跳到第i個點時的最大和
狀態轉移方程 \(dp[i]=max(dp[i],dp[j]+a[i]) (j \in [1,n),x[i]-x[j] \in [l,r])\)
初始值:dp[0]=0 (把起點看成第0號點,權值和座標都為0)
直接轉移的時間複雜度是\(O(n^2)\)
由於此題資料水,\(O(n^2logn)\)可以卡過

(相信熱愛學習,不屑於打暴力的你一定會繼續往下看的)

觀察狀態轉移方程,發現滿足條件的j一定在某個區間內,且區間在不斷移動,類似“滑動視窗問題”,可建立一個單調佇列
另外,j存在決策單調性,即i增加時,j一定也不斷增加,不會再減小(例如對於i=1時我們求出滿足條件的最大j,i=2時滿足條件的j一定比i=1時的j更大)
因此,每次迴圈時j不必重置成0,這樣可顯著減少時間
然後維護單調佇列
對於每個i,我們將滿足條件(與i距離>l)的dp[j]不斷加入隊尾,並同時維護序列的單調性,保證佇列從大到小遞減,這樣隊頭的答案一定最優
接著處理不合格的情況,即彈出隊頭與i距離>r的值
現在隊頭就是滿足\(j \in [1,n),x[i]-x[j] \in [l,r]\)

的最大dp[j]+a[i],因此用隊頭更新dp[i],i++,繼續下一個迴圈
DP時間複雜度\(O(n)\)
總時間複雜度\(O(nlogn)\)

程式碼

暴力卡過:

#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 500005
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
inline int qread() {
    int x=0,sign=1;
    char c=getchar();
    while(c<'0'||c>'9') {
        if(c=='-') sign=-1;
        c=getchar();
    }
    while(c>='0'&&c<='9') {
        x=x*10+c-'0';
        c=getchar();
    }
    return x*sign;
}
int n,d;
int k;
int x[maxn];
int a[maxn];
long long dp[maxn];
int check(int g) {
    int l0,r0;
    memset(dp,-0x3f,sizeof(dp));
    long long ans=dp[0];
    if(g<d) {
        l0=d-g;
        r0=d+g;
    } else {
        l0=1;
        r0=d+g;
    }
    dp[0]=0;
    for(int i=1; i<=n; i++) {
        for(int j=i-1; j>=0; j--) {
            if(x[i]-x[j]<l0) continue;
            if(x[i]-x[j]>r0) break;
            if(dp[j]+a[i]>dp[i]) dp[i]=dp[j]+a[i];
            if(dp[i]>=k) return 1;

        }
        if(dp[i]>ans) ans=dp[i];
    }
    if(ans>=k) return 1;
    else return 0;
}

int main() {
    n=qread();
    d=qread();
    k=qread();
    for(int i=1; i<=n; i++) {
        x[i]=qread();
        a[i]=qread();
    }
    dp[0]=0;
    x[0]=0;
    int l=0,r=1005;
    int mid;
    int ans=-1;
    int t;
    while(l<=r) {
        mid=(l+r)>>1;
        if(check(mid)) {
            ans=mid;
            r=mid-1;
        } else l=mid+1;
    }
    printf("%d\n",ans);
}
//Dedicated to Selina

單調佇列:

#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 500005
#define INF 0x3f3f3f3f3f3f3f3f 
using namespace std;
int n,d;
int k;
int x[maxn];
int a[maxn];
long long dp[maxn];
struct node{
    long long v;
    int x;
    node(){
        
    }
    node(long long val,int pos){
        v=val;
        x=pos;
    }
};
struct deque{
    int head,tail;
    node Q[maxn];
    node front(){
        return Q[head];
    }
    node back(){
        return Q[tail-1];
    }
    void push_back(node p){
        Q[tail]=p;
        tail++;
    }
    void pop_front(){
        head++;
    }
    void pop_back(){
        tail--;
    }
    bool empty(){
        if(head<tail) return 0;
        else return 1;
    }
    deque(){
        head=tail=0;
    }
    void clear(){
        head=tail=0;
    }
}q;
int check(int g){
    int l0,r0; 
    if(g<d) {
        l0=d-g;
        r0=d+g;
    } else {
        l0=1;
        r0=d+g;
    }
    int j=0;
    memset(dp,-0x3f,sizeof(dp));
    q.clear();
    dp[0]=0;
    for(int i=1;i<=n;i++){
        while(x[i]-x[j]>=l0){//由於j有決策單調性,不必清零
            while(!q.empty()&&dp[j]>=q.back().v) q.pop_back();//保證序列單調遞減
            q.push_back(node(dp[j],x[j]));
            j++;
        }
        while(!q.empty()&&x[i]-q.front().x>r0) q.pop_front();//排除不符合條件的情況
        if(q.empty()) dp[i]=-INF;//如果佇列為空,說明該點不能到達,直接設為-INF
        else dp[i]=q.front().v+a[i];
        if(dp[i]>=k) return 1; 
    }
    return 0;
}
int main(){
    scanf("%d %d %d",&n,&d,&k);
    for(int i=1;i<=n;i++){
        scanf("%d %d",&x[i],&a[i]);
    }
    x[0]=0;
    int l=0,r=x[n];
    int mid;
    int ans=-1;
    while(l<=r){
        mid=(l+r)>>1;
        if(check(mid)){
            ans=mid;
            r=mid-1;
        }else l=mid+1;
    }
    printf("%d\n",ans);
}
//Dedicated to Selina