You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

8.0 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##Lights

一、题目大意

给出一个n*m的矩阵,再给出N个点,问其中的任意两个点 (x_1,y_1)(x_2,y_2) 之间,最短路径为 |x_1-x_2| + |y_1-y_2| , 是否存在一条最短路径,使得拐弯的地方都存在着点

视频讲解

目前状态:理解了,但不太会讲,还需要继续消化一下~ 2023.01.05

值得学习的内容

  • 二维坐标去重+按(x,y)排序的办法

  • 思维:思考两个点,必须得x相等或y相等,否则无法连接上,需要引入第三点。此时,如果两个点是按(x,y)排序过的,那么可以在同x,同y情况下讨论相邻点的情况。思考构建一个矩形,矩形内有其它点就是NO,这个思维难度也不低。

  • 为什么要用主席树+扫描线?因为方便,无脑,按顺序 x从小到大,每个 x视为一个版本,每次查询构造矩形中点的个数,如果大于0就是有不合法的点

也不怪我学着费劲,本题对思维的要求确实挺高,没学习过类似的题,现场基本是不可能做上的,都需要提前做过类似的训练,并且熟练才行,慢慢历练吧~

二、扫描线+主席树

对于每个点(x, y)正左方 最近的点记为(x', y) 解读: ① 正左方意味着y坐标相同 ② 最近也意味着y坐标相同

正上(下) 方最近的点记为(x, y') 解读: ① 正上(下)方意味着x坐标相同 ② 最近也意味着x坐标相同

  • 如果以这3个点为顶点的矩形内部存在有其它的点(x'', y''),则从(x, y)(x'', y'')不存在good path.
  • 反之,如果整个图中都不存在这种情况,则任意两点间都有good path

因而可以用 扫描线和可持久化线段树 来做,从左往右扫描,每个x对应线段树的一个版本,每遇到一个点(x, y)和它正上(下)方最近的点(x, y')就在当前版本的线段树和版本为x'的线段树上查询yy'之间的点数,如果这两个值不相等,则意味着存在上述的点(x'',y''). 对于每条扫描线处理完查询之后再将点插入到当前版本的线段树中. 复杂度O(NlogN)

对于每个x,在其每个y处对线段树进行更新,并记录左边最近的点的横坐标,最后再将该x与最后版本的线段树对应起来。从左向右扫一遍,先判断,后更新。

实现代码

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;

//快读
int read() {
    int x = 0, f = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-') f = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = (x << 3) + (x << 1) + (ch ^ 48);
        ch = getchar();
    }
    return x * f;
}

const int N = 500010;
const int M = 50010;

int n;            //点的数量
vector<int> a[M]; //记录每个点的坐标

//主席树的结构体
struct Node {
    int cnt;
    int l, r;
} tr[N << 5]; //主席树一般开32倍空间
int idx;      //主席树的节点生成器
int root[M];  //版本与根节点的对应关系
int ver[M];   //记录y坐标是在哪个版本加入进去的,这是一个桶性能用静态数组模拟应该是最强的不要使用什么unordered_map之流

//标准的主席树insert
int insert(int p, int l, int r, int x) {
    int q = ++idx;
    tr[q] = tr[p];
    tr[q].cnt++;
    if (l == r) return q;
    int mid = (l + r) >> 1;
    if (x <= mid)
        tr[q].l = insert(tr[p].l, l, mid, x);
    else
        tr[q].r = insert(tr[p].r, mid + 1, r, x);
    return q;
}

//功能:主席树,在两个版本间 查询 某个区间中数字的增加个数
int query(int q, int p, int ql, int qr, int l, int r) {
    if (ql <= l && qr >= r) return tr[p].cnt - tr[q].cnt; //完整区间包含,直接返回两个版本的数字个数差,表示两个版本间增加的数字数量
    int ans = 0;
    int mid = (l + r) >> 1;
    if (ql <= mid) ans = query(tr[q].l, tr[p].l, ql, qr, l, mid);     //递归查询左半区间
    if (qr > mid) ans += query(tr[q].r, tr[p].r, ql, qr, mid + 1, r); //递归查询右半区间
    return ans;
}

bool solve() {
    //对于主席树的0号版本进行清空
    root[0] = idx = tr[0].l = tr[0].r = tr[0].cnt = 0; //索引号生成器idx修改为0主席树中0号结点左儿子右儿子都不存在0号版本中数字个数为0, 0号版本的根节点是0
    memset(ver, 0, sizeof(ver));                       //清空ver数组

    for (int i = 1; i <= 50000; i++) {                          //从小到大枚举每一个x坐标
        for (int j = 0; j < a[i].size(); j++) {                 //从小到大枚举每一个y坐标
            //如果是第一个y坐标那么l=0,这个0是哨兵的意思。如果不是第一个y坐标那么l=当前y坐标的前一个y坐标prev_y,
            int l = j == 0 ? 0 : a[i][j - 1];

            //如果是最后一个y坐标那么r=50001,否则r=当前y坐标的下一个y坐标next_y
            int r = j == a[i].size() - 1 ? 50001 : a[i][j + 1];

            // root[i]:当前版本的根
            // root[x[a[i][j]]]:前一版本的根
            // query的返回值是个整数表示两个版本间两个区间内增加的数字个数如果个数大于0就return false
            if (query(root[i - 1], root[ver[a[i][j]]], l + 1, r - 1, 1, 50000)) return false;
        }

        root[i] = root[i - 1];
        for (int j = 0; j < a[i].size(); j++) {           //从小到大枚举每一个y坐标
            root[i] = insert(root[i], 1, 50000, a[i][j]);
            ver[a[i][j]] = i; //记录a[i][j]这个y坐标是第i个版本中增加进去的
        }
    }
    //如果上面所有的坐标点都讨论过全都没有返回false,那就只能返回true
    return true;
}

/*
  NO
  YES
*/
int main() {
//文件输入输出
#ifndef ONLINE_JUDGE
    freopen("HDU5820.in", "r", stdin);
#endif

    while (n = read(), n) { //在使用快读时,要注意使用 ,n 而不是使用 && n!!!! read没有返回值

        // a数组装的是坐标(x,y),只不过它是以x坐标为第一维第二维的y1,y2,y3,... 通过push_back加入到一维的x中
        for (int i = 0; i < M; i++) a[i].clear(); //二维vector的清空操作不用循环还真不行

        int x, y;
        while (n--) {
            x = read(), y = read(); // 每个点的坐标
            a[x].push_back(y);      // 记录每个点的坐标记录的方法很有意思是一个vector,一维是x坐标二维是y坐标
        }

        //枚举每个可能的x坐标对x坐标相同的点按y坐标小由到大进行排序这样就是一个二维有序的数组啦和结构体两个属性进行排序效果和速度是一样的
        for (int i = 1; i <= 50000; i++) {                            //这样是以x从小到大排序的
            sort(a[i].begin(), a[i].end());                           //对y也要从小到大排序,这和整一个Node{x,y},然后自定义sort operator < 定义先按x后按y是一个意思有时间再写一个结构体版本的
            a[i].erase(unique(a[i].begin(), a[i].end()), a[i].end()); //对于相同的(x,y)进行排序+去重
        }

        //开始每一轮计算的主函数solve
        puts(solve() ? "YES" : "NO"); //存在题目要求的答案返回YES否则返回NO
    }

    return 0;
}

三、zkw线段树+扫描线

https://yukizzz.github.io/2016/08/23/HDU-5820-Lights%E3%80%90zkw%E7%BA%BF%E6%AE%B5%E6%A0%91%EF%BC%8C%E6%89%AB%E6%8F%8F%E7%BA%BF%E3%80%91/

https://www.cnblogs.com/Judge/p/9514862.html

https://blog.csdn.net/keshuqi/article/details/52205884