1. 程式人生 > >Dividing the Path POJ - 2373 詳細題解 (線性DP + 單調佇列優化)

Dividing the Path POJ - 2373 詳細題解 (線性DP + 單調佇列優化)

這道題目是一道較為複雜的線性Dp題目, 我會按照思路詳細列舉一些本題解題過程

按照動態規劃問題的一般解題思路來, 首先, 我們需要定義一個狀態dp[i] 表示恰好到覆蓋到 [0, i]時最少需要的噴水裝置數量, 答案自然也就是dp[L], 把長度作為容量, 是線性Dp的一種常見方案

然後對於dp[i], 我們需要根據題意可以抽取到以下幾點資訊.

1. i一定是偶數, 因為我們的目的是放置合適的噴水裝置來覆蓋, 2*r(直徑)一定是偶數

2.i一定不在奶牛的範圍之內, 因為如果i在某奶牛範圍之內, 該奶牛範圍也就被分割為了兩塊, 不符合題中要求的恰好位於一                個噴水器的覆蓋範圍內

3.i一定大於2*a, 因為最小半徑是a

4.對於i在[2*a, 2*b]這個區間時, 我們需要考慮初始化, 把符合 1 2 條件的dp[i]初始化為1

這樣的話我們不妨直接把所有元素都初始化為正無窮

這應該就是全部題中可以提取出的條件資訊了, 下面我們就要去找到一個合適的狀態轉移方程

            5.對於i > 2*a時, dp[i]在滿足12的情況下, 應該在[i-2*b, i-2*a]這個區間中找到一個最小的+1. 這個地方有一點需要仔細理解              一下, 就是dp[i]並不表示把噴水器放在了i, 而是放在了[i-2*b, i-2*a]這個區間中, 依次遞推.

可得狀態轉移方程: dp[i] = min{dp[j] | i-2*b <= j <= i-2*a} +1 

程式碼如下:

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <vector>
#include <queue>
#include <cmath>
using namespace std;
#define ms(x, n) memset(x,n,sizeof(x));
typedef  long long LL;

const LL maxn = 1010, maxl = 1e6+5;
int n, l, a, b;
struct cow{
    int s, e; //一個奶牛
}cows[maxn];
int dp[maxl]; //恰好覆蓋到0,i時的最少裝置數量
bool vis[maxl]; //該點是否在奶牛範圍內

int main()
{
    cin >> n >> l >> a >> b;
    for(int i = 1; i <= n; i++){
        cin >> cows[i].s >> cows[i].e;
        for(int j = cows[i].s+1; j < cows[i].e; j++)
            vis[j] = 1;  //注意是開區間
    }

    for(int i = 0; i <= l; i++)
        dp[i] = 1<<30; //初始化為正無窮
    for(int i = 2*a; i < 2*b; i+=2)
        if(!vis[i]) dp[i] = 1; //不在奶牛活動範圍內初始化為1

    for(int i = 2*b; i <= l; i+=2){
        if(!vis[i])
            for(int j = i-2*b; j <= i-2*a; j++)
                dp[i] = min(dp[i], dp[j]+1); //狀態轉移方程
    }

    if(dp[l] != 1<<30)
        cout << dp[l] << endl;
    else
        cout << -1 << endl;
    return 0;
}

但是問題來了, 如果你老老實實把上面這個程式寫了去交的話, 你可能發現, 他的時間複雜度太大了

時間複雜度大致為(L*(2*b-2*a)), 極端情況可能達到1000000 * 1000, 達到十億量級, 而1s的最大量級一般不超過一億, 所有要考慮優化

在這裡使用的是單調佇列優化, 因為對於1*L這個迴圈它是揹包的容量, 是沒有辦法去優化的, 只能考慮如果優化(2*b-2*a)這個複雜度, 也就是考慮如何才能更快的從[i-2*b, i-2*a]找到最小的dp[j], 毫無意義要使用一個單調佇列, 因為單調佇列可以定義一個優先順序排序的方法, 讓優先順序最大的元素永遠在隊首(常數複雜度)

考慮的問題同上大致相同, 注意對邊界的一些判定, 還有就是我們要保證佇列中不能出現 > i-2*a的點, 因為那樣的話就會一直擾亂狀態轉移方程, 所以在佇列初始化時要加個判斷 和求出一個dp[i]後也不能直接入隊, 而是把dp[i-2*a+2]入隊就可以了

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <vector>
#include <queue>
#include <cmath>
using namespace std;
#define ms(x, n) memset(x,n,sizeof(x));
typedef  long long LL;

const LL maxn = 1010, maxl = 1e6+5;
const LL inf = 1<<30;
int n, l, a, b;
struct node{
    int i, f;
    bool operator < (const node &a) const { return f > a.f;}
    node(int ii=0, int ff=0) : i(ii), f(ff) { }
};
priority_queue<node> minDp; //dp(i-2*b,i-2*a)
int dp[maxl]; //恰好覆蓋到0,i時的最少裝置數量
bool vis[maxl]; //該點是否在奶牛範圍內

int main()
{
    cin >> n >> l >> a >> b;
    a <<= 1, b <<= 1; //直接把a, b*2;

    int s, e; //直接用s,e提取出f(恰好覆蓋到0,i時的最少裝置數量)即可
    for(int i = 1; i <= n; i++){
        cin >> s >> e;
        for(int j = s+1; j < e; j++)
            vis[j] = 1;  //注意是開區間
    }

    for(int i = 0; i <= l; i++)
        dp[i] = inf; //初始化為正無窮
    for(int i = a; i <= b; i+=2)
        if(!vis[i]){
            dp[i] = 1;
            if(i <= b+2-a) //下一步要從b+2開始求
                minDp.push(node(i,1)); //minDp的初始化
        }

    for(int i = b+2; i <= l; i+=2){
        if(!vis[i]){
            node cur;
            while(!minDp.empty()){
                cur = minDp.top();
                if(cur.i < i-b) minDp.pop(); //不符合條件的元素直接移除
                else break;
            }
            if(!minDp.empty())
                dp[i] = cur.f+1; //狀態轉移方程
        }
        if(dp[i-a+2] != inf)
            minDp.push(node(i-a+2, dp[i-a+2])); //為求dp[i+2]做好準備
    }

    if(dp[l] != 1<<30)
        cout << dp[l] << endl;
    else
        cout << -1 << endl;
    return 0;
}