1. 程式人生 > >[Luogu2365]任務安排(斜率優化)

[Luogu2365]任務安排(斜率優化)

新的 機器 getc turn 前綴 最小化 枚舉 play memset

[Luogu2365]任務安排

題目描述

N個任務排成一個序列在一臺機器上等待完成(順序不得改變),這N個任務被分成若幹批,每批包含相鄰的若幹任務。從時刻0開始,這些任務被分批加工,第i個任務單獨完成所需的時間是Ti。在每批任務開始前,機器需要啟動時間S,而完成這批任務所需的時間是各個任務需要時間的總和(同一批任務將在同一時刻完成)。每個任務的費用是它的完成時刻乘以一個費用系數Fi。請確定一個分組方案,使得總費用最小。

例如:S=1;T={1,3,4,2,1};F={3,2,3,3,4}。如果分組方案是{1,2}、{3}、{4,5},則完成時間分別為{5,5,10,14,14},費用C={15,10,30,42,56},總費用就是153。

輸入輸出格式

輸入格式:

第一行是N(1<=N<=5000)。

第二行是S(0<=S<=50)。

下面N行每行有一對數,分別為Ti和Fi,均為不大於100的正整數,表示第i個任務單獨完成所需的時間是Ti及其費用系數Fi。

輸出格式:

一個數,最小的總費用。

輸入輸出樣例

輸入樣例:

5
1
1 3
3 2
4 3
2 3
1 4

輸出樣例:

153

這類將一列東西分批處理,並且每分一批會對後面的答案造成影響的dp題目基本都可以用一種方法處理,將其對後續產生的影響直接算入這次的答案裏面。
考慮這道題目,由於時間復雜度只能承受\(O(N^2)\),所以我們只能枚舉當前的\(i\)和之前的某個任務\(j\)

,無法得知之前已經啟動了幾次。但我們知道,機器因為執行這個任務而花費的啟動時間\(S\),會累加到在此之後所有任務的完成時刻上。設\(dp[i]\)表示把前\(i\)個任務分成若幹批的最小費用,所以這道題的狀態轉移方程可以寫成
\[dp[i]=dp[j]+(sumf[i]-sumf[j])*sumt[i]+s*(sumf[N]-sumf[j])(0<=j<i)\]
\(sumf[i]\)\(sumt[i]\)為前綴和數組

在上式中,第\(j+1\)\(i\)個任務在同一批內完成,\(sunt[i]\)是忽略機器啟動時,這批任務的完成時刻。因為這批任務的執行,機器的啟動時間\(S\)

會對第\(j+1\)個之後的所有任務產生影響,故我們把這部分影響補充到費用中。
也就是說,我們沒有直接求出每批任務的完成時刻,而是在一批任務"開始"後對後續任務產生影響時,就先把費用累計到答案中。這是名為費用提前計算的經典思想。
該解法的時間復雜度為\(O(N^2)\)

#include<bits/stdc++.h>
using namespace std;
int read()
{
    int x=0,w=1;char ch=getchar();
    while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return x*w;
}
int sumt[5010],sumf[5010],dp[5010];
int main()
{
    memset(dp,0x3f,sizeof(dp));
    int n=read(),s=read(),ti,fi;
    for(int i=1;i<=n;i++)
    {
        ti=read();fi=read();
        sumt[i]=sumt[i-1]+ti;
        sumf[i]=sumf[i-1]+fi;
    }
    dp[0]=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<i;j++)
        {
            if(dp[i]>dp[j]+(sumf[i]-sumf[j])*sumt[i]+s*(sumf[n]-sumf[j]))
            {
                dp[i]=dp[j]+(sumf[i]-sumf[j])*sumt[i]+s*(sumf[n]-sumf[j]);
            }
        }
    }
    cout<<dp[n];
}

