1. 程式人生 > >計算幾何 - 凸包 - 判斷某點是否可以被一點集中的某三個點圍成的三角形包圍

計算幾何 - 凸包 - 判斷某點是否可以被一點集中的某三個點圍成的三角形包圍

pan 根據 模板庫 printf main esp space code ret

描述

二維平面上,給定 n 個點 {ai} m 個點 {bi},且保證這 n+m 個點中任意兩個點的 x 坐標和 y 坐標均不相同。

對於每個bi,判斷是否存在由3個 ai, aj, ak (1 i, j, k n, i j k ) 點組成的三角形包含 bi(在三角形邊上也算包含;允許三點共線的三角形,此時只有 bi 在三點中任意兩點的線段上才算包含)。

輸入

第一行為一個整數 n。接下來 n 行,其中第 i 行有兩個整數,表示 ai 的橫、縱坐標。

第一行為一個整數 m。接下來 m 行,其中第 i 行有兩個整數,表示 bi 的橫、縱坐標。

輸出

輸出 m 行,第 i 行為一個整數 0 或 1,分別表示是否存在一個三角形包含該 bi

樣例輸入

3
1 -6
-10 -3
1 6
3
-2 7
-4 -3
-3 2

樣例輸出

0
1
1

限制

n, m 3;

其中30%的數據,n, m 200

另外30%的數據,n, m 2000

剩下40%的數據,n, m 300000

時間:3 sec

空間:256 MB

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

思路

這是一道凸包應用的題目。凸包是一個點集所能圍成的最大凸多邊形區域。那麽:

① 若點集 B 中的某點 b 能被點集 A 中的某三個點包圍,那麽它一定在點集 A 的凸包內(包括邊界上)。

② 從點集 A 凸包上的某點開始,它所輻射的對角線(凸包上一點與凸包上其他點的連線)可以將凸包分成若幹三角形區域。若 b 能被點集 A 中的某三個點包圍,那麽它一定在某兩條對角線構成的三角形區域內。

技術分享圖片

如下圖所示,紅色的點為點集 A,綠色的點為點集 B,紅線圍成的區域為 A 的凸包,LTL為我們選定的凸包上一點(它可以任意選擇,一般選擇 y 最小的點,若不唯一,再選擇 x

最小的),它與凸包上其他點的連線將凸包分割成若幹三角形部分。可以看到,B 中能被 A 包圍的點一定在某個這些對角線構成的三角形內。

根據上面的分析,我們可以先求點集 A 的凸包(比如,卷包裹法、上下凸殼法等);然後從 A 凸包上某點開始(比如上圖中的LTL點),找到 b 點最近的兩條對角線,然後判斷 b 能否被這兩條對角線上三個 A 中的點包圍。例如,對於上圖中的 b 點,設法找到 atas,然後判斷 b 能否被 LTL、atas圍成的三角形包圍。

如何快速找到離某點最近的兩條對角線呢。我們可以使用二分查找。這就要對 A 凸包上的點排序,順序就是其相對 LTL 的極角。比如上圖中的 asat 兩點,從LTL看去,asat 左側,則 as > at 。註意,如果兩個點與LTL位於同一直線上,則距離 LTL 更近的點更小。這樣,就可以通過二分查找快速的找到 b 離最近的兩條對角線了。

綜上所述,可以將算法要旨總結如下圖所示。註:① lower_bound 就是 C++ 標準模板庫中的 lower_bound,返回有序序列中不小於我們查找對象的最小值。② 判斷 b 是否在三角形內是利用三次 toLeft 判斷實現的。

技術分享圖片

關於 toLeft(x, a, b):判斷點 x 是否在向量 ab 的左側。下圖中所示的情況返回值均為 true。這樣做是為了保證 b 在某條對角線上甚至與凸包上某點重合時都能正確判斷(對於此題,數據保證無重合)。判斷的方法是:先觀察 abax 的外積,若外積大於0則返回 true;若外積等於0,則再觀察 abax 的內積,內積大於等於0則返回 true。其他情況均返回 false。可以證明,對於如下圖所示的所有可能的 LLT, a0, a1, b 的幾何關系,應用三次上述 toLeft 測試都可以得到正確的判斷。

技術分享圖片 技術分享圖片

復雜度:找到凸包 O(nlogn),二分查找 O(logn),三角形內部判斷 O(1),故總的時間復雜度 O(mlog(n) + nlog(n)),nA 點集中點個數,mB 點集中點個數。

C++代碼

這裏為了方便,用了手頭已有的上下凸殼法的模板求凸包。其實,用卷包裹法更方便,因為卷包裹法求凸包時就要先找LTL點,然後關於LTL點排序。

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const int N = 300005;

// 存儲二維平面點
struct ip
{
    int x, y;
    ip(int x = 0, int y = 0) : x(x), y(y) { }
    void ri() { scanf("%d %d", &x, &y); }
};

// iv表示一個向量類型,其存儲方式和ip一樣
typedef ip iv;

// 先比較x軸再比較y軸
bool operator < (const ip &a, const ip &b)
{
    return a.x == b.x ? a.y < b.y : a.x < b.x;
} 

// 兩點相減得到的向量
iv operator - (const ip &a, const ip &b)
{
    return iv(a.x - b.x, a.y - b.y);
}

// 計算a和b的叉積(外積)
ll operator ^ (const iv &a, const iv &b)
{
    return (ll)a.x * b.y - (ll)a.y * b.x;
}

// 計算a和b的點積(內積) 
ll operator * (const iv &a, const iv &b)
{
    return (ll)a.x * b.x + (ll) a.y * b.y;
}

