[SMOJ1763]偉大的航路

題目有點繞,注意讀題的時候提取關鍵信息。

分析:既然是要按編號順序進行,就有了一個固定的順序,可以 DP。而且要注意:

由上圖可以發現,對於每個點 i ,都能找到一個點 j ,使最優方案中從 j 運完之後回原點,再從 j+1 運到 i ,這樣就可以轉移了。

定義 d(i,j) 爲第 i 點與第 j 個點之間的歐幾里德距離(設原點爲第 0 個點),fi 表示解決完前 i 個點且回到起點的最小費用,那麼有

fi=min{fj+d(0,j+1)+d(i,0)+k=j+1i1d(k,k+1)} where k=j+1iQkC

很顯然 d(i,i+1) 可以預先計算,因此上式中兩個連續求和都可以用前綴和做到 O(1),不妨令

sumdi=j=0i1d(j,j+1)
sumqi=j=1iQj
則上式化爲
fi=min{fj+d(0,j+1)+d(i,0)+sumdisumdj+1} where sumqisumqjC

對於上式直接枚舉 ij 進行轉移,時間複雜度是 O(n2) 的,可以得到 60% 的分數。
參考代碼:

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>

using namespace std;

const int MAXN = 5e4 + 100;

int N, C;
int X[MAXN], Y[MAXN], Q[MAXN], sumq[MAXN];
double d[MAXN], sumd[MAXN], f[MAXN];

inline double calc(int i, int j) {
    return hypot(X[i] - X[j], Y[i] - Y[j]);
}

int main(void) {
    freopen("1763.in", "r", stdin);
    freopen("1763.out", "w", stdout);
    scanf("%d%d", &N, &C);
    X[0] = Y[0] = 0; sumq[0] = 0;
    for (int i = 1; i <= N; i++) {
        scanf("%d%d%d", &X[i], &Y[i], &Q[i]);
        sumq[i] = sumq[i - 1] + Q[i];
        d[i] = calc(i - 1, i);
        sumd[i] = sumd[i - 1] + d[i];
    }
    for (int i = 1; i <= N; i++) {
        f[i] = f[i - 1] + calc(0, i) * 2;
        for (int j = i - 2; j >= 0 && sumq[i] <= sumq[j] + C; j--)
            f[i] = min(f[i], f[j] + calc(0, j + 1) + (sumd[i] - sumd[j + 1]) + calc(i, 0));
    }
    printf("%.2lf\n", f[N]);
    return 0;
}


下面來考慮通過優化這個 DP 獲得正解。

不難發現,可以將上式中涉及 i 的部分單獨提出來,則變形爲

Fi=d(i,0)+sumdi+min{Fj+d(0,j+1)sumdj+1} where sumqiCsumqj

很顯然 sumqj 是單調遞增的,由後面的限制條件可知,計算 fi 時決策 j 的起點是單調不遞減的,因此本質上是維護一個限定 sumqj 下限的區間,要求的是這個區間內一個式子能取到的最小值,則可以用單調遞增隊列維護,時間複雜度爲 O(n) ,可以獲得全部的分數。
參考代碼:

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <deque>

using namespace std;

typedef pair <int, double> pid;

const int MAXN = 5e4 + 100;

int N, C;
int X[MAXN], Y[MAXN], Q[MAXN], sumq[MAXN];
double d[MAXN], sumd[MAXN], f[MAXN];

deque <pid> dq;

inline double calc(int i, int j) {
    return hypot(X[i] - X[j], Y[i] - Y[j]); //注意:這個函數不是 ANSI C 的
}

void print_queue() {
    deque<pid>::iterator it = dq.begin();
    while (it != dq.end()) {
        printf("(%d,%.2lf)", it->first, it->second);
        it++;
    }
    putchar('\n');
}

int main(void) {
    freopen("1763.in", "r", stdin);
    freopen("1763.out", "w", stdout);
    scanf("%d%d", &N, &C);
    X[0] = Y[0] = 0; sumq[0] = sumd[0] = 0;
    for (int i = 1; i <= N; i++) {
        scanf("%d%d%d", &X[i], &Y[i], &Q[i]);
        sumq[i] = sumq[i - 1] + Q[i];
        sumd[i] = sumd[i - 1] + calc(i - 1, i);
//      printf("to O:%.2lf  sumd:%.2lf  to last:%.2lf\n", calc(0, i), sumd[i], sumd[i] - sumd[i - 1]);
    }
    dq.push_back(make_pair(0, 0.0));
    for (int i = 1; i <= N; i++) {
        while (!dq.empty() && sumq[i] > sumq[dq.front().first] + C) dq.pop_front();
//      printf("i=%d ", i);print_queue();
        f[i] = sumd[i] + calc(i, 0) + dq.front().second;
//      printf("%.2lf\n", f[i]);
        double t = f[i] + calc(0, i + 1) - sumd[i + 1]; //要利用單調隊列維護的部分,注意下標不能都寫成 i,要跟原來的式子一致
//      printf("back=%.2lf\n", dq.back().second);
        while (!dq.empty() && t < dq.back().second) dq.pop_back();
        dq.push_back(make_pair(i, t));
//      print_queue();
    }
    printf("%.2lf\n", f[N]);
    return 0;
}


後記:這題我從 60 分改到 100 分的時候,中間爆零了兩次,最後發現我維護隊尾的時候把 while (!dq.empty() && t < dq.back().second) 裏面的 back 打成了 front……調了半天之後發現這個傻*錯誤,什麼也不想說了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章