但是我們考慮當數據範圍變大時
數據範圍:\(1<=N<=3*10^5,1<=S,Ti,Fi<=512\)
這時候我們發現\(O(N^2)\)做不了了,我們想辦法把算法時間復雜度優化成\(O(N*logN)\)
先對狀態轉移方程稍作變形,把常數,僅與\(i\)有關的項、僅與\(j\)有關的項以及\(i,j\)的乘積項分開。
\[dp[i]=min(dp[j]-(S+sumt[i])*sumf[i])\]
\[+sumt[i]*sumf[i]+S*sumf[N]\]
\(min\)函數去掉,把關於\(j\)的值\(dp[j]\)\(sumf[i]\)看作變量,把其余部分看作常量,得到:
\[dp[i]=(S+sumt[i])*sumf[j]+dp[i]-sumt[i]*sumf[i]-S*sumf[N]\]
\(sumf[j]\)為橫坐標,\(dp[j]\)為縱坐標的平面直角坐標系中,這是一條以\(S+sumt[i]\)為斜率,\(dp[i]-sumt[i]*sumf[i]-S*sumf[N]\)為截距的直線,也就是說,決策候選集合是坐標系中的一個點集,每個決策\(j\)都對應著坐標系中的一個點\((sumf[j],dp[j])\)。每個待求解的狀態\(dp[i]\)都對應著一條直線的截距,直線的斜率是一個固定的值\(S+sumt[i]\),截距未知。當截距最小化的時候,dp[i]也取到最小值。
令直線過每個決策點\((sumf[j],dp[j])\),都可以解出一個截距,其中使截距最小的一個就是最優決策。體現在坐標系中,就是一條斜率固定為正整數的斜線自下而上平移,第一次接觸到某個決策點時,就得到了最小截距。(沒找到圖片,自行yy一下)。
對於任意三個決策點\((sumf[j1],dp[j1]),(sumf[j2],dp[j2]),(sumf[j3],dp[j3])\),不妨設\(j1<j2<j3\),因為\(T,F\)均為正整數,亦有\(sumf[j1]<sumf[j2]<sumf[j3]\)。根據及時排除無用決策的思想,j2成為最優決策,當且僅當:
\[\frac{dp[j2]-dp[j1]}{sumf[j2]-sumf[j1]}<\frac{dp[j3]-dp[j2]}{sumf[j3]-sumf[j2]}\]
不等號兩邊實際上都是連接兩個決策點線段的斜率。通俗的講,我們應該維護連接相鄰兩點的線段斜率單調遞增的一個下凸殼。(在斜率優化中,我們一般稱"連接相鄰兩點的線段斜率"單調遞增的一組頂點構成下凸殼,稱"連接相鄰兩點的線段斜率"單調遞減的一組頂點構成上凸殼)只有這個下凸殼上的點才有可能成為最優決策。實際上,對於一條斜率為\(k\)的直線,若某個頂點左側線段的斜率比\(k\)小、右側線段的斜率比\(k\)大,則該點\(k\)為最優決策。
在本題中,\(j\)的取值範圍是\(0<=j<i\),隨著\(i\)的增大,每次會有一個新的決策進入候選集合。因為\(sumf\)的單調性,新決策在坐標系中的橫坐標一定大於之前的所有決策,出現在整個凸殼的最右端。另外,因為\(sumt\)的單調性,每次求解最小截距的直線斜率\(S+sumt[i]\)也單調遞增,如果我們只保留凸殼上相鄰兩點的線段斜率大於\(S+sumt[i]\)的部分,那麽凸殼的最左端頂點一定就是最優決策。
綜上所述,我們可以建立單調隊列\(q\),維護這個下凸殼。隊列中保存若幹個決策變量,它們對應凸殼上的頂點,且滿足橫坐標\(sumf\)遞增、相鄰兩點的線段斜率也遞增。需要支持的操作有:
1.檢查隊頭的兩個決策變量\(q[l]\)\(q[l+1]\),若斜率\(\frac{dp[q[l+1]-dp[q[l]]}{sumf[q[l+1]]-sumf[q[l]]}<=S+sumt[i]\),則把\(q[l]\)出隊,繼續檢查新的隊頭。
2.直接取隊頭\(j=q[l]\)為最優決策,執行狀態轉移,更新dp[i]。
3.把新決策\(i\)從隊尾插入,在插入之前,若三個決策點\(j1=q[r-1],j2=q[r],j3=i\)不滿足斜率單調遞增(不滿足下凸性,即\(j2\)是無用決策),則直接從隊尾把\(q[r]\)出隊,繼續檢查新的隊尾。
整個算法的時間復雜度為\(O(N)\)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
using namespace std;
int read()
{
    int x=0,w=1;char ch=getchar();
    while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return x*w;
}
int sumt[5010],sumf[5010],dp[5010],team[5010];
int main()
{
    memset(dp,0x3f,sizeof(dp));
    int n=read(),s=read(),ti,fi;
    for(int i=1;i<=n;i++)
    {
        ti=read();fi=read();
        sumt[i]=sumt[i-1]+ti;
        sumf[i]=sumf[i-1]+fi;
    }
    int l=1,r=1;
    dp[0]=0;
    for(int i=1;i<=n;i++)
    {
        while(l<r&&dp[team[l+1]]-dp[team[l]]<=(sumf[team[l+1]]-sumf[team[l]])*(s+sumt[i]))
        l++;
        dp[i]=dp[team[l]]+sumt[i]*(sumf[i]-sumf[team[l]])+s*(sumf[n]-sumf[team[l]]);
        while(l<r&&(dp[team[r]]-dp[team[r-1]])*(sumf[i]-sumf[team[r]])>=(dp[i]-dp[team[r]])*(sumf[team[r]]-sumf[team[r-1]])) r--;
        team[++r]=i;
    }
    cout<<dp[n];
}

[Luogu2365]任務安排(斜率優化)