习题5-1
凸包
-
给定n个二维平面上的点(无重合点但可能有三点共线),求他们的凸包
-
最后输出的答案是构成凸包的点按编号相乘再乘以点数,再对m取余的结果
-
卷包裹
-
求凸壳
解法1
- 求上下凸壳,类似卷包裹,但是不是极角排序(任取一个点,做极角排序)
- 核心算法是toLeft算法
- 按照先x轴后y轴的方式升序排序,这是为了找到一个一定在凸包上的点
- 然后求上凸壳和下凸壳,可以想象在y轴负无穷处有一个点,然后求凸包得到上凸壳,y轴正无穷有一个点,然后求凸包得到下凸壳
- 单调栈维护凸包集合
代码解析
-
convex函数,对a数组求凸包,求的结果存储在b数组中
-
可以想象在y轴负无穷处有一个点,然后求凸包得到上凸壳,上凸壳加上y轴无穷远处的点,可以把内部的点全部包住,同理可求得下凸壳,上凸壳和下凸壳合起来就是凸包
-
最左边最下面的那个点一定在凸包上,(x,y)都取最小值时
-
按照排序后的顺序,从左下点开始,依次与剩下的点相连,然后下一个是否在这条边的左边,如果在右边则去掉这条边,与新的点相连构成一条新的边,然后如果新一个点在当前边的左边,则又可以构成一条新边(第二条边),如果是在右边,则第一条边又被覆盖掉,如果重复下去,则可以求出下凸壳
-
骚操作,把屏幕倒过来,上凸壳的最后一个点恰好是求上凸壳的起点(倒过来看的最左边同时最下面的点),不需要再排序了,按照之前的反序来求,即可求出上凸壳
-
for (int i = 0; i < n; i++) { for (;m>1 && cross(sub(b[m-1],b[m-2]),sub(a[i],b[m-2]))<0;--m); b[m++] = a[i]; } for (int i = n-2,t=m; i >=0 ; --i) { for (;m>t && cross(sub(b[m-1],b[m-2]),sub(a[i],b[m-2]))<0;m--); b[m++] = a[i]; }
-
这就是toLeft操作,>=0则在右边
-
向量相减,被减的是起点
-
m=t,其中t-1是下凸壳的数目,t=m+1时,才能保证上凸壳至少有两个点,就跟m至少等于2是一个道理
-
最后返回m-1是因为最开始的起点实际算了两次,也就是下凸壳的起点和上凸壳的终点是一个点,但是算了两次
标程
static class Task {
final int N = 300005;
// 坐标类
class ip {
int x, y, i;
ip(int x, int y) {
this.x = x;
this.y = y;
}
}
// 两点相减得到的向量
ip sub(ip a, ip b) {
return new ip(a.x - b.x, a.y - b.y);
}
// 计算a和b的叉积(外积)
// 如果b在a的左边,求出来的叉积表示a和b构成的平行四边形的面积是正的
// 此时a X b >0
long cross(ip a, ip b) {
return (long)a.x * b.y - (long)a.y * b.x;
}
static class cmp implements Comparator<ip> {
// 先比较x轴再比较y轴,
// 按顺序排序(从小到大)
@Override
public int compare(ip a, ip b) {
if (a.x < b.x)
return -1;
else if (a.x > b.x)
return 1;
else {
if (a.y < b.y)
return -1;
else if (a.y > b.y)
return 1;
return 0;
}
}
}
// 计算二维点数组a的凸包,将凸包放入b数组中,下标均从0开始
// a, b:如上
// n:表示a中元素个数
// 返回凸包元素个数
int convex(ip[] a, ip[] b, int n) {
// 利用cmp排序函数进行排序
// 1.先按x升序排序
// 2.如果x相等,再按y正序排序
Arrays.sort(a,new cmp());
// 凸壳上点的编号
int m = 0;
// 最左边同时最下面的点一定在凸包上
// cross(sub(b[m-1],b[m-2]),sub(a[i],b[m-2]))表示toLeft操作
// sub(b[m-1],b[m-2])表示从m-2出发,指向m-1的向量
// 求下凸壳
// m>1表示至少有两个点
for (int i = 0; i < n; i++) {
for (;m>1 && cross(sub(b[m-1],b[m-2]),sub(a[i],b[m-2]))<0;--m);
b[m++] = a[i];
}
// 求上凸壳
// t是下凸壳中点的数量
// m>t才表示上凸壳中有两个点
for (int i = n-2,t=m; i >=0 ; --i) {
for (;m>t && cross(sub(b[m-1],b[m-2]),sub(a[i],b[m-2]))<0;m--);
b[m++] = a[i];
}
// 1号点算了两次,一开始最左下的那个点
return m-1;
}
void solve(InputReader in, PrintWriter out) {
int n = in.nextInt();
ip[] a = new ip[n];
ip[] b = new ip[n + 1];
for (int i = 0; i < n; ++i) {
a[i] = new ip(0, 0);
b[i] = new ip(0, 0);
a[i].x = in.nextInt();
a[i].y = in.nextInt();
a[i].i = i + 1;
}
int m = convex(a, b, n);
long ans = m;
for (int i = 0; i < m; ++i)
ans = (ans * b[i].i) % (n + 1);
out.println(ans);
}
}
坑
- 坑1:如果只对x排序,不对y排序,求出来的下凸壳是不对的
- 坑2:三点共线,因为每次判断toLeft只考虑了<0的情况,但是共线的点如果在凸包上,此时是等于0的,但是没有将这个点考虑进来。
- 三点共线,等于0的话,判断一下距离大小,距离小的点在前面
- 坑3: 如果有重复点是要去重的
- 浮点数的问题,有精度问题
图
- 在有向无环图里面,序列是1,2,3 ,不存在2号点连向1号点,不存在3号点连向1号点,不存在3号点连向2点,不存在后面序号连向前面序号的边,这样的序列就叫做拓扑序列
- 有向无环图中至少存在一个拓扑序列
- 这个序列不一定唯一
- 这个合法序列实际就是拓扑序列吗,拓扑序列只存在有向无环图中
- 怎么求唯一拓扑序列呢?可以在O(n)时间求出
有向无环图
- 怎么寻找拓扑序列?
- 在有向无环图中至少存在一个入度为0的点
- 每次找一个入度为0的点,作为拓扑序列的第一个点
- 然后删除这个点和它相连的所有边(相连的点的入度都减一)
- 然后又找一个入度为0的点,重复这个操作
代码解析
-
模拟上面那个入度为0的点,并由此寻找的过程,遍历完整个图,就得到了拓扑序列
-
for (int i = 0; i < m; ++i) { int x = _in.nextInt(), y = _in.nextInt(); e[x].add(y); ++in[y]; }
-
m条边,e(i)(j)表示点i的第j条边指向的点,in(y)表示y点的入度加1
-
for (int i = 1; i <= n; i++) { if (in[i]==0){ if (x!=0){ return 0; } x = i; } }
-
in[i]表示点i的入度
-
存在多个入度为0的点,直接返回0,说明不存在唯一的拓扑序列
-
for (int i = 1; i <= n; i++) { int z = 0; for (int j = 0; j < e[x].size(); j++) { int y = e[x].get(j); --in[y]; if (in[y]==0){ if (z!=0){ return 0; } z = y; } } x = z; }
-
此处的x是上一步遍历找到的唯一的入度为0的点
-
if (in[y]==0){ if (z!=0){ return 0; } z = y; }
-
y是与当前入度为0的点相连的点,找出这些点,如果有多个,则存在多个,则在下一步循环存在多个入度为0的点,则直接返回0,遍历完当前整个入度为0的相连点,将找出的唯一一个入度为1的点赋值给x,变成新的入度为0的点。
-
此处为什么要有一个1到n的循环,是因为,最简单的循环中每个点只有1条边,这样彼此两两相连,恰好要遍历n次,才能遍历完
仅求拓扑序
-
for(int i=1;i<=n;i++) if(in[i]==0) q.push_back(i); while(q.size()){ int x = q.front(); ans.push_back(x); q.pop(); for(int i=0;i<int(e[x].size());++i){ int y = e[x][i]; --in[y]; if(in[y]==0){ q.push_back(y); } } }
-
q是一个队列
标程
-
static class Task { final int N = 10005; // 为了减少复制开销,我们直接读入信息到全局变量中,并统计了每个点的入度到数组in中 // n, m:点数和边数 // in:in[i]表示点i的入度 // e:e[i][j]表示点i的第j条边指向的点 int n, m; int[] in = new int[N]; List<Integer>[] e = new ArrayList[N]; Queue<Integer> q= new Queue<>(); // 判断所给有向无环图是否存在唯一的合法数列 // 返回值:若存在返回1;否则返回0。 int getAnswer() { int x = 0; // 找到唯一的入度为0的点 for (int i = 1; i <= n; i++) { if (in[i]==0){ // 之前已经找到了一个 if (x!=0){ return 0; } x = i; } } // x表示的就是图中唯一的入度为0的点 for (int i = 1; i <= n; i++) { int z = 0; for (int j = 0; j < e[x].size(); j++) { int y = e[x].get(j);// x->y // y是与当前入度为0的点相连的点 // 删除入度为0的点,与其相连的点的边 --in[y]; // 类似上边 if (in[y]==0){ if (z!=0){ return 0; } z = y; } } x = z; } return 1; } void solve(InputReader _in, PrintWriter out) { int T = _in.nextInt(); while (T-- != 0) { n = _in.nextInt(); m = _in.nextInt(); for (int i = 1; i <= n; ++i) { in[i] = 0; e[i] = new ArrayList<>(); } for (int i = 0; i < m; ++i) { int x = _in.nextInt(), y = _in.nextInt(); e[x].add(y); ++in[y]; } out.println(getAnswer()); } } }
-
// 求拓扑序列 int getAnswer() { int x = 0; // 辅助计算的队列 Queue<Integer> q = new Queue<Integer>(); // 保存的拓扑序列 List<Integer> list = new ArrayList<>(); // 找到唯一的入度为0的点 for (int i = 1; i <= n; i++) { if (in[i]==0){ q.push_back(i); } } // x表示的就是图中唯一的入度为0的点 while(q.size()!=0){ int x = q.pop(); list.add(x); for(int i=0;i<int(e[x].size());++i){ int y = e[x][i]; --in[y]; if(in[y]==0){ q.push_back(y); } } } return list; }
Fruit Ninja
- 水果忍者
- 对平行线段的上端点求一个下凸壳
- 对平行线段的下端点求一个上凸壳
- 判断两个凸壳是否相交,不相交则存在一条直线穿过所有的平行线段
Great Wall
- 在x点,选若干个点(尽量少),则视线可以覆盖所有的点
- 从右到左,枚举,求上凸壳,每次求解过程中,如果求出的点不是在栈顶位置,则这个点是要被选择的,