例题8-6 UVA1606 Amphiphilic Carbon Molecules(43行AC代码)

做~自己de王妃 提交于 2020-03-02 22:22:11

紫书刷题进行中,题解系列【GitHub|CSDN

例题8-6 UVA1606 Amphiphilic Carbon Molecules(43行AC代码)

题目大意

在笛卡尔坐标系中给出n个点的坐标,点有黑白两种颜色,问用一个直板分割平面,如何令平面一侧的白点数目和另一侧的黑点数目和最大(在直板上的点全部加入总和

思路分析

通过分析,可假设直板一定至少经过两个点,如果不是,则可以通过平移得到该状态

那么可枚举任意两个点,然后判断其余n-2个点分布情况,时间复杂度O(n^3),显然会超时

可先枚举一个基准点,然后将一条直线绕该点选择一周。每当直线扫过一个点时,即可更新两侧的点数。由于扫描前要对所有点根据极角排序,时间复杂度为O(nlogn),加之n个基准点枚举和计算,所以时间复杂度为O(n^2logn)

这样一分析似乎很容易,但是实现起来有许多的技巧

坐标变换

  • 简化计算:当选择点i为基准点时,可计算其余的点到i点的相对坐标(等效将i作为坐标原点),后续计算很方便

  • 对称性:将黑点相对于当前隔板做轴对称映射(x和y均取相反数),到时候只需计算隔板的一侧所有白点数量即可。

    可能看到这会有疑问:题目不是说黑白点在任意一侧均可以嘛,那这样会不会只计算到一种情况,比如白左黑右,而白右黑左不会被计算?

    实际上不会出现这种情况,因为我们枚举了每个基准点,意味着一条直线会以i-j和j-i形式出现,即考虑以上两种情况。

极角计算

利用反正切函数atan2可便捷计算出结果,若有精度要求需注意

扫描更新两侧点

这个技巧恐怕是本题最难理解之处,也是扫描法的核心

最好的方式是画几个点,带入算法跑一遍理解。

首先,令i-j,表示点i和j确定的直线,因为经过坐标变换,i其实可等效为原点坐标O,因此分隔线O-i用L1表示,为了统计L1左侧/上侧的点,定义cnt=2统计数量,扫描线O-j用L2表示(离散化技巧,计算)。

其次,旋转L1时,左侧点cnt–,同时在之前的L2基础上,统计新增的点(有点类似dp/递推的思想,利用之前已有的结果,无需重复计算,以此优化算法)。

看到这可能有疑问:当L1上有3个点以上时,结论还成立吗?

答案是成立,其实这个过程中针对每条分隔线计算的结果存在递减的结果,比如一条线上有3个点,如下所示,以O-A为分隔线比以O-B为分隔线时的cnt多1,因为我们总取最大值,所以结果不影响

---O---A----B---
  • L2由于一直在转动,因此可用模运算模拟

思维小结

扫描法:类似于有序的枚举法,与普通枚举不同之处在于维护一些重要的量,从而简化计算;也有点类似dp和递推思想,利用已有结论,避免重复计算,优化算法

本题就是维护了L2这条扫描线,当分隔线L1转动时,不用每次从头开始计算,而是从上次L2所在位置继续计算,而本身再-1即可

AC代码(C++11,极角扫描,坐标变换,对称)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1005;
struct Point{
    int x, y, color;
    double theta; // 相对于基准点的极角;acrtan计算
}p[maxn], pt[maxn]; // p:原数据;pt:变换后的坐标
int n;
bool isLeft(const Point& a, const Point& b) { // O-a为分隔线,判断b是否在O-a上侧
    return a.x * b.y - a.y * b.x >= 0; // 直线方程判断
}
int solve() {
    if (n <= 3) return n; // 1/2/3直接返回
    int ans=0;
    for (int i=0; i < n; i ++) { // 枚举n个基准点
        int k=0;
        for (int j=0; j < n; j ++) { // 相对坐标变换(将i作为j的原点)
            if ( i == j) continue; // 同一个点跳过
            pt[k].x = p[j].x - p[i].x; // 求点j相对于基准点i的坐标 
            pt[k].y = p[j].y - p[i].y;
            if (p[j].color == 1) {pt[k].x = -pt[k].x; pt[k].y = -pt[k].y;} // 将黑色点对称变换,到时候只需扫描180即可
            pt[k].theta = atan2(pt[k].y, pt[k].x); // 利用反正切求角度
            k ++;
        }
        sort(pt, pt+k, [](Point& a, Point& b) {return a.theta < b.theta;}); // 按照极角升序排列
        int cnt=2, pcur=0, prot=0; // pcur:当前分隔线,prot:旋转线
        while (pcur < n-1) { // 所有分隔线
            if (pcur == prot) {prot = (prot+1)%(n-1); cnt ++;} // 后面扣除
            while (pcur != prot && isLeft(pt[pcur],pt[prot])) {prot = (prot+1)%(n-1); cnt ++;} // 只计数一侧,因为之前黑色点变换过
            cnt --; // 前面多加一次
            ans = max(ans, cnt);
            pcur ++; // 下一个分隔线
        }
    }
    return ans;
}
int main() {
    while (scanf("%d", &n) == 1 && n != 0) {
        for (int i=0; i < n; i ++) scanf("%d%d%d", &p[i].x, &p[i].y, &p[i].color);
        printf("%d\n", solve());
    }
    return 0;
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!