CSP歷年複賽題-P3957 [NOIP2017 普及組] 跳房子

江城伍月發表於2024-06-09

原題連結:https://www.luogu.com.cn/problem/P3957

題意解讀:有n個格子,每個格子有不同的距離和分數,從起點,每次可跳距離為d,用g金幣後可跳距離範圍可以變成max(d-g,1) ~ d+g,

求最小的g,使得可跳躍得分不少於k。

解題思路:

1、單調性分析:

如果g越大,可跳躍的範圍就越大,理論上能得的分數越多,因此可以透過二分g,來判斷是否能得到k分,如果可以則繼續縮小g,如果不行則放大g。

2、如何判斷能否至少得k分:

聯想到最大子段和,但這裡沒有必要連續,可以認為是最大子序列的和

設每次跳躍距離範圍l = max(d-g,1), r = d+g

設x[i]表示i的距離,s[i]表示i的分數

設f[i]表示以i結尾的最大子序列的和,

f[i] = max(f[j] + s[i]),j取i-1 ~ 1,並且j的距離要在合理範圍內

什麼是合理範圍?

如果x[j] + l > x[i],就不合理,因為從j跳不到i

如果x[j] + r < x[i],也不合理,同樣從j跳不到i

以上演算法的複雜度為O(n^2*logn)

先嚐試編寫程式碼,得到部分分!

80分程式碼:

#include <bits/stdc++.h>
using namespace std;

const int N = 500005;

int n, d, k;
int x[N], s[N];
long long f[N];

bool check(int g)
{
    int l = max(d - g, 1), r = d + g;
    memset(f, -0x3f, sizeof(f));
    f[0] = 0;
    for(int i = 1; i <= n; i++)
    {
        for(int j = i - 1; j >= 0; j--)
        {
            if(x[j] + l > x[i]) continue; //i要從1開始,因為第一個點也不一定能到
            if(x[j] + r < x[i]) break;
            f[i] = max(f[i], f[j] + s[i]);
            if(f[i] >= k) return true;
        }
    }
    return false;
}

int main()
{
    cin >> n >> d >> k;
    for(int i = 1; i <= n; i++) cin >> x[i] >> s[i];
    int l = 1, r = 1e9, ans = -1;
    while(l <= r)
    {
        int mid = (l + r) >> 1;
        if(check(mid)) ans = mid, r = mid - 1;
        else l = mid + 1;
    }
    cout << ans;
    return 0;
}

如果資料增強,O(n^2*logn)理論上是無法透過的,需要進一步最佳化

3、單調佇列最佳化

根據轉移方程f[i] = max(f[j] + w[i])可知,i 只與一定範圍內的j有關,且只和最大的f[j]有關,因此可以透過單調佇列快速得到i之前有效範圍內的最大f[j]

100分程式碼:

#include <bits/stdc++.h>
using namespace std;

const int N = 500005;

int n, d, k;
int x[N], s[N];
long long f[N];

bool check(int g)
{
    int l = max(d - g, 1), r = d + g;
    memset(f, -0x3f, sizeof(f));
    deque<int> q; //優先佇列
    f[0] = 0;
    int j = 0; //j用來列舉i前面所有數,入隊
    for(int i = 1; i <= n; i++)
    {
        //列舉所有j,去尾,入隊
        while(x[j] + l <= x[i]) //j的位置+最小跳躍距離不能超過i
        {
            while(!q.empty() && f[q.back()] <= f[j]) q.pop_back();
            q.push_back(j);
            j++;
        }
        //去頭
        while(!q.empty() && x[q.front()] + r < x[i]) q.pop_front();
        //計算
        if(!q.empty()) f[i] = f[q.front()] + s[i];

        if(f[i] >= k) return true;
    }
    return false;
}

int main()
{
    cin >> n >> d >> k;
    for(int i = 1; i <= n; i++) cin >> x[i] >> s[i];
    int l = 1, r = 1e9, ans = -1;
    while(l <= r)
    {
        int mid = (l + r) >> 1;
        if(check(mid)) ans = mid, r = mid - 1;
        else l = mid + 1;
    }
    cout << ans;
    return 0;
}

相關文章