习题5-1(凸壳、拓扑排序)

习题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则在右边

  • 向量相减,被减的是起点

    扫描二维码关注公众号,回复: 12476629 查看本文章
  • 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点,选若干个点(尽量少),则视线可以覆盖所有的点
  • 从右到左,枚举,求上凸壳,每次求解过程中,如果求出的点不是在栈顶位置,则这个点是要被选择的,

猜你喜欢

转载自blog.csdn.net/Markland_l/article/details/109402737
5-1