1. 程式人生 > >Convex hull trick演算法

Convex hull trick演算法

Convex hull trick是一種演算法或者說資料結構,用於在一組線性函式(形如y=mi*x+bi)中,每次查詢給以具體的x,可以快速求出最大/最小的y。

舉個例子。

現在有y=4, y=4/3+2/3x, y=12-3x和 y=3-1/2x這四條直線,問當x取值為1時,這些線性函式的y裡面的最小值。


四條直線如上圖,可以發現當x=2時,直線y=4/3+2/3x有最小值,為2。

樸素的演算法很容易就能想到,假設查詢Q次,共有M條直線,那麼時間複雜度就是O(QM)。

現在講一下用Convex hull trick求最小值。

對於上面的幾條直線,y=4始終不可能是最優解因此剔除,餘下的只取它們為最小值的那段區間(上圖綠色部分),它們的斜率是依次減小的,形成了一個凸包,它只有一個峰值。

因此,在去掉無關緊要的直線後,將餘下的直線按照斜率降序排序,這樣形成了N個區間,其中每段都是每條直線取最小值的那部分。如果我們可以確定每個區間的端點,那麼這就變成了一個簡單的問題:使用二分查詢每個答案。

在前面已經發現,如果相關的線段已經確定和排序,那很明顯它可以在O(lgN)內使用二分求解。因此,如果我們一次新增一條直線在我們資料結構中,然後再維護這個結構,將得到一個可行的演算法:開始沒有線(也可能一條或兩條,取決於具體實現細節),每次新增一條直線直到整個的資料結構完成。

假設在回答任何查詢前我們能夠處理所有直線。我們按照斜率降序排序,每次僅新增一條直線。當新增一條新的直線時,之前存在的一些直線可能被移除,因為它們不再有用。如果我們想象所有直線位於棧(stack)上,最先被新增的直線位於在棧頂上。當我們新增一個新的直線時,考慮棧頂的直線是否還有用。如果是,我們加入新的直線並繼續;如果不是,則彈出棧頂並重復這個過程直到棧頂的直線不應被丟棄或只有一條直線(底部的這個可能永遠不會被刪除)。 那麼,我們如何才能確定是否應該從堆疊中彈出?假設l_1,l_2,和l_3分別是棧頂方向的第二條直線、棧頂的直線和待新增的直線。然後,當且僅當l_1和l_3交點位於l_1和l_2交點的左側時,l_2無用可以被刪除。(這是因為它意味著l_3已經包含l_2以前的部分。)

Convex hull trick空間複雜度為O(M),演算法過程包括排序O(MlgM),Q次查詢,每次是O(lgM),故時間複雜度是O((M+Q)lgM)。

最後貼一個模版。跟前面的例子不一樣,這裡是用來求最大值的,外部使用的時候直線要按斜率升序新增。要改成求最小值也很容易:二分改成求最小值,另外外部使用的時候直線要按降序依次新增。

struct Line
{
    LL m,b;
    LL get(LL x)
    {
        return m*x+b;
    }
};
struct ConvexHull
{
    int size;
    vector<Line> hull;
    ConvexHull(int maxSize)
    {
        hull=vector<Line>(maxSize+1);
        size=0;
    }
    bool isBad(int l1,int l2,int l3)
    {
        double left=1.0*(hull[l3].b-hull[l1].b)/(hull[l1].m-hull[l3].m);
        double right=1.0*(hull[l1].b-hull[l2].b)/(hull[l2].m-hull[l1].m);
        return left<right;
    }
    void addLine(LL m,LL b)
    {
        hull[size++]=Line{m,b};
        while(size>2&&isBad(size-3,size-2,size-1))
        {
            hull[size-2]=hull[size-1];
            --size;
        }
    }
    LL query(LL x)
    {
        int l=0,r=size-1;
        while(l<r)
        {
            int m=(l+r)/2;
            if(hull[m].get(x)<=hull[m+1].get(x))
                l=m+1;
            else
                r=m;
        }
        return hull[l].get(x);
    }
};