紫书刷题进行中,题解系列【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;
}
来源:CSDN
作者:是阿俊呐
链接:https://blog.csdn.net/qq_40738840/article/details/104618218