← Home
#include <bits/stdc++.h>
using namespace std;
 
#define __Yang_Yang__() int main()
int test = 1;
using ll = long long;
using ull = unsigned long long;
using ldb = long double;
using db = double;
 
// BẮT ĐẦU CÁC HÀM HỖ TRỢ (KHÔNG DÙNG CLASS)
 
struct Point {
    ll x, y;
    int type; // 0: Algoland (Đỏ), 1: Berland (Xanh)
    int id;   // ID của thành phố
    int r;    // Bậc yêu cầu (chỉ dùng cho điểm Xanh)
};
 
// Hàm so sánh sắp xếp từ trái sang phải, dưới lên trên
bool cmp(const Point& a, const Point& b) {
    if (a.x != b.x) return a.x < b.x;
    return a.y < b.y;
}
 
// Tích có hướng vector
ll cross_product(Point a, Point b, Point c) {
    return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
}
 
// Hàm tìm bao lồi (Không tự sort lại vì tập pts truyền vào ĐÃ ĐƯỢC SORT)
vector<Point> get_convex_hull(const vector<Point>& pts) {
    int n = pts.size(), k = 0;
    if (n <= 2) return pts;
    vector<Point> hull(2 * n);
    
    for (int i = 0; i < n; ++i) {
        while (k >= 2 && cross_product(hull[k - 2], hull[k - 1], pts[i]) <= 0) k--;
        hull[k++] = pts[i];
    }
    for (int i = n - 2, t = k + 1; i >= 0; i--) {
        while (k >= t && cross_product(hull[k - 2], hull[k - 1], pts[i]) <= 0) k--;
        hull[k++] = pts[i];
    }
    hull.resize(k - 1);
    return hull;
}
 
// Hàm xây dựng cây khung (TỐI ƯU BỘ NHỚ VÀ THỜI GIAN)
void build_tree(vector<Point> pts) {
    // Dùng vòng lặp while để khử đệ quy ở Case 1 (Tail Call Optimization)
    while (pts.size() >= 2) {
        if (pts.size() == 2) {
            if (pts[0].type == 1) cout << pts[0].id << " " << pts[1].id << "\n";
            else cout << pts[1].id << " " << pts[0].id << "\n";
            return;
        }
 
        // TRƯỜNG HỢP 1: Hai điểm ngoài cùng KHÁC màu
        if (pts.front().type != pts.back().type) {
            vector<Point> hull = get_convex_hull(pts);
            
            int idx1 = -1, idx2 = -1;
            for (size_t i = 0; i < hull.size(); ++i) {
                size_t nxt = (i + 1) % hull.size();
                if (hull[i].type != hull[nxt].type) {
                    idx1 = i; idx2 = nxt; 
                    break;
                }
            }
            
            Point p1 = hull[idx1];
            Point p2 = hull[idx2];
            
            if (p1.type == 1) cout << p1.id << " " << p2.id << "\n";
            else cout << p2.id << " " << p1.id << "\n";
 
            Point blue_pt = (p1.type == 1) ? p1 : p2;
            Point red_pt = (p1.type == 0) ? p1 : p2;
 
            int remove_id = -1;
            int remove_type = -1;
 
            if (blue_pt.r == 1) {
                remove_id = blue_pt.id;
                remove_type = 1;
            } else {
                remove_id = red_pt.id;
                remove_type = 0;
                // Giảm bậc r của điểm Xanh trực tiếp trong mảng pts
                for (auto& p : pts) {
                    if (p.id == blue_pt.id && p.type == 1) {
                        p.r--;
                        break;
                    }
                }
            }
 
            // Xóa điểm dư thừa (O(N) thay vì O(N log N) cấp phát lại mảng mới)
            for (auto it = pts.begin(); it != pts.end(); ++it) {
                if (it->id == remove_id && it->type == remove_type) {
                    pts.erase(it);
                    break;
                }
            }
            // Vòng lặp while tiếp tục với mảng pts đã co lại 1 phần tử
        } 
        // TRƯỜNG HỢP 2: Hai điểm ngoài cùng ĐỀU LÀ Berland (Xanh)
        else if (pts.front().type == 1) {
            int pref = 0;
            int split_idx = -1;
            
            for (int i = 0; i < pts.size(); ++i) {
                pref += (pts[i].type == 1 ? pts[i].r - 1 : -1);
                if (pref == -1) {
                    split_idx = i;
                    break;
                }
            }
            
            // Cắt mảng inplace. Mảng right lấy từ split_idx -> end
            vector<Point> right(pts.begin() + split_idx, pts.end());
            // Mảng pts được thu hẹp lại thành mảng left (từ 0 -> split_idx)
            pts.erase(pts.begin() + split_idx + 1, pts.end());
            
            // Gọi đệ quy cho nửa phải. Dùng std::move để CHUYỂN QUYỀN SỞ HỮU, chống sao chép bộ nhớ
            build_tree(std::move(right));
            // Nửa trái chính là pts, sẽ được tiếp tục chạy ở vòng lặp while hiện tại!
        } 
        // TRƯỜNG HỢP 3: Hai điểm ngoài cùng ĐỀU LÀ Algoland (Đỏ)
        else {
            int pref = 0;
            int split_idx = -1;
            
            for (int i = 0; i < pts.size(); ++i) {
                int w = (pts[i].type == 1 ? pts[i].r - 1 : -1);
                if (pref < 0 && pref + w >= 0) {
                    split_idx = i;
                    break;
                }
                pref += w;
            }
            
            int r_left = -pref; 
            int r_right = pts[split_idx].r - r_left;
 
            vector<Point> right(pts.begin() + split_idx, pts.end());
            right.front().r = r_right;
            
            pts.erase(pts.begin() + split_idx + 1, pts.end());
            pts.back().r = r_left;
            
            build_tree(std::move(right));
            // Tương tự Case 2, nửa trái tiếp tục trong vòng lặp hiện tại.
        }
    }
}
 
