NOIP 2018【擺渡車】題解

updated on 2019.10.25:

  1. 之前的程序真的醜……如今已經把碼風改良了;html

  2. 去掉了以前那些徹底是行爲藝術的優化。數組

    向那些被個人naive講解和醜陋程序勸退的盆友們誠懇地道個歉(霧函數

    (由於筆者是\(2020\)屆中考生,因此這應該是最後一次大改了,之後就不會有時間了……)優化


建議你們在博客裏食用:傳送門spa


要是PJ組再考這麼難的DP,我就當官把CCF取締了code


開個玩笑。htm

此題正解:\(\mathrm{DP}\)+各類剪枝 or 優化


1、引理

  • 對於每一個乘客,能搭載ta的車的發車時間只有\(m\)種狀況
  • 設這個乘客開始等候的時間是\(t_i\),則對應的\(m\)種狀況是\([t_i,t_i+m)\)

證實

  1. 若是存在一種狀況,其發車時間是\(\geqslant t_i+m\)的,則由題意可知,發車時間能夠提前若干輪(也就是減去若干個\(m\)到達\([t_i,t_i+m)\)這個區間,這樣作不會影響發車時間\(\geqslant t_i+m\)的那趟車
  2. 若是\(<m\)的話,那這個乘客根本就坐不上這趟車,因此不須要考慮。

2、基本思想

  • 首先,題目給定咱們的這\(n\)我的開始等候的時間是亂的,因此咱們要先按照開始等車的時間把這\(n\)我的排個序,而後再離散化(具體來講就是將等待時間相同的若干我的「合併」成一我的)blog

    在結構體中,用pos表示這一堆人的等待時間num表示這一堆人的人數。(具體過程看代碼)get

  • \(f(i,j)\)表示用擺渡車已經載了前\(i\)我的,且搭載了第\(i\)我的(不必定只搭載第\(i\)我的)的那趟擺渡車的發車時間是(\(t_i+j\))的最小等候時間和。(\(t_i\)的意義與題意相同)博客

  • 這裏要注意:\(t_i+j\)除了要知足\(j<m\)(對應上面的引理),同時還要知足\(j<t_{i+1}-t_i\)(即\(t_i+j<t_{i+1}\)

    • 由於若是\(j\geqslant t_{i+1}\),那這趟車就能夠把第\(i+1\)我的也搭上了,顯然違反了\(\mathrm{DP}\)狀態的定義

      (在代碼中,咱們用一個名爲border(i)#define表示了這兩個限制)

  • 對於每一個\(f(i,j)\),枚舉上一趟擺渡車的出發時間。

等等!數據範圍寫着:

\[1 \leqslant t_i \leqslant 4\times10^6 \]

你跟我說枚舉時間?你這最起碼都\(O(nt_i) \sim \mathrm{T}(2\times10^9)\) 的時間複雜度了,怎麼\(\mathrm{AC}\)

彆着急啊,我還沒說完呢。

  • 其實引理已經告訴咱們,咱們不須要把整個\(t_i\)枚舉完

    由引理可得,對於前\(i-1\)個乘客,每一個乘客能搭載的擺渡車的發車時間只有\(m\)種狀況,因此咱們只須要枚舉這\((i-1)\times m\)種狀況便可。其餘狀況都是廢的,不須要去考慮。

    這樣作的枚舉量爲\(O(nm) \sim \mathrm{T}(5 \times 10^4)\),相比以前直接枚舉\(t_i\)的時間複雜度\(\mathrm{T}(4 \times 10^6)\)來說,已經小不少了。

  • 接着,假設前一趟擺渡車已經載了前\(k\)我的,那麼咱們要作的就只有兩件事:

    1. 再枚舉一個\(l\),獲得\(f(k,l)\)的最小值。
    2. 計算出第\(k+1\)我的到第\(i\)我的等候當前這趟擺渡車的等候時間和。

敲重點!敲重點!敲重點!

  • 這裏,\(l\)的取值範圍有三個條件:

    • 前兩個條件和前面的border()同樣,再也不贅述。

    • 第三個條件是\(l\leqslant (t_i+j)-m-t_k\)(即\(t_k+l\leqslant (t_i+j)-m\)

      • 緣由很簡單,若是\(t_k+l> (t_i+j)-m\),那麼兩趟車之間相隔的時間確定就\(<m\),顯然不合題意。

        (因此這裏還要再定義一個border2(i)=min( border(i),第三個條件 )

  • 在狀態轉移方程中的體現就是:

    \[f(i,j)=\min_\limits{0 \leqslant k < i,l\leqslant \mathrm{border2(k)}} \{f(k,l)+col(k+1,i,t_i+j)\}\]

    這當中,\(col(k+1,i,t_i+j)\)表示\(k+1\)個乘客到第\(i\)個乘客等候發車時間爲\(t_i+j\)的那趟擺渡車的時間和,直接用一個for循環累計便可。

    (固然,當\(k=0\)時,\(f(k,l)\)恆爲零,表示這趟車直接把前\(i\)我的所有載完,這時等式右側就直接等於\(col(1,i,t_i+j)\)

  • 算一下上面這個狀態轉移方程的時間複雜度:

    1. 首先,\(i\)\(j\)必須枚舉,因此是\(O(nm)\)的。
    2. 其次,\(k\)\(l\)也是要枚舉的,因此又是一個\(O(nm)\)
    3. 最後,每次枚舉\(i,j,k,l\),都要計算一次\(col\)函數,而這個\(col\)函數的時間複雜度是\(O(n)\)的。

    綜上所述,這個狀態轉移方程的時間複雜度爲\(O(nm\times nm\times n)=O(n^3m^2)\)

    這時間複雜度……也太可觀了吧

    因此咱們須要優化!優化!優化!


3、程序實現 or 剪枝

  • 咱們來關注一下這個式子:\[col(k+1,i,t_i+j)\]
    對於每一個\(i,j\),當\(k\)每增長\(1\)時,\(col\)的值就只會減掉\((t_i+j-t_k)\times num_k\)\(num_k\)就是上文中提到的,結構體中\(k\)堆人的人數)。

    因此咱們能夠在枚舉每一個\(i\)\(j\)時,就把\(col(1,i,t_i+j)\)算出來(用一個變量\(val\)存起來)

    而後,\(k\)\(1\)開始枚舉,每當\(k\)在循環一開始等於某個值\(x\)時,\(val\)就減去\((t_i+j-t_x)\times num_x\)

    狀態轉移方程就變爲:\[f(i,j)=\min_\limits{0 \leqslant k < i,l \leqslant \mathrm{border2(k)}} \{f(k,l)+val\}\]

    這樣一抽出來,時間複雜度就變成了\(O(nm(n+nm))=O(n^2m+n^2m^2)\)
    只保留最高次項後,時間複雜度就降爲了\(O(n^2m^2)\)這就是\(60\)分的作法!


  • 其實你們有沒有想過,枚舉\(l\)這個操做顯得有些多餘,可不能夠省去呢?(畢竟只是求一個最小值而已,我求完一次就把這個最小值存起來不就好了嗎?)

    沒錯,上面的想法是正確的!

  • 咱們開多一個數組\(\mathrm{Min}(i,j)= \min_\limits{j\leqslant \mathrm{border}(i)} \{f(i,j)\}\)

    則以前的狀態轉移方程能夠簡化爲:

    \[f(i,j)=\min_\limits{0 \leqslant k < i} \{\mathrm{Min}(k,\mathrm{border2}(k))+val\}\]

    \(\mathrm{Min}\)能夠在求每一個\(f(k,l)\)的時候順帶維護。

    由於這裏只枚舉了\(i,j,k\),因此\(\mathrm{DP}\)的時間複雜度是\(O(n^2m)\)!!!

這個時間複雜度能夠經過本題!!!


4、考場代碼

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=502,maxm=102;
const int INF=0x7fffffff;

#define border2(x) min( border(x),lpos-Mem[x].pos ) //第三個條件是用小於等於號鏈接的,因此不用-1
#define border(x) min( m-1,Mem[x+1].pos-Mem[x].pos-1 )  //由於兩個條件都是用小於號鏈接的,因此在循環中要-1

int f[maxn][maxm];
int Min[maxn][maxm];

int a[maxn];

struct Node{int pos,num;}Mem[maxn];int sz;

int col(int l,int r,int pos)
{
    int res=0;
    for(int i=l;i<=r;i++) res+=(pos-Mem[i].pos)*Mem[i].num;

    return res;
}

int main()
{
    int n,m;
    scanf("%d%d",&n,&m);

    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    sort(a+1,a+n+1);

    a[0]=-1;

    for(int i=1;i<=n;i++)
    {
        if( a[i]!=a[i-1] ) Mem[++sz].pos=a[i];
        Mem[sz].num++;
    }

    Mem[sz+1]=(Node){INF,0};
    n=sz;

    for(int i=1;i<=n;i++) for(int j=0;j<=m;j++) f[i][j]=Min[i][j]=INF;

    for(int i=1;i<=n;i++)
        for(int j=0;j<=border(i);j++)
        {
            int pos=Mem[i].pos+j,lpos=pos-m;

            int val=col(1,i,pos);
            f[i][j]=val;

            for(int k=1; k<i and Mem[k].pos<=lpos ;k++)
            {
                val-=(pos-Mem[k].pos)*Mem[k].num;
                f[i][j]=min( f[i][j],Min[k][border2(k)]+val );
            }
            
            Min[i][j]=f[i][j];
            if( j>0 ) Min[i][j]=min( Min[i][j],Min[i][j-1] );
        }

    printf("%d",Min[n][m-1]);return 0;
}

又是一年過去了,這裏就祝你們\(\mathrm{CSP\ 2019\ J/S}\)認證rp++!

(話說,老子打完此次也要隱退了呢。。。

相關文章
相關標籤/搜索