class myComp
{
private:
    long long ltl_x, ltl_y;    // 存儲左下角點的坐標 
public:
    myComp() : ltl_x(0), ltl_y(0) { }
    myComp(int x, int y){ ltl_x = (long long)x; ltl_y = (long long)y; }
    bool operator()(const ip & p1, const ip & p2)
    {
        long long x1 = (long long) p1.x - ltl_x, y1 = (long long) p1.y - ltl_y;
        long long x2 = (long long) p2.x - ltl_x, y2 = (long long) p2.y - ltl_y;
        long long outter_product = x1 * y2 - x2 * y1;
        if ( outter_product > 0 )
            return true;
        else if ( outter_product == 0 )    // 若LTL、p1和p2共線 
        {
            long long x3 = (long long) p2.x - (long long) p1.x;
            long long y3 = (long long) p2.y - (long long) p1.y;
            if ( x3 * x1 + y3 * y1 > 0 )    // 若 LTL->p1 與 p1->p2 方向相同(用內積判斷),則p1到LTL距離更短 
                return true;
        }
        return false;
    }
};

/* 判斷點 c 是否在 ab 向量的左側。若 c 在 ab 上,或者 a→b 正向延長線上,或者 c 與 a 或 b 重合,返回皆為 true。 */
bool toLeft(ip & c, ip & a, ip & b)
{
    long long x_ab = (long long) b.x - (long long) a.x; 
    long long y_ab = (long long) b.y - (long long) a.y;
    long long x_ac = (long long) c.x - (long long) a.x; 
    long long y_ac = (long long) c.y - (long long) a.y;
    long long outter_product = x_ab * y_ac - x_ac * y_ab;
    long long inner_product = x_ab * x_ac + y_ab * y_ac;
    if ( outter_product > 0 || (outter_product == 0 && inner_product >= 0) )
        return true;
    return false;
} 

/* 計算二維點數組a的凸包,將凸包放入b數組中,下標均從0開始
   a, b:如上 
   n:表示a中元素個數
   返回:凸包元素個數 
*/
int convex(ip * a, ip * b, int n)
{
    // 升序排序
    sort(a, a+n);
    // n = unique(a, a+n) - a;    // 若題目中有重復點,必須去重
    
    int m = 0;
    
    // 求下凸殼
    for ( int i = 0; i < n; ++i )
    {
        for ( ; m > 1 && ((b[m-1] - b[m-2]) ^ (a[i] - b[m-2])) < 0; --m );
        b[m++] = a[i];
    }
    
    // 求上凸殼
    for ( int i = n - 2, t = m; i >= 0; --i )
    {
        for ( ; m > t && ((b[m-1] - b[m-2]) ^ (a[i] - b[m-2])) < 0; --m );
        b[m++] = a[i];
    }
     
    return m - 1;
}

/* 對第n_b個點構成的點集B(坐標從b開始存儲)中每個點,判斷是否能被n_a個點構成的點集A(坐標從a開始存儲)中的三個點包圍。 */
vector<int> isSurrounded(ip * a, ip * b, int n_a, int n_b);

ip A[N], B[N];

int main()
{   
    int n = 0, m = 0;    // 點集A中點的數量n和點集B中點的數量m 
    
    // 讀入各點坐標 
    scanf("%d", &n);    
    for ( int i = 0; i < n; ++i )
        A[i].ri();
    scanf("%d", &m);    
    for ( int i = 0; i < m; ++i )
        B[i].ri();
    
    // 求解 
    vector<int> ans = isSurrounded(A, B, n, m);
    
    // 打印結果 
    for ( int i = 0; i < m; ++i )
        printf("%d\n", ans[i]);
    
    return 0;
}

vector<int> isSurrounded(ip * a, ip * b, int n_a, int n_b)
{
    vector<int> ans; ans.resize(n_b);
    
    // 求a的凸包 (如果不找到凸包,直接在整個點集A上執行下面的判斷是不正確的)
    ip * conv = new ip[n_a];
    int n_conv = convex(a, conv, n_a);
    
    // 找到A中的LTL,並將它換到conv指向的位置 (即conv數組的第0個元素) 
    ip ltl(conv->x, conv->y);
    ip * p = conv;
    for ( int i = 1; i < n_conv; ++i )
    {
        if ( (conv+i)->y < ltl.y || ((conv+i)->y == ltl.y && (conv+i)->x < ltl.x ) )
        {
            ltl.x = (conv+i)->x;
            ltl.y = (conv+i)->y;
            p = conv+i;
        }
    }
    p->x = conv->x; p->y = conv->y;
    conv->x = ltl.x; conv->y = ltl.y;
    
    // 對conv中各點關於LTL進行極角排序
    sort(conv+1, conv+n_conv, myComp(ltl.x, ltl.y)); 
     
    // 對B中各點判斷其是否能被A中的三個點包圍
    for ( int i = 0; i < n_b; ++i )
    {
        if ( b[i].y < ltl.y || (b[i].y == ltl.y && b[i].x < ltl.x ) )
        {
            ans[i] = 0;
            continue;
        }
        
        // 找到A凸包中不小於b[i]的最小點a1
        ip * a1 = lower_bound(conv+1, conv+n_conv, b[i], myComp(ltl.x, ltl.y));
        ip * a0 = a1 - 1;
        if ( (a1 - a) == n_conv )    // 若找不到這樣的a1,則不能被包圍 
            ans[i] = 0;
        else
        {
            if ( toLeft(b[i], ltl, *a0) && toLeft(b[i], *a0, *a1) && toLeft(b[i], *a1, ltl) )
                ans[i] = 1;
            else
                ans[i] = 0;
        }
    } 
    
    return ans;
}

計算幾何 - 凸包 - 判斷某點是否可以被一點集中的某三個點圍成的三角形包圍