void solve() {
    int a, b;
    cin >> a >> b;
    
    vector<int> r(b + 1);
    for (int i = 1; i <= b; ++i) {
        cin >> r[i];
    }
    
    vector<Point> pts;
    pts.reserve(a + b);
    for (int i = 1; i <= a; ++i) {
        ll x, y; cin >> x >> y;
        pts.push_back({x, y, 0, i, 0});
    }
    for (int i = 1; i <= b; ++i) {
        ll x, y; cin >> x >> y;
        pts.push_back({x, y, 1, i, r[i]});
    }
    
    // Chỉ sort ĐÚNG 1 LẦN duy nhất ở đầu vào. Các phép erase không làm hỏng tính chất sort.
    sort(pts.begin(), pts.end(), cmp);
 
    cout << "YES\n";
    build_tree(std::move(pts)); // Move bộ nhớ đi để tiết kiệm
}
 
__Yang_Yang__()
{
    ios_base::sync_with_stdio(0);
    cin.tie(nullptr); cout.tie(nullptr);
    
    cin >> test;
    while(test--) solve();
    return 0;
}
/*
TRẢ LỜI CÁC CÂU HỎI:
 
Cấu trúc dữ liệu nào dùng trong code, và nhiệm vụ của chúng?
=> std::vector<Point> để lưu danh sách điểm. Việc tái sử dụng vùng nhớ bằng std::move giúp vector không bị phình to (Memory Leak/Stack Overflow) ở cấp độ sâu của hàm đệ quy. 
 
Thuật toán nào dùng trong code, và nhiệm vụ của chúng?
=> Thuật toán Bao lồi Convex Hull (Monotone Chain) để tìm lớp biên. Thuật toán Chia để trị (Divide and Conquer). Tail Call Optimization (tối ưu hóa đệ quy đuôi) bằng việc thay thế lời gọi đệ quy của Case 1 thành vòng lặp while để tránh tràn bộ nhớ.
 
Công thức toán học nào hoặc công thức truy hồi nào dùng để làm gì?
=> Công thức Tích có hướng (Cross Product) để xây bao lồi. Quy hoạch trọng số: điểm đỏ w = -1, điểm xanh w = r - 1 nhằm tìm điểm cắt mảng thông qua Prefix Sums.
 
Độ phức tạp thuật toán là bao nhiêu?
=> Thời gian: O(N^2) thay vì O(N^2 log N) do ta loại bỏ hoàn toàn hàm std::sort thừa thãi, chỉ tận dụng std::vector::erase tốn O(N) cho thao tác shift bộ nhớ. Không gian bộ nhớ: O(N) tuyệt đối nhờ kỹ thuật In-place và std::move, triệt tiêu hoàn toàn lỗi Memory Limit Exceeded (MLE).
 
Dùng đến kỹ thuật thiết kế bài toán nào?
=> Constructive Algorithms, Computational Geometry, In-place Divide & Conquer.
 
Độ khó của bài toán khoảng bao nhiều trên thang 10?
=> 9/10. Bản chất ý tưởng hình học đã khó (8.5), việc phải tinh ý nhận ra hàm đệ quy sinh ra quá nhiều bản sao (Copy states) và phải tái cấu trúc lại memory management đẩy độ khó kỹ thuật lên rất cao.
*/